From 7834251c84a50a59853476051c9f91d43a8d8f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozan=20Hac=C4=B1bekiro=C4=9Flu?= Date: Mon, 8 Jun 2020 19:54:24 +0300 Subject: [PATCH] spread last call argument v2 (#302) --- compiler.go | 10 +++++--- compiler_test.go | 37 +++++++++++++++++++++------- docs/tengo-cli.md | 6 +++++ docs/tutorial.md | 38 +++++++++++++++++++++++++++++ parser/expr.go | 12 ++++++--- parser/opcodes.go | 2 +- parser/parser.go | 17 ++++++++----- parser/parser_test.go | 43 +++++++++++++++++++++++--------- vm.go | 26 +++++++++++++++++++- vm_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 213 insertions(+), 35 deletions(-) diff --git a/compiler.go b/compiler.go index cb1c8f30..1937a1d6 100644 --- a/compiler.go +++ b/compiler.go @@ -506,7 +506,11 @@ func (c *Compiler) Compile(node parser.Node) error { return err } } - c.emit(node, parser.OpCall, len(node.Args)) + ellipsis := 0 + if node.Ellipsis.IsValid() { + ellipsis = 1 + } + c.emit(node, parser.OpCall, len(node.Args), ellipsis) case *parser.ImportExpr: if node.ModuleName == "" { return c.errorf(node, "empty module name") @@ -526,7 +530,7 @@ func (c *Compiler) Compile(node parser.Node) error { return err } c.emit(node, parser.OpConstant, c.addConstant(compiled)) - c.emit(node, parser.OpCall, 0) + c.emit(node, parser.OpCall, 0, 0) case Object: // builtin module c.emit(node, parser.OpConstant, c.addConstant(v)) default: @@ -556,7 +560,7 @@ func (c *Compiler) Compile(node parser.Node) error { return err } c.emit(node, parser.OpConstant, c.addConstant(compiled)) - c.emit(node, parser.OpCall, 0) + c.emit(node, parser.OpCall, 0, 0) } else { return c.errorf(node, "module '%s' not found", node.ModuleName) } diff --git a/compiler_test.go b/compiler_test.go index 09f609f5..905fa154 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -483,6 +483,25 @@ func TestCompiler_Compile(t *testing.T) { intObject(3), intObject(0)))) + expectCompile(t, `f1 := func(a) { return a }; f1([1, 2]...);`, + bytecode( + concatInsts( + tengo.MakeInstruction(parser.OpConstant, 0), + tengo.MakeInstruction(parser.OpSetGlobal, 0), + tengo.MakeInstruction(parser.OpGetGlobal, 0), + tengo.MakeInstruction(parser.OpConstant, 1), + tengo.MakeInstruction(parser.OpConstant, 2), + tengo.MakeInstruction(parser.OpArray, 2), + tengo.MakeInstruction(parser.OpCall, 1, 1), + tengo.MakeInstruction(parser.OpPop), + tengo.MakeInstruction(parser.OpSuspend)), + objectsArray( + compiledFunction(1, 1, + tengo.MakeInstruction(parser.OpGetLocal, 0), + tengo.MakeInstruction(parser.OpReturn, 1)), + intObject(1), + intObject(2)))) + expectCompile(t, `func() { return 5 + 10 }`, bytecode( concatInsts( @@ -601,7 +620,7 @@ func TestCompiler_Compile(t *testing.T) { bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 1), - tengo.MakeInstruction(parser.OpCall, 0), + tengo.MakeInstruction(parser.OpCall, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -615,7 +634,7 @@ func TestCompiler_Compile(t *testing.T) { bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 1), - tengo.MakeInstruction(parser.OpCall, 0), + tengo.MakeInstruction(parser.OpCall, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -630,7 +649,7 @@ func TestCompiler_Compile(t *testing.T) { tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), - tengo.MakeInstruction(parser.OpCall, 0), + tengo.MakeInstruction(parser.OpCall, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -646,7 +665,7 @@ func TestCompiler_Compile(t *testing.T) { tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), - tengo.MakeInstruction(parser.OpCall, 0), + tengo.MakeInstruction(parser.OpCall, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -710,7 +729,7 @@ func TestCompiler_Compile(t *testing.T) { tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), - tengo.MakeInstruction(parser.OpCall, 1), + tengo.MakeInstruction(parser.OpCall, 1, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -728,7 +747,7 @@ func TestCompiler_Compile(t *testing.T) { tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), - tengo.MakeInstruction(parser.OpCall, 3), + tengo.MakeInstruction(parser.OpCall, 3, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -746,7 +765,7 @@ func TestCompiler_Compile(t *testing.T) { tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), - tengo.MakeInstruction(parser.OpCall, 3), + tengo.MakeInstruction(parser.OpCall, 3, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -782,7 +801,7 @@ func TestCompiler_Compile(t *testing.T) { concatInsts( tengo.MakeInstruction(parser.OpGetBuiltin, 0), tengo.MakeInstruction(parser.OpArray, 0), - tengo.MakeInstruction(parser.OpCall, 1), + tengo.MakeInstruction(parser.OpCall, 1, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) @@ -797,7 +816,7 @@ func TestCompiler_Compile(t *testing.T) { compiledFunction(0, 0, tengo.MakeInstruction(parser.OpGetBuiltin, 0), tengo.MakeInstruction(parser.OpArray, 0), - tengo.MakeInstruction(parser.OpCall, 1), + tengo.MakeInstruction(parser.OpCall, 1, 0), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `func(a) { func(b) { return a + b } }`, diff --git a/docs/tengo-cli.md b/docs/tengo-cli.md index 2e7179e1..ec45b9be 100644 --- a/docs/tengo-cli.md +++ b/docs/tengo-cli.md @@ -52,6 +52,12 @@ chmod +x myapp.tengo **Note: Your source file must have `.tengo` extension.** +## Resolving Relative Import Paths + +If there are tengo source module files which are imported with relative import +paths, CLI has `-resolve` flag. Flag enables to import a module relative to +importing file. This behavior will be default at version 3. + ## Tengo REPL You can run Tengo [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) diff --git a/docs/tutorial.md b/docs/tutorial.md index 75b7594b..9f98358b 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -194,6 +194,30 @@ Only the last parameter can be variadic. The following code is also illegal: illegal := func(a..., b) { /*... */ } ``` +When calling a function, the number of passing arguments must match that of +function definition. + +```golang +f := func(a, b) {} +f(1, 2, 3) // Runtime Error: wrong number of arguments: want=2, got=3 +``` + +Like Go, you can use ellipsis `...` to pass array-type value as its last parameter: + +```golang +f1 := func(a, b, c) { return a + b + c } +f1([1, 2, 3]...) // => 6 +f1(1, [2, 3]...) // => 6 +f1(1, 2, [3]...) // => 6 +f1([1, 2]...) // Runtime Error: wrong number of arguments: want=3, got=2 + +f2 := func(a, ...b) {} +f2(1) // valid; a = 1, b = [] +f2(1, 2) // valid; a = 1, b = [2] +f2(1, 2, 3) // valid; a = 1, b = [2, 3] +f2([1, 2, 3]...) // valid; a = 1, b = [2, 3] +``` + ## Variables and Scopes A value can be assigned to a variable using assignment operator `:=` and `=`. @@ -382,6 +406,20 @@ d := "hello world"[2:10] // == "llo worl" c := [1, 2, 3, 4, 5][-1:10] // == [1, 2, 3, 4, 5] ``` +**Note: Keywords cannot be used as selectors.** + +```golang +a := {in: true} // Parse Error: expected map key, found 'in' +a.func = "" // Parse Error: expected selector, found 'func' +``` + +Use double quotes and indexer to use keywords with maps. + +```golang +a := {"in": true} +a["func"] = "" +``` + ## Statements ### If Statement diff --git a/parser/expr.go b/parser/expr.go index 71e5155b..b6b6c62b 100644 --- a/parser/expr.go +++ b/parser/expr.go @@ -111,10 +111,11 @@ func (e *BoolLit) String() string { // CallExpr represents a function call expression. type CallExpr struct { - Func Expr - LParen Pos - Args []Expr - RParen Pos + Func Expr + LParen Pos + Args []Expr + Ellipsis Pos + RParen Pos } func (e *CallExpr) exprNode() {} @@ -134,6 +135,9 @@ func (e *CallExpr) String() string { for _, e := range e.Args { args = append(args, e.String()) } + if len(args) > 0 && e.Ellipsis.IsValid() { + args[len(args)-1] = args[len(args)-1] + "..." + } return e.Func.String() + "(" + strings.Join(args, ", ") + ")" } diff --git a/parser/opcodes.go b/parser/opcodes.go index a4fbfbaf..d97f4896 100644 --- a/parser/opcodes.go +++ b/parser/opcodes.go @@ -120,7 +120,7 @@ var OpcodeOperands = [...][]int{ OpImmutable: {}, OpIndex: {}, OpSliceIndex: {}, - OpCall: {1}, + OpCall: {1, 1}, OpReturn: {1}, OpGetLocal: {1}, OpSetLocal: {1}, diff --git a/parser/parser.go b/parser/parser.go index 501a9106..fd20423b 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -270,9 +270,13 @@ func (p *Parser) parseCall(x Expr) *CallExpr { p.exprLevel++ var list []Expr - for p.token != token.RParen && p.token != token.EOF { + var ellipsis Pos + for p.token != token.RParen && p.token != token.EOF && !ellipsis.IsValid() { list = append(list, p.parseExpr()) - + if p.token == token.Ellipsis { + ellipsis = p.pos + p.next() + } if !p.expectComma(token.RParen, "call argument") { break } @@ -281,10 +285,11 @@ func (p *Parser) parseCall(x Expr) *CallExpr { p.exprLevel-- rparen := p.expect(token.RParen) return &CallExpr{ - Func: x, - LParen: lparen, - RParen: rparen, - Args: list, + Func: x, + LParen: lparen, + RParen: rparen, + Ellipsis: ellipsis, + Args: list, } } diff --git a/parser/parser_test.go b/parser/parser_test.go index 476d4e39..4ee6ad7a 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -290,12 +290,23 @@ func TestParseCall(t *testing.T) { exprStmt( callExpr( ident("add", p(1, 1)), - p(1, 4), p(1, 12), + p(1, 4), p(1, 12), NoPos, intLit(1, p(1, 5)), intLit(2, p(1, 8)), intLit(3, p(1, 11))))) }) + expectParse(t, "add(1, 2, v...)", func(p pfn) []Stmt { + return stmts( + exprStmt( + callExpr( + ident("add", p(1, 1)), + p(1, 4), p(1, 15), p(1, 12), + intLit(1, p(1, 5)), + intLit(2, p(1, 8)), + ident("v", p(1, 11))))) + }) + expectParse(t, "a = add(1, 2, 3)", func(p pfn) []Stmt { return stmts( assignStmt( @@ -304,7 +315,7 @@ func TestParseCall(t *testing.T) { exprs( callExpr( ident("add", p(1, 5)), - p(1, 8), p(1, 16), + p(1, 8), p(1, 16), NoPos, intLit(1, p(1, 9)), intLit(2, p(1, 12)), intLit(3, p(1, 15)))), @@ -321,7 +332,7 @@ func TestParseCall(t *testing.T) { exprs( callExpr( ident("add", p(1, 8)), - p(1, 11), p(1, 19), + p(1, 11), p(1, 19), NoPos, intLit(1, p(1, 12)), intLit(2, p(1, 15)), intLit(3, p(1, 18)))), @@ -334,7 +345,7 @@ func TestParseCall(t *testing.T) { exprStmt( callExpr( ident("add", p(1, 1)), - p(1, 4), p(1, 26), + p(1, 4), p(1, 26), NoPos, binaryExpr( ident("a", p(1, 5)), intLit(1, p(1, 9)), @@ -381,7 +392,7 @@ func TestParseCall(t *testing.T) { ident("b", p(1, 18)), token.Add, p(1, 16))))), - p(1, 21), p(1, 26), + p(1, 21), p(1, 26), NoPos, intLit(1, p(1, 22)), intLit(2, p(1, 25))))) }) @@ -393,7 +404,7 @@ func TestParseCall(t *testing.T) { selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), - p(1, 4), p(1, 5)))) + p(1, 4), p(1, 5), NoPos))) }) expectParse(t, `a.b.c()`, func(p pfn) []Stmt { @@ -405,7 +416,7 @@ func TestParseCall(t *testing.T) { ident("a", p(1, 1)), stringLit("b", p(1, 3))), stringLit("c", p(1, 5))), - p(1, 6), p(1, 7)))) + p(1, 6), p(1, 7), NoPos))) }) expectParse(t, `a["b"].c()`, func(p pfn) []Stmt { @@ -418,8 +429,17 @@ func TestParseCall(t *testing.T) { stringLit("b", p(1, 3)), p(1, 2), p(1, 6)), stringLit("c", p(1, 8))), - p(1, 9), p(1, 10)))) + p(1, 9), p(1, 10), NoPos))) }) + + expectParseError(t, `add(...a, 1)`) + expectParseError(t, `add(a..., 1)`) + expectParseError(t, `add(a..., b...)`) + expectParseError(t, `add(1, a..., b...)`) + expectParseError(t, `add(...)`) + expectParseError(t, `add(1, ...)`) + expectParseError(t, `add(1, ..., )`) + expectParseError(t, `add(...a)`) } func TestParseChar(t *testing.T) { @@ -1001,7 +1021,7 @@ func TestParseImport(t *testing.T) { selectorExpr( importExpr("mod1", p(1, 1)), stringLit("func1", p(1, 16))), - p(1, 21), p(1, 22)))) + p(1, 21), p(1, 22), NoPos))) }) expectParse(t, `for x, y in import("mod1") {}`, func(p pfn) []Stmt { @@ -1753,10 +1773,11 @@ func parenExpr(x Expr, lparen, rparen Pos) *ParenExpr { func callExpr( f Expr, - lparen, rparen Pos, + lparen, rparen, ellipsis Pos, args ...Expr, ) *CallExpr { - return &CallExpr{Func: f, LParen: lparen, RParen: rparen, Args: args} + return &CallExpr{Func: f, LParen: lparen, RParen: rparen, + Ellipsis: ellipsis, Args: args} } func indexExpr( diff --git a/vm.go b/vm.go index af8783f0..811ecef9 100644 --- a/vm.go +++ b/vm.go @@ -537,12 +537,36 @@ func (v *VM) run() { } case parser.OpCall: numArgs := int(v.curInsts[v.ip+1]) - v.ip++ + spread := int(v.curInsts[v.ip+2]) + v.ip += 2 + value := v.stack[v.sp-1-numArgs] if !value.CanCall() { v.err = fmt.Errorf("not callable: %s", value.TypeName()) return } + + if spread == 1 { + v.sp-- + switch arr := v.stack[v.sp].(type) { + case *Array: + for _, item := range arr.Value { + v.stack[v.sp] = item + v.sp++ + } + numArgs += len(arr.Value) - 1 + case *ImmutableArray: + for _, item := range arr.Value { + v.stack[v.sp] = item + v.sp++ + } + numArgs += len(arr.Value) - 1 + default: + v.err = fmt.Errorf("not an array: %s", arr.TypeName()) + return + } + } + if callee, ok := value.(*CompiledFunction); ok { if callee.VarArgs { // if the closure is variadic, diff --git a/vm_test.go b/vm_test.go index b9962809..c20fcd12 100644 --- a/vm_test.go +++ b/vm_test.go @@ -3472,6 +3472,63 @@ func() { }()`, nil, 25) } +func TestSpread(t *testing.T) { + expectRun(t, ` + f := func(...a) { + return append(a, 3) + } + out = f([1, 2]...) + `, nil, ARR{1, 2, 3}) + + expectRun(t, ` + f := func(a, ...b) { + return append([a], append(b, 3)...) + } + out = f([1, 2]...) + `, nil, ARR{1, 2, 3}) + + expectRun(t, ` + f := func(a, ...b) { + return append(append([a], b), 3) + } + out = f(1, [2]...) + `, nil, ARR{1, ARR{2}, 3}) + + expectRun(t, ` + f1 := func(...a){ + return append([3], a...) + } + f2 := func(a, ...b) { + return f1(append([a], b...)...) + } + out = f2([1, 2]...) + `, nil, ARR{3, 1, 2}) + + expectRun(t, ` + f := func(a, ...b) { + return func(...a) { + return append([3], append(a, 4)...) + }(a, b...) + } + out = f([1, 2]...) + `, nil, ARR{3, 1, 2, 4}) + + expectRun(t, ` + f := func(a, ...b) { + c := append(b, 4) + return func(){ + return append(append([a], b...), c...) + }() + } + out = f(1, immutable([2, 3])...) + `, nil, ARR{1, 2, 3, 2, 3, 4}) + + expectError(t, `func(a) {}([1, 2]...)`, nil, + "Runtime Error: wrong number of arguments: want=1, got=2") + expectError(t, `func(a, b, c) {}([1, 2]...)`, nil, + "Runtime Error: wrong number of arguments: want=3, got=2") +} + func expectRun( t *testing.T, input string,