add variadic function parameters ()

* parsing variable function types

* finished variadic functions

* fix case where number of passed args to variadic function is 0

* remove extraneous OpVarArgs

* allow multiple variables in variadic function declaration

* fix IdentList string method to print multi-arg variadic functions correctly

* round 2 of fix IdentList string method to print multi-arg variadic functions correctly

round 2 of fix IdentList string method to print multi-arg variadic functions correctly

* clean up tasks in OpCall handling, add tests for variadic closures

* cleanup for pr

- add syntax documentation
- cleanup parseIdentList
- cleanup OpCall handling for functions and closures
- cleanup tests
This commit is contained in:
Jacob R. McCollum 2019-04-26 00:28:27 -04:00 committed by Daniel
parent 2e0ea3a4c1
commit adcf05d26f
12 changed files with 237 additions and 17 deletions

View file

@ -8,9 +8,10 @@ import (
// IdentList represents a list of identifiers.
type IdentList struct {
LParen source.Pos
List []*Ident
RParen source.Pos
LParen source.Pos
VarArgs bool
List []*Ident
RParen source.Pos
}
// Pos returns the position of first character belonging to the node.
@ -50,8 +51,12 @@ func (n *IdentList) NumFields() int {
func (n *IdentList) String() string {
var list []string
for _, e := range n.List {
list = append(list, e.String())
for i, e := range n.List {
if n.VarArgs && i == len(n.List)-1 {
list = append(list, "..."+e.String())
} else {
list = append(list, e.String())
}
}
return "(" + strings.Join(list, ", ") + ")"

View file

@ -0,0 +1,37 @@
package ast_test
import (
"testing"
"github.com/d5/tengo/compiler/ast"
)
func TestIdentListString(t *testing.T) {
identListVar := &ast.IdentList{
List: []*ast.Ident{
{Name: "a"},
{Name: "b"},
{Name: "c"},
},
VarArgs: true,
}
expectedVar := "(a, b, ...c)"
if str := identListVar.String(); str != expectedVar {
t.Fatalf("expected string of %#v to be %s, got %s", identListVar, expectedVar, str)
}
identList := &ast.IdentList{
List: []*ast.Ident{
{Name: "a"},
{Name: "b"},
{Name: "c"},
},
VarArgs: false,
}
expected := "(a, b, c)"
if str := identList.String(); str != expected {
t.Fatalf("expected string of %#v to be %s, got %s", identList, expected, str)
}
}

View file

@ -477,6 +477,7 @@ func (c *Compiler) Compile(node ast.Node) error {
Instructions: instructions,
NumLocals: numLocals,
NumParameters: len(node.Type.Params.List),
VarArgs: node.Type.Params.VarArgs,
SourceMap: sourceMap,
}

View file

@ -670,6 +670,23 @@ func TestCompiler_Compile(t *testing.T) {
compiler.MakeInstruction(compiler.OpReturn, 1)),
intObject(24))))
expect(t, `varTest := func(...a) { return a }; varTest(1,2,3);`,
bytecode(
concat(
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpSetGlobal, 0),
compiler.MakeInstruction(compiler.OpGetGlobal, 0),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpConstant, 3),
compiler.MakeInstruction(compiler.OpCall, 3),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
compiledFunction(1, 1,
compiler.MakeInstruction(compiler.OpGetLocal, 0),
compiler.MakeInstruction(compiler.OpReturn, 1)),
intObject(1), intObject(2), intObject(3))))
expect(t, `f1 := func(a, b, c) { a; b; return c; }; f1(24, 25, 26);`,
bytecode(
concat(

View file

@ -610,19 +610,31 @@ func (p *Parser) parseIdentList() *ast.IdentList {
var params []*ast.Ident
lparen := p.expect(token.LParen)
isVarArgs := false
if p.token != token.RParen {
params = append(params, p.parseIdent())
for p.token == token.Comma {
if p.token == token.Ellipsis {
isVarArgs = true
p.next()
}
params = append(params, p.parseIdent())
for !isVarArgs && p.token == token.Comma {
p.next()
if p.token == token.Ellipsis {
isVarArgs = true
p.next()
}
params = append(params, p.parseIdent())
}
}
rparen := p.expect(token.RParen)
return &ast.IdentList{
LParen: lparen,
RParen: rparen,
List: params,
LParen: lparen,
RParen: rparen,
VarArgs: isVarArgs,
List: params,
}
}

View file

@ -90,6 +90,7 @@ func TestCall(t *testing.T) {
funcType(
identList(
p(1, 5), p(1, 10),
false,
ident("a", p(1, 6)),
ident("b", p(1, 9))),
p(1, 1)),

View file

@ -16,7 +16,7 @@ func TestFunction(t *testing.T) {
exprs(
funcLit(
funcType(
identList(p(1, 9), p(1, 17),
identList(p(1, 9), p(1, 17), false,
ident("b", p(1, 10)),
ident("c", p(1, 13)),
ident("d", p(1, 16))),
@ -27,3 +27,60 @@ func TestFunction(t *testing.T) {
p(1, 3)))
})
}
func TestVariableFunction(t *testing.T) {
expect(t, "a = func(...args) { return args }", func(p pfn) []ast.Stmt {
return stmts(
assignStmt(
exprs(
ident("a", p(1, 1))),
exprs(
funcLit(
funcType(
identList(
p(1, 9), p(1, 17),
true,
ident("args", p(1, 13)),
), p(1, 5)),
blockStmt(p(1, 19), p(1, 33),
returnStmt(p(1, 21),
ident("args", p(1, 28)),
),
),
),
),
token.Assign,
p(1, 3)))
})
}
func TestVariableFunctionWithArgs(t *testing.T) {
expect(t, "a = func(x, y, ...z) { return z }", func(p pfn) []ast.Stmt {
return stmts(
assignStmt(
exprs(
ident("a", p(1, 1))),
exprs(
funcLit(
funcType(
identList(
p(1, 9), p(1, 20),
true,
ident("x", p(1, 10)),
ident("y", p(1, 13)),
ident("z", p(1, 19)),
), p(1, 5)),
blockStmt(p(1, 22), p(1, 33),
returnStmt(p(1, 24),
ident("z", p(1, 31)),
),
),
),
),
token.Assign,
p(1, 3)))
})
expectError(t, "a = func(x, y, ...z, invalid) { return z }")
expectError(t, "a = func(...args, invalid) { return args }")
}

View file

@ -180,8 +180,8 @@ func ident(name string, pos source.Pos) *ast.Ident {
return &ast.Ident{Name: name, NamePos: pos}
}
func identList(opening, closing source.Pos, list ...*ast.Ident) *ast.IdentList {
return &ast.IdentList{List: list, LParen: opening, RParen: closing}
func identList(opening, closing source.Pos, varArgs bool, list ...*ast.Ident) *ast.IdentList {
return &ast.IdentList{VarArgs: varArgs, List: list, LParen: opening, RParen: closing}
}
func binaryExpr(x, y ast.Expr, op token.Token, pos source.Pos) *ast.BinaryExpr {

View file

@ -154,6 +154,29 @@ func my_func(arg1, arg2) { // illegal
}
```
Tengo also supports variadic functions/closures:
```golang
variadic := func (a, b, ...c) {
return [a, b, c]
}
variadic(1, 2, 3, 4) // [1, 2, [3, 4]]
variadicClosure := func(a) {
return func(b, ...c) {
return [a, b, c]
}
}
variadicClosure(1)(2, 3, 4) // [1, 2, [3, 4]]
```
Only the last parameter can be variadic. The following code is also illegal:
```golang
// illegal, because a is variadic and is not the last parameter
illegal := func(a..., b) { /*... */ }
```
## Variables and Scopes
A value can be assigned to a variable using assignment operator `:=` and `=`.

View file

@ -10,6 +10,7 @@ type CompiledFunction struct {
Instructions []byte
NumLocals int // number of local variables (including function parameters)
NumParameters int
VarArgs bool
SourceMap map[int]source.Pos
}
@ -34,6 +35,7 @@ func (o *CompiledFunction) Copy() Object {
Instructions: append([]byte{}, o.Instructions...),
NumLocals: o.NumLocals,
NumParameters: o.NumParameters,
VarArgs: o.VarArgs,
}
}

View file

@ -642,9 +642,31 @@ func (v *VM) run() {
switch callee := value.(type) {
case *objects.Closure:
if callee.Fn.VarArgs {
// if the closure is variadic,
// roll up all variadic parameters into an array
realArgs := callee.Fn.NumParameters - 1
varArgs := numArgs - realArgs
if varArgs >= 0 {
numArgs = realArgs + 1
args := make([]objects.Object, varArgs)
spStart := v.sp - varArgs
for i := spStart; i < v.sp; i++ {
args[i-spStart] = v.stack[i]
}
v.stack[spStart] = &objects.Array{Value: args}
v.sp = spStart + 1
}
}
if numArgs != callee.Fn.NumParameters {
v.err = fmt.Errorf("wrong number of arguments: want=%d, got=%d",
callee.Fn.NumParameters, numArgs)
if callee.Fn.VarArgs {
v.err = fmt.Errorf("wrong number of arguments: want>=%d, got=%d",
callee.Fn.NumParameters-1, numArgs)
} else {
v.err = fmt.Errorf("wrong number of arguments: want=%d, got=%d",
callee.Fn.NumParameters, numArgs)
}
return
}
@ -674,9 +696,31 @@ func (v *VM) run() {
v.sp = v.sp - numArgs + callee.Fn.NumLocals
case *objects.CompiledFunction:
if callee.VarArgs {
// if the closure is variadic,
// roll up all variadic parameters into an array
realArgs := callee.NumParameters - 1
varArgs := numArgs - realArgs
if varArgs >= 0 {
numArgs = realArgs + 1
args := make([]objects.Object, varArgs)
spStart := v.sp - varArgs
for i := spStart; i < v.sp; i++ {
args[i-spStart] = v.stack[i]
}
v.stack[spStart] = &objects.Array{Value: args}
v.sp = spStart + 1
}
}
if numArgs != callee.NumParameters {
v.err = fmt.Errorf("wrong number of arguments: want=%d, got=%d",
callee.NumParameters, numArgs)
if callee.VarArgs {
v.err = fmt.Errorf("wrong number of arguments: want>=%d, got=%d",
callee.NumParameters-1, numArgs)
} else {
v.err = fmt.Errorf("wrong number of arguments: want=%d, got=%d",
callee.NumParameters, numArgs)
}
return
}

View file

@ -12,6 +12,27 @@ func TestFunction(t *testing.T) {
expect(t, `f1 := func() {}; f2 := func() { return f1(); }; f1(); out = f2();`, nil, objects.UndefinedValue)
expect(t, `f := func(x) { x; }; out = f(5);`, nil, objects.UndefinedValue)
expect(t, `f := func(...x) { return x; }; out = f(1,2,3);`, nil, ARR{1, 2, 3})
expect(t, `f := func(a, b, ...x) { return [a, b, x]; }; out = f(8,9,1,2,3);`, nil, ARR{8, 9, ARR{1, 2, 3}})
expect(t, `f := func(v) { x := 2; return func(a, ...b){ return [a, b, v+x]}; }; out = f(5)("a", "b");`, nil,
ARR{"a", ARR{"b"}, 7})
expect(t, `f := func(...x) { return x; }; out = f();`, nil, &objects.Array{Value: []objects.Object{}})
expect(t, `f := func(a, b, ...x) { return [a, b, x]; }; out = f(8, 9);`, nil,
ARR{8, 9, ARR{}})
expect(t, `f := func(v) { x := 2; return func(a, ...b){ return [a, b, v+x]}; }; out = f(5)("a");`, nil,
ARR{"a", ARR{}, 7})
expectError(t, `f := func(a, b, ...x) { return [a, b, x]; }; f();`, nil,
"Runtime Error: wrong number of arguments: want>=2, got=0\n\tat test:1:46")
expectError(t, `f := func(a, b, ...x) { return [a, b, x]; }; f(1);`, nil,
"Runtime Error: wrong number of arguments: want>=2, got=1\n\tat test:1:46")
expect(t, `f := func(x) { return x; }; out = f(5);`, nil, 5)
expect(t, `f := func(x) { return x * 2; }; out = f(5);`, nil, 10)
expect(t, `f := func(x, y) { return x + y; }; out = f(5, 5);`, nil, 10)