To illustrate features of Cminor language in a somewhat more realistic context, we go through creating a desk calculator in this chapter.
Error handling in Cminor programs is best implemented using exceptions, because exceptions separate concerns of detecting an error condition and handling it using close to minimal code devoted to error handling.
For example, in the following main function, there are two nested try blocks, where the inner block handles known errors caused by incomplete or otherwise invalid input – ParsingException, trying to divide by zero – DivisionByZeroException, referring to a nonexisting variable – VariableNotFoundException, or trying to parse badly formatted string to a number – System.ConversionException. These user errors are handled by printing just the error message represented by the Message property of System.Exception class.
The outer try block handles all remaining errors, or errors that "should not happen". The System.Exception.ToString() method prints the call stack along with the error message, so that exact source code position of the error can be found.
The main loop consists of reading a line of input from the console, parsing and processing it, and also handling errorneous input. Then next line of input is read and processed, and so on, until user enters EOF followed by an ENTER to end the loop.
Here are the exception classes for various error conditions. In Cminor as in C# all exception classes must be derived from the System.Exception class. By catching System.Exception class all exceptions can be handled.
Unlike C#, Cminor contains nonmember functions, also called "free functions". They are functionally equivalent to static member functions in C#. Nonmember functions can be used to implement program logic that does not naturally belong to any class. Also operator functions operator+, operator-, operator*, and so on, are required to be nonmember functions in Cminor.
The following code creates an object of a Scanner class that is used to break up the input into tokens and also to skip any whitespace in input. Then the first token is read. If it is a PrintToken the current contents of variables are printed. Otherwise if it is a variable name token, and the next token is '=', an expression is parsed and its value is assigned to a variable. Otherwise the input is rewound and an expression is parsed and its value is printed:
An expression of the form "x is C", tests if the class of expression x is equal to class C,
or derived from it. If that is the case the value of expression "x is C" is true,
otherwise it's false. The language implementation of is-expression in Cminor does not need to crawl the class hierarchy,
because in intermediate code link time, each class is assigned an identifier of type ulong that is a product of prime factors.
The is test is reduced to an integer divisibility test:
"(id of class of x) % (id of class C) == 0".
This scheme is invented by Michael Gibbs and Bjarne Stroustrup and described in the article Fast Dynamic Casting
authored by them. The implementation of this scheme in Cminor is simpler than implementation for C++ described in the article because Cminor lacks multiple inheritance.
As already mentioned Cminor lacks multiple inheritance, but it allows a class to implement any number of interfaces. However in this simple program interfaces are not used. Here are the various token classes. The Token class is abstract meaning an object of it cannot be created. A class that contains abstract member functions, must also be declared abstract.
In Cminor a class may have properties that can be read-only – having a getter, write-only – having a setter, or read-write – having both getter and a setter. In the code above classes NumberToken, VariableNameToken and OperatorToken have read-only properties. Unlike in C#, properties in Cminor cannot be polymorphic – virtual, abstract or overridden. Properties can be static in Cminor, however. Static properties are accessed using ClassName.PropertyName notation.
Like in C#, values of basic value types are automatically boxed when needed, meaning an object containing a value of a basic value type is created. For example, statement object x = 1; creates an object of BoxedInt8 class and places an sbyte value 1 into that object. The reverse operation of unboxing converts an object of boxed value type back to an instance of basic value type. Unboxing must be performed explicitly by using a cast. For example, sbyte y = cast<sbyte>(x);. If the type of x is not BoxedInt8, an exception of class System.InvalidCastException is thrown. The Cminor system library source file BoxedTypes.cminor contains the boxed types and their operations. Explicit type conversion, casting is performed using syntax cast<sbyte>(x), not using the C# syntax (sbyte)x, because this way casts can be found from source code with search tools using regular expressions.
Cminor contains minimal support for Unicode, not perfect, but usable at least in western Europe countries where the author is from. The support is implemented as static member functions of the System.BoxedChar class. For example, char.IsLetter(x) returns true if character x has Unicode letter character category. The following Scanner class illustrates using character classification functions char.IsLetter(char) and char.IsNumber(char) and also parsing a double to a string – double.Parse(string):
Cminor system library has the following generic collections implemented as templates:
The calculator stores the values of variables in a System.Collections.Generic.Map<string, double> by mapping the name of the variable to the value of the variable. If the key of an element is not already found in the map, the subscripting operator of the map inserts the element to the map. Otherwise, if the key is already found in the map, the new value replaces the old value.
Each collection class implements an interface called Enumerable. An Enumerable class has a member function called GetEnumerator() that returns an object of a class that in turn implements an interface called Enumerator. The Enumerator interface differs from the C# language interface of the same name by providing a simplified functionality. The Cminor Enumerator interface has three functions instead of two:
The Print() member function of the SymbolTable class above uses enumerator interface implicitly to enumerate all the variables in the map, because the foreach statement is implemented in terms of Enumerable and Enumerator interfaces: statement foreach (Type x in collection) { statement; } is lowered by the compiler to the following code:
A parser can be implemented by hand as functions that call each other possibly recursively. This kind of parser is called a recursive-descent parser. The syntax of arithmetic expressions we are parsing is as follows:
We will write each production as a nonmember function that returns the value of the parsed subexpression. Here's the Expr() function that corresponds to the <expr> production. It parses <term>s separated by '+' and '-' characters:
The expression "x as C" is functionally equavalent to the similar C# language construct: If class of x is class C or derived from C the result of the as expression is x converted to C, otherwise it is null. The class-of test is implemented using similar integer divisibility expression in terms of class identifiers as for the is-expression.
Here's the Term() function that corresponds to the <term> production. It parses <factors>s separated by '*' and '/' characters:
The <factor> production is implemented by the following Factor() function. It parses <primaries> prefixed by '+' and '-' signs:
Here comes finally the implementation of the <primary> production. It parses numbers, variable names and subexpressions:
When the primary starts with the left parenthesis the Primary function calls the Expr function to parse a subexpression recursively.
That closes up the calculator example. Here's the entire source code for the parser portion of the example: