Skip to content

Commit

Permalink
3.10 Evaluation (functions and call expressions)
Browse files Browse the repository at this point in the history
Implements:
* Environment to keep track of value by associating them with a name
* Closure
* Higher-order functions
* Functions as first-class citizens
  • Loading branch information
cedrickchee committed Apr 1, 2020
1 parent 4a9d450 commit 7ae0c37
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 2 deletions.
91 changes: 91 additions & 0 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,30 @@ func Eval(node ast.Node, env *object.Environment) object.Object {

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

case *ast.FunctionLiteral:
// We just reuse the Parameters and Body fields of the AST node.
params := node.Parameters
body := node.Body
return &object.Function{Parameters: params, Env: env, Body: body}

case *ast.CallExpression:
// Using Eval to get the function we want to call.
// Whether that's an *ast.Identifier or an *ast.FunctionLiteral: Eval
// returns an *object.Function.
function := Eval(node.Function, env)
if isError(function) {
return function
}

// Evaluate the arguments of a call expression.
args := evalExpressions(node.Arguments, env)
if len(args) == 1 && isError(args[0]) {
return args[0]
}

// Call the function. Apply the function to the arguments.
return applyFunction(function, args)
}

return nil
Expand Down Expand Up @@ -327,3 +351,70 @@ func isError(obj object.Object) bool {
}
return false
}

func evalExpressions(
exps []ast.Expression,
env *object.Environment,
) []object.Object {
// Evaluating the arguments is nothing more than evaluating a list of
// expressions and keeping track of the produced values. But we also
// have to stop the evaluation process as soon as it encounters an
// error.

var result []object.Object

// This part is where we decided to evaluate the arguments from
// left-to-right.
for _, e := range exps {
// Evaluate ast.Expression in the context of the current environment.
evaluated := Eval(e, env)
if isError(evaluated) {
return []object.Object{evaluated}
}
result = append(result, evaluated)
}

return result
}

func applyFunction(fn object.Object, args []object.Object) object.Object {
// Convert the fn parameter to a *object.Function reference.
function, ok := fn.(*object.Function)
if !ok {
return newError("not a function: %s", fn.Type())
}

extendedEnv := extendFunctionEnv(function, args)
evaluated := Eval(function.Body, extendedEnv)
return unwrapReturnValue(evaluated)
}

func extendFunctionEnv(
fn *object.Function,
args []object.Object,
) *object.Environment {
// Creates a new *object.Environment that's enclosed by the function's
// environment.
env := object.NewEnclosedEnvironment(fn.Env)

for paramIdx, param := range fn.Parameters {
// In this new, enclosed environment, binds the arguments of the
// function call to the function's parameter names.
env.Set(param.Value, args[paramIdx])
}

return env
}

