builtins
Some checks failed
Build / build (push) Failing after 1h2m55s
Test / build (push) Failing after 29m38s

This commit is contained in:
Chuck Smith
2024-03-12 16:35:24 -04:00
parent 1d2c7f0a51
commit e373e9f68a
11 changed files with 354 additions and 125 deletions

View File

@@ -37,6 +37,7 @@ const (
OpReturn OpReturn
OpGetLocal OpGetLocal
OpSetLocal OpSetLocal
OpGetBuiltin
) )
type Definition struct { type Definition struct {
@@ -71,6 +72,7 @@ var definitions = map[Opcode]*Definition{
OpReturn: {"OpReturn", []int{}}, OpReturn: {"OpReturn", []int{}},
OpGetLocal: {"OpGetLocal", []int{1}}, OpGetLocal: {"OpGetLocal", []int{1}},
OpSetLocal: {"OpSetLocal", []int{1}}, OpSetLocal: {"OpSetLocal", []int{1}},
OpGetBuiltin: {"OpGetBuiltin", []int{1}},
} }
func Lookup(op byte) (*Definition, error) { func Lookup(op byte) (*Definition, error) {

View File

@@ -34,9 +34,16 @@ func New() *Compiler {
lastInstruction: EmittedInstruction{}, lastInstruction: EmittedInstruction{},
previousInstruction: EmittedInstruction{}, previousInstruction: EmittedInstruction{},
} }
symbolTable := NewSymbolTable()
for i, v := range object.Builtins {
symbolTable.DefineBuiltin(i, v.Name)
}
return &Compiler{ return &Compiler{
constants: []object.Object{}, constants: []object.Object{},
symbolTable: NewSymbolTable(), symbolTable: symbolTable,
scopes: []CompilationScope{mainScope}, scopes: []CompilationScope{mainScope},
scopeIndex: 0, scopeIndex: 0,
} }
@@ -207,11 +214,7 @@ func (c *Compiler) Compile(node ast.Node) error {
return fmt.Errorf("undefined varible %s", node.Value) return fmt.Errorf("undefined varible %s", node.Value)
} }
if symbol.Scope == GlobalScope { c.loadSymbol(symbol)
c.emit(code.OpGetGlobal, symbol.Index)
} else {
c.emit(code.OpGetLocal, symbol.Index)
}
case *ast.StringLiteral: case *ast.StringLiteral:
str := &object.String{Value: node.Value} str := &object.String{Value: node.Value}
@@ -425,3 +428,14 @@ type Bytecode struct {
Instructions code.Instructions Instructions code.Instructions
Constants []object.Object 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)
}
}

View File

@@ -832,3 +832,43 @@ func testStringObject(expected string, actual object.Object) error {
return nil 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)
}

View File

@@ -5,6 +5,7 @@ type SymbolScope string
const ( const (
LocalScope SymbolScope = "LOCAL" LocalScope SymbolScope = "LOCAL"
GlobalScope SymbolScope = "GLOBAL" GlobalScope SymbolScope = "GLOBAL"
BuiltinScope SymbolScope = "BUILTIN"
) )
type Symbol struct { type Symbol struct {
@@ -53,3 +54,9 @@ func (s *SymbolTable) Resolve(name string) (Symbol, bool) {
return obj, ok 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
}

View File

@@ -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)
}
}
}
}

View File

@@ -1,119 +1,14 @@
package evaluator package evaluator
import ( import (
"fmt"
"monkey/object" "monkey/object"
"unicode/utf8"
) )
var builtins = map[string]*object.Builtin{ var builtins = map[string]*object.Builtin{
"len": &object.Builtin{ "len": object.GetBuiltinByName("len"),
Fn: func(args ...object.Object) object.Object { "first": object.GetBuiltinByName("first"),
if len(args) != 1 { "last": object.GetBuiltinByName("last"),
return newError("wrong number of arguments. got=%d, want=1", len(args)) "rest": object.GetBuiltinByName("rest"),
} "push": object.GetBuiltinByName("push"),
"puts": object.GetBuiltinByName("puts"),
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
},
},
} }

View File

@@ -319,7 +319,10 @@ func applyFunction(fn object.Object, args []object.Object) object.Object {
return unwrapReturnValue(evaluated) return unwrapReturnValue(evaluated)
case *object.Builtin: case *object.Builtin:
return fn.Fn(args...) if result := fn.Fn(args...); result != nil {
return result
}
return NULL
default: default:
return newError("not a function: %s", fn.Type()) return newError("not a function: %s", fn.Type())

139
object/builtins.go Normal file
View File

@@ -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
}

View File

@@ -19,6 +19,9 @@ func Start(in io.Reader, out io.Writer) {
constants := []object.Object{} constants := []object.Object{}
globals := make([]object.Object, vm.GlobalsSize) globals := make([]object.Object, vm.GlobalsSize)
symbolTable := compiler.NewSymbolTable() symbolTable := compiler.NewSymbolTable()
for i, v := range object.Builtins {
symbolTable.DefineBuiltin(i, v.Name)
}
for { for {
fmt.Fprintf(out, PROMPT) fmt.Fprintf(out, PROMPT)

View File

@@ -206,7 +206,7 @@ func (vm *VM) Run() error {
numArgs := code.ReadUint8(ins[ip+1:]) numArgs := code.ReadUint8(ins[ip+1:])
vm.currentFrame().ip += 1 vm.currentFrame().ip += 1
err := vm.callFunction(int(numArgs)) err := vm.executeCall(int(numArgs))
if err != nil { if err != nil {
return err return err
} }
@@ -249,6 +249,17 @@ func (vm *VM) Run() error {
if err != nil { if err != nil {
return err 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}) return vm.push(&object.Integer{Value: -value})
} }
func (vm *VM) callFunction(numArgs int) error { func (vm *VM) executeCall(numArgs int) error {
fn, ok := vm.stack[vm.sp-1-numArgs].(*object.CompiledFunction) callee := vm.stack[vm.sp-1-numArgs]
if !ok { switch callee := callee.(type) {
return fmt.Errorf("calling non-function") 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 { if numArgs != fn.NumParameters {
return fmt.Errorf("wrong number of arguments: want=%d, got=%d", fn.NumParameters, numArgs) 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 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 { func nativeBoolToBooleanObject(input bool) *object.Boolean {
if input { if input {
return True return True

View File

@@ -106,6 +106,16 @@ func testExpectedObject(t *testing.T, expected interface{}, actual object.Object
if actual != Null { if actual != Null {
t.Errorf("object is not Null: %T (%+v)", actual, actual) 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)
}