This project is divided into 5 components:
- Lexer
- Parser
- Compiler
- Evaluator
- Interpreter
Given a string, the LambdaLexer class generates a list of tokens of type LambdaToken that the LambdaParser uses to construct an abstract syntax tree of type LambdaAST.
There are eight distinct tokens that the lexer recognizes:
-
DOT
The symbol separating a function args and function body.
-
LAMBDA
The symbol indicating the start of a function.
-
OPEN
The symbol indicating the opening parenthesis.
-
CLOSE
The symbol indicating the closing parenthesis.
-
LET
The symbol indicating the start of a let expression.
-
EQUAL
The symbol separating the key and value in a let expression.
-
IN
The symbol separating the value and the body in a let expression.
-
ID
The symbol indicating a variable.
Given a sequence of LambdaToken, the LambdaParser generates an abstract syntax tree of type LambdaAST that represents the lambda expression.
The grammar for the lambda expression is defined as:
<expr> ::= ID
| <expr>, <expr>
| OPEN, <expr>, CLOSE
| LAMBDA, ID+, DOT, <expr>
| LET, ID, EQUAL, <expr>, IN, <expr>
Because <expr> ::= <expr>, <expr> we have left recursion in the grammar and we need to eliminate it. Hence, we simplify it as follows:
<expr> ::= <simpleExpr>+
simpleExpr ::= ID
| OPEN, <expr>, CLOSE
| LAMBDA, ID+, DOT, <expr>
| LET, ID, EQUAL, <expr>, IN, <expr>
Now, <expr> doesn't have left recursion anymore because to match <expr>, we need to first match atleast one simpleExpr.
We also note that since, FApp is left associative, we get a list of <expr> and then reduce that list from left to right with FApp. I.e. <expr> = rep1(<simpleExpr>).reduce(FApp).
Additionally, we note that the let expression is redundant for the language as it can be simplified to an application expression. I.e. LET, x, EQUAL, y, IN, z can be parsed as FApp(Fun(x, z), y). However, we continue to parse the let expression as a Let node.
Given a string representing a lambda expression, the LambdaCompiler makes use of LambdaLexer and LambdaParser to generate an abstract syntax tree of type LambdaAST.
Given, a string representing a lambda expression, the LambdaEvaluator makes use of LambdaCompiler to obtain the abstract syntax tree of the lambda expression. It then evaluates the abstract syntax tree in a call-by-value evaluation order. It does so using alpha-renaming and beta-reduction.
LambdaEvaluator has two import methods, eval and substitute. eval does the beta-reduction and substitute does the alpha-renaming.
eval repeatedly makes use of substitute to reduce the abstract syntax tree according to the following rules:
- If the abstract syntax tree is of type
Fun(a function) orVar(a variable), then result is the same abstract syntax tree. I.e. do not beta-reduce the function body. - If the abstract syntax tree is of type
Let(a let expression), then rewrite the abstract syntax tree into aFApptype as follows:Let(x, y, z)is re-written asFApp(Fun(x, z), y). - If the abstract syntax tree is of type
FAppthen:- Evaluate the caller
- Evaluate the callee
- If the caller evaluates to a function (
Funtype), then substitute the callee into the body of the evaluated caller. - If the caller does not evaluate to a function, then return the abstract syntax tree representing the application of the evalued caller to the evaluated callee.
As, the above routine can be difficult to follow, here is the relevant code for eval:
def eval(ast: LambdaAST): LambdaAST = {
ast match {
case FApp(f, v) => {
val ef = eval(f)
val ev = eval(v)
ef match {
case Fun(arg, body) => eval(substitute(body, arg, ev))
case _ => FApp(ef, ev)
}
}
case Let(key, value, body) => {
eval(FApp(Fun(key, body), value))
}
case Var(_) => ast
case Fun(_, _) => ast
case _ => throw new Exception("Invalid expression.")
}
}
As mentioned above, eval makes use of substitute to do alpha-renaming. substitute takes three arguments ast, key and alue. It replaces all occurrences of key in ast with value using the following rules:
- If
astis a variable then:- If the variable has the same name as the key, return the
value. - If the variable has a different name as the key, return the variable instead.
- If the variable has the same name as the key, return the
- If
astis an application then, substitutekeywithvaluein both the caller and the caller of the application. - If
astis a function then: 3. If the argument of the function is the same as the key, don't substitute anything and return the function instead. 3. If the argument of the function is different from the key, substitute thekeyin the body of the function with thevalue.
Again, here's the code for substitute:
def substitute(ast: LambdaAST, key: String, value: LambdaAST): LambdaAST = {
ast match {
case Var(x) => {
if (x == key) value
else ast
}
case FApp(f, v) => {
val f_sub = substitute(f, key, value)
val v_sub = substitute(v, key, value)
FApp(f_sub, v_sub)
}
case Fun(arg, body) => {
if (arg == key) ast
else Fun(arg, substitute(body, key, value))
}
}
}The LambdaInterpreter is a REPL that makes use of LambdaEvaluator to evaluate the expressions read from the standard input.
LambdaInterpreter calls LambdaEvaluator on every line read from the standard input.