diff --git a/monkey/README.md b/monkey/README.md index a2a01a1..25ba00e 100644 --- a/monkey/README.md +++ b/monkey/README.md @@ -109,6 +109,9 @@ is represented as: * `REPL` - `Read-Eval-Print Loop` * sometimes - 'console', 'interactive mode', etc. +### built-in functions +* design choice - should they be evalueated in top-level environment or in their own environment? (`object.Environment`) + ### compiler * goal is turning `source code` into `bytecode` and then use VM to execute it diff --git a/monkey/interpreter/evaluator/builtins.go b/monkey/interpreter/evaluator/builtins.go new file mode 100644 index 0000000..3c5255c --- /dev/null +++ b/monkey/interpreter/evaluator/builtins.go @@ -0,0 +1,22 @@ +package evaluator + +import ( + "monkey/interpreter/object" +) + +var builtins = map[string]*object.Builtin{ + "len": &object.Builtin{ + Fn: func(args ...object.Object) object.Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", len(args)) + } + + switch arg := args[0].(type) { + case *object.String: + return &object.Integer{Value: int64(len(arg.Value))} + default: + return newError("argument to `len` not supported, got %s", args[0].Type()) + } + }, + }, +} diff --git a/monkey/interpreter/evaluator/evaluator.go b/monkey/interpreter/evaluator/evaluator.go index e6b80ad..08f4107 100644 --- a/monkey/interpreter/evaluator/evaluator.go +++ b/monkey/interpreter/evaluator/evaluator.go @@ -96,14 +96,19 @@ func Eval(node ast.Node, env *object.Environment) object.Object { } func applyFunction(fn object.Object, args []object.Object) object.Object { - function, ok := fn.(*object.Function) - if !ok { + switch fn := fn.(type) { + + case *object.Function: + extendedEnv := extendFunctionEnv(fn, args) + evaluated := Eval(fn.Body, extendedEnv) + return unwrapReturnValue(evaluated) + + case *object.Builtin: + return fn.Fn(args...) + + default: return newError("not a function: %s", fn.Type()) } - - extendedEnv := extendFunctionEnv(function, args) - evaluated := Eval(function.Body, extendedEnv) - return unwrapReturnValue(evaluated) } func extendFunctionEnv( @@ -178,13 +183,19 @@ func evalBlockStatement(block *ast.BlockStatement, env *object.Environment) obje return result } -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) +func evalIdentifier( + node *ast.Identifier, + env *object.Environment, +) object.Object { + if val, ok := env.Get(node.Value); ok { + return val + } + + if builtins, ok := builtins[node.Value]; ok { + return builtins } - return val + return newError("identifier not found: " + node.Value) } func nativeBooltoBooleanObject(input bool) *object.Boolean { @@ -237,6 +248,8 @@ func evalInfixExpression( // so we can compare them directly (checking for identity) // this assumption does not hold for other types, because creating integer object allocates new memory // so we need to compare their values (otherwise 5 == 5 would yield false) + case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ: + return evalStringInfixExpression(operator, left, right) case operator == "==": return nativeBooltoBooleanObject(left == right) case operator == "!=": @@ -277,6 +290,19 @@ func evalIntegerInfixExpression( } } +func evalStringInfixExpression(operator string, left, right object.Object) object.Object { + leftVal := left.(*object.String).Value + rightVal := right.(*object.String).Value + + switch operator { + case "+": + return &object.String{Value: leftVal + rightVal} + // TODO: maybe add more string operations (like comparison) + default: + return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type()) + } +} + func evalIfExpression(ie *ast.IfExpression, env *object.Environment) object.Object { condition := Eval(ie.Condition, env) if isError(condition) { diff --git a/monkey/interpreter/evaluator/evaluator_test.go b/monkey/interpreter/evaluator/evaluator_test.go index 1a49656..d26d506 100644 --- a/monkey/interpreter/evaluator/evaluator_test.go +++ b/monkey/interpreter/evaluator/evaluator_test.go @@ -184,6 +184,10 @@ func TestErrorHandling(t *testing.T) { "foobar", "identifier not found: foobar", }, + { + `"Hello" - "World"`, + "unknown operator: STRING - STRING", + }, { ` if (10 > 1) { @@ -273,6 +277,49 @@ func TestClosures(t *testing.T) { testIntegerObject(t, testEval(input), 4) } +func TestStringConcatenation(t *testing.T) { + input := `"Hello" + " " + "World!"` + evaluated := testEval(input) + str, ok := evaluated.(*object.String) + if !ok { + t.Fatalf("object is not String. got=%T (%+v)", evaluated, evaluated) + } + if str.Value != "Hello World!" { + t.Errorf("String has wrong value. got=%q", str.Value) + } +} + +func TestBuiltinFunctions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + {`len("")`, 0}, + {`len("four")`, 4}, + {`len("hello world")`, 11}, + {`len(1)`, "argument to `len` not supported, got INTEGER"}, + {`len("one", "two")`, "wrong number of arguments. got=2, want=1"}, + } + for _, tt := range tests { + evaluated := testEval(tt.input) + switch expected := tt.expected.(type) { + case int: + testIntegerObject(t, evaluated, int64(expected)) + case string: + errObj, ok := evaluated.(*object.Error) + if !ok { + t.Errorf("object is not Error. got=%T (%+v)", + evaluated, evaluated) + continue + } + if errObj.Message != expected { + t.Errorf("wrong error message. expected=%q, got=%q", + expected, errObj.Message) + } + } + } +} + func testEval(input string) object.Object { l := lexer.New(input) p := parser.New(l) diff --git a/monkey/interpreter/object/object.go b/monkey/interpreter/object/object.go index c9215f6..a54a707 100644 --- a/monkey/interpreter/object/object.go +++ b/monkey/interpreter/object/object.go @@ -9,6 +9,8 @@ import ( type ObjectType string +type BuiltinFunction func(args ...Object) Object + const ( INTEGER_OBJ = "INTEGER" BOOLEAN_OBJ = "BOOLEAN" @@ -17,6 +19,7 @@ const ( ERROR_OBJ = "ERROR" FUNCTION_OBJ = "FUNCTION" STRING_OBJ = "STRING" + BUILTIN_OBJ = "BUILTIN" ) type Object interface { @@ -88,3 +91,10 @@ func (f *Function) Inspect() string { return out.String() } + +type Builtin struct { + Fn BuiltinFunction +} + +func (b *Builtin) Inspect() string { return "builtin function" } +func (b *Builtin) Type() ObjectType { return BUILTIN_OBJ }