diff --git a/.gitignore b/.gitignore index 77738287..4fc275fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -dist/ \ No newline at end of file +dist/ +.idea diff --git a/builtins.go b/builtins.go index fcda81cd..d7c98d5e 100644 --- a/builtins.go +++ b/builtins.go @@ -109,6 +109,10 @@ var builtinFuncs = []*BuiltinFunction{ Name: "is_function", Value: builtinIsFunction, }, + { + Name: "is_method", + Value: builtinIsMethod, + }, { Name: "is_callable", Value: builtinIsCallable, @@ -276,6 +280,19 @@ func builtinIsFunction(args ...Object) (Object, error) { return FalseValue, nil } +func builtinIsMethod(args ...Object) (Object, error) { + if len(args) != 1 { + return nil, ErrWrongNumArguments + } + switch args[0].(type) { + case *CompiledFunction: + if args[0].(*CompiledFunction).UsesReceiver { + return TrueValue, nil + } + } + return FalseValue, nil +} + func builtinIsCallable(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments diff --git a/compiler.go b/compiler.go index 53cc7d38..38799403 100644 --- a/compiler.go +++ b/compiler.go @@ -368,7 +368,9 @@ func (c *Compiler) Compile(node parser.Node) error { if err := c.Compile(node.Sel); err != nil { return err } - c.emit(node, parser.OpIndex) + method := 0 + if node.Reci { method = 1 } + c.emit(node, parser.OpIndex, method) case *parser.IndexExpr: if err := c.Compile(node.Expr); err != nil { return err @@ -376,7 +378,9 @@ func (c *Compiler) Compile(node parser.Node) error { if err := c.Compile(node.Index); err != nil { return err } - c.emit(node, parser.OpIndex) + method := 0 + if node.Reci { method = 1 } + c.emit(node, parser.OpIndex, method) case *parser.SliceExpr: if err := c.Compile(node.Expr); err != nil { return err @@ -398,6 +402,8 @@ func (c *Compiler) Compile(node parser.Node) error { c.emit(node, parser.OpSliceIndex) case *parser.FuncLit: c.enterScope() + reci := node.Type.Receiver + usesReceiver := reci != nil && len(reci.List) == 1 for _, p := range node.Type.Params.List { s := c.symbolTable.Define(p.Name) @@ -405,6 +411,11 @@ func (c *Compiler) Compile(node parser.Node) error { // function arguments is not assigned directly. s.LocalAssigned = true } + if usesReceiver { + c.symbolTable.defineFree(&Symbol{ Name: reci.List[0].Name }) + } else { + c.symbolTable.defineFree(&Symbol{ Name: "" }) + } if err := c.Compile(node.Body); err != nil { return err @@ -413,7 +424,7 @@ func (c *Compiler) Compile(node parser.Node) error { // code optimization c.optimizeFunc(node) - freeSymbols := c.symbolTable.FreeSymbols() + freeSymbols := c.symbolTable.FreeSymbols()[1:] numLocals := c.symbolTable.MaxSymbols() instructions, sourceMap := c.leaveScope() @@ -476,6 +487,8 @@ func (c *Compiler) Compile(node parser.Node) error { NumParameters: len(node.Type.Params.List), VarArgs: node.Type.Params.VarArgs, SourceMap: sourceMap, + UsesReceiver: usesReceiver, + Free: []*ObjectPtr{{Value: &UndefinedValue}}, } if len(freeSymbols) > 0 { c.emit(node, parser.OpClosure, diff --git a/compiler_test.go b/compiler_test.go index 0fe93121..39b24dfb 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -388,7 +388,7 @@ func TestCompiler_Compile(t *testing.T) { tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), - tengo.MakeInstruction(parser.OpIndex), + tengo.MakeInstruction(parser.OpIndex, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -405,7 +405,7 @@ func TestCompiler_Compile(t *testing.T) { tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpBinaryOp, 12), - tengo.MakeInstruction(parser.OpIndex), + tengo.MakeInstruction(parser.OpIndex, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -502,6 +502,21 @@ func TestCompiler_Compile(t *testing.T) { intObject(1), intObject(2)))) + expectCompile(t, `f1 := func(r)() { return r }; f1();`, + bytecode( + concatInsts( + tengo.MakeInstruction(parser.OpConstant, 0), + tengo.MakeInstruction(parser.OpSetGlobal, 0), + tengo.MakeInstruction(parser.OpGetGlobal, 0), + tengo.MakeInstruction(parser.OpCall, 0, 0), + tengo.MakeInstruction(parser.OpPop), + tengo.MakeInstruction(parser.OpSuspend)), + objectsArray( + compiledFunction(1, 1, + tengo.MakeInstruction(parser.OpGetFree, 0), + tengo.MakeInstruction(parser.OpReturn, 1)), + ))) + expectCompile(t, `func() { return 5 + 10 }`, bytecode( concatInsts( @@ -827,7 +842,7 @@ func TestCompiler_Compile(t *testing.T) { tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(1, 1, - tengo.MakeInstruction(parser.OpGetFree, 0), + tengo.MakeInstruction(parser.OpGetFree, 1), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpReturn, 1)), @@ -852,14 +867,14 @@ func(a) { tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(1, 1, - tengo.MakeInstruction(parser.OpGetFree, 0), tengo.MakeInstruction(parser.OpGetFree, 1), + tengo.MakeInstruction(parser.OpGetFree, 2), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 1, - tengo.MakeInstruction(parser.OpGetFreePtr, 0), + tengo.MakeInstruction(parser.OpGetFreePtr, 1), tengo.MakeInstruction(parser.OpGetLocalPtr, 0), tengo.MakeInstruction(parser.OpClosure, 0, 2), tengo.MakeInstruction(parser.OpReturn, 1)), @@ -900,17 +915,17 @@ func() { tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), - tengo.MakeInstruction(parser.OpGetFree, 0), - tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpGetFree, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), + tengo.MakeInstruction(parser.OpGetFree, 2), + tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpDefineLocal, 0), - tengo.MakeInstruction(parser.OpGetFreePtr, 0), + tengo.MakeInstruction(parser.OpGetFreePtr, 1), tengo.MakeInstruction(parser.OpGetLocalPtr, 0), tengo.MakeInstruction(parser.OpClosure, 4, 2), tengo.MakeInstruction(parser.OpReturn, 1)), diff --git a/docs/builtins.md b/docs/builtins.md index 940a215a..bafff084 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -290,13 +290,18 @@ Returns `true` if the object's type is undefined. Or it returns `false`. ## is_function -Returns `true` if the object's type is function or closure. Or it returns +Returns `true` if the object's type is function, method, or closure. Or it returns `false`. Note that `is_function` returns `false` for builtin functions and user-provided callable objects. +## is_method + +Returns `true` if the object's type is method. Or it returns +`false`. + ## is_callable -Returns `true` if the object is callable (e.g. function, closure, builtin +Returns `true` if the object is callable (e.g. function, closure, method, builtin function, or user-provided callable objects). Or it returns `false`. ## is_array diff --git a/docs/tutorial.md b/docs/tutorial.md index 9f98358b..050b6392 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -38,6 +38,7 @@ Here's a list of all available value types in Tengo. | immutable map | [immutable](#immutable-values) map | - | | undefined | [undefined](#undefined-values) value | - | | function | [function](#function-values) value | - | +| method | [method](#method-values) value | - | | _user-defined_ | value of [user-defined types](https://github.com/d5/tengo/blob/master/docs/objects.md) | - | ### Error Values @@ -218,6 +219,85 @@ f2(1, 2, 3) // valid; a = 1, b = [2, 3] f2([1, 2, 3]...) // valid; a = 1, b = [2, 3] ``` +## Method Values + +Sometimes, when creating structures in Tengo, it becomes desirable to be able +to call back to the parent map or array. This is handled by a method, a +specialized subset of a function. When a method is defined, it creates a receiver in +the local scope that takes on the identity of the parent slice or array during a +call. + +```golang +my_counter := { + counter: 21, + getCount: func(reci)() { + return reci.counter + } +} + +my_counter.counter += 4 + +my_counter.getCount() //returns 25 + +my_arr := [ 0, 5, 17, func(r)() { return r[1] } ] + +my_arr[3]() //returns 5 +``` + +Notably, and unlike Go, a method receiver is not dependent on being instanced inside +a map or array. + +```golang +concat := func(reci)(str) { + reci.buffer += str +} + +my_writer := { + buffer: "", + append: concat +} + +my_writer.append("hello ") // my_obj.buffer = "hello " +my_writer.append("world!") // my_obj.buffer = "hello world!" +``` + +Furthermore, when called from outside a method-call, the receiver on a method call will +be ```undefined``` + +```golang +f := func(r)() { + return r +} + +f() //returns undefined +``` + +Finally, a copy() of an array or slice containing a method will reference the new copy. + +```golang +m1 := { + c: 21, + getC: func(r)() { return r.c } +} + +m2 := copy(m1) + +m2.c = 7 +m1.getC() //21 +m1.c = 2 +m1.getC() //2 +m2.getC() //7 + +a1 := [ func (r)() { return r[1] + " " + r[2] }, "some", "other data" ] +a2 := copy(a1) + +a2[2] = "more things" + +a2[0]() //returns "some more things" +a1[0]() //returns "some other data" + +``` + ## Variables and Scopes A value can be assigned to a variable using assignment operator `:=` and `=`. diff --git a/go.mod b/go.mod index 732ff142..2b2909b8 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/d5/tengo/v2 +module github.com/DawDavis/tengo/v2 go 1.13 diff --git a/objects.go b/objects.go index 30913db5..4da6d4db 100644 --- a/objects.go +++ b/objects.go @@ -576,6 +576,7 @@ type CompiledFunction struct { VarArgs bool SourceMap map[int]parser.Pos Free []*ObjectPtr + UsesReceiver bool } // TypeName returns the name of the type. @@ -595,6 +596,7 @@ func (o *CompiledFunction) Copy() Object { NumParameters: o.NumParameters, VarArgs: o.VarArgs, Free: append([]*ObjectPtr{}, o.Free...), // DO NOT Copy() of elements; these are variable pointers + UsesReceiver: o.UsesReceiver, } } diff --git a/parser/expr.go b/parser/expr.go index b6b6c62b..0f54c5b3 100644 --- a/parser/expr.go +++ b/parser/expr.go @@ -256,13 +256,14 @@ func (e *FuncLit) End() Pos { } func (e *FuncLit) String() string { - return "func" + e.Type.Params.String() + " " + e.Body.String() + return e.Type.String() + " " + e.Body.String() } // FuncType represents a function type definition. type FuncType struct { FuncPos Pos Params *IdentList + Receiver*IdentList } func (e *FuncType) exprNode() {} @@ -278,6 +279,9 @@ func (e *FuncType) End() Pos { } func (e *FuncType) String() string { + if e.Receiver != nil { + return "func" + e.Receiver.String() + e.Params.String() + } return "func" + e.Params.String() } @@ -360,6 +364,7 @@ type IndexExpr struct { LBrack Pos Index Expr RBrack Pos + Reci bool } func (e *IndexExpr) exprNode() {} @@ -483,6 +488,7 @@ func (e *ParenExpr) String() string { type SelectorExpr struct { Expr Expr Sel Expr + Reci bool } func (e *SelectorExpr) exprNode() {} diff --git a/parser/opcodes.go b/parser/opcodes.go index d97f4896..ce54edf5 100644 --- a/parser/opcodes.go +++ b/parser/opcodes.go @@ -118,7 +118,7 @@ var OpcodeOperands = [...][]int{ OpMap: {2}, OpError: {}, OpImmutable: {}, - OpIndex: {}, + OpIndex: {1}, OpSliceIndex: {}, OpCall: {1, 1}, OpReturn: {1}, diff --git a/parser/parser.go b/parser/parser.go index fd20423b..1f64a623 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -345,11 +345,13 @@ func (p *Parser) parseIndexOrSlice(x Expr) Expr { High: index[1], } } + receiver := p.token == token.LParen return &IndexExpr{ Expr: x, LBrack: lbrack, RBrack: rbrack, Index: index[0], + Reci: receiver, } } @@ -359,7 +361,8 @@ func (p *Parser) parseSelector(x Expr) Expr { } sel := p.parseIdent() - return &SelectorExpr{Expr: x, Sel: &StringLit{ + receiver := p.token == token.LParen + return &SelectorExpr{Expr: x, Reci: receiver, Sel: &StringLit{ Value: sel.Name, ValuePos: sel.NamePos, Literal: sel.Name, @@ -578,10 +581,20 @@ func (p *Parser) parseFuncType() *FuncType { } pos := p.expect(token.Func) + var receiver *IdentList params := p.parseIdentList() + if p.token == token.LParen { + if !params.VarArgs && len(params.List) <= 1 { + receiver = params + params = p.parseIdentList() + } else { + p.errorExpected(params.Pos(), "1 receiver") + } + } return &FuncType{ FuncPos: pos, Params: params, + Receiver:receiver, } } diff --git a/parser/parser_test.go b/parser/parser_test.go index 4ee6ad7a..6d646474 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -401,9 +401,10 @@ func TestParseCall(t *testing.T) { return stmts( exprStmt( callExpr( - selectorExpr( + selectorMethodExpr( ident("a", p(1, 1)), - stringLit("b", p(1, 3))), + stringLit("b", p(1, 3)), + true), p(1, 4), p(1, 5), NoPos))) }) @@ -411,11 +412,12 @@ func TestParseCall(t *testing.T) { return stmts( exprStmt( callExpr( - selectorExpr( + selectorMethodExpr( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), - stringLit("c", p(1, 5))), + stringLit("c", p(1, 5)), + true), p(1, 6), p(1, 7), NoPos))) }) @@ -423,15 +425,28 @@ func TestParseCall(t *testing.T) { return stmts( exprStmt( callExpr( - selectorExpr( + selectorMethodExpr( indexExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3)), p(1, 2), p(1, 6)), - stringLit("c", p(1, 8))), + stringLit("c", p(1, 8)), + true), p(1, 9), p(1, 10), NoPos))) }) + expectParse(t, `a["b"]()`, func(p pfn) []Stmt { + return stmts( + exprStmt( + callExpr( + indexMethodExpr( + ident("a", p(1, 1)), + stringLit("b", p(1, 3)), + p(1, 2), p(1, 6), + true), + p(1, 7), p(1, 8), NoPos))) + }) + expectParseError(t, `add(...a, 1)`) expectParseError(t, `add(a..., 1)`) expectParseError(t, `add(a..., b...)`) @@ -733,6 +748,32 @@ func TestParseFunction(t *testing.T) { }) } +func TestParseMethod(t *testing.T) { + expectParse(t, "a = func(b)(c, d) { return d }", func(p pfn) []Stmt { + return stmts( + assignStmt( + exprs( + ident("a", p(1, 1))), + exprs( + funcLit( + funcTypeMethod( + identList(p(1, 9), p(1, 11), false, + ident("b", p(1, 10))), + identList(p(1, 12), p(1, 17), false, + ident("c", p(1, 13)), + ident("d", p(1, 16))), + p(1, 5)), + blockStmt(p(1, 19), p(1, 30), + returnStmt(p(1, 21), ident("d", p(1, 28)))))), + token.Assign, + p(1, 3))) + }) + + expectParseError(t, "a = func(f, g)(c, d) { return d }") + expectParseError(t, "a = func(...g)(c, d) { return d }") + expectParseError(t, "a = func(...g, a)(c, d) { return d }") +} + func TestParseVariadicFunction(t *testing.T) { expectParse(t, "a = func(...args) { return args }", func(p pfn) []Stmt { return stmts( @@ -1018,9 +1059,10 @@ func TestParseImport(t *testing.T) { return stmts( exprStmt( callExpr( - selectorExpr( + selectorMethodExpr( importExpr("mod1", p(1, 1)), - stringLit("func1", p(1, 16))), + stringLit("func1", p(1, 16)), + true), p(1, 21), p(1, 22), NoPos))) }) @@ -1669,6 +1711,10 @@ func funcType(params *IdentList, pos Pos) *FuncType { return &FuncType{Params: params, FuncPos: pos} } +func funcTypeMethod(receiver, params *IdentList, pos Pos) *FuncType { + return &FuncType{Receiver: receiver, Params: params, FuncPos: pos} +} + func blockStmt(lbrace, rbrace Pos, list ...Stmt) *BlockStmt { return &BlockStmt{Stmts: list, LBrace: lbrace, RBrace: rbrace} } @@ -1785,7 +1831,17 @@ func indexExpr( lbrack, rbrack Pos, ) *IndexExpr { return &IndexExpr{ - Expr: x, Index: index, LBrack: lbrack, RBrack: rbrack, + Expr: x, Index: index, LBrack: lbrack, RBrack: rbrack, Reci: false, + } +} + +func indexMethodExpr( + x, index Expr, + lbrack, rbrack Pos, + reci bool, +) *IndexExpr { + return &IndexExpr{ + Expr: x, Index: index, LBrack: lbrack, RBrack: rbrack, Reci: reci, } } @@ -1809,7 +1865,11 @@ func errorExpr( } func selectorExpr(x, sel Expr) *SelectorExpr { - return &SelectorExpr{Expr: x, Sel: sel} + return &SelectorExpr{Expr: x, Sel: sel, Reci: false} +} + +func selectorMethodExpr(x, sel Expr, reci bool) *SelectorExpr { + return &SelectorExpr{Expr: x, Sel: sel, Reci: reci} } func equalStmt(t *testing.T, expected, actual Stmt) { @@ -1990,6 +2050,8 @@ func equalExpr(t *testing.T, expected, actual Expr) { actual.(*IndexExpr).LBrack) require.Equal(t, expected.RBrack, actual.(*IndexExpr).RBrack) + require.Equal(t, expected.Reci, + actual.(*IndexExpr).Reci) case *SliceExpr: equalExpr(t, expected.Expr, actual.(*SliceExpr).Expr) @@ -2006,6 +2068,8 @@ func equalExpr(t *testing.T, expected, actual Expr) { actual.(*SelectorExpr).Expr) equalExpr(t, expected.Sel, actual.(*SelectorExpr).Sel) + require.Equal(t, expected.Reci, + actual.(*SelectorExpr).Reci) case *ImportExpr: require.Equal(t, expected.ModuleName, actual.(*ImportExpr).ModuleName) diff --git a/vm.go b/vm.go index 811ecef9..0033fe67 100644 --- a/vm.go +++ b/vm.go @@ -345,6 +345,9 @@ func (v *VM) run() { v.stack[v.sp-1] = immutableMap } case parser.OpIndex: + method := int(v.curInsts[v.ip+1]) + v.ip++ + index := v.stack[v.sp-1] left := v.stack[v.sp-2] v.sp -= 2 @@ -366,6 +369,12 @@ func (v *VM) run() { if val == nil { val = UndefinedValue } + if method == 1 { + if cfnc, ok := val.(*CompiledFunction); ok { + val = cfnc.Copy() + val.(*CompiledFunction).Free[0] = &ObjectPtr{Value: &left} + } + } v.stack[v.sp] = val v.sp++ case parser.OpSliceIndex: @@ -750,13 +759,15 @@ func (v *VM) run() { v.err = fmt.Errorf("not function: %s", fn.TypeName()) return } - free := make([]*ObjectPtr, numFree) + fnFreeDefault := len(fn.Free) + free := make([]*ObjectPtr, numFree + fnFreeDefault) + copy(free[:fnFreeDefault], fn.Free) for i := 0; i < numFree; i++ { switch freeVar := (v.stack[v.sp-numFree+i]).(type) { case *ObjectPtr: - free[i] = freeVar + free[i + fnFreeDefault] = freeVar default: - free[i] = &ObjectPtr{ + free[i + fnFreeDefault] = &ObjectPtr{ Value: &v.stack[v.sp-numFree+i], } } diff --git a/vm_test.go b/vm_test.go index feb91f4f..57d71441 100644 --- a/vm_test.go +++ b/vm_test.go @@ -2771,6 +2771,92 @@ export { x: 1 } 1) } +func TestMethod(t *testing.T) { + expectRun(t, ` + s := func(r)(n, m, o) { + f := "F" + g := "G" + h := n + m * o + return r.h + }; + m := { f1: s, h: 22 } + out = m.f1(4, 5, 6)`, + nil, 22) + + expectRun(t, ` + m := { + f1: func(r)(n) { + if n > 0 { + return r.res + } else { + return r.f1(n-1) + } + }, + res: 22 + } + out = m.f1(12)`, + nil, 22) + + expectRun(t, ` + m := { f1: func(r)() { return r.c }} + n := copy(m) + n.c = 3 + out = n.f1()`, + nil, 3) + + expectRun(t, ` + f1 := func(r)() { return r } + out = f1()`, + nil, tengo.UndefinedValue) + + expectRun(t, ` + m := { f1: func(r)() { return r.c }} + n := copy(m) + n.c = 3 + out = n.f1()`, + nil, 3) + + expectRun(t, ` + m := { f1: func(r)() { return r == undefined}} + n := copy(m) + f := n.f1 + out = f()`, + nil, true) + + expectRun(t, ` + m := { f1: func(r)() { return r == undefined}} + out = m.f1()`, + nil, false) + + expectRun(t, ` + arr := [ func(r)() { return r[1] }, 17 ] + out = arr[0]()`, + nil, 17) + + expectRun(t, ` + arr := [ func(r)() { return r == undefined} ] + out = arr[0]()`, + nil, false) + + expectRun(t, ` + arr := [ func(r)() { return r == undefined}] + f := arr[0] + out = f()`, + nil, true) + + //jump between functions in an array + expectRun(t, ` + arr := [ 0, 1, 3, "s", func(r)() { return r[1] }, 14, func(r)() { return r[4]() }, 17 ] + out = arr[6]()`, + nil, 1) + + expectRun(t, ` + arr := [[ "b", func(r)(){ return r[0] }, 14], func(r)(){ return r[0][1]() }, 17 ] + out = arr[1]()`, + nil, "b") + +} + func TestModuleBlockScopes(t *testing.T) { m := Opts().Module("rand", &tengo.BuiltinModule{