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

Formalities

March 19th, 2007

I’ve added a new article on the Formalities page. It’s a rewrite of some things I wrote shortly after college. It’s a tad long, don’t say I didn’t warn you.

Filed under: Uncategorized

Spaghetti Stack Attack!

March 10th, 2007

When I started writing Rook in January, I had a general idea of how things would work. I’d start with a grammar, then I’d write a recursive descent parser to turn the input source code into an abstract syntax tree. These steps aren’t trivial, but they’re also not as tough as they sound. It’s easy to find examples of these techniques.

A few weeks back I was armed with this totally awesome syntax tree, and didn’t quite know what to do with it next. I knew it was the right tool for the job, but I wasn’t 100% sure what that job was. The trouble is, this is the point where most of my sources just plain had nothing to say about the next step. What I wanted to be able to do was take a reference to the root expression in my syntax tree and ask it, “What is your value, after you evaluate your own subexpressions?”

This is a tricky question to answer, since some expressions introduce new scope, and some variables in one scope may “shadow” variables in the parent scope. A special data structure called a Spaghetti Stack helps us keep track of all these variations in scope. I’ll get to the specifics of spaghetti stacks in a moment, but the bottom line is that this data structure lets you keep track of which variables are in scope at any particular point in the input source code. My particular spaghetti stack also stores the runtime value associated with each identifier in each scope.

Note to self: Data structures named after food are more likely to be remembered. This fact has been demonstrated time and time again: see Spaghetti Stacks & Hash Tables. Commence work on Hamburger Queue immediately.

Spaghetti Stack

A spaghetti stack is really just a plain old tree structure, but there are two special details worth noting. First, all of the references point “up” toward the root rather than the other way around. Second, all of these references are immutable; once a node is created it will always point at the same parent, until it gets garbage collected. The nifty result of these restrictions is that from the point of view of any one node, that node is the top of a singly-linked stack. For instance, the colored nodes in the example do form a stack, with the “top” of the stack located at the bottom of the tree. Nodes have no sense of gravity, so it doesn’t matter that it’s upside down: it’s a stack. To sum up, we have a collection of stacks that are sharing nodes whenever they can, and every node thinks that it is at the top of the stack.

In this diagram, we see the state of a spaghetti stack after evaluation of a program has started. The root node represents the global namespace, and contains things like primitive operator definitions (Rook treats operators as functions, so the operator symbols are just identifiers). Node 1 represents a new scope that introduces three local variables, such as a function with 3 parameters. Node 3 represents a scope that shadows the “x” in the parent scope, but can see the parent scope’s “y” unhindered. Some expressions that can provide new items in the new scope don’t always need to, such as a function with zero parameters, and this case results in uninteresting nodes like node 6. All in all, this is just a tree representation of namespaces, and we probably already build these in our heads while we read code, so it isn’t really anything new.

In Rook, each node in the spaghetti stack is an instance of a class named Environment, which contains a name/object dictionary and a reference to the parent Environment. When lookup attempts fail, we defer to the parent before deciding whether the identifier exists:


public object this[string identifier]
{
	get
	{
		if (_locals.ContainsKey(identifier))
			return _locals[identifier];

		if (_parent != null)
			return _parent[identifier];

		throw new UndefinedIdentifierException(identifier);
	}

	set
	{
		if (_locals.ContainsKey(identifier))
			throw new DuplicateIdentifierException(identifier);

		_locals[identifier] = value;
	}
}

Each node in the accompanying abstract syntax tree is an Expression whose value depends on an Environment instance:

public interface Expression
{
	object Value(Environment environment);
}

To run a Rook program, we just pass the root environment to the Value method of the root expression. We let the Value method “push” a new Environment whenever the expression node introduces new scope. Simple expression types like integer and boolean literals just ignore the Environment, since they can determine their own values without it:


//IntegerLiteral Expressions
//Example: 1234
public override object Value(Environment environment)
{
	return _value;
}

