Adding Type Inference to Rook

July 29th, 2007

Earlier posts on Rook showed that it was a dynamically typed language: the type of an object was not declared, and was not known until the last moment at runtime. Typing mistakes on the part of the programmer would be discovered at runtime. Although I like the brevity of dynamic languages like Python, I have also been interested in using type inference to get some of the benefits of dynamic typing along with the benefits of static typing. The latest version of Rook, which is not quite complete enough to be made public, uses the Hindley-Milner Type Inference Algorithm instead of dynamic typing. The basic idea is that all of the sample source code in previous posts still works unchanged, with the added benefit of having all types checked prior to running.

I found this paper to be the most accessible description of the Hindley-Milner algorithm: Basic Polymorphic Type Checking by Luca Cardelli. It gives a lot of background information in plain English, assumes little about the reader other than familiarity with language constructs similar to those in Scheme, and includes a working implementation of the algorithm. Some of the formal theory went a little over my head, but the remainder of the paper proved very useful.

The sample implementation was in Modula-2, and understandably it did not include tests that would translate directly into NUnit tests. Still, I wanted to make sure that as I translated the sample implementation into C#, I would have a large set of tests to back it up. First, I made a translation from Modula-2 to C#-flavored pseudocode. Then, I attacked that code in several small pieces: whenever a relatively small part of the pseudocode could be tested/developed in isolation, I would move that piece into C# with tests. This approach allowed me to chip away at the stone with confidence that I wasn’t missing anything in the translation. Another fortunate side effect of this approach is that by making myself writes tests for each small piece being translated, I gained a better understanding of how the type inference system actually works.

Filed under: TDD, Language Design

Turn it up to 11

February 26th, 2007

I started running into a little trouble with my TestFixture classes. The glaring example was with the TokenizerTest fixture, which tested every nook and cranny of the Tokenizer class. The tokenizer is responsible for advancing through the input source code, chopping it up into meaningful tokens, such as operators, keywords, identifiers, and the like. One of the features of the tokenizer is the ability to ‘peek ahead’ to the next token without consuming it. Having a separate tokenizing step before the actual parsing starts makes things easier; I get to write the grammar and parser at a higher level than I would otherwise. It’s a separation of concerns.

Well, even when I separate these concerns, the tokenizer has a great deal of state changes and edge cases. At any moment, there are many different kinds of tokens that might come next, and whitespace needs to be skipped, and there are a few unexpected situations that raise exceptions, and…

As a result of some naive test case writing, the TokenizerTest fixture started to feel its age. In order to handle the many different states, I had to “set up” (an unfortunate choice of words that should have been a clue early on) some state inside each test method before making an assertion. This seemed reasonable at the time, since each test was still isolated from all the others, and the thing being tested just plain needed a special initial state.

Around the time that this got unwieldy, I read Specs vs. Tests, which pointed me toward RSpec, one of Ruby’s answers to xUnit. That article got me interested in the idea of using unit tests as a form of automatic documentation, and also reminded me that each TestFixture should really deal with one set of state to be tested (within reason). If different features require notably different setup state, then they belong in different TestFixtures each with their own SetUp method. Having lots of test methods each with their own internal set up code is usually a code smell.

None of this discussion is really news to those who have read much about unit tests, but a distinction between what I have been reading and what I have been doing has been a matter of exactly how disciplined I was being. When you deliberately write your tests with the intention of generating documentation of behavior in various contexts, you just plain write better tests.

I applied this mostly-superficial change to my tokenizer tests, and now they read a whole lot better than before. Also, it didn’t take much to make a tool that transforms a TestFixture into some documentation, similar to what RSpec generates. I applied this technique (rather, turned the existing technique up to 11) to several fixtures, and noticed that it made a big difference for classes that had to deal with many states, or at least had more complex behaviors, than for those classes that don’t really do much yet. I also found that it was much easier to stick close to having one assertion per test method, although I deliberately don’t follow that rule 100% of the time.