func unwrapReturnValue(obj object.Object) object.Object {
// The result of the evaluation (obj) is unwrapped if it's an
// *object.ReturnValue. That’s necessary, because otherwise a return s
// tatement would bubble up through several functions and stop the
// evaluation in all of them. But we only want to stop the evaluation of the
// last called function's body.
if returnValue, ok := obj.(*object.ReturnValue); ok {
return returnValue.Value
}

return obj
}
83 changes: 83 additions & 0 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,25 @@ if (10 > 1) {
`,
10,
},
{
`
let f = fn(x) {
return x;
x + 10;
};
f(10);`,
10,
},
{
`
let f = fn(x) {
let result = x + 10;
return result;
return 10;
};
f(10);`,
20,
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -248,6 +267,70 @@ func TestLetStatements(t *testing.T) {
}
}

func TestFunctionObject(t *testing.T) {
// This test function asserts that evaluating a function literal results in
// the correct *object.Function being returned, with correct parameters and
// the correct body. The function's environment will be tested in other
// tests.
input := "fn(x) { x + 2; };"

evaluated := testEval(input)
fn, ok := evaluated.(*object.Function)
if !ok {
t.Fatalf("object is not Function. got=%T (%+v)", evaluated, evaluated)
}

if len(fn.Parameters) != 1 {
t.Fatalf("function has wrong parameters. Parameters=%+v",
fn.Parameters)
}

if fn.Parameters[0].String() != "x" {
t.Fatalf("parameter is not 'x'. got=%q", fn.Parameters[0])
}

expectedBody := "(x + 2)"

if fn.Body.String() != expectedBody {
t.Fatalf("body is not %q. got=%q", expectedBody, fn.Body.String())
}
}

func TestFunctionApplication(t *testing.T) {
// Each test case here does the same thing: define a function, apply it to
// arguments and then make an assertion about the produced value. But with
// their slight differences they test multiple important things: returning
// values implicitly, returning values using return statements, using
// parameters in expressions, multiple parameters and evaluating arguments
// before passing them to the function.
tests := []struct {
input string
expected int64
}{
{"let identity = fn(x) { x; }; identity(5);", 5},
{"let identity = fn(x) { return x; }; identity(5);", 5},
{"let double = fn(x) { x * 2; }; double(5);", 10},
{"let add = fn(x, y) { x + y; }; add(5, 5);", 10},
{"let add = fn(x, y) { x + y; }; add(5 + 5, add(5, 5));", 20},
{"fn(x) { x; }(5)", 5},
}

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

func TestClosures(t *testing.T) {
input := `
let newAdder = fn(x) {
fn(y) { x + y };
};
let addTwo = newAdder(2);
addTwo(2);`

testIntegerObject(t, testEval(input), 4)
}

func testEval(input string) object.Object {
l := lexer.New(input)
p := parser.New(l)
Expand Down
17 changes: 16 additions & 1 deletion object/environment.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
package object

// NewEnclosedEnvironment returns a new Environment with the outer set to the
// current environment (enclosing environment).
func NewEnclosedEnvironment(outer *Environment) *Environment {
env := NewEnvironment()
env.outer = outer
return env
}

// 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}
return &Environment{store: s, outer: nil}
}

// 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
// outer is a reference to another Environment, which is the enclosing
// environment, the one it’s extending.
outer *Environment
}

// Get returns the object bound by name.
func (e *Environment) Get(name string) (Object, bool) {
obj, ok := e.store[name]
if !ok && e.outer != nil {
// Check the enclosing environment for the given name.
obj, ok = e.outer.Get(name)
}
return obj, ok
}

Expand Down
41 changes: 40 additions & 1 deletion object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ package object
// to both represent values as the evaluator encounters and constructs them as
// well as how the user interacts with values.

import "fmt"
import (
"bytes"
"fmt"
"strings"

"github.com/cedrickchee/hou/ast"
)

const (
// INTEGER_OBJ is the Integer object type.
Expand All @@ -21,6 +27,9 @@ const (

// ERROR_OBJ is the Error object type.
ERROR_OBJ = "ERROR"

// FUNCTION_OBJ is the Function object type.
FUNCTION_OBJ = "FUNCTION"
)

// ObjectType represents the type of an object.
Expand Down Expand Up @@ -101,3 +110,33 @@ func (e *Error) Type() ObjectType { return ERROR_OBJ }

// Inspect returns a stringified version of the object for debugging.
func (e *Error) Inspect() string { return "ERROR:" + e.Message }

// Function is the function type that holds the function's formal parameters,
// body and an environment to support closures.
type Function struct {
Parameters []*ast.Identifier
Body *ast.BlockStatement
Env *Environment
}

// Type returns the type of the object.
func (f *Function) Type() ObjectType { return FUNCTION_OBJ }

// Inspect returns a stringified version of the object for debugging.
func (f *Function) Inspect() string {
var out bytes.Buffer

params := []string{}
for _, p := range f.Parameters {
params = append(params, p.String())
}

out.WriteString("fn")
out.WriteString("(")
out.WriteString(strings.Join(params, ", "))
out.WriteString(") {\n")
out.WriteString(f.Body.String())
out.WriteString("\n}")

return out.String()
}

0 comments on commit 7ae0c37

Please sign in to comment.