diff --git a/code/code.go b/code/code.go index 8cea249..b199e5e 100644 --- a/code/code.go +++ b/code/code.go @@ -37,6 +37,7 @@ const ( OpReturn OpGetLocal OpSetLocal + OpGetBuiltin ) type Definition struct { @@ -71,6 +72,7 @@ var definitions = map[Opcode]*Definition{ OpReturn: {"OpReturn", []int{}}, OpGetLocal: {"OpGetLocal", []int{1}}, OpSetLocal: {"OpSetLocal", []int{1}}, + OpGetBuiltin: {"OpGetBuiltin", []int{1}}, } func Lookup(op byte) (*Definition, error) { diff --git a/compiler/compiler.go b/compiler/compiler.go index 1ceb5a6..a489cc4 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -34,9 +34,16 @@ func New() *Compiler { lastInstruction: EmittedInstruction{}, previousInstruction: EmittedInstruction{}, } + + symbolTable := NewSymbolTable() + + for i, v := range object.Builtins { + symbolTable.DefineBuiltin(i, v.Name) + } + return &Compiler{ constants: []object.Object{}, - symbolTable: NewSymbolTable(), + symbolTable: symbolTable, scopes: []CompilationScope{mainScope}, scopeIndex: 0, } @@ -207,11 +214,7 @@ func (c *Compiler) Compile(node ast.Node) error { return fmt.Errorf("undefined varible %s", node.Value) } - if symbol.Scope == GlobalScope { - c.emit(code.OpGetGlobal, symbol.Index) - } else { - c.emit(code.OpGetLocal, symbol.Index) - } + c.loadSymbol(symbol) case *ast.StringLiteral: str := &object.String{Value: node.Value} @@ -425,3 +428,14 @@ type Bytecode struct { Instructions code.Instructions Constants []object.Object } + +func (c *Compiler) loadSymbol(s Symbol) { + switch s.Scope { + case GlobalScope: + c.emit(code.OpGetGlobal, s.Index) + case LocalScope: + c.emit(code.OpGetLocal, s.Index) + case BuiltinScope: + c.emit(code.OpGetBuiltin, s.Index) + } +} diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 3ea3bbc..35d0da8 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -832,3 +832,43 @@ func testStringObject(expected string, actual object.Object) error { return nil } + +func TestBuiltins(t *testing.T) { + tests := []compilerTestCase{ + { + input: ` + len([]); + push([], 1); + `, + expectedConstants: []interface{}{1}, + expectedInstructions: []code.Instructions{ + code.Make(code.OpGetBuiltin, 0), + code.Make(code.OpArray, 0), + code.Make(code.OpCall, 1), + code.Make(code.OpPop), + code.Make(code.OpGetBuiltin, 5), + code.Make(code.OpArray, 0), + code.Make(code.OpConstant, 0), + code.Make(code.OpCall, 2), + code.Make(code.OpPop), + }, + }, + { + input: `fn() { len([]) }`, + expectedConstants: []interface{}{ + []code.Instructions{ + code.Make(code.OpGetBuiltin, 0), + code.Make(code.OpArray, 0), + code.Make(code.OpCall, 1), + code.Make(code.OpReturnValue), + }, + }, + expectedInstructions: []code.Instructions{ + code.Make(code.OpConstant, 0), + code.Make(code.OpPop), + }, + }, + } + + runCompilerTests(t, tests) +} diff --git a/compiler/symbol_table.go b/compiler/symbol_table.go index 1835e78..11d45af 100644 --- a/compiler/symbol_table.go +++ b/compiler/symbol_table.go @@ -3,8 +3,9 @@ package compiler type SymbolScope string const ( - LocalScope SymbolScope = "LOCAL" - GlobalScope SymbolScope = "GLOBAL" + LocalScope SymbolScope = "LOCAL" + GlobalScope SymbolScope = "GLOBAL" + BuiltinScope SymbolScope = "BUILTIN" ) type Symbol struct { @@ -53,3 +54,9 @@ func (s *SymbolTable) Resolve(name string) (Symbol, bool) { return obj, ok } + +func (s *SymbolTable) DefineBuiltin(index int, name string) Symbol { + symbol := Symbol{Name: name, Index: index, Scope: BuiltinScope} + s.store[name] = symbol + return symbol +} diff --git a/compiler/symbol_table_test.go b/compiler/symbol_table_test.go index bca4de2..6b7f74a 100644 --- a/compiler/symbol_table_test.go +++ b/compiler/symbol_table_test.go @@ -166,3 +166,34 @@ func TestResolveNestedLocal(t *testing.T) { } } } + +func TestDefineResolveBuiltins(t *testing.T) { + global := NewSymbolTable() + firstLocal := NewEnclosedSymbolTable(global) + secondLocal := NewEnclosedSymbolTable(firstLocal) + + expected := []Symbol{ + Symbol{Name: "a", Scope: BuiltinScope, Index: 0}, + Symbol{Name: "c", Scope: BuiltinScope, Index: 1}, + Symbol{Name: "e", Scope: BuiltinScope, Index: 2}, + Symbol{Name: "f", Scope: BuiltinScope, Index: 3}, + } + + for i, v := range expected { + global.DefineBuiltin(i, v.Name) + } + + for _, table := range []*SymbolTable{global, firstLocal, secondLocal} { + for _, sym := range expected { + result, ok := table.Resolve(sym.Name) + if !ok { + t.Errorf("name %s not resolvable", sym.Name) + continue + } + if result != sym { + t.Errorf("expected %s to resolve to %+v, got=%+v", + sym.Name, sym, result) + } + } + } +} diff --git a/evaluator/builtins.go b/evaluator/builtins.go index 065e210..656cf76 100644 --- a/evaluator/builtins.go +++ b/evaluator/builtins.go @@ -1,119 +1,14 @@ package evaluator import ( - "fmt" "monkey/object" - "unicode/utf8" ) 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.Array: - return &object.Integer{Value: int64(len(arg.Elements))} - case *object.String: - return &object.Integer{Value: int64(utf8.RuneCountInString(arg.Value))} - default: - return newError("argument to `len` not supported, got %s", args[0].Type()) - - } - }, - }, - - "first": &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)) - } - if args[0].Type() != object.ARRAY_OBJ { - return newError("argument to `first` must be ARRAY, got %s", args[0].Type()) - } - - arr := args[0].(*object.Array) - if len(arr.Elements) > 0 { - return arr.Elements[0] - } - - return NULL - }, - }, - - "last": &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)) - } - if args[0].Type() != object.ARRAY_OBJ { - return newError("argument to `last` must be ARRAY, got %s", args[0].Type()) - } - - arr := args[0].(*object.Array) - length := len(arr.Elements) - if length > 0 { - return arr.Elements[length-1] - } - - return NULL - }, - }, - - "rest": &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)) - } - if args[0].Type() != object.ARRAY_OBJ { - return newError("argument to `rest` must be ARRAY, got %s", - args[0].Type()) - } - - arr := args[0].(*object.Array) - length := len(arr.Elements) - if length > 0 { - newElements := make([]object.Object, length-1, length-1) - copy(newElements, arr.Elements[1:length]) - return &object.Array{Elements: newElements} - } - - return NULL - }, - }, - - "push": &object.Builtin{ - Fn: func(args ...object.Object) object.Object { - if len(args) != 2 { - return newError("wrong number of arguments. got=%d, want=2", - len(args)) - } - if args[0].Type() != object.ARRAY_OBJ { - return newError("argument to `push` must be ARRAY, got %s", - args[0].Type()) - } - - arr := args[0].(*object.Array) - length := len(arr.Elements) - - newElements := make([]object.Object, length+1, length+1) - copy(newElements, arr.Elements) - newElements[length] = args[1] - - return &object.Array{Elements: newElements} - }, - }, - - "puts": &object.Builtin{ - Fn: func(args ...object.Object) object.Object { - for _, arg := range args { - fmt.Println(arg.Inspect()) - } - - return NULL - }, - }, + "len": object.GetBuiltinByName("len"), + "first": object.GetBuiltinByName("first"), + "last": object.GetBuiltinByName("last"), + "rest": object.GetBuiltinByName("rest"), + "push": object.GetBuiltinByName("push"), + "puts": object.GetBuiltinByName("puts"), } diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 53ce728..9079cad 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -319,7 +319,10 @@ func applyFunction(fn object.Object, args []object.Object) object.Object { return unwrapReturnValue(evaluated) case *object.Builtin: - return fn.Fn(args...) + if result := fn.Fn(args...); result != nil { + return result + } + return NULL default: return newError("not a function: %s", fn.Type()) diff --git a/object/builtins.go b/object/builtins.go new file mode 100644 index 0000000..6b4d8d4 --- /dev/null +++ b/object/builtins.go @@ -0,0 +1,139 @@ +package object + +import "fmt" + +var Builtins = []struct { + Name string + Builtin *Builtin +}{ + { + "len", + &Builtin{Fn: func(args ...Object) Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", + len(args)) + } + + switch arg := args[0].(type) { + case *Array: + return &Integer{Value: int64(len(arg.Elements))} + case *String: + return &Integer{Value: int64(len(arg.Value))} + default: + return newError("argument to `len` not supported, got %s", + args[0].Type()) + } + }, + }, + }, + { + "puts", + &Builtin{Fn: func(args ...Object) Object { + for _, arg := range args { + fmt.Println(arg.Inspect()) + } + + return nil + }, + }, + }, + { + "first", + &Builtin{Fn: func(args ...Object) Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", len(args)) + } + if args[0].Type() != ARRAY_OBJ { + return newError("argument to `first` must be ARRAY, got %s", args[0].Type()) + } + + arr := args[0].(*Array) + if len(arr.Elements) > 0 { + return arr.Elements[0] + } + + return nil + }, + }, + }, + { + "last", + &Builtin{Fn: func(args ...Object) Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", len(args)) + } + if args[0].Type() != ARRAY_OBJ { + return newError("argument to `last` must be ARRAY, got %s", args[0].Type()) + } + + arr := args[0].(*Array) + length := len(arr.Elements) + if length > 0 { + return arr.Elements[length-1] + } + + return nil + }, + }, + }, + { + "rest", + &Builtin{Fn: func(args ...Object) Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", + len(args)) + } + if args[0].Type() != ARRAY_OBJ { + return newError("argument to `rest` must be ARRAY, got %s", + args[0].Type()) + } + + arr := args[0].(*Array) + length := len(arr.Elements) + if length > 0 { + newElements := make([]Object, length-1, length-1) + copy(newElements, arr.Elements[1:length]) + return &Array{Elements: newElements} + } + + return nil + }, + }, + }, + { + "push", + &Builtin{Fn: func(args ...Object) Object { + if len(args) != 2 { + return newError("wrong number of arguments. got=%d, want=2", + len(args)) + } + if args[0].Type() != ARRAY_OBJ { + return newError("argument to `push` must be ARRAY, got %s", + args[0].Type()) + } + + arr := args[0].(*Array) + length := len(arr.Elements) + + newElements := make([]Object, length+1, length+1) + copy(newElements, arr.Elements) + newElements[length] = args[1] + + return &Array{Elements: newElements} + }, + }, + }, +} + +func newError(format string, a ...interface{}) *Error { + return &Error{Message: fmt.Sprintf(format, a...)} +} + +func GetBuiltinByName(name string) *Builtin { + for _, def := range Builtins { + if def.Name == name { + return def.Builtin + } + } + return nil +} diff --git a/repl/repl.go b/repl/repl.go index e1db9f7..ae1f4b9 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -19,6 +19,9 @@ func Start(in io.Reader, out io.Writer) { constants := []object.Object{} globals := make([]object.Object, vm.GlobalsSize) symbolTable := compiler.NewSymbolTable() + for i, v := range object.Builtins { + symbolTable.DefineBuiltin(i, v.Name) + } for { fmt.Fprintf(out, PROMPT) diff --git a/vm/vm.go b/vm/vm.go index a6cb8a2..b86fdac 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -206,7 +206,7 @@ func (vm *VM) Run() error { numArgs := code.ReadUint8(ins[ip+1:]) vm.currentFrame().ip += 1 - err := vm.callFunction(int(numArgs)) + err := vm.executeCall(int(numArgs)) if err != nil { return err } @@ -249,6 +249,17 @@ func (vm *VM) Run() error { if err != nil { return err } + + case code.OpGetBuiltin: + builtinIndex := code.ReadUint8(ins[ip+1:]) + vm.currentFrame().ip += 1 + + definition := object.Builtins[builtinIndex] + + err := vm.push(definition.Builtin) + if err != nil { + return err + } } } @@ -465,12 +476,19 @@ func (vm *VM) executeMinusOperator() error { return vm.push(&object.Integer{Value: -value}) } -func (vm *VM) callFunction(numArgs int) error { - fn, ok := vm.stack[vm.sp-1-numArgs].(*object.CompiledFunction) - if !ok { - return fmt.Errorf("calling non-function") +func (vm *VM) executeCall(numArgs int) error { + callee := vm.stack[vm.sp-1-numArgs] + switch callee := callee.(type) { + case *object.CompiledFunction: + return vm.callFunction(callee, numArgs) + case *object.Builtin: + return vm.callBuiltin(callee, numArgs) + default: + return fmt.Errorf("calling non-function and non-built-in") } +} +func (vm *VM) callFunction(fn *object.CompiledFunction, numArgs int) error { if numArgs != fn.NumParameters { return fmt.Errorf("wrong number of arguments: want=%d, got=%d", fn.NumParameters, numArgs) } @@ -482,6 +500,27 @@ func (vm *VM) callFunction(numArgs int) error { return nil } +func (vm *VM) callBuiltin(builtin *object.Builtin, numArgs int) error { + args := vm.stack[vm.sp-numArgs : vm.sp] + + result := builtin.Fn(args...) + vm.sp = vm.sp - numArgs - 1 + + if result != nil { + err := vm.push(result) + if err != nil { + return err + } + } else { + err := vm.push(Null) + if err != nil { + return err + } + } + + return nil +} + func nativeBoolToBooleanObject(input bool) *object.Boolean { if input { return True diff --git a/vm/vm_test.go b/vm/vm_test.go index 13c005e..0cbdb60 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -106,6 +106,16 @@ func testExpectedObject(t *testing.T, expected interface{}, actual object.Object if actual != Null { t.Errorf("object is not Null: %T (%+v)", actual, actual) } + + case *object.Error: + errObj, ok := actual.(*object.Error) + if !ok { + t.Errorf("object is not Error: %T (%+v)", actual, actual) + return + } + if errObj.Message != expected.Message { + t.Errorf("wrong error message. expected=%q, got=%q", expected.Message, errObj.Message) + } } } @@ -555,3 +565,49 @@ func TestCallingFunctionsWithWrongArguments(t *testing.T) { } } } + +func TestBuiltinFunctions(t *testing.T) { + tests := []vmTestCase{ + {`len("")`, 0}, + {`len("four")`, 4}, + {`len("hello world")`, 11}, + { + `len(1)`, + &object.Error{ + Message: "argument to `len` not supported, got INTEGER", + }, + }, + {`len("one", "two")`, + &object.Error{ + Message: "wrong number of arguments. got=2, want=1", + }, + }, + {`len([1, 2, 3])`, 3}, + {`len([])`, 0}, + {`puts("hello", "world!")`, Null}, + {`first([1, 2, 3])`, 1}, + {`first([])`, Null}, + {`first(1)`, + &object.Error{ + Message: "argument to `first` must be ARRAY, got INTEGER", + }, + }, + {`last([1, 2, 3])`, 3}, + {`last([])`, Null}, + {`last(1)`, + &object.Error{ + Message: "argument to `last` must be ARRAY, got INTEGER", + }, + }, + {`rest([1, 2, 3])`, []int{2, 3}}, + {`rest([])`, Null}, + {`push([], 1)`, []int{1}}, + {`push(1, 1)`, + &object.Error{ + Message: "argument to `push` must be ARRAY, got INTEGER", + }, + }, + } + + runVmTests(t, tests) +}