For those who love word games, the distinction being made is between Test Driven Development and Behavior Driven Development, but all this really means is that from the very beginning of the xUnit libraries, it was misleading to even call them “tests”. Now that “tests” has been beaten into our heads, it’s kind of hard to avoid using the word, so making the new buzzword “BDD” isn’t entirely lame or pointless. It forces your mind to take notice long enough to hear the real lesson: it has always been about designing meaningful interfaces, rather than just verifying that an interface produces expected results. I still call them tests, and that doesn’t matter. What matters is that when I write them I can use a more appropriate point of view.

There’s more to RSpec than writing smaller fixtures with smaller tests and more appropriate setup methods. RSpec assertions are written a little differently, too, making them read more like English. For the .NET world, we’ll need to wait for C# 3 before we can do such nifty things as obj.ShouldBeNull().

Here’s the documentation generated for the Tokenizer and Parser classes. Each context line came from the Description property on a TestFixture attribute, and each specification line (the indented rules) came from turning “CamelCaseMethodName” into “camel case method name”.

An empty tokenizer
	- returns null upon consume attempts
	- returns false upon all peek attempts
	- returns false upon try consume
	- cannot consume expected tokens
	- cannot consume identifiers

A tokenizer with garbage
	- cannot consume

A single-integer tokenizer
	- consumes entire integer
	- returns true upon peek integer
	- returns false upon peek identifier
	- cannot consume identifiers

A tokenizer with white space
	- separates tokens by white space
	- can consume expected token
	- does nothing when try consume fails
	- advances when try consume succeeds
	- can peek with expected tokens

A tokenizer with operators
	- consumes operators greedily

A tokenizer with identifiers
	- distinguishes identifiers from other tokens

A tokenizer with parenthesized operators
	- treats parenthesized operators as identifiers

A tokenizer with keywords
	- can peek with expected tokens
	- distinguishes keywords from identifiers
	- cannot consume keyword as identifier

An expression parser
	- parses boolean literals
	- parses integer literals
	- parses parenthetical expression
	- demands balanced parentheses
	- parses function calls
	- parses unary negation
	- parses unary not
	- ranks parentheses before unary operators
	- ranks function calls before unary operators
	- parses multiplicative operations
	- treats binary operations as left associative
	- ranks unary before multiplicative operators
	- parses additive operations
	- ranks multiplicative before additive operators
	- parses relational operations
	- ranks additive before relational operators
	- parses equality operations
	- ranks relational before equality operators
	- parses logical and operations
	- ranks equality before logical and operator
	- parses logical or operations
	- ranks logical and before logical or operator
	- parses if expression
	- associates else with nearest preceding if
	- requires parentheses for if expression conditions
	- requires else part for if expressions
	- requires expression bodies inside if expressions

A statement parser
	- parses let statements only in compound statements
	- parses let statements only at start of compound statements
	- parses let statements in compound statements
	- parses return statements
	- parses yield statements
	- parses if statements
	- associates else with nearest preceding if
	- requires parentheses for if statement conditions
	- parses for statements
	- requires parentheses on for statement declaration
	- parses while statements
	- requires parentheses on while statement conditions
	- parses expression statements
	- cannot parse if expressions as expression statements

A function parser
	- allows expression as body with implied return
	- cannot redefine keywords
	- allows compound statement bodies
	- parses untyped arguments
	- parses typed arguments
	- parses multiple arguments
	- parses operator overloads

A program parser
	- parses zero or more functions
Filed under: TDD

Grammar 4: Statements & Function Calls

February 14th, 2007

This is part of a series of articles that started with Test-Driving a Compiler.

Grammar 3 introduced function definitions. Since then, I’ve introduced some unary operators, statements, and function call expressions. First, I added the unary-minus and unary-not operators with a higher priority than multiplication, using tests to confirm the basic parsing routines as well as the relative precedence compared to multiplication/division and parenthetical expressions:

