One of the first things I tried to get working on my simple programming language was being able to define functions. Initially I started just with global functions, and then with local (i.e. nested) functions.
For the most part, this is easy in the DLR. You create a new function by creating a CodeBlock, which has a bunch of properties and methods such as the function name, the local variables it has, any parameters it takes and what the type of the return value is. It also has a Body, which is an Expression (normally a Block expression which contains a list of expressions). You create an empty CodeBlock using Ast.CodeBlock() and then you can fill in the rest using its properties.
Currently, a CodeBlock isn't directly part of the AST itself, that is, it isn't directly a node in the tree. Instead, you build a CodeBlockExpressions out of it (using Ast.CodeBlockExpression(), naturally) and use that as a reference to your function. In fact, at runtime, a CodeBlockExpression eventually becomes just that: a delegate instance.
My initial function implementation was extremely simplistic, meaning that functions weren't really handled using any language-specific object (like [Iron]Ruby's Proc or ToyScript's ToyFunction classes). Instead, I was directly storing CodeBlockExpressions in variables and using them instead. also didn't implementing function calls initially.
To test that my function definitions were working, I instead opted for simply returning the CodeBlockExpression directly. My tests (built using NUnit) make use of the ScriptEngine.Execute() method to parse and evaluate a script and return the result (my language has implicit returns, so all expressions evaluate to something). So what comes out when I evaluated a variable containing a CodeBlockExpression? Yep, a delegate, which I could call directly from the outside to validate it was working!
It's worth saying that the delegate you get directly from such a simple construct might have a bit of an unexpected syntax: The first parameter will always be of type CodeContext, after which the actual parameters you defined in the CodeBlock will appear (many times it's safe to simply pass null for this).
One of the things that I ran into while implementing this was getting variable definitions right. Creating and defining variables on the DLR isn't really hard:
- Global variables are somewhat supported (though at least from some comments in the source this might go away. What's not very obvious is that global variables don't need to be "defined" anywhere; they are just names. You manipulate them using Ast.Read(), Ast.Write() and Ast.Assign(), using the variants that take a SymbolId as argument.
- Local Variables are associated with a specific CodeBlock, so they actually do need to be defined, which is done using the CodeBlock.CreateLocalVariable() method.
- Parameters are much like local variables, but you create them using CodeBlock.CreateParameter() instead.
Obviously, keeping track of variable definitions and their scope is your responsibility. For local and parameter variables, you need to keep track of the Variable objects returned by CreateLocalVariable()/CreateParameter(), since that's how you reference the variable when reading/writing to it using Ast.Read()/ReadDefined()/Write()/Assign() and friends.
If you somehow set up things incorrectly, then some debug asserts in the DLR might get triggered at runtime when you try to use the variables (surprisingly enough things still seemed to work after ignoring the asserts, but that was probably a fluke).