Skip to content

Commit

Permalink
3.9 Evaluation (bindings and environment)
Browse files Browse the repository at this point in the history
* Add bindings to interpreter by adding support for `let` statements
* Implement environment to keep track of value by associating them
with a name
  • Loading branch information
cedrickchee committed Mar 31, 2020
1 parent fb7f562 commit 4a9d450
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 19 deletions.
65 changes: 48 additions & 17 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,44 @@ var (
)

// Eval evaluates the node and returns an object.
func Eval(node ast.Node) object.Object {
func Eval(node ast.Node, env *object.Environment) object.Object {
// Traverse the AST by starting at the top of the tree, receiving an
// *ast.Program, and then traverse every node in it.
// Use object.Environment and keep track of the environment by passing it
// around.

switch node := node.(type) {

// Statements
case *ast.Program:
// Traverse the tree and evaluate every statement of the *ast.Program.
return evalProgram(node)
return evalProgram(node, env)

case *ast.BlockStatement:
return evalBlockStatement(node)
return evalBlockStatement(node, env)

case *ast.ExpressionStatement:
// If the statement is an *ast.ExpressionStatement we evaluate its
// expression. An expression statement (not a return statement and not
// a let statement).
return Eval(node.Expression)
return Eval(node.Expression, env)

case *ast.ReturnStatement:
// Evaluate the expression associated with the return statement.
val := Eval(node.ReturnValue)
val := Eval(node.ReturnValue, env)
if isError(val) {
return val
}
return &object.ReturnValue{Value: val}

case *ast.LetStatement:
val := Eval(node.Value, env)
if isError(val) {
return val
}
// Keep track of values using Environment.
env.Set(node.Name.Value, val)

// Expressions
case *ast.IntegerLiteral:
return &object.Integer{Value: node.Value}
Expand All @@ -64,41 +74,44 @@ func Eval(node ast.Node) object.Object {
case *ast.PrefixExpression:
// The first step is to evaluate its operand and then use the result of
// this evaluation with the operator.
right := Eval(node.Right)
right := Eval(node.Right, env)
if isError(right) {
return right
}
return evalPrefixExpression(node.Operator, right)

case *ast.InfixExpression:
left := Eval(node.Left)
left := Eval(node.Left, env)
if isError(left) {
return left
}

right := Eval(node.Right)
right := Eval(node.Right, env)
if isError(right) {
return right
}

return evalInfixExpression(node.Operator, left, right)

case *ast.IfExpression:
return evalIfExpression(node)
return evalIfExpression(node, env)

case *ast.Identifier:
return evalIdentifier(node, env)
}

return nil
}

func evalProgram(program *ast.Program) object.Object {
func evalProgram(program *ast.Program, env *object.Environment) object.Object {
// evalProgram was renamed from evalStatements and make less generic because
// we can’t reuse evalStatements function for evaluating block statements.
// We are using evalBlockStatement for evaluating block statements.

var result object.Object

for _, statement := range program.Statements {
result = Eval(statement)
result = Eval(statement, env)

switch result := result.(type) {
case *object.ReturnValue:
Expand All @@ -114,13 +127,16 @@ func evalProgram(program *ast.Program) object.Object {
return result
}

func evalBlockStatement(block *ast.BlockStatement) object.Object {
func evalBlockStatement(
block *ast.BlockStatement,
env *object.Environment,
) object.Object {
// Evaluate an *ast.BlockStatement.

var result object.Object

for _, statement := range block.Statements {
result = Eval(statement)
result = Eval(statement, env)

// Here we explicitly don't unwrap the return value and only check the
// Type() of each evaluation result. If it's object.RETURN_VALUE_OBJ we
Expand Down Expand Up @@ -251,23 +267,38 @@ func evalIntegerInfixExpression(
}
}

func evalIfExpression(ie *ast.IfExpression) object.Object {
func evalIfExpression(
ie *ast.IfExpression,
env *object.Environment,
) object.Object {
// Deciding what to evaluate.

condition := Eval(ie.Condition)
condition := Eval(ie.Condition, env)
if isError(condition) {
return condition
}

if isTruthy(condition) {
return Eval(ie.Consequence)
return Eval(ie.Consequence, env)
} else if ie.Alternative != nil {
return Eval(ie.Alternative)
return Eval(ie.Alternative, env)
} else {
return NULL
}
}

func evalIdentifier(
node *ast.Identifier,
env *object.Environment,
) object.Object {
val, ok := env.Get(node.Value)
if !ok {
return newError("identifier not found: " + node.Value)
}

return val
}

func isTruthy(obj object.Object) bool {
switch obj {
case NULL:
Expand Down
34 changes: 33 additions & 1 deletion evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ if (10 > 1) {
`,
"unknown operator: BOOLEAN + BOOLEAN",
},
{
// Test to make sure that we get an error when we try to evaluate an
// unbound identifier.
"foobar",
"identifier not found: foobar",
},
}

for _, tt := range tests {
Expand All @@ -222,13 +228,39 @@ if (10 > 1) {
}
}

func TestLetStatements(t *testing.T) {
// The test cases assert that these two things should work: evaluating the
// value-producing expression in a let statement and evaluating an
// identifier that's bound to a name. But we also need tests to make sure
// that we get an error when we try to evaluate an unbound identifier.
tests := []struct {
input string
expected int64
}{
{"let a = 5; a;", 5},
{"let a = 5 * 5; a;", 25},
{"let a = 5; let b = a; b;", 5},
{"let a = 5; let b = a; let c = a + b + 5; c;", 15},
}

for _, tt := range tests {
testIntegerObject(t, testEval(tt.input), tt.expected)
}
}

func testEval(input string) object.Object {
l := lexer.New(input)
p := parser.New(l)
program := p.ParseProgram()

// We don’t want to keep state around for each test function and each test
// case. Each call to testEval should have a fresh environment so we don't
// run into weird bugs involving global state caused by the order in which
// tests are run.
env := object.NewEnvironment()

// The heart of the test is the call to Eval.
return Eval(program)
return Eval(program, env)
}

func testIntegerObject(t *testing.T, obj object.Object, expected int64) bool {
Expand Down
27 changes: 27 additions & 0 deletions object/environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package object

// NewEnvironment constructs a new Environment object to hold bindings of
// identifiers to their names.
func NewEnvironment() *Environment {
s := make(map[string]Object)
return &Environment{store: s}
}

// Environment is what we use to keep track of value by associating them with a
// name. Technically, it's an object that holds a mapping of names to bound
// objets.
type Environment struct {
store map[string]Object
}

// Get returns the object bound by name.
func (e *Environment) Get(name string) (Object, bool) {
obj, ok := e.store[name]
return obj, ok
}

// Set stores the object with the given name.
func (e *Environment) Set(name string, val Object) Object {
e.store[name] = val
return val
}
4 changes: 3 additions & 1 deletion repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/cedrickchee/hou/evaluator"
"github.com/cedrickchee/hou/lexer"
"github.com/cedrickchee/hou/object"
"github.com/cedrickchee/hou/parser"
)

Expand All @@ -34,6 +35,7 @@ const MONKEYFACE = ` __,__
// Start starts the REPL in a continuous loop.
func Start(in io.Reader, out io.Writer) {
scanner := bufio.NewScanner(in)
env := object.NewEnvironment()

for {
fmt.Printf(PROMPT)
Expand All @@ -55,7 +57,7 @@ func Start(in io.Reader, out io.Writer) {
continue
}

evaluated := evaluator.Eval(program)
evaluated := evaluator.Eval(program, env)
if evaluated != nil {
// Print string representation of the object to stdout.
io.WriteString(out, evaluated.Inspect())
Expand Down

0 comments on commit 4a9d450

Please sign in to comment.