For an Identifier expression, the value of the expression is the value found by looking up the identifier in the Environment’s dictionary:


//Identifier Expressions:
//Example: x
public override object Value(Environment environment)
{
	return environment[_identifier];
}

For If expressions, we also don’t need to introduce new scope. Rather, we just pass the given Environment along to each subexpression:


//If Expressions:
//Example: if (x>1) y else z
public override object Value(Environment environment)
{
	if ((bool)_condition.Value(environment))
		return _whenTrue.Value(environment);

	if (_whenFalse == null)
		return null;

	return _whenFalse.Value(environment);
}

Compound expressions introduce new scope, so they “push” a new Environment onto the stack, add the identifiers for each new variable binding, and then use that new Environment to evaluate subexpressions:


//Compound Expressions:
//Example: { let a = 5; def square(x) x*x; square(a); }
public override object Value(Environment environment)
{
	Environment withBindings = new Environment(environment);

	foreach (Binding binding in _bindings)
		withBindings[binding.Identifier] = binding.Value(withBindings);

	object result = null;

	foreach (Expression expression in _expressions)
		result = expression.Value(withBindings);

	return result;
}

We never explicitly pop from one of these stacks. Each implementation of Value may or may not push a new Environment onto a stack, and will use that Environment, but the caller never gains a direct reference to it. We let the garbage collector do our “pops” for us. In fact, there are cases where we will deliberately preserve some references to these otherwise-temporary Environments, preventing them from being garbage collected. This makes implementing closures painfully simple, but that is a discussion for another day:


//Lambda Expressions:
//Example: fn(x, y) x*y
public override object Value(Environment environment)
{
	return new Closure(this, environment);
}
Filed under: Language Design

(Re)Introducing Rook

March 5th, 2007

Sample Rook Session
For the past few weeks, I’ve been documenting my progress on the development of a new programming language. It is mostly an experiment to see whether or not I could do it, rather than an attempt to actually gain widespread use. Also, it’s giving me an opportunity to learn more about how functional languages work. In fact, I’m not even releasing the source code yet. I think it’ll be worth releasing after a few more key features are implemented. Still, I’ve made a lot of progress in the past week, and it’s worth presenting the first draft of the language’s documentation as well as some examples.

It’s called Rook. If Guido van Rossum can name a language after his favorite comedy troupe, I can name mine after my favorite member of the passerine order of birds.

In this post I’ll just dive right in with some examples. If something seems fishy, I’d recommend skimming the documentation linked to above. The bottom line is that it behaves a lot like a subset of Scheme, but its syntax is supposed to be more familiar to people who use C-derived languages. I’ve even got a little editor with an interactive REPL based loosely on DrScheme.

In the example screenshot, I start by defining a recursive function (factorial) as well as two mutually-recursive functions (even and odd). The ‘def’ expressions define functions. Rook is dynamically typed, so if we tried to pass a boolean value to the factorial function, it wouldn’t let you know something was wrong until it actually tried to evaluate the function body. Also, the identifiers inside method bodies aren’t checked until they are actually used, so we can define even(n) before odd(n) actually exists, and all is well. By being lazy about these checks, recursion works with no need for special-case code in the interpreter.

Next, the screenshot shows a function named apply, which takes in a function and two additional arguments, and calls that function with those arguments. first(a,b) and second(a,b) return the respective argument, ignoring the other one. In the REPL, you can see that apply(first, 1, 2) evaluates to 1 and apply(second, 1, 2) evaluates to 2. Also, note that we can treat operators as identifiers, passing in the plus operator so that apply((+), 1, 2) evaluates to 3.

The more exhaustive documentation linked to above goes into more about scope, nested scope, local variables, closures, and the like, but more examples won’t be very interesting until I have support for some collection data types like linked lists.

Filed under: 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
Next Page »

Powered by WordPress