MultiplicativeExpression := UnaryExpression (('*' / '/') UnaryExpression)*
UnaryExpression := ('-' / '!')? Value

Next, I introduced function calls. I expect to have higher-order functions, so function calls can be made against an identifier, as well as parenthetical expressions:

FunctionName(arg1, arg2);
(SomeExpressionThatIsCallable)(arg1, arg2);

To deal with the complexity of the grammar for these cases, I took advantage of the fact that PEG grammars have ordered sub-rules that allow backtracking, as I already discussed for Grammar 2:

Value := boolean-literal
       / integer-literal
       / '(' Expression ')' '(' ArgumentList ')'
       / '(' Expression ')'
       / identifier '(' ArgumentList ')'
       / identifier

Function definitions are already set up for a special shorthand: whenever the entire body is an expression whose value should be returned, curly-braces and an explicit ‘return’ can be omitted. Now, I added the more common scenario for multi-statement function bodies.

Function := identifier '(' ParameterList ')' &'{' CompoundStatement
          / identifier '(' ParameterList ')' Expression

Lastly, I introduced the usual statements available in imperative languages. Here’s the complete grammar to date:

Program := Function*
Function := identifier '(' ParameterList ')' &'{' CompoundStatement
          / identifier '(' ParameterList ')' Expression
ParameterList := &')' / Parameter (',' Parameter)*
Parameter := identifier identifier?

Statement := &'{' CompoundStatement
           / &'if' IfStatement
           / &'for' ForStatement
           / &'while' WhileStatement
           / &'return' ReturnStatement
           / &'yield' YieldStatement
           / identifier '=' Expression ';'
           / Expression ';'
CompoundStatement := '{' Statement* '}'
IfStatement := 'if' '(' Expression ')' Statement ('else' Statement)?
ForStatement := 'for' '(' identifier 'in' Expression ')' Statement
WhileStatement := 'while' '(' Expression ')' Statement
ReturnStatement := 'return' (Expression)? ';'
YieldStatement := 'yield' Expression ';'

Expression := IfExpression
IfExpression := 'if' '(' Expression ')' Expression 'else' Expression
              / OrExpression
OrExpression := AndExpression ('||' AndExpression)*
AndExpression := EqualityExpression ('&&' EqualityExpression)*
EqualityExpression := RelationalExpression (('==' / '!=') RelationalExpression)*
RelationalExpression := AdditiveExpression (('<' / '<=' / '>' / '>=') AdditiveExpression)*
AdditiveExpression := MultiplicativeExpression (('+' / '-') MultiplicativeExpression)*
MultiplicativeExpression := UnaryExpression (('*' / '/') UnaryExpression)*
UnaryExpression := ('-' / '!')? Value
Value := boolean-literal
       / integer-literal
       / '(' Expression ')' '(' ArgumentList ')'
       / '(' Expression ')'
       / identifier '(' ArgumentList ')'
       / identifier
ArgumentList := &')' / Expression (',' Expression)*

I’ve noticed a pattern while developing each of these changes to the grammar. Usually, when I decide on a single rule change or rule addition, that change suggests a change to the tokenizer, such as recognizing a new keyword or symbol. The new rule often suggests a new AST node type as well. So, I have been tending toward the following cycle:

  1. Write a test for the new tokens to be recognized.
  2. Update the tokenizer to pass the test (this has become a smaller and smaller task and is now trivial).
  3. Write a stub implemention of the new AST node type, as well as a test for the new class, asserting that ToString yields an expected S-Expression. The stub ToString returns null so that the test initially fails.
  4. Flesh out the AST node type to pass the test.
  5. Write a test for the Recursive Descent Parser class, for the new grammar rule.
  6. Change the RDP to match the grammar and pass the test.

