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/README.md b/README.md index aa9caa73..a36e2199 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,18 @@ sum := func(init, seq) { return init } +trunc := func(text; limit=3) { + return len(text) > limit ? text[:limit]+"..." : text +} + fmt.println(sum(0, [1, 2, 3])) // "6" fmt.println(sum("", [1, 2, 3])) // "123" +fmt.println([ + trunc("abcd"), + trunc("abc"), + trunc("ab"), + trunc("abcd", limit=2) +]) // ["abc...", "abc", "ab", "ab..."] ``` > Test this Tengo code in the diff --git a/builtins.go b/builtins.go index b954d072..be5358d6 100644 --- a/builtins.go +++ b/builtins.go @@ -1,5 +1,7 @@ package tengo +import "strconv" + var builtinFuncs = []*BuiltinFunction{ { Name: "len", @@ -125,6 +127,10 @@ var builtinFuncs = []*BuiltinFunction{ Name: "range", Value: builtinRange, }, + { + Name: "map", + Value: builtinMap, + }, } // GetAllBuiltinFunctions returns all builtin function objects. @@ -132,180 +138,180 @@ func GetAllBuiltinFunctions() []*BuiltinFunction { return append([]*BuiltinFunction{}, builtinFuncs...) } -func builtinTypeName(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinTypeName(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - return &String{Value: args[0].TypeName()}, nil + return &String{Value: ctx.Args[0].TypeName()}, nil } -func builtinIsString(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsString(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*String); ok { + if _, ok := ctx.Args[0].(*String); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsInt(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsInt(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Int); ok { + if _, ok := ctx.Args[0].(*Int); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsFloat(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsFloat(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Float); ok { + if _, ok := ctx.Args[0].(*Float); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsBool(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsBool(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Bool); ok { + if _, ok := ctx.Args[0].(*Bool); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsChar(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsChar(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Char); ok { + if _, ok := ctx.Args[0].(*Char); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsBytes(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsBytes(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Bytes); ok { + if _, ok := ctx.Args[0].(*Bytes); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsArray(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsArray(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Array); ok { + if _, ok := ctx.Args[0].(*Array); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsImmutableArray(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsImmutableArray(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*ImmutableArray); ok { + if _, ok := ctx.Args[0].(*ImmutableArray); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsMap(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsMap(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Map); ok { + if _, ok := ctx.Args[0].(*Map); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsImmutableMap(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsImmutableMap(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*ImmutableMap); ok { + if _, ok := ctx.Args[0].(*ImmutableMap); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsTime(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsTime(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Time); ok { + if _, ok := ctx.Args[0].(*Time); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsError(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsError(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Error); ok { + if _, ok := ctx.Args[0].(*Error); ok { return TrueValue, nil } return FalseValue, nil } -func builtinIsUndefined(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsUndefined(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if args[0] == UndefinedValue { + if ctx.Args[0] == UndefinedValue { return TrueValue, nil } return FalseValue, nil } -func builtinIsFunction(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsFunction(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - switch args[0].(type) { + switch ctx.Args[0].(type) { case *CompiledFunction: return TrueValue, nil } return FalseValue, nil } -func builtinIsCallable(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsCallable(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if args[0].CanCall() { + if ctx.Args[0].CanCall() { return TrueValue, nil } return FalseValue, nil } -func builtinIsIterable(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinIsIterable(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if args[0].CanIterate() { + if ctx.Args[0].CanIterate() { return TrueValue, nil } return FalseValue, nil } // len(obj object) => int -func builtinLen(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinLen(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - switch arg := args[0].(type) { + switch arg := ctx.Args[0].(type) { case *Array: return &Int{Value: int64(len(arg.Value))}, nil case *ImmutableArray: @@ -327,16 +333,16 @@ func builtinLen(args ...Object) (Object, error) { } } -//range(start, stop[, step]) -func builtinRange(args ...Object) (Object, error) { - numArgs := len(args) +// range(start, stop[, step]) +func builtinRange(ctx *CallContext) (Object, error) { + numArgs := len(ctx.Args) if numArgs < 2 || numArgs > 3 { return nil, ErrWrongNumArguments } var start, stop, step *Int - for i, arg := range args { - v, ok := args[i].(*Int) + for i, arg := range ctx.Args { + v, ok := ctx.Args[i].(*Int) if !ok { var name string switch i { @@ -392,46 +398,46 @@ func buildRange(start, stop, step int64) *Array { return array } -func builtinFormat(args ...Object) (Object, error) { - numArgs := len(args) +func builtinFormat(ctx *CallContext) (Object, error) { + numArgs := len(ctx.Args) if numArgs == 0 { return nil, ErrWrongNumArguments } - format, ok := args[0].(*String) + format, ok := ctx.Args[0].(*String) if !ok { return nil, ErrInvalidArgumentType{ Name: "format", Expected: "string", - Found: args[0].TypeName(), + Found: ctx.Args[0].TypeName(), } } if numArgs == 1 { // okay to return 'format' directly as String is immutable return format, nil } - s, err := Format(format.Value, args[1:]...) + s, err := Format(format.Value, ctx.Args[1:]...) if err != nil { return nil, err } return &String{Value: s}, nil } -func builtinCopy(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinCopy(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - return args[0].Copy(), nil + return ctx.Args[0].Copy(), nil } -func builtinString(args ...Object) (Object, error) { - argsLen := len(args) +func builtinString(ctx *CallContext) (Object, error) { + argsLen := len(ctx.Args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*String); ok { - return args[0], nil + if _, ok := ctx.Args[0].(*String); ok { + return ctx.Args[0], nil } - v, ok := ToString(args[0]) + v, ok := ToString(ctx.Args[0]) if ok { if len(v) > MaxStringLen { return nil, ErrStringLimit @@ -439,55 +445,55 @@ func builtinString(args ...Object) (Object, error) { return &String{Value: v}, nil } if argsLen == 2 { - return args[1], nil + return ctx.Args[1], nil } return UndefinedValue, nil } -func builtinInt(args ...Object) (Object, error) { - argsLen := len(args) +func builtinInt(ctx *CallContext) (Object, error) { + argsLen := len(ctx.Args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Int); ok { - return args[0], nil + if _, ok := ctx.Args[0].(*Int); ok { + return ctx.Args[0], nil } - v, ok := ToInt64(args[0]) + v, ok := ToInt64(ctx.Args[0]) if ok { return &Int{Value: v}, nil } if argsLen == 2 { - return args[1], nil + return ctx.Args[1], nil } return UndefinedValue, nil } -func builtinFloat(args ...Object) (Object, error) { - argsLen := len(args) +func builtinFloat(ctx *CallContext) (Object, error) { + argsLen := len(ctx.Args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Float); ok { - return args[0], nil + if _, ok := ctx.Args[0].(*Float); ok { + return ctx.Args[0], nil } - v, ok := ToFloat64(args[0]) + v, ok := ToFloat64(ctx.Args[0]) if ok { return &Float{Value: v}, nil } if argsLen == 2 { - return args[1], nil + return ctx.Args[1], nil } return UndefinedValue, nil } -func builtinBool(args ...Object) (Object, error) { - if len(args) != 1 { +func builtinBool(ctx *CallContext) (Object, error) { + if len(ctx.Args) != 1 { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Bool); ok { - return args[0], nil + if _, ok := ctx.Args[0].(*Bool); ok { + return ctx.Args[0], nil } - v, ok := ToBool(args[0]) + v, ok := ToBool(ctx.Args[0]) if ok { if v { return TrueValue, nil @@ -497,38 +503,38 @@ func builtinBool(args ...Object) (Object, error) { return UndefinedValue, nil } -func builtinChar(args ...Object) (Object, error) { - argsLen := len(args) +func builtinChar(ctx *CallContext) (Object, error) { + argsLen := len(ctx.Args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Char); ok { - return args[0], nil + if _, ok := ctx.Args[0].(*Char); ok { + return ctx.Args[0], nil } - v, ok := ToRune(args[0]) + v, ok := ToRune(ctx.Args[0]) if ok { return &Char{Value: v}, nil } if argsLen == 2 { - return args[1], nil + return ctx.Args[1], nil } return UndefinedValue, nil } -func builtinBytes(args ...Object) (Object, error) { - argsLen := len(args) +func builtinBytes(ctx *CallContext) (Object, error) { + argsLen := len(ctx.Args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } // bytes(N) => create a new bytes with given size N - if n, ok := args[0].(*Int); ok { + if n, ok := ctx.Args[0].(*Int); ok { if n.Value > int64(MaxBytesLen) { return nil, ErrBytesLimit } return &Bytes{Value: make([]byte, int(n.Value))}, nil } - v, ok := ToByteSlice(args[0]) + v, ok := ToByteSlice(ctx.Args[0]) if ok { if len(v) > MaxBytesLen { return nil, ErrBytesLimit @@ -536,39 +542,39 @@ func builtinBytes(args ...Object) (Object, error) { return &Bytes{Value: v}, nil } if argsLen == 2 { - return args[1], nil + return ctx.Args[1], nil } return UndefinedValue, nil } -func builtinTime(args ...Object) (Object, error) { - argsLen := len(args) +func builtinTime(ctx *CallContext) (Object, error) { + argsLen := len(ctx.Args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Time); ok { - return args[0], nil + if _, ok := ctx.Args[0].(*Time); ok { + return ctx.Args[0], nil } - v, ok := ToTime(args[0]) + v, ok := ToTime(ctx.Args[0]) if ok { return &Time{Value: v}, nil } if argsLen == 2 { - return args[1], nil + return ctx.Args[1], nil } return UndefinedValue, nil } // append(arr, items...) -func builtinAppend(args ...Object) (Object, error) { - if len(args) < 2 { +func builtinAppend(ctx *CallContext) (Object, error) { + if len(ctx.Args) < 2 { return nil, ErrWrongNumArguments } - switch arg := args[0].(type) { + switch arg := ctx.Args[0].(type) { case *Array: - return &Array{Value: append(arg.Value, args[1:]...)}, nil + return &Array{Value: append(arg.Value, ctx.Args[1:]...)}, nil case *ImmutableArray: - return &Array{Value: append(arg.Value, args[1:]...)}, nil + return &Array{Value: append(arg.Value, ctx.Args[1:]...)}, nil default: return nil, ErrInvalidArgumentType{ Name: "first", @@ -581,21 +587,21 @@ func builtinAppend(args ...Object) (Object, error) { // builtinDelete deletes Map keys // usage: delete(map, "key") // key must be a string -func builtinDelete(args ...Object) (Object, error) { - argsLen := len(args) +func builtinDelete(ctx *CallContext) (Object, error) { + argsLen := len(ctx.Args) if argsLen != 2 { return nil, ErrWrongNumArguments } - switch arg := args[0].(type) { + switch arg := ctx.Args[0].(type) { case *Map: - if key, ok := args[1].(*String); ok { + if key, ok := ctx.Args[1].(*String); ok { delete(arg.Value, key.Value) return UndefinedValue, nil } return nil, ErrInvalidArgumentType{ Name: "second", Expected: "string", - Found: args[1].TypeName(), + Found: ctx.Args[1].TypeName(), } default: return nil, ErrInvalidArgumentType{ @@ -609,30 +615,30 @@ func builtinDelete(args ...Object) (Object, error) { // builtinSplice deletes and changes given Array, returns deleted items. // usage: // deleted_items := splice(array[,start[,delete_count[,item1[,item2[,...]]]]) -func builtinSplice(args ...Object) (Object, error) { - argsLen := len(args) +func builtinSplice(ctx *CallContext) (Object, error) { + argsLen := len(ctx.Args) if argsLen == 0 { return nil, ErrWrongNumArguments } - array, ok := args[0].(*Array) + array, ok := ctx.Args[0].(*Array) if !ok { return nil, ErrInvalidArgumentType{ Name: "first", Expected: "array", - Found: args[0].TypeName(), + Found: ctx.Args[0].TypeName(), } } arrayLen := len(array.Value) var startIdx int if argsLen > 1 { - arg1, ok := args[1].(*Int) + arg1, ok := ctx.Args[1].(*Int) if !ok { return nil, ErrInvalidArgumentType{ Name: "second", Expected: "int", - Found: args[1].TypeName(), + Found: ctx.Args[1].TypeName(), } } startIdx = int(arg1.Value) @@ -643,12 +649,12 @@ func builtinSplice(args ...Object) (Object, error) { delCount := len(array.Value) if argsLen > 2 { - arg2, ok := args[2].(*Int) + arg2, ok := ctx.Args[2].(*Int) if !ok { return nil, ErrInvalidArgumentType{ Name: "third", Expected: "int", - Found: args[2].TypeName(), + Found: ctx.Args[2].TypeName(), } } delCount = int(arg2.Value) @@ -669,7 +675,7 @@ func builtinSplice(args ...Object) (Object, error) { if argsLen > 3 { items = make([]Object, 0, argsLen-3) for i := 3; i < argsLen; i++ { - items = append(items, args[i]) + items = append(items, ctx.Args[i]) } } items = append(items, array.Value[endIdx:]...) @@ -678,3 +684,36 @@ func builtinSplice(args ...Object) (Object, error) { // return deleted items return &Array{Value: deleted}, nil } + +// builtinMap make new map merging args of map and kwargs +// Usage: map([map...]...[,key=value,keyN=value]) +// Examples: +// map(a=1,b=2) => {"a":1,"b":2} +// map({"a":1},{"b":2};c=3) => {"a":1,"b":2,"c":3} +func builtinMap(ctx *CallContext) (Object, error) { + res := ctx.Kwargs + if res == nil { + res = make(map[string]Object) + } + + for i, v := range ctx.Args { + switch v { + case nil: + case UndefinedValue: + default: + switch t := v.(type) { + case *Map: + for key, value := range t.Value { + res[key] = value + } + default: + return nil, ErrInvalidArgumentType{ + Name: "arg #"+strconv.Itoa(i), + Expected: "map", + Found: t.TypeName(), + } + } + } + } + return &Map{Value: res}, nil +} diff --git a/builtins_test.go b/builtins_test.go index eca2d5bb..220d51bb 100644 --- a/builtins_test.go +++ b/builtins_test.go @@ -9,7 +9,7 @@ import ( ) func Test_builtinDelete(t *testing.T) { - var builtinDelete func(args ...tengo.Object) (tengo.Object, error) + var builtinDelete func(p *tengo.CallContext) (tengo.Object, error) for _, f := range tengo.GetAllBuiltinFunctions() { if f.Name == "delete" { builtinDelete = f.Value @@ -98,7 +98,7 @@ func Test_builtinDelete(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := builtinDelete(tt.args.args...) + got, err := builtinDelete(&tengo.CallContext{Args: tt.args.args}) if (err != nil) != tt.wantErr { t.Errorf("builtinDelete() error = %v, wantErr %v", err, tt.wantErr) @@ -133,7 +133,7 @@ func Test_builtinDelete(t *testing.T) { } func Test_builtinSplice(t *testing.T) { - var builtinSplice func(args ...tengo.Object) (tengo.Object, error) + var builtinSplice func(p *tengo.CallContext) (tengo.Object, error) for _, f := range tengo.GetAllBuiltinFunctions() { if f.Name == "splice" { builtinSplice = f.Value @@ -331,7 +331,7 @@ func Test_builtinSplice(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := builtinSplice(tt.args...) + got, err := builtinSplice(&tengo.CallContext{Args: tt.args}) if (err != nil) != tt.wantErr { t.Errorf("builtinSplice() error = %v, wantErr %v", err, tt.wantErr) @@ -353,7 +353,7 @@ func Test_builtinSplice(t *testing.T) { } func Test_builtinRange(t *testing.T) { - var builtinRange func(args ...tengo.Object) (tengo.Object, error) + var builtinRange func(p *tengo.CallContext) (tengo.Object, error) for _, f := range tengo.GetAllBuiltinFunctions() { if f.Name == "range" { builtinRange = f.Value @@ -400,12 +400,12 @@ func Test_builtinRange(t *testing.T) { Name: "step", Expected: "int", Found: "string"}, }, {name: "zero step", - args: []tengo.Object{&tengo.Int{}, &tengo.Int{}, &tengo.Int{}}, //must greate than 0 + args: []tengo.Object{&tengo.Int{}, &tengo.Int{}, &tengo.Int{}}, // must greate than 0 wantErr: true, wantedErr: tengo.ErrInvalidRangeStep, }, {name: "negative step", - args: []tengo.Object{&tengo.Int{}, &tengo.Int{}, intObject(-2)}, //must greate than 0 + args: []tengo.Object{&tengo.Int{}, &tengo.Int{}, intObject(-2)}, // must greate than 0 wantErr: true, wantedErr: tengo.ErrInvalidRangeStep, }, @@ -487,7 +487,7 @@ func Test_builtinRange(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := builtinRange(tt.args...) + got, err := builtinRange(&tengo.CallContext{Args: tt.args}) if (err != nil) != tt.wantErr { t.Errorf("builtinRange() error = %v, wantErr %v", err, tt.wantErr) @@ -504,3 +504,71 @@ func Test_builtinRange(t *testing.T) { }) } } + +func Test_map(t *testing.T) { + var fn func(p *tengo.CallContext) (tengo.Object, error) + for _, f := range tengo.GetAllBuiltinFunctions() { + if f.Name == "map" { + fn = f.Value + break + } + } + if fn == nil { + t.Fatal("builtin map not found") + } + tests := []struct { + name string + args []tengo.Object + kwargs map[string]tengo.Object + result *tengo.Map + wantErr bool + wantedErr error + }{ + {name: "no args", + result: &tengo.Map{Value: map[string]tengo.Object{}}, + }, + {name: "only kargs", + kwargs: map[string]tengo.Object{"a": &tengo.Int{Value: 1}}, + result: &tengo.Map{Value: map[string]tengo.Object{"a": &tengo.Int{Value: 1}}}, + }, + {name: "undefined arg and kargs", + args: []tengo.Object{tengo.UndefinedValue, nil}, + kwargs: map[string]tengo.Object{"a": &tengo.Int{Value: 1}}, + result: &tengo.Map{Value: map[string]tengo.Object{"a": &tengo.Int{Value: 1}}}, + }, + {name: "args and kargs", + args: []tengo.Object{&tengo.Map{Value: map[string]tengo.Object{"b": &tengo.Int{Value: 2}}}}, + kwargs: map[string]tengo.Object{"a": &tengo.Int{Value: 1}}, + result: &tengo.Map{Value: map[string]tengo.Object{"a": &tengo.Int{Value: 1}, "b": &tengo.Int{Value: 2}}}, + }, + {name: "args without kargs", + args: []tengo.Object{ + &tengo.Map{Value: map[string]tengo.Object{"b": &tengo.Int{Value: 2}}}, + &tengo.Map{Value: map[string]tengo.Object{"a": &tengo.Int{Value: 1}}}, + }, + result: &tengo.Map{Value: map[string]tengo.Object{"a": &tengo.Int{Value: 1}, "b": &tengo.Int{Value: 2}}}, + }, + {name: "bad arg", + args: []tengo.Object{&tengo.Int{Value: 2}}, + wantErr: true, wantedErr: errors.New("invalid type for argument 'arg #0': expected map, found int"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := fn(&tengo.CallContext{Args: tt.args, Kwargs: tt.kwargs}) + if (err != nil) != tt.wantErr { + t.Errorf("builtinMap() error = %v, wantErr %v", + err, tt.wantErr) + return + } + if tt.wantErr && tt.wantedErr.Error() != err.Error() { + t.Errorf("builtinMap() error = %v, wantedErr %v", + err, tt.wantedErr) + } + if tt.result != nil && !reflect.DeepEqual(tt.result.Value, got.(*tengo.Map).Value) { + t.Errorf("builtinMap() map are not equal expected"+ + " %s, got %s", tt.result, got.(*tengo.Map)) + } + }) + } +} diff --git a/cmd/tengo/main.go b/cmd/tengo/main.go index a8e14987..fca023d1 100644 --- a/cmd/tengo/main.go +++ b/cmd/tengo/main.go @@ -291,7 +291,7 @@ func addPrints(file *parser.File) *parser.File { stmts = append(stmts, &parser.ExprStmt{ Expr: &parser.CallExpr{ Func: &parser.Ident{Name: "__repl_println__"}, - Args: []parser.Expr{s.Expr}, + Args: parser.CallExprArgs{Values: []parser.Expr{s.Expr}}, }, }) case *parser.AssignStmt: @@ -302,7 +302,7 @@ func addPrints(file *parser.File) *parser.File { Func: &parser.Ident{ Name: "__repl_println__", }, - Args: s.LHS, + Args: parser.CallExprArgs{Values: s.LHS}, }, }) default: diff --git a/compiler.go b/compiler.go index e4e04303..f4454f18 100644 --- a/compiler.go +++ b/compiler.go @@ -225,6 +225,14 @@ func (c *Compiler) Compile(node parser.Node) error { c.addConstant(&Char{Value: node.Value})) case *parser.UndefinedLit: c.emit(node, parser.OpNull) + case *parser.DefaultLit: + c.emit(node, parser.OpNullKwarg) + case *parser.CalleeLit: + c.emit(node, parser.OpCallee) + case *parser.CalledArgsLit: + c.emit(node, parser.OpCalledArgs) + case *parser.CalledKwargsLit: + c.emit(node, parser.OpCalledKwargs) case *parser.UnaryExpr: if err := c.Compile(node.Expr); err != nil { return err @@ -403,11 +411,101 @@ func (c *Compiler) Compile(node parser.Node) error { case *parser.FuncLit: c.enterScope() - for _, p := range node.Type.Params.List { - s := c.symbolTable.Define(p.Name) + compiledFunction := &CompiledFunction{} - // function arguments is not assigned directly. - s.LocalAssigned = true + if node.Type.Params.Args != nil { + args := node.Type.Params.Args.List + + if node.Type.Params.Args.VarArgs { + compiledFunction.VarArgs.Valid = true + compiledFunction.VarArgs.Name = args[len(args)-1].Name + args = args[0 : len(args)-1] + } + + for _, p := range args { + s := c.symbolTable.Define(p.Name) + // function arguments is not assigned directly. + s.LocalAssigned = true + } + + if compiledFunction.VarArgs.Name != "" { + s := c.symbolTable.Define(compiledFunction.VarArgs.Name) + // function arguments is not assigned directly. + s.LocalAssigned = true + } + } + + if kwargs := node.Type.Params.Kwargs; kwargs != nil { + names := node.Type.Params.Kwargs.Names + + if node.Type.Params.Kwargs.VarArgs { + compiledFunction.VarKwargs.Valid = true + compiledFunction.VarKwargs.Name = names[len(names)-1].Name + names = names[0 : len(names)-1] + } + + for _, p := range names { + s := c.symbolTable.Define(p.Name) + + // function arguments is not assigned directly. + s.LocalAssigned = true + } + if compiledFunction.VarKwargs.Name != "" { + s := c.symbolTable.Define(compiledFunction.VarKwargs.Name) + // function arguments is not assigned directly. + s.LocalAssigned = true + } + + compiledFunction.KwargsDefaults = make([]Object, len(names)) + compiledFunction.Kwargs = make(map[string]int, len(names)) + compiledFunction.KwargsNames = make([]string, len(names)) + + for i, name := range names { + compiledFunction.KwargsNames[i] = name.Name + } + + var stmts []parser.Stmt + for i, v := range kwargs.Values { + switch t := v.(type) { + case *parser.UndefinedLit: + compiledFunction.KwargsDefaults[i] = UndefinedValue + case *parser.DefaultLit: + compiledFunction.KwargsDefaults[i] = DefaultValue + case *parser.IntLit: + compiledFunction.KwargsDefaults[i] = &Int{Value: t.Value} + case *parser.StringLit: + compiledFunction.KwargsDefaults[i] = &String{Value: t.Value} + case *parser.BoolLit: + if t.Value { + compiledFunction.KwargsDefaults[i] = TrueValue + } else { + compiledFunction.KwargsDefaults[i] = FalseValue + } + case *parser.CharLit: + compiledFunction.KwargsDefaults[i] = &Char{Value: t.Value} + default: + compiledFunction.KwargsDefaults[i] = DefaultValue + + stmts = append(stmts, &parser.IfStmt{ + Cond: &parser.BinaryExpr{ + Token: token.Equal, + LHS: kwargs.Names[i], + RHS: &parser.DefaultLit{}, + }, + Body: &parser.BlockStmt{ + Stmts: []parser.Stmt{ + &parser.AssignStmt{ + Token: token.Assign, + LHS: []parser.Expr{kwargs.Names[i]}, + RHS: []parser.Expr{v}, + }, + }, + }, + }) + } + } + + node.Body.Stmts = append(stmts, node.Body.Stmts...) } if err := c.Compile(node.Body); err != nil { @@ -418,7 +516,7 @@ func (c *Compiler) Compile(node parser.Node) error { c.optimizeFunc(node) freeSymbols := c.symbolTable.FreeSymbols() - numLocals := c.symbolTable.MaxSymbols() + compiledFunction.NumLocals = c.symbolTable.MaxSymbols() instructions, sourceMap := c.leaveScope() for _, s := range freeSymbols { @@ -474,13 +572,18 @@ func (c *Compiler) Compile(node parser.Node) error { } } - compiledFunction := &CompiledFunction{ - Instructions: instructions, - NumLocals: numLocals, - NumParameters: len(node.Type.Params.List), - VarArgs: node.Type.Params.VarArgs, - SourceMap: sourceMap, + compiledFunction.NumArgs = len(node.Type.Params.Args.List) + if node.Type.Params.Args.VarArgs { + compiledFunction.NumArgs-- } + + compiledFunction.Instructions = instructions + compiledFunction.SourceMap = sourceMap + + for i, name := range compiledFunction.KwargsNames { + compiledFunction.Kwargs[name] = i + } + if len(freeSymbols) > 0 { c.emit(node, parser.OpClosure, c.addConstant(compiledFunction), len(freeSymbols)) @@ -502,19 +605,60 @@ func (c *Compiler) Compile(node parser.Node) error { c.emit(node, parser.OpReturn, 1) } case *parser.CallExpr: + // FUNC + // ARGS + // VAR ARGS + // KWARGS + // VAR KWARGS if err := c.Compile(node.Func); err != nil { return err } - for _, arg := range node.Args { + + for _, arg := range node.Args.Values { if err := c.Compile(arg); err != nil { return err } } - ellipsis := 0 - if node.Ellipsis.IsValid() { - ellipsis = 1 + + var ( + normalValues = node.Kwargs.Values + hasVarKwargs = node.Kwargs.Ellipsis.IsValid() + varArgs, + varKwargs, + kw int + ) + + if node.Args.Ellipsis.IsValid() { + varArgs = 1 } - c.emit(node, parser.OpCall, len(node.Args), ellipsis) + + if hasVarKwargs { + normalValues = normalValues[:len(normalValues)-1] + } + + if len(normalValues) > 0 { + kw = len(normalValues) + kwargs := &parser.MapLit{Elements: make([]*parser.MapElementLit, kw)} + + for i, arg := range normalValues { + kwargs.Elements[i] = &parser.MapElementLit{ + Key: node.Kwargs.Names[i].Name, + Value: arg, + } + } + if err := c.Compile(kwargs); err != nil { + return err + } + } + + if hasVarKwargs { + varKwargs = 1 + if err := c.Compile(node.Kwargs.Values[len(node.Kwargs.Values)-1]); err != nil { + return err + } + } + + c.emit(node, parser.OpCall, len(node.Args.Values)-varArgs, varArgs, kw, varKwargs) case *parser.ImportExpr: if node.ModuleName == "" { return c.errorf(node, "empty module name") @@ -534,7 +678,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, 0) + c.emit(node, parser.OpCall, 0, 0, 0, 0) case Object: // builtin module c.emit(node, parser.OpConstant, c.addConstant(v)) default: @@ -560,7 +704,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, 0) + c.emit(node, parser.OpCall, 0, 0, 0, 0) } else { return c.errorf(node, "module '%s' not found", node.ModuleName) } @@ -1030,7 +1174,6 @@ func (c *Compiler) compileModule( if err := moduleCompiler.Compile(file); err != nil { return nil, err } - // code optimization moduleCompiler.optimizeFunc(node) compiledFunc := moduleCompiler.Bytecode().MainFunction diff --git a/compiler_test.go b/compiler_test.go index 21d1f02d..5b4382d6 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -14,6 +14,30 @@ import ( ) func TestCompiler_Compile(t *testing.T) { + expectCompile(t, `callee`, + bytecode( + concatInsts( + tengo.MakeInstruction(parser.OpCallee), + tengo.MakeInstruction(parser.OpPop), + tengo.MakeInstruction(parser.OpSuspend)), + objectsArray())) + + expectCompile(t, `argv`, + bytecode( + concatInsts( + tengo.MakeInstruction(parser.OpCalledArgs), + tengo.MakeInstruction(parser.OpPop), + tengo.MakeInstruction(parser.OpSuspend)), + objectsArray())) + + expectCompile(t, `kwargv`, + bytecode( + concatInsts( + tengo.MakeInstruction(parser.OpCalledKwargs), + tengo.MakeInstruction(parser.OpPop), + tengo.MakeInstruction(parser.OpSuspend)), + objectsArray())) + expectCompile(t, `1 + 2`, bytecode( concatInsts( @@ -495,7 +519,7 @@ func TestCompiler_Compile(t *testing.T) { tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpArray, 2), - tengo.MakeInstruction(parser.OpCall, 1, 1), + tengo.MakeInstruction(parser.OpCall, 0, 1, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -732,7 +756,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, 0), + tengo.MakeInstruction(parser.OpCall, 1, 0, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -750,7 +774,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, 0), + tengo.MakeInstruction(parser.OpCall, 3, 0, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -768,7 +792,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, 0), + tengo.MakeInstruction(parser.OpCall, 3, 0, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( @@ -804,7 +828,7 @@ func TestCompiler_Compile(t *testing.T) { concatInsts( tengo.MakeInstruction(parser.OpGetBuiltin, 0), tengo.MakeInstruction(parser.OpArray, 0), - tengo.MakeInstruction(parser.OpCall, 1, 0), + tengo.MakeInstruction(parser.OpCall, 1, 0, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) @@ -819,7 +843,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, 0), + tengo.MakeInstruction(parser.OpCall, 1, 0, 0, 0), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `func(a) { func(b) { return a + b } }`, @@ -1447,8 +1471,8 @@ func compiledFunction( insts ...[]byte, ) *tengo.CompiledFunction { return &tengo.CompiledFunction{ - Instructions: concatInsts(insts...), - NumLocals: numLocals, - NumParameters: numParams, + Instructions: concatInsts(insts...), + NumLocals: numLocals, + NumArgs: numParams, } } diff --git a/context.go b/context.go new file mode 100644 index 00000000..bd2a2670 --- /dev/null +++ b/context.go @@ -0,0 +1,74 @@ +package tengo + +import ( + "context" + "fmt" +) + +// VmContext represents the VM Context +type VmContext struct { + context.Context + VM *VM +} + +// CallContext represents the Go function Call Context +type CallContext struct { + // VM current VM + VM *VM + // Args called args + Args []Object + // Kwargs called keyword arguments + Kwargs map[string]Object +} + +// GetArgs destructure args into dest +func (this *CallContext) GetArgs(dest ...*Object) error { + if len(dest) != len(this.Args) { + return fmt.Errorf( + "wrong number of arguments: want=%d, got=%d", + len(dest), len(this.Args)) + } + for i, v := range dest { + *v = this.Args[i] + } + return nil +} + +// GetArgsVar destructure args into dest and other args into argVar +func (this *CallContext) GetArgsVar(argVar *[]Object, dest ...*Object) error { + if len(dest) != len(this.Args) { + return fmt.Errorf( + "wrong number of arguments: want=>%d, got=%d", + len(dest), len(this.Args)) + } + for i, v := range this.Args { + *dest[i] = v + } + *argVar = this.Args[len(dest):] + return nil +} + +// GetKwargs destructure kwargs into dest +func (this *CallContext) GetKwargs(dest map[string]*Object) error { + for k, v := range this.Kwargs { + if _, ok := dest[k]; ok { + *dest[k] = v + } else { + return fmt.Errorf("unexpected kwarg: %q", k) + } + } + return nil +} + +// GetKwargsVar destructure kwargs into dest and other kwargs into argVar +func (this *CallContext) GetKwargsVar(argVar map[string]Object, dest map[string]*Object) { + for k, v := range this.Kwargs { + if _, ok := dest[k]; ok { + if v != UndefinedValue { + *dest[k] = v + } + } else { + argVar[k] = v + } + } +} diff --git a/docs/builtins.md b/docs/builtins.md index 940a215a..7fe7055d 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -323,3 +323,13 @@ immutable map, string, and bytes are iterable types in Tengo. ## is_time Returns `true` if the object's type is time. Or it returns `false`. + +## map + +Make new `map` object from arguments and keyword arguments. + +```golang +map(a=1,b=2) // == {"a":1,"b":2} +map({"a":1},{"b":2};c=3) // == {"a":1,"b":2,"c":3} +map({"a":1},{"b":2};c=3, {"d":4}...) // == {"a":1,"b":2,"c":3,"d":4} +``` diff --git a/docs/objects.md b/docs/objects.md index 4b150601..ac4a5e6e 100644 --- a/docs/objects.md +++ b/docs/objects.md @@ -134,7 +134,7 @@ CanCall should return whether the Object can be called. When this function returns true, the Object is considered Callable. ```golang -Call(args ...Object) (ret Object, err error) +Call(ctx *CallContext) (ret Object, err error) ``` Call should take an arbitrary number of arguments and return a return value @@ -208,6 +208,7 @@ These are the basic types Tengo runtime supports out of the box: [CompiledFunction](https://godoc.org/github.com/d5/tengo#CompiledFunction), [BuiltinFunction](https://godoc.org/github.com/d5/tengo#BuiltinFunction), [UserFunction](https://godoc.org/github.com/d5/tengo#UserFunction) + [UserFunctionCtx](https://godoc.org/github.com/d5/tengo#UserFunctionCtx) - [Iterators](https://godoc.org/github.com/d5/tengo#Iterator): [StringIterator](https://godoc.org/github.com/d5/tengo#StringIterator), [ArrayIterator](https://godoc.org/github.com/d5/tengo#ArrayIterator), @@ -218,6 +219,7 @@ These are the basic types Tengo runtime supports out of the box: - Other internal objects: [Break](https://godoc.org/github.com/d5/tengo#Break), [Continue](https://godoc.org/github.com/d5/tengo#Continue), [ReturnValue](https://godoc.org/github.com/d5/tengo#ReturnValue) + [CallContext](https://godoc.org/github.com/d5/tengo#CallContext) See [Runtime Types](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) @@ -359,17 +361,20 @@ func (o *StringArray) CanCall() bool { return true } -func (o *StringArray) Call(args ...tengo.Object) (ret tengo.Object, err error) { - if len(args) != 1 { +func (o *StringArray) Call(ctx *CallContext) (ret tengo.Object, err error) { + if len(ctx.Args) != 1 { return nil, tengo.ErrWrongNumArguments } + if len(ctx.Kwargs) > 0 { + return nil, tengo.ErrUnexpectedKwargs + } - s1, ok := tengo.ToString(args[0]) + s1, ok := tengo.ToString(ctx.Args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string", - Found: args[0].TypeName(), + Found: ctx.Args[0].TypeName(), } } diff --git a/docs/tutorial.md b/docs/tutorial.md index 92c7a3ce..a19a30a5 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -22,14 +22,14 @@ func() { /*...*/ } // function value Here's a list of all available value types in Tengo. -| Tengo Type | Description | Equivalent Type in Go | -| :---: | :---: | :---: | +| Tengo Type | Description | Equivalent Type in Go | +|:---:| :---: | :---: | | int | signed 64-bit integer value | `int64` | | float | 64-bit floating point value | `float64` | | bool | boolean value | `bool` | | char | unicode character | `rune` | -| string | unicode string | `string` | -| bytes | byte array | `[]byte` | +| string | unicode string | `string` | +| bytes | byte array | `[]byte` | | error | [error](#error-values) value | - | | time | time value | `time.Time` | | array | value array _(mutable)_ | `[]interface{}` | @@ -37,6 +37,7 @@ Here's a list of all available value types in Tengo. | map | value map with string keys _(mutable)_ | `map[string]interface{}` | | immutable map | [immutable](#immutable-values) map | - | | undefined | [undefined](#undefined-values) value | - | +| default | [default](#default-values) value | - | | function | [function](#function-values) value | - | | _user-defined_ | value of [user-defined types](https://github.com/d5/tengo/blob/master/docs/objects.md) | - | @@ -117,6 +118,19 @@ c := {a: "foo"}["b"] // c == undefined d := int("foo") // d == undefined ``` +### Default Values + +In Tengo, an "default" value can be used to represent an default keyword argument value: + + +```golang +a := func(;b=2) { return b }() // a == 2 +b := func(;b=2) { return b }(b=5) // b == 5 +c := func(;b=default) { return b }() // c == default +d := func(;b=2) { return b }(b=default) // d == 2 +e := func(;b=default) { return b }(b=default) // e == default +``` + ### Array Values In Tengo, array is an ordered list of values of any types. Elements of an array @@ -142,7 +156,7 @@ m["b"] // == false m.c // == "foo" m.x // == undefined -{a: [1,2,3], b: {c: "foo", d: "bar"}} // ok: map with an array element and a map element +{a: [1,2,3], b: {c: "foo", d: "bar"}} // ok: map with an array element and a map element ``` ### Function Values @@ -151,6 +165,39 @@ In Tengo, function is a callable value with a number of function arguments and a return value. Just like any other values, functions can be passed into or returned from another function. +Syntaxe: `func( [ARGS] [; KWARGS] )`. + + +Basic examples: + - Only args: + - `func( arg1, ...other_args )` + - `func( arg1, argN, ...other_args )` + - `func( ...args )` + - `func( ... )` - anonymous args variadic + - Only kwargs (see call examples below): + - `func( ;kw1=1, ...other_kwargs )` + - `func( ;kw1=1, kwN="n", ...other_kwargs )` + - `func( ;...kwargs )` + - `func( ;... )` - anonymous kwargs variadic + - Args and Kwargs (see call examples below): + - `func( arg1, ...other_args ; kw1=1, ...other_kwargs )` + - `func( arg1, argN, ...other_args ; kw1=1, kwN="n", ...other_kwargs )` + - `func( ...args ;...kwargs )` + - `func( ...;... )` - anonymous args and kwargs variadic + - Callee context: this context has three special variables: + - `args`: `argv` is `array` with all called arguments; + - `kwargs`: `kwargv` is `map` with all called keyword arguments; + - `callee`: is a pointer of this function value. + - Examples: + - Only `args`: + - `func( arg1, ...other_args ) { return [argv, kwargv, callee] }` + - `func( ... ; )` + - `args`, `kwargs`: + - `func( arg1, ...other_args ; kw1=1, ...other_kwargs ) { return [argv, kwargv, callee] }` + - `func( ... ; ... ) { return [argv, kwargv, callee] }` + +Examples: + ```golang my_func := func(arg1, arg2) { return arg1 + arg2 @@ -161,6 +208,12 @@ adder := func(base) { } add5 := adder(5) nine := add5(4) // == 9 + +// recursive multiplication implementation +mul := func(n , x) { + return x == 0 ? 0 : n + callee(n, x-1) +} +mul_value = mul(7, 5) // == 35 ``` Unlike Go, Tengo does not have declarations. So the following code is illegal: @@ -185,6 +238,29 @@ variadicClosure := func(a) { } } variadicClosure(1)(2, 3, 4) // [1, 2, [3, 4]] + +// direct call closure +then := func() { return 10 }() // == 10 +// or into parenthesis +eleven := (func() { return 11 })() // == 11 +// direct call recursive closure +mul7to5 := func(n , x) { + return x == 0 ? 0 : n + callee(n, x-1) +}(7, 5) // == 35 +``` + +Anonymous variadic. + +```golang +f := func (a, b, ...) { + return [a, b] +} +f(1, 2, 3, 4) // [1,2] + +f2 := func (...) { + return [] +} +f2(1, 2, 3, 4) // [] ``` Only the last parameter can be variadic. The following code is also illegal: @@ -218,6 +294,92 @@ f2(1, 2, 3) // valid; a = 1, b = [2, 3] f2([1, 2, 3]...) // valid; a = 1, b = [2, 3] ``` +#### Keyword arguments + +Tengo also supports keywords of functions/closures. + +```golang +f := func(;a=1, b=2) { + return a + b +} +f() // == 3 +f(a=default) // == 3 +f(a=2) // == 4 +f(a=2,b=3) // == 5 +f(b=6,{a:6}...) // == 12 +f(b=6,{a:default}...) // (a = 1, b = 6 ) -> a+b == 7 +f(b=6,map(a=6)...) // == 12 +f(b=6,{a:6,c:3}...) // Runtime Error: wrong number of kwargs: want=2, got=3 +``` + +Only the last parameter can be variadic. + +```golang +f := func(;a=1, b=2, ...kw) { + return [a, b, kw] +} +f() // == [1,2,{}] +f(a=2) // == [2,2,{}] +f(b=6,{a:2,c:7}...) // == [2,6,{c:7}] + +f := func(;...kw) { + return kw +} +f() // == {} +f(a=2) // == {a:2} +f(b=6,{a:2,c:7}...) // == {a:2,b:6,c:7} + +// pass only kwargs variadic +f(;{a:2,c:7}...) // == {a:2,b:6,c:7} +``` + +Anonymous variadic: + +```golang +f := func(;a=1, b=2, ...) { + return [a, b] +} +f() // == [1,2] +f(a=2) // == [2,2] +f(b=6,{a:2,c:7}...) // == [2,6] +``` + +Mix args and kwargs. + +```golang +f := func(a, ...args; b=2, ...kwargs) { return [a, b, args, kwargs]} +f = func(a, ...; b=2, ...) { return [a, b] } +// anonymous variadict of args and kwargs +f = func(...; ...) {} +``` + +#### CALLEE + +`callee` - is a keyword to refer a current function scope data. + +```golang +f := func() { return [argv, kwargv, callee] } +f() // == {[], {}, } + +f = func(...;...) { return [argv, kwargv, callee] } +f(1) // == {[1], {}, } +f(1, 2, x = 3, y = 4) // == [[1,2], {x:3,y:4}, ] +f(1, 2; x = 3, y = 4) // == [[1,2], {x:3,y:4}, ] +f([1, 2]...; {x: 3, y: 4}...) // == [[1,2], {x:3,y:4}, ] + +my_args := [1, 2]; +my_kwargs := {x: 3, y: 4} +f(my_args...; my_kwargs...) // == [[1,2], {x:3,y:4}] +f(0, my_args...; z=5, my_kwargs...) // == [[0,1,2], {x:3,y:4,z:5}] +f(append(my_args, 200)...; map(;t="TVAL", my_kwargs...)...) // == [[1,2,200], {t:"TVAL",x:3,y:4}] + +f = func(a, ...args; z="Z_DEFAULT", ...kwargs) { + return [a, z, argv,kwargv] +} +f("a val", my_args...; my_kwargs...) // == ["a val", "Z_DEFAULT", ["a val", 1, 2], {x: 3, y: 4}] +f("a val", my_args...; z="NEW_Z", my_kwargs...)// == ["a val", "NEW_Z", ["a val", 1, 2], {z: "NEW_Z", x: 3, y: 4}] +``` + ## Variables and Scopes A value can be assigned to a variable using assignment operator `:=` and `=`. diff --git a/errors.go b/errors.go index 8ef610a3..080838c9 100644 --- a/errors.go +++ b/errors.go @@ -31,6 +31,9 @@ var ( // ErrWrongNumArguments represents a wrong number of arguments error. ErrWrongNumArguments = errors.New("wrong number of arguments") + // ErrUnexpectedKwargs represents a unexpected kwargs. + ErrUnexpectedKwargs = errors.New("unexpected kwargs") + // ErrBytesLimit represents an error where the size of bytes value exceeds // the limit. ErrBytesLimit = errors.New("exceeding bytes size limit") diff --git a/examples/interoperability/main.go b/examples/interoperability/main.go index b62e2bff..d413a458 100644 --- a/examples/interoperability/main.go +++ b/examples/interoperability/main.go @@ -18,7 +18,8 @@ import ( // a channel to listen result of function. type CallArgs struct { Func string - Params []tengo.Object + Args []tengo.Object + Kwargs map[string]tengo.Object Result chan<- tengo.Object } @@ -29,9 +30,9 @@ func NewGoProxy(ctx context.Context) *GoProxy { mod.callbacks = make(map[string]tengo.Object) mod.callChan = make(chan *CallArgs, 1) mod.moduleMap = map[string]tengo.Object{ - "next": &tengo.UserFunction{Value: mod.next}, - "register": &tengo.UserFunction{Value: mod.register}, - "args": &tengo.UserFunction{Value: mod.args}, + "next": &tengo.UserFunctionCtx{Value: mod.next}, + "register": &tengo.UserFunctionCtx{Value: mod.register}, + "args": &tengo.UserFunctionCtx{Value: mod.args}, } mod.tasks = list.New() return mod @@ -69,7 +70,7 @@ func (mod *GoProxy) CallChan() chan<- *CallArgs { return mod.callChan } -func (mod *GoProxy) next(args ...tengo.Object) (tengo.Object, error) { +func (mod *GoProxy) next(*tengo.CallContext) (tengo.Object, error) { mod.mtx.Lock() defer mod.mtx.Unlock() select { @@ -83,7 +84,8 @@ func (mod *GoProxy) next(args ...tengo.Object) (tengo.Object, error) { } } -func (mod *GoProxy) register(args ...tengo.Object) (tengo.Object, error) { +func (mod *GoProxy) register(ctx *tengo.CallContext) (tengo.Object, error) { + args := ctx.Args if len(args) == 0 { return nil, tengo.ErrWrongNumArguments } @@ -105,7 +107,7 @@ func (mod *GoProxy) register(args ...tengo.Object) (tengo.Object, error) { return tengo.UndefinedValue, nil } -func (mod *GoProxy) args(args ...tengo.Object) (tengo.Object, error) { +func (mod *GoProxy) args(*tengo.CallContext) (tengo.Object, error) { mod.mtx.Lock() defer mod.mtx.Unlock() @@ -126,10 +128,14 @@ func (mod *GoProxy) args(args ...tengo.Object) (tengo.Object, error) { if !ok { return tengo.UndefinedValue, nil } - params := callArgs.Params + params := callArgs.Args if params == nil { params = make([]tengo.Object, 0) } + kwargs := callArgs.Kwargs + if kwargs == nil { + kwargs = map[string]tengo.Object{} + } // callable.VarArgs implementation is omitted. return &tengo.ImmutableMap{ Value: map[string]tengo.Object{ @@ -145,9 +151,9 @@ func (mod *GoProxy) args(args ...tengo.Object) (tengo.Object, error) { } return tengo.UndefinedValue, nil }}, - "num_params": &tengo.Int{Value: int64(compiledFunc.NumParameters)}, - "callable": compiledFunc, - "params": &tengo.Array{Value: params}, + "callable": compiledFunc, + "args": &tengo.Array{Value: params}, + "kwargs": &tengo.Map{Value: callArgs.Kwargs}, }, }, nil } @@ -166,19 +172,7 @@ var ProxySource = ` return } result := args.result - num_params := args.num_params - v := undefined - // add more else if conditions for different number of parameters. - if num_params == 0 { - v = callable() - } else if num_params == 1 { - v = callable(args.params[0]) - } else if num_params == 2 { - v = callable(args.params[0], args.params[1]) - } else if num_params == 3 { - v = callable(args.params[0], args.params[1], args.params[2]) - } - result(v) + result(callable(args.args...;args.kwargs...)) } ` @@ -191,10 +185,10 @@ func main() { global := 0 callbacks := { - sum: func(a, b) { - return a + b + sum: func(a, b;x=0) { + return a + b + x }, - multiply: func(a, b) { + multiply: func(a, b;...) { return a * b }, increment: func() { @@ -243,8 +237,11 @@ func main() { fmt.Println("Calling tengo sum function") i1, i2 := rand.Int63n(100), rand.Int63n(100) callChan <- &CallArgs{Func: "sum", - Params: []tengo.Object{&tengo.Int{Value: i1}, + Args: []tengo.Object{&tengo.Int{Value: i1}, &tengo.Int{Value: i2}}, + Kwargs: map[string]tengo.Object{ + "x": &tengo.Int{Value: 50}, + }, Result: result, } v := <-result @@ -253,7 +250,7 @@ func main() { fmt.Println("Calling tengo multiply function") i1, i2 = rand.Int63n(20), rand.Int63n(20) callChan <- &CallArgs{Func: "multiply", - Params: []tengo.Object{&tengo.Int{Value: i1}, + Args: []tengo.Object{&tengo.Int{Value: i1}, &tengo.Int{Value: i2}}, Result: result, } diff --git a/instructions.go b/instructions.go index eb1fbf27..c67b0273 100644 --- a/instructions.go +++ b/instructions.go @@ -54,6 +54,14 @@ func FormatInstructions(b []byte, posOffset int) []string { out = append(out, fmt.Sprintf("%04d %-7s %-5d %-5d", posOffset+i, parser.OpcodeNames[b[i]], operands[0], operands[1])) + case 3: + out = append(out, fmt.Sprintf("%04d %-7s %-5d %-5d %-5d", + posOffset+i, parser.OpcodeNames[b[i]], + operands[0], operands[1], operands[2])) + case 4: + out = append(out, fmt.Sprintf("%04d %-7s %-5d %-5d %-5d %-5d", + posOffset+i, parser.OpcodeNames[b[i]], + operands[0], operands[1], operands[2], operands[3])) } i += 1 + read } diff --git a/objects.go b/objects.go index 25745586..3a228c4c 100644 --- a/objects.go +++ b/objects.go @@ -21,6 +21,9 @@ var ( // UndefinedValue represents an undefined value. UndefinedValue Object = &Undefined{} + + // DefaultValue represents an undefined kwarg value. + DefaultValue Object = &Default{} ) // Object represents an object in the VM. @@ -73,7 +76,7 @@ type Object interface { // Call should take an arbitrary number of arguments and returns a return // value and/or an error, which the VM will consider as a run-time error. - Call(args ...Object) (ret Object, err error) + Call(ctx *CallContext) (ret Object, err error) // CanCall should return whether the Object can be Called. CanCall() bool @@ -139,7 +142,7 @@ func (o *ObjectImpl) CanIterate() bool { // Call takes an arbitrary number of arguments and returns a return value // and/or an error. -func (o *ObjectImpl) Call(_ ...Object) (ret Object, err error) { +func (o *ObjectImpl) Call(*CallContext) (ret Object, err error) { return nil, nil } @@ -321,7 +324,7 @@ func (o *Bool) GobEncode() (b []byte, err error) { type BuiltinFunction struct { ObjectImpl Name string - Value CallableFunc + Value CallableFuncCtx } // TypeName returns the name of the type. @@ -345,8 +348,8 @@ func (o *BuiltinFunction) Equals(_ Object) bool { } // Call executes a builtin function. -func (o *BuiltinFunction) Call(args ...Object) (Object, error) { - return o.Value(args...) +func (o *BuiltinFunction) Call(ctx *CallContext) (Object, error) { + return o.Value(ctx) } // CanCall returns whether the Object can be Called. @@ -567,15 +570,40 @@ func (o *Char) Equals(x Object) bool { return o.Value == t.Value } +type VarArgMode uint8 + +func (m VarArgMode) Pos() int { + if m == VarArgNamed { + return 1 + } + return 0 +} + +const ( + VarArgNone = iota + VarArgNamed + VarArgAnonymous +) + +// Variadic represents a variadic expression. +type Variadic struct { + Valid bool + Name string +} + // CompiledFunction represents a compiled function. type CompiledFunction struct { ObjectImpl - Instructions []byte - NumLocals int // number of local variables (including function parameters) - NumParameters int - VarArgs bool - SourceMap map[int]parser.Pos - Free []*ObjectPtr + Instructions []byte + NumLocals int // number of local variables (including function parameters) + NumArgs int + VarArgs Variadic + Kwargs map[string]int + KwargsDefaults []Object + KwargsNames []string + VarKwargs Variadic + SourceMap map[int]parser.Pos + Free []*ObjectPtr } // TypeName returns the name of the type. @@ -590,11 +618,15 @@ func (o *CompiledFunction) String() string { // Copy returns a copy of the type. func (o *CompiledFunction) Copy() Object { return &CompiledFunction{ - Instructions: append([]byte{}, o.Instructions...), - NumLocals: o.NumLocals, - NumParameters: o.NumParameters, - VarArgs: o.VarArgs, - Free: append([]*ObjectPtr{}, o.Free...), // DO NOT Copy() of elements; these are variable pointers + Instructions: append([]byte{}, o.Instructions...), + NumLocals: o.NumLocals, + NumArgs: o.NumArgs, + VarArgs: o.VarArgs, + Kwargs: o.Kwargs, + KwargsNames: o.KwargsNames, + KwargsDefaults: o.KwargsDefaults, + VarKwargs: o.VarKwargs, + Free: append([]*ObjectPtr{}, o.Free...), // DO NOT Copy() of elements; these are variable pointers } } @@ -1574,6 +1606,66 @@ func (o *Undefined) Value() Object { return o } +// Default represents an undefined value of keyword argument. +type Default struct { + ObjectImpl +} + +// TypeName returns the name of the type. +func (o *Default) TypeName() string { + return "default" +} + +func (o *Default) String() string { + return "" +} + +// Copy returns a copy of the type. +func (o *Default) Copy() Object { + return o +} + +// IsFalsy returns true if the value of the type is falsy. +func (o *Default) IsFalsy() bool { + return true +} + +// Equals returns true if the value of the type is equal to the value of +// another object. +func (o *Default) Equals(x Object) bool { + return o == x +} + +// IndexGet returns an element at a given index. +func (o *Default) IndexGet(_ Object) (Object, error) { + return UndefinedValue, nil +} + +// Iterate creates a map iterator. +func (o *Default) Iterate() Iterator { + return o +} + +// CanIterate returns whether the Object can be Iterated. +func (o *Default) CanIterate() bool { + return true +} + +// Next returns true if there are more elements to iterate. +func (o *Default) Next() bool { + return false +} + +// Key returns the key or index value of the current element. +func (o *Default) Key() Object { + return o +} + +// Value returns the value of the current element. +func (o *Default) Value() Object { + return o +} + // UserFunction represents a user function. type UserFunction struct { ObjectImpl @@ -1602,11 +1694,70 @@ func (o *UserFunction) Equals(_ Object) bool { } // Call invokes a user function. -func (o *UserFunction) Call(args ...Object) (Object, error) { - return o.Value(args...) +func (o *UserFunction) Call(ctx *CallContext) (Object, error) { + if len(ctx.Kwargs) > 0 { + return nil, ErrUnexpectedKwargs + } + return o.Value(ctx.Args...) } // CanCall returns whether the Object can be Called. func (o *UserFunction) CanCall() bool { return true } + +// UserFunctionCtx represents a user function with call context. +type UserFunctionCtx struct { + ObjectImpl + Name string + Value CallableFuncCtx +} + +// TypeName returns the name of the type. +func (o *UserFunctionCtx) TypeName() string { + return "user-function-ctx:" + o.Name +} + +func (o *UserFunctionCtx) String() string { + return "" +} + +// Copy returns a copy of the type. +func (o *UserFunctionCtx) Copy() Object { + return &UserFunctionCtx{Value: o.Value} +} + +// Equals returns true if the value of the type is equal to the value of +// another object. +func (o *UserFunctionCtx) Equals(_ Object) bool { + return false +} + +// Call invokes a user function. +func (o *UserFunctionCtx) Call(ctx *CallContext) (ret Object, err error) { + return o.Value(ctx) +} + +// CanCall returns whether the Object can be Called. +func (o *UserFunctionCtx) CanCall() bool { + return true +} + +// VmContextObject represents the VmContext object +type VmContextObject struct { + ObjectImpl + Value *VmContext +} + +// Copy returns a copy of the type. +func (c *VmContextObject) Copy() Object { + return &VmContextObject{Value: c.Value} +} + +func (c *VmContextObject) TypeName() string { + return "context" +} + +func (c *VmContextObject) String() string { + return "" +} diff --git a/objects_test.go b/objects_test.go index 29a0b7d3..4a609284 100644 --- a/objects_test.go +++ b/objects_test.go @@ -33,6 +33,8 @@ func TestObject_TypeName(t *testing.T) { require.Equal(t, "builtin-function:fn", o.TypeName()) o = &tengo.UserFunction{Name: "fn"} require.Equal(t, "user-function:fn", o.TypeName()) + o = &tengo.UserFunctionCtx{Name: "fn"} + require.Equal(t, "user-function-ctx:fn", o.TypeName()) o = &tengo.CompiledFunction{} require.Equal(t, "compiled-function", o.TypeName()) o = &tengo.Undefined{} @@ -41,6 +43,8 @@ func TestObject_TypeName(t *testing.T) { require.Equal(t, "error", o.TypeName()) o = &tengo.Bytes{} require.Equal(t, "bytes", o.TypeName()) + o = &tengo.VmContextObject{} + require.Equal(t, "context", o.TypeName()) } func TestObject_IsFalsy(t *testing.T) { @@ -125,6 +129,8 @@ func TestObject_String(t *testing.T) { require.Equal(t, "", o.String()) o = &tengo.Bytes{Value: []byte("foo")} require.Equal(t, "foo", o.String()) + o = &tengo.VmContextObject{} + require.Equal(t, "", o.String()) } func TestObject_BinaryOp(t *testing.T) { diff --git a/parser/ast.go b/parser/ast.go index 8c2f7c07..5dec6517 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -1,6 +1,7 @@ package parser import ( + "bytes" "strings" ) @@ -67,3 +68,130 @@ func (n *IdentList) String() string { } return "(" + strings.Join(list, ", ") + ")" } + +// ValuedIdentList represents a list of identifier with value pairs. +type ValuedIdentList struct { + LParen Pos + VarArgs bool + Names []*Ident + Values []Expr + RParen Pos +} + +// Pos returns the position of first character belonging to the node. +func (n *ValuedIdentList) Pos() Pos { + if n.LParen.IsValid() { + return n.LParen + } + if len(n.Names) > 0 { + return n.Names[0].Pos() + } + return NoPos +} + +// End returns the position of first character immediately after the node. +func (n *ValuedIdentList) End() Pos { + if n.RParen.IsValid() { + return n.RParen + 1 + } + if l := len(n.Names); l > 0 { + if n.VarArgs { + return n.Names[l-1].End() + } + return n.Values[l-1].End() + } + return NoPos +} + +// NumFields returns the number of fields. +func (n *ValuedIdentList) NumFields() int { + if n == nil { + return 0 + } + return len(n.Names) +} + +func (n *ValuedIdentList) String() string { + var list []string + for i, e := range n.Names { + if n.VarArgs && i == len(n.Names)-1 { + list = append(list, "..."+e.String()) + } else { + list = append(list, e.String()+" = "+n.Values[i].String()) + } + } + return "(" + strings.Join(list, ", ") + ")" +} + +// FuncParams represents a function paramsw. +type FuncParams struct { + LParen Pos + Args *IdentList + ArgVar *Ident + Kwargs *ValuedIdentList + KwargVar *Ident + RParen Pos +} + +// Pos returns the position of first character belonging to the node. +func (n *FuncParams) Pos() Pos { + if n.LParen.IsValid() { + return n.LParen + } + if n.Args != nil && len(n.Args.List) > 0 { + return n.Args.List[0].Pos() + } + if n.ArgVar != nil { + return n.ArgVar.Pos() + } + if n.Kwargs != nil && len(n.Kwargs.Names) > 0 { + return n.Kwargs.Names[0].Pos() + } + if n.KwargVar != nil { + return n.KwargVar.Pos() + } + return NoPos +} + +// End returns the position of first character immediately after the node. +func (n *FuncParams) End() Pos { + if n.RParen.IsValid() { + return n.RParen + 1 + } + if n.Kwargs != nil && len(n.Kwargs.Names) > 0 { + return n.Kwargs.End() + } + if n.Args != nil && len(n.Args.List) > 0 { + return n.Args.End() + } + return NoPos +} + +func (n *FuncParams) String() string { + buf := bytes.NewBufferString("(") + if n.Args != nil && len(n.Args.List) > 0 { + v := n.Args.String() + buf.WriteString(v[1 : len(v)-1]) + if n.ArgVar != nil { + buf.WriteString(", ") + } + } + if n.ArgVar != nil { + buf.WriteString("...") + buf.WriteString(n.ArgVar.Name) + } + if n.Kwargs != nil && len(n.Kwargs.Names) > 0 { + buf.WriteString("; ") + v := n.Kwargs.String() + buf.WriteString(v[1 : len(v)-1]) + if n.KwargVar != nil { + buf.WriteString(", ") + } + } + if n.KwargVar != nil { + buf.WriteString("...") + buf.WriteString(n.KwargVar.Name) + } + buf.WriteString(")") + return buf.String() +} diff --git a/parser/ast_test.go b/parser/ast_test.go index eb0eb938..4503a5f6 100644 --- a/parser/ast_test.go +++ b/parser/ast_test.go @@ -37,3 +37,44 @@ func TestIdentListString(t *testing.T) { identList, expected, str) } } + +func TestValuedIdentListString(t *testing.T) { + identListVar := &parser.ValuedIdentList{ + Names: []*parser.Ident{ + {Name: "a"}, + {Name: "b"}, + {Name: "c"}, + }, + Values: []parser.Expr{ + &parser.IntLit{Literal: "2"}, + &parser.IntLit{Literal: "3"}, + }, + VarArgs: true, + } + + expectedVar := "(a = 2, b = 3, ...c)" + if str := identListVar.String(); str != expectedVar { + t.Fatalf("expected string of %#v to be %s, got %s", + identListVar, expectedVar, str) + } + + identList := &parser.ValuedIdentList{ + Names: []*parser.Ident{ + {Name: "a"}, + {Name: "b"}, + {Name: "c"}, + }, + Values: []parser.Expr{ + &parser.IntLit{Literal: "2"}, + &parser.IntLit{Literal: "3"}, + &parser.IntLit{Literal: "4"}, + }, + VarArgs: false, + } + + expected := "(a = 2, b = 3, c = 4)" + if str := identList.String(); str != expected { + t.Fatalf("expected string of %#v to be %s, got %s", + identList, expected, str) + } +} diff --git a/parser/expr.go b/parser/expr.go index b6b6c62b..2a2590a3 100644 --- a/parser/expr.go +++ b/parser/expr.go @@ -1,6 +1,7 @@ package parser import ( + "bytes" "strings" "github.com/d5/tengo/v2/token" @@ -109,13 +110,26 @@ func (e *BoolLit) String() string { return e.Literal } +// CallExprArgs represents a call expression arguments. +type CallExprArgs struct { + Values []Expr + Ellipsis Pos +} + +// CallExprKwargs represents a call expression keyword arguments. +type CallExprKwargs struct { + Names []*Ident + Values []Expr + Ellipsis Pos +} + // CallExpr represents a function call expression. type CallExpr struct { - Func Expr - LParen Pos - Args []Expr - Ellipsis Pos - RParen Pos + Func Expr + LParen Pos + Args CallExprArgs + Kwargs CallExprKwargs + RParen Pos } func (e *CallExpr) exprNode() {} @@ -131,14 +145,48 @@ func (e *CallExpr) End() Pos { } func (e *CallExpr) String() string { - var args []string - for _, e := range e.Args { - args = append(args, e.String()) + var buf = bytes.NewBufferString(e.Func.String()) + buf.WriteString("(") + if l := len(e.Args.Values); l > 0 { + for i, e := range e.Args.Values { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(e.String()) + } + if e.Args.Ellipsis.IsValid() { + buf.WriteString("...") + } } - if len(args) > 0 && e.Ellipsis.IsValid() { - args[len(args)-1] = args[len(args)-1] + "..." + if l := len(e.Kwargs.Values); l > 0 { + if len(e.Args.Values) == 0 { + buf.WriteString(";") + } else { + buf.WriteString(", ") + } + + qnt := l + if e.Kwargs.Ellipsis.IsValid() { + qnt-- + } + for i, n := range e.Kwargs.Names[0:qnt] { + if i != 0 { + buf.WriteString(", ") + } + buf.WriteString(n.String()) + buf.WriteString(" = ") + buf.WriteString(e.Kwargs.Values[i].String()) + } + if e.Kwargs.Ellipsis.IsValid() { + if qnt > 0 { + buf.WriteString(", ") + } + buf.WriteString(e.Kwargs.Values[qnt].String()) + buf.WriteString("...") + } } - return e.Func.String() + "(" + strings.Join(args, ", ") + ")" + buf.WriteString(")") + return buf.String() } // CharLit represents a character literal. @@ -262,7 +310,7 @@ func (e *FuncLit) String() string { // FuncType represents a function type definition. type FuncType struct { FuncPos Pos - Params *IdentList + Params *FuncParams } func (e *FuncType) exprNode() {} @@ -599,3 +647,87 @@ func (e *UndefinedLit) End() Pos { func (e *UndefinedLit) String() string { return "undefined" } + +// DefaultLit represents an undefined kwarg literal. +type DefaultLit struct { + TokenPos Pos +} + +func (e *DefaultLit) exprNode() {} + +// Pos returns the position of first character belonging to the node. +func (e *DefaultLit) Pos() Pos { + return e.TokenPos +} + +// End returns the position of first character immediately after the node. +func (e *DefaultLit) End() Pos { + return e.TokenPos + 7 // len("default") == 7 +} + +func (e *DefaultLit) String() string { + return "default" +} + +// CalleeLit represents an literal callee function/closure flag. +type CalleeLit struct { + TokenPos Pos +} + +func (e *CalleeLit) exprNode() {} + +// Pos returns the position of first character belonging to the node. +func (e *CalleeLit) Pos() Pos { + return e.TokenPos +} + +// End returns the position of first character immediately after the node. +func (e *CalleeLit) End() Pos { + return e.TokenPos + 6 // len(callee) == 15 +} + +func (e *CalleeLit) String() string { + return "callee" +} + +// CalledArgsLit represents an literal called arguments. +type CalledArgsLit struct { + TokenPos Pos +} + +func (e *CalledArgsLit) exprNode() {} + +// Pos returns the position of first character belonging to the node. +func (e *CalledArgsLit) Pos() Pos { + return e.TokenPos +} + +// End returns the position of first character immediately after the node. +func (e *CalledArgsLit) End() Pos { + return e.TokenPos + 4 // len("argv") == 4 +} + +func (e *CalledArgsLit) String() string { + return "argv" +} + +// CalledArgsLit represents an literal called arguments. +type CalledKwargsLit struct { + TokenPos Pos +} + +func (e *CalledKwargsLit) exprNode() {} + +// Pos returns the position of first character belonging to the node. +func (e *CalledKwargsLit) Pos() Pos { + return e.TokenPos +} + +// End returns the position of first character immediately after the node. +func (e *CalledKwargsLit) End() Pos { + return e.TokenPos + 6 // len("kwargv") == 6 +} + +func (e *CalledKwargsLit) String() string { + return "kwargv" +} diff --git a/parser/opcodes.go b/parser/opcodes.go index d97f4896..f9cc0a11 100644 --- a/parser/opcodes.go +++ b/parser/opcodes.go @@ -3,7 +3,7 @@ package parser // Opcode represents a single byte operation code. type Opcode = byte -// List of opcodes +// Names of opcodes const ( OpConstant Opcode = iota // Load constant OpBComplement // bitwise complement @@ -19,6 +19,10 @@ const ( OpOrJump // Logical OR jump OpJump // Jump OpNull // Push null + OpNullKwarg // Push null_kwarg + OpCallee // Push callee + OpCalledArgs // Push argv + OpCalledKwargs // Push kwargv OpArray // Array object OpMap // Map object OpError // Error object @@ -65,6 +69,7 @@ var OpcodeNames = [...]string{ OpOrJump: "ORJMP", OpJump: "JMP", OpNull: "NULL", + OpNullKwarg: "NULLKW", OpGetGlobal: "GETG", OpSetGlobal: "SETG", OpSetSelGlobal: "SETSG", @@ -111,6 +116,7 @@ var OpcodeOperands = [...][]int{ OpOrJump: {2}, OpJump: {2}, OpNull: {}, + OpNullKwarg: {}, OpGetGlobal: {2}, OpSetGlobal: {2}, OpSetSelGlobal: {2, 1}, @@ -120,7 +126,7 @@ var OpcodeOperands = [...][]int{ OpImmutable: {}, OpIndex: {}, OpSliceIndex: {}, - OpCall: {1, 1}, + OpCall: {1, 1, 1, 1}, OpReturn: {1}, OpGetLocal: {1}, OpSetLocal: {1}, diff --git a/parser/parser.go b/parser/parser.go index fd20423b..4bd9dc78 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -269,27 +269,94 @@ func (p *Parser) parseCall(x Expr) *CallExpr { lparen := p.expect(token.LParen) p.exprLevel++ - var list []Expr - 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 + var ( + args CallExprArgs + kwargs CallExprKwargs + ) + + for p.token != token.RParen && p.token != token.EOF && p.token != token.Semicolon && !args.Ellipsis.IsValid() { + args.Values = append(args.Values, p.parseExpr()) + switch p.token { + case token.Semicolon: + goto kw + case token.Assign: // is kwarg + name := args.Values[len(args.Values)-1] + + if ident, ok := name.(*Ident); !ok { + p.errorExpected(name.Pos(), "kwarg name") + return nil + } else { + args.Values = args.Values[0 : len(args.Values)-1] + kwargs.Names = []*Ident{ident} + p.next() + kwargs.Values = append(kwargs.Values, p.parseExpr()) + goto kw + } + case token.Ellipsis: + args.Ellipsis = p.pos p.next() + goto kw } if !p.expectComma(token.RParen, "call argument") { break } } +kw: + if p.token == token.Semicolon || p.token == token.Comma { + p.next() + + for p.token != token.RParen && p.token != token.EOF && !kwargs.Ellipsis.IsValid() { + if p.token == token.LBrace { + val := p.parseMapLit() + kwargs.Ellipsis = p.pos + p.expect(token.Ellipsis) + kwargs.Values = append(kwargs.Values, val) + break + } + expr := p.parsePrimaryExpr() + switch t := expr.(type) { + case *Ident: + kwargs.Names = append(kwargs.Names, t) + case *CallExpr, *SelectorExpr: + kwargs.Values = append(kwargs.Values, t) + kwargs.Ellipsis = p.pos + p.expect(token.Ellipsis) + if !p.expectComma(token.RParen, "call argument") { + goto done + } + default: + pos := p.pos + p.errorExpected(pos, "ident|selector|call") + p.advance(stmtStart) + goto done + } + if p.token == token.Ellipsis { + kwargs.Values = append(kwargs.Values, kwargs.Names[len(kwargs.Names)-1]) + kwargs.Names = kwargs.Names[0 : len(kwargs.Names)-1] + kwargs.Ellipsis = p.pos + p.next() + break + } + + p.expect(token.Assign) + kwargs.Values = append(kwargs.Values, p.parseExpr()) + + if !p.expectComma(token.RParen, "call argument") { + break + } + } + } + +done: p.exprLevel-- rparen := p.expect(token.RParen) return &CallExpr{ - Func: x, - LParen: lparen, - RParen: rparen, - Ellipsis: ellipsis, - Args: list, + Func: x, + LParen: lparen, + RParen: rparen, + Args: args, + Kwargs: kwargs, } } @@ -423,8 +490,24 @@ func (p *Parser) parseOperand() Expr { x := &UndefinedLit{TokenPos: p.pos} p.next() return x + case token.Default: + x := &DefaultLit{TokenPos: p.pos} + p.next() + return x case token.Import: return p.parseImportExpr() + case token.Callee: + x := &CalleeLit{TokenPos: p.pos} + p.next() + return x + case token.CalledArgs: + x := &CalledArgsLit{TokenPos: p.pos} + p.next() + return x + case token.CalledKwargs: + x := &CalledKwargsLit{TokenPos: p.pos} + p.next() + return x case token.LParen: lparen := p.pos p.next() @@ -578,7 +661,7 @@ func (p *Parser) parseFuncType() *FuncType { } pos := p.expect(token.Func) - params := p.parseIdentList() + params := p.parseFuncParams() return &FuncType{ FuncPos: pos, Params: params, @@ -627,37 +710,100 @@ func (p *Parser) parseIdent() *Ident { } } -func (p *Parser) parseIdentList() *IdentList { +func (p *Parser) parseFuncParams() *FuncParams { if p.trace { - defer untracep(tracep(p, "IdentList")) + defer untracep(tracep(p, "FuncParams")) } - var params []*Ident + var ( + args = &IdentList{} + kwargs = &ValuedIdentList{} + ) + lparen := p.expect(token.LParen) - isVarArgs := false if p.token != token.RParen { - if p.token == token.Ellipsis { - isVarArgs = true + if p.token == token.Semicolon { + goto kws + } else if p.token == token.Ellipsis { + args.VarArgs = true + p.next() + switch p.token { + case token.Semicolon, token.Comma, token.RParen: + args.List = append(args.List, &Ident{}) + goto kws + } + } + + args.List = append(args.List, p.parseIdent()) + for !args.VarArgs && p.token == token.Comma { p.next() + if p.token == token.Semicolon { + goto kws + } else if p.token == token.Ellipsis { + args.VarArgs = true + p.next() + switch p.token { + case token.Semicolon, token.RParen: + args.List = append(args.List, &Ident{}) + goto kws + } + } + args.List = append(args.List, p.parseIdent()) } - params = append(params, p.parseIdent()) - for !isVarArgs && p.token == token.Comma { + kws: + if p.token == token.Semicolon { p.next() - if p.token == token.Ellipsis { - isVarArgs = true + switch p.token { + case token.Semicolon: + goto done + case token.Ellipsis: + kwargs.VarArgs = true p.next() + switch p.token { + case token.RParen, token.Semicolon: + kwargs.Names = append(kwargs.Names, &Ident{}) + goto done + } + kwargs.Names = append(kwargs.Names, p.parseIdent()) + default: + kwargs.Names = append(kwargs.Names, p.parseIdent()) + p.expect(token.Assign) + kwargs.Values = append(kwargs.Values, p.parseUnaryExpr()) + + for p.token == token.Comma { + p.next() + if p.token == token.Ellipsis { + p.next() + kwargs.VarArgs = true + switch p.token { + case token.Semicolon, token.RParen: + kwargs.Names = append(kwargs.Names, &Ident{}) + goto done + } + kwargs.Names = append(kwargs.Names, p.parseIdent()) + break + } else { + kwargs.Names = append(kwargs.Names, p.parseIdent()) + p.expect(token.Assign) + if p.token == token.LParen { + val := p.parseUnaryExpr().(*ParenExpr) + kwargs.Values = append(kwargs.Values, val.Expr) + } else { + kwargs.Values = append(kwargs.Values, p.parseUnaryExpr()) + } + } + } } - params = append(params, p.parseIdent()) } } - +done: rparen := p.expect(token.RParen) - return &IdentList{ - LParen: lparen, - RParen: rparen, - VarArgs: isVarArgs, - List: params, + return &FuncParams{ + LParen: lparen, + RParen: rparen, + Args: args, + Kwargs: kwargs, } } @@ -670,9 +816,9 @@ func (p *Parser) parseStmt() (stmt Stmt) { case // simple statements token.Func, token.Error, token.Immutable, token.Ident, token.Int, token.Float, token.Char, token.String, token.True, token.False, - token.Undefined, token.Import, token.LParen, token.LBrace, - token.LBrack, token.Add, token.Sub, token.Mul, token.And, token.Xor, - token.Not: + token.Undefined, token.Default, token.Import, token.LParen, + token.LBrace, token.LBrack, token.Add, token.Sub, token.Mul, token.And, + token.Xor, token.Not, token.Callee, token.CalledArgs, token.CalledKwargs: s := p.parseSimpleStmt(false) p.expectSemi() return s diff --git a/parser/parser_test.go b/parser/parser_test.go index 4ee6ad7a..12bb3ab1 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -251,6 +251,24 @@ func TestParseAssignment(t *testing.T) { }) } +func TestParseCallee(t *testing.T) { + expectParse(t, "callee", func(p pfn) []Stmt { + return stmts( + exprStmt( + caleeLit(p(1, 1)))) + }) + expectParse(t, "argv", func(p pfn) []Stmt { + return stmts( + exprStmt( + caledArgsLit(p(1, 1)))) + }) + expectParse(t, "kwargv", func(p pfn) []Stmt { + return stmts( + exprStmt( + caledKwargsLit(p(1, 1)))) + }) +} + func TestParseBoolean(t *testing.T) { expectParse(t, "true", func(p pfn) []Stmt { return stmts( @@ -285,15 +303,39 @@ func TestParseBoolean(t *testing.T) { } func TestParseCall(t *testing.T) { + expectParse(t, "add(;x=2)", func(p pfn) []Stmt { + return stmts( + exprStmt( + callExpr( + ident("add", p(1, 1)), + p(1, 4), p(1, 9), + kwargs(NoPos, + ident("x", p(1, 6)), + intLit(2, p(1, 8)), + )))) + }) + expectParse(t, "add(x=2)", func(p pfn) []Stmt { + return stmts( + exprStmt( + callExpr( + ident("add", p(1, 1)), + p(1, 4), p(1, 8), + kwargs(NoPos, + ident("x", p(1, 5)), + intLit(2, p(1, 7)), + )))) + }) expectParse(t, "add(1, 2, 3)", func(p pfn) []Stmt { return stmts( exprStmt( callExpr( ident("add", p(1, 1)), - p(1, 4), p(1, 12), NoPos, - intLit(1, p(1, 5)), - intLit(2, p(1, 8)), - intLit(3, p(1, 11))))) + p(1, 4), p(1, 12), + args(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 { @@ -301,10 +343,12 @@ func TestParseCall(t *testing.T) { 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))))) + p(1, 4), p(1, 15), + args(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 { @@ -315,10 +359,12 @@ func TestParseCall(t *testing.T) { exprs( callExpr( ident("add", p(1, 5)), - p(1, 8), p(1, 16), NoPos, - intLit(1, p(1, 9)), - intLit(2, p(1, 12)), - intLit(3, p(1, 15)))), + p(1, 8), p(1, 16), + args(NoPos, + intLit(1, p(1, 9)), + intLit(2, p(1, 12)), + intLit(3, p(1, 15)), + ))), token.Assign, p(1, 3))) }) @@ -332,10 +378,12 @@ func TestParseCall(t *testing.T) { exprs( callExpr( ident("add", p(1, 8)), - p(1, 11), p(1, 19), NoPos, - intLit(1, p(1, 12)), - intLit(2, p(1, 15)), - intLit(3, p(1, 18)))), + p(1, 11), p(1, 19), + args(NoPos, + intLit(1, p(1, 12)), + intLit(2, p(1, 15)), + intLit(3, p(1, 18)), + ))), token.Assign, p(1, 6))) }) @@ -345,24 +393,26 @@ func TestParseCall(t *testing.T) { exprStmt( callExpr( ident("add", p(1, 1)), - p(1, 4), p(1, 26), NoPos, - binaryExpr( - ident("a", p(1, 5)), - intLit(1, p(1, 9)), - token.Add, - p(1, 7)), - binaryExpr( - intLit(2, p(1, 12)), - intLit(1, p(1, 16)), - token.Mul, - p(1, 14)), - parenExpr( + p(1, 4), p(1, 26), + args(NoPos, binaryExpr( - ident("b", p(1, 20)), - ident("c", p(1, 24)), + ident("a", p(1, 5)), + intLit(1, p(1, 9)), token.Add, - p(1, 22)), - p(1, 19), p(1, 25))))) + p(1, 7)), + binaryExpr( + intLit(2, p(1, 12)), + intLit(1, p(1, 16)), + token.Mul, + p(1, 14)), + parenExpr( + binaryExpr( + ident("b", p(1, 20)), + ident("c", p(1, 24)), + token.Add, + p(1, 22)), + p(1, 19), p(1, 25)), + )))) }) expectParseString(t, "a + add(b * c) + d", "((a + add((b * c))) + d)") @@ -378,12 +428,13 @@ func TestParseCall(t *testing.T) { callExpr( funcLit( funcType( - identList( - p(1, 5), p(1, 10), + identList(NoPos, NoPos, false, ident("a", p(1, 6)), ident("b", p(1, 9))), - p(1, 1)), + nil, + p(1, 1), + p(1, 5), p(1, 10)), blockStmt( p(1, 12), p(1, 20), exprStmt( @@ -392,9 +443,11 @@ func TestParseCall(t *testing.T) { ident("b", p(1, 18)), token.Add, p(1, 16))))), - p(1, 21), p(1, 26), NoPos, - intLit(1, p(1, 22)), - intLit(2, p(1, 25))))) + p(1, 21), p(1, 26), + args(NoPos, + intLit(1, p(1, 22)), + intLit(2, p(1, 25)), + )))) }) expectParse(t, `a.b()`, func(p pfn) []Stmt { @@ -404,7 +457,7 @@ func TestParseCall(t *testing.T) { selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), - p(1, 4), p(1, 5), NoPos))) + p(1, 4), p(1, 5)))) }) expectParse(t, `a.b.c()`, func(p pfn) []Stmt { @@ -416,7 +469,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), NoPos))) + p(1, 6), p(1, 7)))) }) expectParse(t, `a["b"].c()`, func(p pfn) []Stmt { @@ -429,13 +482,11 @@ 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), NoPos))) + p(1, 9), p(1, 10)))) }) 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, ..., )`) @@ -721,11 +772,12 @@ func TestParseFunction(t *testing.T) { exprs( funcLit( funcType( - identList(p(1, 9), p(1, 17), false, + identList(NoPos, NoPos, false, ident("b", p(1, 10)), ident("c", p(1, 13)), ident("d", p(1, 16))), - p(1, 5)), + nil, + p(1, 5), p(1, 9), p(1, 17)), blockStmt(p(1, 19), p(1, 30), returnStmt(p(1, 21), ident("d", p(1, 28)))))), token.Assign, @@ -743,10 +795,10 @@ func TestParseVariadicFunction(t *testing.T) { funcLit( funcType( identList( - p(1, 9), p(1, 17), + NoPos, NoPos, true, ident("args", p(1, 13)), - ), p(1, 5)), + ), nil, p(1, 5), p(1, 9), p(1, 17)), blockStmt(p(1, 19), p(1, 33), returnStmt(p(1, 21), ident("args", p(1, 28)), @@ -769,12 +821,12 @@ func TestParseVariadicFunctionWithArgs(t *testing.T) { funcLit( funcType( identList( - p(1, 9), p(1, 20), + NoPos, NoPos, true, ident("x", p(1, 10)), ident("y", p(1, 13)), ident("z", p(1, 19)), - ), p(1, 5)), + ), nil, p(1, 5), p(1, 9), p(1, 20)), blockStmt(p(1, 22), p(1, 33), returnStmt(p(1, 24), ident("z", p(1, 31)), @@ -790,6 +842,77 @@ func TestParseVariadicFunctionWithArgs(t *testing.T) { expectParseError(t, "a = func(...args, invalid) { return args }") } +func TestParseVariadicFunctionWithKwargs(t *testing.T) { + expectParse(t, `a = func(; x=1, y="a", ...z) { return z }`, func(p pfn) []Stmt { + return stmts( + assignStmt( + exprs( + ident("a", p(1, 1))), + exprs( + funcLit( + funcType(nil, + valuedIdentList( + NoPos, NoPos, + true, + ident("x", p(1, 12)), + intLit(1, 14), + ident("y", p(1, 17)), + stringLit("a", 19), + ident("z", p(1, 27)), + ), p(1, 5), p(1, 9), p(1, 28)), + blockStmt(p(1, 30), p(1, 41), + returnStmt(p(1, 32), + ident("z", p(1, 39)), + ), + ), + ), + ), + token.Assign, + p(1, 3))) + }) + + expectParseError(t, "a = func(;x=1, y=2, ...z, invalid) { return z }") + expectParseError(t, "a = func(;x=1, y=2, ...z, invalid=3) { return z }") + expectParseError(t, "a = func(;...kargs, invalid) { return kargs }") + expectParseError(t, "a = func(;...kargs, invalid=3) { return kargs }") +} + +func TestParseVariadicFunctionWithArgsAndKwargs(t *testing.T) { + expectParse(t, `a = func(d; x=1, y="a", ...z) { return z }`, func(p pfn) []Stmt { + return stmts( + assignStmt( + exprs( + ident("a", p(1, 1))), + exprs( + funcLit( + funcType( + identList(NoPos, NoPos, false, ident("d", 10)), + valuedIdentList( + NoPos, NoPos, + true, + ident("x", p(1, 13)), + intLit(1, 15), + ident("y", p(1, 18)), + stringLit("a", 20), + ident("z", p(1, 28)), + ), p(1, 5), p(1, 9), p(1, 29)), + blockStmt(p(1, 31), p(1, 42), + returnStmt(p(1, 33), + ident("z", p(1, 40)), + ), + ), + ), + ), + token.Assign, + p(1, 3))) + }) + + expectParseError(t, "a = func(a;x=1, y=2, ...z, invalid) { return z }") + expectParseError(t, "a = func(a;x=1, y=2, ...z, invalid=3) { return z }") + expectParseError(t, "a = func(a;...kargs, invalid) { return kargs }") + expectParseError(t, "a = func(a;...kargs, invalid=3) { return kargs }") +} + func TestParseIf(t *testing.T) { expectParse(t, "if a == 5 {}", func(p pfn) []Stmt { return stmts( @@ -1021,7 +1144,7 @@ func TestParseImport(t *testing.T) { selectorExpr( importExpr("mod1", p(1, 1)), stringLit("func1", p(1, 16))), - p(1, 21), p(1, 22), NoPos))) + p(1, 21), p(1, 22)))) }) expectParse(t, `for x, y in import("mod1") {}`, func(p pfn) []Stmt { @@ -1517,14 +1640,14 @@ func (o *parseTracer) Write(p []byte) (n int, err error) { return len(p), nil } -//type slowPrinter struct { -//} +// type slowPrinter struct { +// } // -//func (o *slowPrinter) Write(p []byte) (n int, err error) { +// func (o *slowPrinter) Write(p []byte) (n int, err error) { // fmt.Print(string(p)) // time.Sleep(25 * time.Millisecond) // return len(p), nil -//} +// } func expectParse(t *testing.T, input string, fn expectedFn) { testFileSet := NewFileSet() @@ -1665,8 +1788,22 @@ func incDecStmt( return &IncDecStmt{Expr: expr, Token: tok, TokenPos: pos} } -func funcType(params *IdentList, pos Pos) *FuncType { - return &FuncType{Params: params, FuncPos: pos} +func funcType(args *IdentList, kwargs *ValuedIdentList, pos, pStartPos, pEndPos Pos) *FuncType { + if args == nil { + args = &IdentList{} + } + if kwargs == nil { + kwargs = &ValuedIdentList{} + } + return &FuncType{ + Params: &FuncParams{ + LParen: pStartPos, + Args: args, + Kwargs: kwargs, + RParen: pEndPos, + }, + FuncPos: pos, + } } func blockStmt(lbrace, rbrace Pos, list ...Stmt) *BlockStmt { @@ -1687,6 +1824,26 @@ func identList( } } +func valuedIdentList( + opening, closing Pos, + varArgs bool, + pairs ...interface{}, +) *ValuedIdentList { + r := &ValuedIdentList{VarArgs: varArgs, LParen: opening, RParen: closing} + var l = len(pairs) + if varArgs { + l-- + } + for i := 0; i < l; i += 2 { + r.Names = append(r.Names, pairs[i].(*Ident)) + r.Values = append(r.Values, pairs[i+1].(Expr)) + } + if varArgs { + r.Names = append(r.Names, pairs[l].(*Ident)) + } + return r +} + func binaryExpr( x, y Expr, op token.Token, @@ -1741,6 +1898,18 @@ func boolLit(value bool, pos Pos) *BoolLit { return &BoolLit{Value: value, ValuePos: pos} } +func caleeLit(pos Pos) *CalleeLit { + return &CalleeLit{TokenPos: pos} +} + +func caledArgsLit(pos Pos) *CalledArgsLit { + return &CalledArgsLit{TokenPos: pos} +} + +func caledKwargsLit(pos Pos) *CalledKwargsLit { + return &CalledKwargsLit{TokenPos: pos} +} + func arrayLit(lbracket, rbracket Pos, list ...Expr) *ArrayLit { return &ArrayLit{LBrack: lbracket, RBrack: rbracket, Elements: list} } @@ -1771,13 +1940,38 @@ func parenExpr(x Expr, lparen, rparen Pos) *ParenExpr { return &ParenExpr{Expr: x, LParen: lparen, RParen: rparen} } -func callExpr( - f Expr, - lparen, rparen, ellipsis Pos, - args ...Expr, -) *CallExpr { - return &CallExpr{Func: f, LParen: lparen, RParen: rparen, - Ellipsis: ellipsis, Args: args} +func callExpr(f Expr, lparen, rparen Pos, args ...interface{}) *CallExpr { + e := &CallExpr{Func: f, LParen: lparen, RParen: rparen} + for _, arg := range args { + switch t := arg.(type) { + case CallExprKwargs: + e.Kwargs = t + case CallExprArgs: + e.Args = t + } + } + return e +} + +func args(ellipsis Pos, args ...Expr) CallExprArgs { + return CallExprArgs{Ellipsis: ellipsis, Values: args} +} + +func kwargs(ellipsis Pos, pairs ...interface{}) (kw CallExprKwargs) { + kw = CallExprKwargs{Ellipsis: ellipsis} + l := len(pairs) + if ellipsis.IsValid() { + l-- + } + for i := 0; i < l; i += 2 { + kw.Names = append(kw.Names, pairs[i].(*Ident)) + kw.Values = append(kw.Values, pairs[i+1].(Expr)) + } + if ellipsis.IsValid() { + kw.Values = append(kw.Values, pairs[l].(Expr)) + } + + return } func indexExpr( @@ -1920,6 +2114,15 @@ func equalExpr(t *testing.T, expected, actual Expr) { actual.(*BoolLit).Value) require.Equal(t, int(expected.ValuePos), int(actual.(*BoolLit).ValuePos)) + case *CalleeLit: + require.Equal(t, expected.TokenPos, + actual.(*CalleeLit).TokenPos) + case *CalledArgsLit: + require.Equal(t, expected.TokenPos, + actual.(*CalledArgsLit).TokenPos) + case *CalledKwargsLit: + require.Equal(t, expected.TokenPos, + actual.(*CalledKwargsLit).TokenPos) case *CharLit: require.Equal(t, expected.Value, actual.(*CharLit).Value) @@ -1972,8 +2175,12 @@ func equalExpr(t *testing.T, expected, actual Expr) { actual.(*CallExpr).LParen) require.Equal(t, expected.RParen, actual.(*CallExpr).RParen) - equalExprs(t, expected.Args, - actual.(*CallExpr).Args) + equalExprs(t, expected.Args.Values, + actual.(*CallExpr).Args.Values) + equalExprs(t, expected.Kwargs.Values, + actual.(*CallExpr).Kwargs.Values) + equalIdents(t, expected.Kwargs.Names, + actual.(*CallExpr).Kwargs.Names) case *ParenExpr: equalExpr(t, expected.Expr, actual.(*ParenExpr).Expr) @@ -2041,7 +2248,9 @@ func equalExpr(t *testing.T, expected, actual Expr) { func equalFuncType(t *testing.T, expected, actual *FuncType) { require.Equal(t, expected.Params.LParen, actual.Params.LParen) require.Equal(t, expected.Params.RParen, actual.Params.RParen) - equalIdents(t, expected.Params.List, actual.Params.List) + equalIdents(t, expected.Params.Args.List, actual.Params.Args.List) + equalIdents(t, expected.Params.Kwargs.Names, actual.Params.Kwargs.Names) + equalExprs(t, expected.Params.Kwargs.Values, actual.Params.Kwargs.Values) } func equalIdents(t *testing.T, expected, actual []*Ident) { diff --git a/parser/scanner.go b/parser/scanner.go index f1d820a4..386b752f 100644 --- a/parser/scanner.go +++ b/parser/scanner.go @@ -14,7 +14,7 @@ const bom = 0xFEFF // ScanMode represents a scanner mode. type ScanMode int -// List of scanner modes. +// Names of scanner modes. const ( ScanComments ScanMode = 1 << iota DontInsertSemis @@ -90,7 +90,10 @@ func (s *Scanner) Scan() ( tok = token.Lookup(literal) switch tok { case token.Ident, token.Break, token.Continue, token.Return, - token.Export, token.True, token.False, token.Undefined: + token.Export, token.True, token.False, token.Undefined, + token.Default: + insertSemi = true + case token.CalledArgs, token.CalledKwargs, token.Callee: insertSemi = true } case '0' <= ch && ch <= '9': @@ -284,7 +287,7 @@ func (s *Scanner) scanComment() string { var numCR int if s.ch == '/' { - //-style comment + // -style comment // (the final '\n' is not considered part of the comment) s.next() for s.ch != '\n' && s.ch >= 0 { @@ -342,7 +345,7 @@ func (s *Scanner) findLineEnd() bool { // read ahead until a newline, EOF, or non-comment tok is found for s.ch == '/' || s.ch == '*' { if s.ch == '/' { - //-style comment always contains a newline + // -style comment always contains a newline return true } /*-style comment: look for newline */ diff --git a/parser/scanner_test.go b/parser/scanner_test.go index 1f6ca562..5acdabcb 100644 --- a/parser/scanner_test.go +++ b/parser/scanner_test.go @@ -116,6 +116,10 @@ func TestScanner_Scan(t *testing.T) { {token.If, "if"}, {token.Return, "return"}, {token.Export, "export"}, + {token.Callee, "callee"}, + {token.CalledArgs, "argv"}, + {token.CalledKwargs, "kwargv"}, + {token.Default, "default"}, } // combine @@ -154,7 +158,7 @@ func TestScanner_Scan(t *testing.T) { expectedLiteral = string(parser.StripCR([]byte(tc.literal), tc.literal[1] == '*')) - //-style comment literal doesn't contain newline + // -style comment literal doesn't contain newline if expectedLiteral[1] == '/' { expectedLiteral = expectedLiteral[:len(expectedLiteral)-1] } diff --git a/script.go b/script.go index 82b02f52..74fd57fb 100644 --- a/script.go +++ b/script.go @@ -231,7 +231,7 @@ func (c *Compiled) RunContext(ctx context.Context) (err error) { } } }() - ch <- v.Run() + ch <- v.RunContext(ctx) }() select { diff --git a/script_test.go b/script_test.go index 5e8f3630..319d53cc 100644 --- a/script_test.go +++ b/script_test.go @@ -159,7 +159,7 @@ a += b * 2 arr := [a, b, c] arrstr := string(arr) -map := {a: a, b: b, c: c} +mymap := {a: a, b: b, c: c} d := a + b + c s := 0 @@ -279,7 +279,7 @@ func (o *Counter) Copy() tengo.Object { return &Counter{value: o.value} } -func (o *Counter) Call(_ ...tengo.Object) (tengo.Object, error) { +func (o *Counter) Call(*tengo.CallContext) (tengo.Object, error) { return &tengo.Int{Value: o.value}, nil } diff --git a/stdlib/func_typedefs_test.go b/stdlib/func_typedefs_test.go index 091c5c47..40c241f4 100644 --- a/stdlib/func_typedefs_test.go +++ b/stdlib/func_typedefs_test.go @@ -562,7 +562,7 @@ func funcCall( args ...tengo.Object, ) (tengo.Object, error) { userFunc := &tengo.UserFunction{Value: fn} - return userFunc.Call(args...) + return userFunc.Call(&tengo.CallContext{Args: args}) } func array(elements ...tengo.Object) *tengo.Array { diff --git a/symbol_table.go b/symbol_table.go index 73aaad3b..f4220621 100644 --- a/symbol_table.go +++ b/symbol_table.go @@ -3,7 +3,7 @@ package tengo // SymbolScope represents a symbol scope. type SymbolScope string -// List of symbol scopes +// Names of symbol scopes const ( ScopeGlobal SymbolScope = "GLOBAL" ScopeLocal SymbolScope = "LOCAL" diff --git a/tengo.go b/tengo.go index 490e9aed..42a6efeb 100644 --- a/tengo.go +++ b/tengo.go @@ -34,6 +34,9 @@ const ( // CallableFunc is a function signature for the callable functions. type CallableFunc = func(args ...Object) (ret Object, err error) +// CallableFuncCtx is a function signature for the callable functions. +type CallableFuncCtx = func(ctx *CallContext) (ret Object, err error) + // CountObjects returns the number of objects that a given object o contains. // For scalar value types, it will always be 1. For compound value types, // this will include its elements and all of their elements recursively. @@ -304,6 +307,8 @@ func FromInterface(v interface{}) (Object, error) { return v, nil case CallableFunc: return &UserFunction{Value: v}, nil + case CallableFuncCtx: + return &UserFunctionCtx{Value: v}, nil } return nil, fmt.Errorf("cannot convert to object: %T", v) } diff --git a/token/token.go b/token/token.go index 4e6aa80a..40a71b39 100644 --- a/token/token.go +++ b/token/token.go @@ -7,7 +7,7 @@ var keywords map[string]Token // Token represents a token. type Token int -// List of tokens +// Names of tokens const ( Illegal Token = iota EOF @@ -83,7 +83,11 @@ const ( False In Undefined + Default Import + Callee + CalledArgs + CalledKwargs _keywordEnd ) @@ -157,7 +161,11 @@ var tokens = [...]string{ False: "false", In: "in", Undefined: "undefined", + Default: "default", Import: "import", + Callee: "callee", + CalledArgs: "argv", + CalledKwargs: "kwargv", } func (tok Token) String() string { diff --git a/vm.go b/vm.go index c8365252..4a832ea7 100644 --- a/vm.go +++ b/vm.go @@ -1,6 +1,7 @@ package tengo import ( + "context" "fmt" "sync/atomic" @@ -11,6 +12,8 @@ import ( // frame represents a function call frame. type frame struct { fn *CompiledFunction + args Array + kwargs Map freeVars []*ObjectPtr ip int basePointer int @@ -18,6 +21,7 @@ type frame struct { // VM is a virtual machine that executes the bytecode compiled by Compiler. type VM struct { + context *VmContext constants []Object stack [StackSize]Object sp int @@ -66,6 +70,11 @@ func (v *VM) Abort() { // Run starts the execution. func (v *VM) Run() (err error) { + return v.RunContext(context.Background()) +} + +// RunContext starts the execution with context. +func (v *VM) RunContext(ctx context.Context) (err error) { // reset VM states v.sp = 0 v.curFrame = &(v.frames[0]) @@ -74,6 +83,12 @@ func (v *VM) Run() (err error) { v.ip = -1 v.allocs = v.maxAllocs + 1 + // set context cancelation + defer func() { + v.context = nil + }() + v.context = &VmContext{Context: ctx, VM: v} + v.run() atomic.StoreInt64(&v.aborting, 0) err = v.err @@ -108,6 +123,18 @@ func (v *VM) run() { case parser.OpNull: v.stack[v.sp] = UndefinedValue v.sp++ + case parser.OpNullKwarg: + v.stack[v.sp] = DefaultValue + v.sp++ + case parser.OpCallee: + v.stack[v.sp] = v.curFrame.fn + v.sp++ + case parser.OpCalledArgs: + v.stack[v.sp] = &ImmutableArray{Value: v.curFrame.args.Value} + v.sp++ + case parser.OpCalledKwargs: + v.stack[v.sp] = &ImmutableMap{Value: v.curFrame.kwargs.Value} + v.sp++ case parser.OpBinaryOp: v.ip++ right := v.stack[v.sp-1] @@ -536,65 +563,166 @@ func (v *VM) run() { v.sp++ } case parser.OpCall: - numArgs := int(v.curInsts[v.ip+1]) - spread := int(v.curInsts[v.ip+2]) - v.ip += 2 + var ( + numArgs = int(v.curInsts[v.ip+1]) + hasVarArgs = int(v.curInsts[v.ip+2]) + numKws = int(v.curInsts[v.ip+3]) + hasVarKw = int(v.curInsts[v.ip+4]) + kwargs map[string]Object + hasKws int + ) - value := v.stack[v.sp-1-numArgs] - if !value.CanCall() { - v.err = fmt.Errorf("not callable: %s", value.TypeName()) + v.ip += 4 + if numKws > 0 { + hasKws = 1 + } + + start := v.sp - numArgs - hasVarArgs - hasKws - hasVarKw + value := v.stack[start-1] + + if !v.stack[start-1].CanCall() { + v.err = fmt.Errorf("not callable: %s", v.stack[start-1].TypeName()) return } - if spread == 1 { - v.sp-- - switch arr := v.stack[v.sp].(type) { + args := make([]Object, numArgs) + copy(args, v.stack[start:start+numArgs]) + + if hasVarArgs == 1 { + switch arr := v.stack[start+numArgs].(type) { case *Array: - for _, item := range arr.Value { - v.stack[v.sp] = item - v.sp++ - } - numArgs += len(arr.Value) - 1 + newArgs := make([]Object, numArgs+len(arr.Value)) + copy(newArgs, args) + copy(newArgs[numArgs:], arr.Value) + args = append(args, arr.Value...) case *ImmutableArray: - for _, item := range arr.Value { - v.stack[v.sp] = item - v.sp++ - } - numArgs += len(arr.Value) - 1 + newArgs := make([]Object, numArgs+len(arr.Value)) + copy(newArgs, args) + copy(newArgs[numArgs:], arr.Value) + args = newArgs 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, - // roll up all variadic parameters into an array - realArgs := callee.NumParameters - 1 - varArgs := numArgs - realArgs - if varArgs >= 0 { - numArgs = realArgs + 1 - args := make([]Object, varArgs) - spStart := v.sp - varArgs - for i := spStart; i < v.sp; i++ { - args[i-spStart] = v.stack[i] + if hasKws == 1 { + kwargs = v.stack[start+numArgs+hasVarArgs].(*Map).Value + } + + if hasVarKw == 1 { + if len(kwargs) == 0 { + switch t := v.stack[start+numArgs+hasVarArgs+hasKws].(type) { + case *Map: + kwargs = t.Value + case *ImmutableMap: + kwargs = t.Value + } + } else { + switch t := v.stack[start+numArgs+hasVarArgs+hasKws].(type) { + case *Map: + for k, v := range t.Value { + kwargs[k] = v + } + case *ImmutableMap: + for k, v := range t.Value { + kwargs[k] = v } - v.stack[spStart] = &Array{Value: args} - v.sp = spStart + 1 } } - if numArgs != callee.NumParameters { - if callee.VarArgs { + } + + if callee, ok := v.stack[start-1].(*CompiledFunction); ok { + if numArgs := len(args); numArgs > callee.NumArgs && !callee.VarArgs.Valid { + v.err = fmt.Errorf( + "wrong number of arguments: want=%d, got=%d", + callee.NumArgs, numArgs) + return + } else if numArgs < callee.NumArgs { + if !callee.VarArgs.Valid { v.err = fmt.Errorf( - "wrong number of arguments: want>=%d, got=%d", - callee.NumParameters-1, numArgs) + "wrong number of arguments: want=%d, got=%d", + callee.NumArgs, numArgs) } else { v.err = fmt.Errorf( - "wrong number of arguments: want=%d, got=%d", - callee.NumParameters, numArgs) + "wrong number of arguments: want>=%d, got=%d", + callee.NumArgs, numArgs) } return + } else if numArgs > 0 { + copy(v.stack[start:], args[:callee.NumArgs]) + + if callee.VarArgs.Name != "" { + v.stack[start+callee.NumArgs] = &Array{Value: args[callee.NumArgs:]} + } + } else if callee.VarArgs.Name != "" { + v.stack[start+callee.NumArgs] = &Array{} + } + + v.sp = start + callee.NumArgs + if callee.VarArgs.Name != "" { + v.sp++ + } + + if len(kwargs) == 0 { + for i := range callee.KwargsNames { + v.stack[v.sp] = callee.KwargsDefaults[i] + v.sp++ + } + if callee.VarKwargs.Name != "" { + v.stack[v.sp] = &Map{Value: kwargs} + v.sp++ + } + } else { + if !callee.VarKwargs.Valid { + for name := range kwargs { + if _, ok := callee.Kwargs[name]; !ok { + if len(callee.KwargsNames) == 0 { + v.err = fmt.Errorf( + "unexpected kwargs") + return + } + v.err = fmt.Errorf( + "unexpected kwarg %q", name) + return + } + } + } + + var ( + kwIntersection bool + varKwargs map[string]Object + ) + for i, name := range callee.KwargsNames { + if val, ok := kwargs[name]; ok { + if val == DefaultValue { + v.stack[v.sp] = callee.KwargsDefaults[i] + } else { + v.stack[v.sp] = val + } + + if !kwIntersection { + kwIntersection = true + varKwargs = map[string]Object{} + for key, value := range kwargs { + varKwargs[key] = value + } + } + delete(varKwargs, name) + } else { + v.stack[v.sp] = DefaultValue + } + v.sp++ + } + + if callee.VarKwargs.Name != "" { + if varKwargs == nil { + v.stack[v.sp] = &Map{Value: kwargs} + } else { + v.stack[v.sp] = &Map{Value: varKwargs} + } + v.sp++ + } } // test if it's tail-call @@ -603,12 +731,12 @@ func (v *VM) run() { if nextOp == parser.OpReturn || (nextOp == parser.OpPop && parser.OpReturn == v.curInsts[v.ip+2]) { - for p := 0; p < numArgs; p++ { - v.stack[v.curFrame.basePointer+p] = - v.stack[v.sp-numArgs+p] + max := numArgs + hasVarArgs + numKws + hasKws + for p := 0; p < max; p++ { + v.stack[v.curFrame.basePointer+p] = v.stack[start+p] } - v.sp -= numArgs + 1 - v.ip = -1 // reset IP to beginning of the frame + v.sp = start - 1 // 2 => 1 is cur func + v.ip = -1 // reset IP to beginning of the frame continue } } @@ -619,19 +747,24 @@ func (v *VM) run() { // update call frame v.curFrame.ip = v.ip // store current ip before call - v.curFrame = &(v.frames[v.framesIndex]) + v.curFrame = &v.frames[v.framesIndex] v.curFrame.fn = callee + v.curFrame.args.Value = args + v.curFrame.kwargs.Value = kwargs v.curFrame.freeVars = callee.Free - v.curFrame.basePointer = v.sp - numArgs + v.curFrame.basePointer = start v.curInsts = callee.Instructions v.ip = -1 + v.framesIndex++ - v.sp = v.sp - numArgs + callee.NumLocals + v.sp = v.curFrame.basePointer + callee.NumLocals } else { - var args []Object - args = append(args, v.stack[v.sp-numArgs:v.sp]...) - ret, e := value.Call(args...) - v.sp -= numArgs + 1 + ret, e := v.stack[start-1].Call(&CallContext{ + VM: v, + Args: args, + Kwargs: kwargs, + }) + v.sp = start - 1 // runtime error if e != nil { @@ -641,6 +774,12 @@ func (v *VM) run() { value.TypeName()) return } + if e == ErrUnexpectedKwargs { + v.err = fmt.Errorf( + "unexpected kwargs in call to '%s'", + value.TypeName()) + return + } if e, ok := e.(ErrInvalidArgumentType); ok { v.err = fmt.Errorf( "invalid type for argument '%s' in call to '%s': "+ @@ -672,16 +811,16 @@ func (v *VM) run() { } else { retVal = UndefinedValue } - //v.sp-- + // v.sp-- v.framesIndex-- v.curFrame = &v.frames[v.framesIndex-1] v.curInsts = v.curFrame.fn.Instructions v.ip = v.curFrame.ip - //v.sp = lastFrame.basePointer - 1 + // v.sp = lastFrame.basePointer - 1 v.sp = v.frames[v.framesIndex].basePointer // skip stack overflow check because (newSP) <= (oldSP) v.stack[v.sp-1] = retVal - //v.sp++ + // v.sp++ case parser.OpDefineLocal: v.ip++ localIndex := int(v.curInsts[v.ip]) @@ -763,11 +902,14 @@ func (v *VM) run() { } v.sp -= numFree cl := &CompiledFunction{ - Instructions: fn.Instructions, - NumLocals: fn.NumLocals, - NumParameters: fn.NumParameters, - VarArgs: fn.VarArgs, - Free: free, + Instructions: fn.Instructions, + NumLocals: fn.NumLocals, + NumArgs: fn.NumArgs, + VarArgs: fn.VarArgs, + KwargsNames: fn.KwargsNames, + Kwargs: fn.Kwargs, + VarKwargs: fn.VarKwargs, + Free: free, } v.allocs-- if v.allocs == 0 { @@ -877,6 +1019,11 @@ func (v *VM) IsStackEmpty() bool { return v.sp == 0 } +// VmContext get current context +func (v *VM) Context() *VmContext { + return v.context +} + func indexAssign(dst, src Object, selectors []Object) error { numSel := len(selectors) for sidx := numSel - 1; sidx > 0; sidx-- { diff --git a/vm_test.go b/vm_test.go index feb91f4f..26fa4fcd 100644 --- a/vm_test.go +++ b/vm_test.go @@ -922,12 +922,14 @@ func TestBytes(t *testing.T) { } func TestCall(t *testing.T) { - expectRun(t, `a := { b: func(x) { return x + 2 } }; out = a.b(5)`, - nil, 7) - expectRun(t, `a := { b: { c: func(x) { return x + 2 } } }; out = a.b.c(5)`, - nil, 7) - expectRun(t, `a := { b: { c: func(x) { return x + 2 } } }; out = a["b"].c(5)`, - nil, 7) + expectRun(t, `out = func(;a=2) { return a }()`, nil, 2) + expectRun(t, `out = func() { return 7 }()`, nil, 7) + expectRun(t, `out = func(x, y) { return x+y }(1,2)`, nil, 3) + expectRun(t, `out = func(x, y, ...z) { return x+y+z[0] }(1,2,3)`, nil, 6) + expectRun(t, `out = func(x, y, ...z) { return x+y+z[0]+z[1] }(1,2,3,4)`, nil, 10) + expectRun(t, `a := { b: func(x) { return x + 2 } }; out = a.b(5)`, nil, 7) + expectRun(t, `a := { b: { c: func(x) { return x + 2 } } }; out = a.b.c(5)`, nil, 7) + expectRun(t, `a := { b: { c: func(x) { return x + 2 } } }; out = a["b"].c(5)`, nil, 7) expectError(t, `a := 1 b := func(a, c) { c(a) @@ -938,6 +940,34 @@ c := func(a) { } b(a, c) `, nil, "Runtime Error: not callable: int\n\tat test:7:4\n\tat test:3:4\n\tat test:9:1") + expectRun(t, `out = func(;a=2) { return a }(;a=3)`, nil, 3) + expectRun(t, `out = func(x;a=2) { return x+a }(1)`, nil, 3) + expectRun(t, `out = func(x;a=2,b=3) { return x+a+b }(1)`, nil, 6) + expectRun(t, `out = func(x;a=2) { return x+a }(1;a=3)`, nil, 4) + expectRun(t, `out = func(x;a=2) { return x+a }(1;a=3,{"a":4}...)`, nil, 5) + expectRun(t, `out = func(x;a=2) { return x+a }(1;a=3,{"a":default}...)`, nil, 3) + expectRun(t, `out = func(...z;a="A", b="B", ...c) { return [z,a,b,c] }(5,[6,7,8,9]...;{"a":"na", "b":"nb", "c":"C", "d":"D"}...)`, + nil, ARR{ARR{5, 6, 7, 8, 9}, "na", "nb", MAP{"c": "C", "d": "D"}}) + expectRun(t, `out = func(...z;a=false, b="B", ...c) { return [a,b,c] }(5,[6,7,8,9]...;a=true,{"a":"na", "b":"nb", "c":"C", "d":"D"}...)`, + nil, ARR{"na", "nb", MAP{"c": "C", "d": "D"}}) + expectRun(t, `out = func(...z;a=false, b="B", ...c) { return [a,b,c] }(5,[6,7,8,9]...;a=true,{"b":"nb", "c":"C", "d":"D"}...)`, + nil, ARR{true, "nb", MAP{"c": "C", "d": "D"}}) + expectRun(t, `out = func(x, y, ...z;a="A", b="B", ...c) { return [x,y,z,a,b,c] }(5,[6,7,8,9]...;{"a":"na", "b":"nb", "c":"C", "d":"D"}...)`, + nil, ARR{5, 6, ARR{7, 8, 9}, "na", "nb", MAP{"c": "C", "d": "D"}}) + expectRun(t, `out = func(x, y, ...z;a="A", b="B", ...c) { return [x,y,z,a,b,c] }(5,[6,7,8,9]...;{}...)`, + nil, ARR{5, 6, ARR{7, 8, 9}, "A", "B", MAP{}}) + expectRun(t, `out = func(x, y, ...z;a="A", b="B", ...c) { return [argv, kwargv] }(5,[6,7,8,9]...;{"a":"na", "b":"nb", "c":"C", "d":"D"}...)`, + nil, ARR{IARR{5, 6, 7, 8, 9}, IMAP{"a": "na", "b": "nb", "c": "C", "d": "D"}}) + expectRun(t, `truncate := func(text; limit=3) { + return len(text) > limit ? text[:limit]+"..." : text +} +out = [ + truncate("abcd"), + truncate("abc"), + truncate("ab"), + truncate("abcd",limit=2) +] +`, nil, ARR{"abc...", "abc", "ab", "ab..."}) } func TestChar(t *testing.T) { @@ -1890,7 +1920,6 @@ for x in [1, 2, 3] { } func TestIf(t *testing.T) { - expectRun(t, `if (true) { out = 10 }`, nil, 10) expectRun(t, `if (false) { out = 10 }`, nil, tengo.UndefinedValue) expectRun(t, `if (false) { out = 10 } else { out = 20 }`, nil, 20) @@ -2284,19 +2313,20 @@ func (o *StringArray) IndexSet(index, value tengo.Object) error { return tengo.ErrInvalidIndexType } -func (o *StringArray) Call( - args ...tengo.Object, -) (ret tengo.Object, err error) { - if len(args) != 1 { +func (o *StringArray) Call(param *tengo.CallContext) (ret tengo.Object, err error) { + if len(param.Args) != 1 { return nil, tengo.ErrWrongNumArguments } + if len(param.Kwargs) != 0 { + return nil, tengo.ErrUnexpectedKwargs + } - s1, ok := tengo.ToString(args[0]) + s1, ok := tengo.ToString(param.Args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", - Found: args[0].TypeName(), + Found: param.Args[0].TypeName(), } } @@ -3257,7 +3287,7 @@ func TestSourceModules(t *testing.T) { testEnumModule(t, `out = enum.find({a:1}, enum.value)`, 1) testEnumModule(t, `out = enum.find({a:false,b:0,c:undefined,d:1}, enum.value)`, 1) - //testEnumModule(t, `out = enum.find({a:1,b:2,c:3}, enum.value)`, 1) + // testEnumModule(t, `out = enum.find({a:1,b:2,c:3}, enum.value)`, 1) testEnumModule(t, `out = enum.find(0, enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.find("123", enum.value)`, @@ -3279,7 +3309,7 @@ func TestSourceModules(t *testing.T) { "a") testEnumModule(t, `out = enum.find_key({a:false,b:0,c:undefined,d:1}, enum.value)`, "d") - //testEnumModule(t, `out = enum.find_key({a:1,b:2,c:3}, enum.value)`, "a") + // testEnumModule(t, `out = enum.find_key({a:1,b:2,c:3}, enum.value)`, "a") testEnumModule(t, `out = enum.find_key(0, enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.find_key("123", enum.value)`, @@ -3461,6 +3491,17 @@ func TestString(t *testing.T) { } func TestTailCall(t *testing.T) { + expectRun(t, ` + mul := func(n , x) { + return x == 0 ? 0 : n + callee(n, x-1) + } + out = mul(7, 5)`, nil, 35) + + expectRun(t, ` + out = func(n , x) { + return x == 0 ? 0 : n + callee(n, x-1) + }(7, 5)`, nil, 35) + expectRun(t, ` fac := func(n, a) { if n == 1 { @@ -3468,8 +3509,19 @@ func TestTailCall(t *testing.T) { } return fac(n-1, n*a) } + out = fac(5, 1)`, nil, 120) + expectRun(t, ` + fac := func(n, a) { + if n == 1 { + return a + } + return callee(n-1, n*a) + } + + out = fac(3, 1)`, nil, 6) + expectRun(t, ` fac := func(n, a) { if n == 1 { @@ -3537,6 +3589,7 @@ iter := func(n, max) { } out = iter(0, 9999) `, nil, 9999) + expectRun(t, ` c := 0 iter := func(n, max) { @@ -3550,6 +3603,77 @@ iter := func(n, max) { iter(0, 9999) out = c `, nil, 9999) + + expectRun(t, ` +range_ := func(start, ...args;step=default) { + end := undefined + loop := [] + + if len(args) == 1 { + end = args[0] + } + + if end == undefined { + value := 0 + if step == default { + step = 1 + } + func(left) { + if left > 0 { + loop = append(loop, value) + value += step + callee(left-1) + } + }(start) + } else { + if step == default { + step = start < end ? 1 : -1 + } + + if step > 0 { + if start > end { + return loop + } + func(value) { + if value <= end { + loop = append(loop, value) + callee(value+step) + } + }(start) + } else if step < 0 { + if start < end { + return loop + } + func(value) { + if value >= end { + loop = append(loop, value) + callee(value+step) + } + }(start) + } + } + + return loop +} + +out = [ + range_(0), + range_(3), + range_(3, 5), + range_(3, 5;step=2), + range_(5,0;step=-2), + range_(5,-5), + range_(5,-5;step=-3) +] +`, nil, ARR{ + ARR{}, + ARR{0, 1, 2}, + ARR{3, 4, 5}, + ARR{3, 5}, + ARR{5, 3, 1}, + ARR{5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5}, + ARR{5, 2, -1, -4}, + }) } // tail call with free vars @@ -3625,6 +3749,45 @@ func TestSpread(t *testing.T) { "Runtime Error: wrong number of arguments: want=3, got=2") } +func TestKwargs(t *testing.T) { + expectRun(t, `data:={args:[1,2,3],kwargs:{a:4,b:5}};out = func(...args; ...kwargs) { return [args,kwargs] }(data.args...;data.kwargs...)`, + nil, ARR{ARR{1, 2, 3}, MAP{"a": 4, "b": 5}}) + expectRun(t, `data:={key1:{args:[1,2,3],kwargs:{a:4,b:5}}};out = func(...args; ...kwargs) { return [args,kwargs] }(data.key1.args...;data.key1.kwargs...)`, + nil, ARR{ARR{1, 2, 3}, MAP{"a": 4, "b": 5}}) + expectRun(t, `x := 5;out = func(a,b,...;...) { z := 100; return [a,b,z] }(1,[2,3]...;a=4,b=5,{"c":6}...)`, + nil, ARR{1, 2, 100}) + expectRun(t, `x := 5;out = func(a,b,...;...) { z := 100; return [a,b,z] }(1,[2,3]...,a=4,b=5,{"c":6}...)`, + nil, ARR{1, 2, 100}) + expectRun(t, `x := 5;out = func(a,b,...;...) { z := 100; return [a,b,argv, kwargv,z] }(1,[2,3]...;a=4,b=5,{"c":6}...)`, + nil, ARR{1, 2, IARR{1, 2, 3}, IMAP{"a": 4, "b": 5, "c": 6}, 100}) + expectRun(t, `x := 5;out = func(a,b,...;...) { z := 100; return [a,b,argv, kwargv,z] }(1,[2,3]...,a=4,b=5,{"c":6}...)`, + nil, ARR{1, 2, IARR{1, 2, 3}, IMAP{"a": 4, "b": 5, "c": 6}, 100}) + expectRun(t, `x := 5;out = func(...args;...kwargs) { z := 100; return [argv, kwargv,z] }(1,[2,3]...,a=4,b=5,{"c":6}...)`, + nil, ARR{IARR{1, 2, 3}, IMAP{"a": 4, "b": 5, "c": 6}, 100}) + expectRun(t, `x := 5;out = func(...args;...kwargs) { z := 100; return [argv, kwargv,z] }(1,[2,3]...,a=4,b=5,{"c":6}...)`, + nil, ARR{IARR{1, 2, 3}, IMAP{"a": 4, "b": 5, "c": 6}, 100}) + expectRun(t, `x := 5;out = func(;...kwargs) { z := 100; return [argv, kwargv,z] }(a=4,b=5,{"c":6}...)`, + nil, ARR{IARR{}, IMAP{"a": 4, "b": 5, "c": 6}, 100}) + expectRun(t, `x := 5;out = func(;...kwargs) { z := 100; return [kwargs,z] }(a=4,b=5,{"c":6}...)`, + nil, ARR{MAP{"a": 4, "b": 5, "c": 6}, 100}) + expectRun(t, `kw := {"c":6};x := 5;out = func(;...kwargs) { z := 100; return [kwargs,z] }(a=4,b=5,kw...)`, + nil, ARR{MAP{"a": 4, "b": 5, "c": 6}, 100}) + expectRun(t, `x := 5;f := func(a,b,...;...kwargs) { z := 100; return [a,b,z,kwargs] }; out = f(1,[2,3]...;a=4,b=5,map(c=6)...)`, + nil, ARR{1, 2, 100, MAP{"a": 4, "b": 5, "c": 6}}) + expectRun(t, `x := 5;out = func(a,b,...;...kwargs) { z := 100; return [a,b,z,kwargs] }(1,[2,3]...;a=4,b=5,map(c=6)...)`, + nil, ARR{1, 2, 100, MAP{"a": 4, "b": 5, "c": 6}}) + expectRun(t, `x := 5;out = func(a,b,...;...kwargs) { z := 100; return [a,b,z,kwargs] }(1,[2,3]...;a=4,b=5,map(c=6)...)`, + nil, ARR{1, 2, 100, MAP{"a": 4, "b": 5, "c": 6}}) + expectError(t, `func() {}(a=1)`, nil, + "Runtime Error: unexpected kwargs") + expectError(t, `func(;a=default) {}(a=2,b=1)`, nil, + "Runtime Error: unexpected kwarg \"b\"") + expectError(t, `func() {}(a=1,{"b":2}...)`, nil, + "Runtime Error: unexpected kwargs") + expectError(t, `kw := {"b":2, "c":3};out := func(){}(a=1,kw...)`, nil, + "Runtime Error: unexpected kwargs") +} + func expectRun( t *testing.T, input string,