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 aFApp
type as follows:Let(x, y, z)
is re-written asFApp(Fun(x, z), y)
. - If the abstract syntax tree is of type
FApp
then:- Evaluate the caller
- Evaluate the callee
- If the caller evaluates to a function (
Fun
type), 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
ast
is 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
ast
is an application then, substitutekey
withvalue
in both the caller and the caller of the application. - If
ast
is 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 thekey
in 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.