For nontrivial rules, or rules that need to be composed with other rules, I’ll repeat the last two steps until I’m satisfied that all the relevant variations have been tested.

Filed under: TDD, Language Design

Big, Ugly, Naggingly Red 99s

February 11th, 2007

I’ve been using C# Express Edition so far to write my parser, and I’ve been running NUnit as a separate process. I haven’t been running NCover at all, so far. I like the idea of integrating NUnit and NCover into the IDE, but Express Edition doesn’t allow for plugins (as far as I know). This got me interested in SharpDevelop, which seems to have come a long way since the last time I tried it out. I got the latest version (2.1 RC 1), as well as the latest NCover (1.5.5), opened up the solution file that Express Edition had created, and everything just worked. Kudos to the SharpDevelop crew.

I ran the unit tests with code coverage from inside the IDE, and it turns out that I already have 100% code coverage. This was a pleasant surprise. There was one previous project where I used NUnit, but I only occasionally wrote tests first. In that project, reaching 80% coverage felt like a success, and I considered 90% to be a realistic goal. That was kind of silly of me; I was defining success as having confidence in only 4 out of every 5 lines of code I wrote.

On this project, reaching 100% coverage has been trivial. In fact, it’s been accidental. Since I never wrote a new line without first having a failing test, the chances of slipping in untested code would only come during the short refactoring step in each “Red, green, refactor” cycle. Since the refactoring steps are all about removing duplication, they mostly involve removing code rather than writing entirely new lines. There is always going to be a chance that a little untested code will slip through, despite taking things in these disciplined steps, but that chance is small and will bathe my screen with big, ugly, naggingly red 99s just moments after introducing the offending line of code. Over time, responding immediately to those red 99s will have a Pavlovian effect: every time I introduce untrusted code, I’ll feel the need to 1) immediately reach 100% again before proceeding and 2) crave me some Kibbles ‘n Bits.

Filed under: TDD

Grammar 1: Basic Operators

February 1st, 2007

This is part of a series of articles that started with Test-Driving a Compiler.

Grammar 0 was silly. We went to the trouble of creating a tokenizer and a recursive descent parser for a language where the only valid programs were integer constants. With that silliness out of the way, we can start doing things with these integers. Grammar 1 includes arithmetic and relational operators, as well as nesting expressions with parentheses. The precedence of operators will be as follows, where parentheses are the highest priority, and equality tests are lowest:

Operators Priority
( ) Grouping
* / Multiplicative
+ - Additive
< <= > >= Relational
== != Equality

Starting with Grammar 0, we'll grow this in pieces. Using the arithmetic grammar on this PEG wiki page as a guide, we introduce low-priority operators earlier within the grammar.

	//Grammar 0
	Expression := integer-literal

	//Introduce grouping operators.
	Expression := Value
	Value := integer-literal / '(' Expression ')'

	//Introduce multiplication and division.
	Expression := MultiplicativeExpression
	MultiplicativeExpression := Value (('*' / '/') Value)*
	Value := integer-literal / '(' Expression ')'

	//Introduce addition and subtraction.
	Expression := AdditiveExpression
	AdditiveExpression := MultiplicativeExpression (('+' / '-') MultiplicativeExpression)*
	MultiplicativeExpression := Value (('*' / '/') Value)*
	Value := integer-literal / '(' Expression ')'

	//Introduce comparisons.
	Expression := RelationalExpression
	RelationalExpression := AdditiveExpression (('<' / '<=' / '>' / '>=') AdditiveExpression)*
	AdditiveExpression := MultiplicativeExpression (('+' / '-') MultiplicativeExpression)*
	MultiplicativeExpression := Value (('*' / '/') Value)*
	Value := integer-literal / '(' Expression ')'

