Console Calculator with Roslyn (Part 2) - Pavel's Blog

# Console Calculator with Roslyn (Part 2)

In the first part we created a simple enough calculator, but it lacked two features I wanted to have:

• 1. work with degrees or radians in trigonometric functions.
• 2. allow simple variables to be used without first declaring them.

Let’s see how we can implement these features, starting with the first.

Trigonometric functions work in radians, which is sometimes inconvenient.What we need is a way to change the parameter to the trigonometric functions by multiplying it by PI/180 if degrees was requested.

First, we’ll create a simple state managing class for the calculator with just one property:

1. class CalculatorOptions {
2.     public bool Degrees { get; set; }
3. }

Next, we’ll create a constructor for our CalculatorRewriter to accept a CalculatorOptions object and store it:

1. public CalculatorOptions Options { get; private set; }
2.
3. public CalculatorRewriter(CalculatorOptions options) {
4.     Options = options;
5. }

Next, we need to identify functions that require adjustments – that is, the trigonometric functions. Recall from part 1 that we had a dictionary that mapped a function friendly name (e.g. “sin”) to a QualifiedNameSyntax Roslyn object that would be used to replace the friendly name so that the code can compile. We’ll add another Boolean parameter that would indicate whether the function is, in fact, a trigonometric one.

Because of laziness, I’ve used a Tuple<QualifiedNameSyntax, bool> as the Value type for the dictionary, indicating for each function whether it’s a trigonometric one. Here’s the complete dictionary definition inside the CalculatorRewriter class:

1. static readonly Dictionary<string, Tuple<QualifiedNameSyntax, bool>> _functions =
2.     new Dictionary<string, Tuple<QualifiedNameSyntax, bool>>(32) {
3.     { "sin", Tuple.Create(Syntax.QualifiedName(Syntax.IdentifierName("Math"), Syntax.IdentifierName("Sin")), true) },
4.     { "cos", Tuple.Create(Syntax.QualifiedName(Syntax.IdentifierName("Math"), Syntax.IdentifierName("Cos")), true) },
5.     { "pi", Tuple.Create(Syntax.QualifiedName(Syntax.IdentifierName("Math"), Syntax.IdentifierName("PI")), false) },
6.     { "tan", Tuple.Create(Syntax.QualifiedName(Syntax.IdentifierName("Math"), Syntax.IdentifierName("Tan")), true) },
7.     { "sqrt", Tuple.Create(Syntax.QualifiedName(Syntax.IdentifierName("Math"), Syntax.IdentifierName("Sqrt")), false) },
8.     { "max", Tuple.Create(Syntax.QualifiedName(Syntax.IdentifierName("Math"), Syntax.IdentifierName("Max")), false) },
9.     { "min", Tuple.Create(Syntax.QualifiedName(Syntax.IdentifierName("Math"), Syntax.IdentifierName("Min")), false) },
10. };

Note that the trigonometric functions have their Boolean set to true.

This means that VisitIdentifierName has to change slightly because of that Tuple:

1. public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) {
2.     Tuple<QualifiedNameSyntax, bool> newNode;
3.     if(_functions.TryGetValue(node.PlainName, out newNode))
4.         return newNode.Item1;
5.     return node;
6. }

Now we’ll add a simple method to indicate whether a particular function is a trigonometric one:

1. private static bool IsTrig(string functionName) {
2.     Tuple<QualifiedNameSyntax, bool> func;
3.     return _functions.TryGetValue(functionName, out func) && func.Item2;
4. }

We need to call it from somewhere. We’ll add an override for the VisitArgument method (from SyntaxRewriter), look at the function call, and if it’s a trigonometric one and we’re in degrees mode, modify the argument accordingly. This is how this function may look:

1. public override SyntaxNode VisitArgument(ArgumentSyntax node) {
2.     if(Options.Degrees) {
3.         var invoke = node.Parent.Parent as InvocationExpressionSyntax;
4.         if (invoke != null) {
5.             var function = invoke.GetFirstToken().GetText();
6.             if(IsTrig(function))
7.                 return Syntax.BinaryExpression(SyntaxKind.MultiplyExpression,
8.                     ((ArgumentSyntax)base.VisitArgument(node)).Expression,
9.                     Syntax.LiteralExpression(SyntaxKind.NumericLiteralExpression, Syntax.Literal("Math.PI / 180", 0)));
10.         }
11.     }
12.     return base.VisitArgument(node);
13. }

We check if we’re in degrees mode (otherwise no need to bother with anything), and then look at the invocation expression which is the arguemnt’s parent’s parent (the first parent is the argument list). Then, we look at the function name by looking at the first token of the invocation, and if it’s trigonometric we create a BinaryExpressionSyntax object that represents an operation involving two arguments. It’s a multiplication, with the left side being what we got as an argument (we need to visit it as well in case we have a nesting of special functions) and the literal “Math.PI / 180” which is the factor to turn degrees into radians.

To allow switching modes, I’ve added some simple if statements inside the loop in Main that change the property accordingly:

1. while(true) {
2.     Console.Write(">> ");
4.     if(input == "exit")
5.         break;
7.         calcOptions.Degrees = false;
8.         continue;
9.     }
10.     if(input == "deg") {
11.         calcOptions.Degrees = true;
12.         continue;
13.     }

Here’s an example interaction with the calculator:

Let’s move on to the second problem. If we type something like a=10 we get a compiler error, because a does not exist. We need to define it with something like var a = 10; or double a = 10; this is inconvenient.

To fix that, we need to declare the variable automatically before its use. We can try using the rewriter, but let’s do something else for fun. We’ll examine the parse tree of the user’s input, and if it contains assignment expressions, we’ll define the variables that are there:

1. var tree = SyntaxTree.ParseCompilationUnit(input, options: options);
2. var nodes = tree.GetRoot().DescendantNodes().Where(node => node.Kind == SyntaxKind.AssignExpression).Cast<BinaryExpressionSyntax>();
3. foreach(var node in nodes) {
4.     string declarartion = string.Format("double {0};", node.Left.GetFirstToken());
5.     engine.Execute(declarartion, session);
6. }

We look for an assignment expression nodes (there can be more than one in lines like a=10;b=20), and for each one found we execute a declaration using the regular script engine before executing the “real” input – the assignment, which now should work. Here are some examples:

Roslyn has a lot of potential. It’s sometimes verbose, and currently lacks real documentation, but it certainly looks promising.

Roslyn Console Calculator Source Code