One last pair of operators to go, and we have the completed grammar:

	Grammar 1
	Expression := EqualityExpression
	EqualityExpression := RelationalExpression (('==' / '!=') RelationalExpression)*
	RelationalExpression := AdditiveExpression (('<' / '<=' / '>' / '>=') AdditiveExpression)*
	AdditiveExpression := MultiplicativeExpression (('+' / '-') MultiplicativeExpression)*
	MultiplicativeExpression := Value (('*' / '/') Value)*
	Value := integer-literal / '(' Expression ')'

Each change in the above sequence was driven by tests. I won't belabor the point by showing every test and every change in the parsing code as I did in the development of Grammar 0.

The tokenizer recognizes each operator as single token, even when an operator consists of 2 characters. The tokenizer had to be greedy, so that the input string "1<=2" is interpreted as 3 tokens ("1", "<=", "2") rather than 2 tokens followed by garbage ("1", "<", "=2").

The recursive descent parsing code very closely mimics the grammar. Each nonterminal in the grammar has a mutually-recursive method with the same name. The body of the method corresponds with the right-hand-side of that grammar rule. Notice that several of the grammar rules are nearly identical, and the BinaryOperation helper method embodies what those rules have in common (everything other than the particular operators to use and the name of the nonterminal method to call recursively). Here's the part of the code that matches the grammar:


	public sealed class Expression
	{
		private delegate AstNode NodeParser();

		private readonly AstNode _abstractSyntaxTree;
		private readonly Tokenizer _tokenizer;

		public Expression(Tokenizer tokenizer)
		{
			_tokenizer = tokenizer;
			_abstractSyntaxTree = ParseExpression();
		}

		private AstNode ParseExpression()
		{
			return EqualityExpression();
		}

		private AstNode EqualityExpression()
		{
			return BinaryOperation(ParseRelationalExpression, “==”, “!=”);
		}

		private AstNode ParseRelationalExpression()
		{
			return BinaryOperation(AdditiveExpression, “<”, “<=”, “>”, “>=”);
		}

		private AstNode AdditiveExpression()
		{
			return BinaryOperation(MultiplicativeExpression, “+”, “-”);
		}

		private AstNode MultiplicativeExpression()
		{
			return BinaryOperation(Value, “*”, “/”);
		}

		private AstNode Value()
		{
			string token = _tokenizer.Consume();

			if (token == “(”)
			{
				AstNode node = ParseExpression();
				_tokenizer.Consume(“)”);
				return node;
			}

			return new IntegerNode(token);
		}

		private AstNode BinaryOperation(NodeParser parseOperand, params string[] operators)
		{
			AstNode node = parseOperand();

			while (_tokenizer.Peek(operators))
				node = new OperatorNode(node, _tokenizer.Consume(), parseOperand());

			return node;
		}

		public override string ToString()
		{
			return _abstractSyntaxTree.ToString();
		}
	}

At this point, it helps to see the relationship between a sample input string, the tree of nodes produced by the recursion, and the S-Expression that is used as shorthand for that tree. For the input string "1 + 2 * 3 + 4 <= 12 / ( 2 + 1 )", we have the following tree and S-Expression:

Sample Abstract Syntax Tree

Armed with this tree, it's actually pretty easy to evaluate the expression. A pretty straight-forward recursive algorithm would work here.

//Psueodcode
Value(node):
	if node is a leaf:
		return node contents
	else:
		operator = function corresponding with node's token
		return operator(Value(node child A), Value(node child B))

Lastly, the code for the tokenizer started to get hard to read, even though it was passing all of the tests and contained no obvious duplication. There was a lot of string-index-twiddling as I walked along the input string, occasionally peeking at the next token and jumping back to where I was before, etc. Because I had high code coverage in my tests, I was able to confidently rewrite the private helpers, without having to change the tests or the public interface of the tokenizer. At each step in the rewrite, the tests still passed, so I knew that I (probably) didn’t break anything. Now, I use a series of prioritized regular expressions to recognize and extract each new token.

Filed under: TDD, Language Design
Next Page »

Powered by WordPress