././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713112004.5847838 golang-github-d5-tengo/0000755000175000017500000000000014607001705015147 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/.github/0000755000175000017500000000000014607001502016502 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/.github/workflows/0000755000175000017500000000000014607001502020537 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/.github/workflows/release.yml0000644000175000017500000000104014607001502022675 0ustar00maythammaythamname: release on: push: tags: - '*' jobs: goreleaser: runs-on: ubuntu-latest steps: - name: check out uses: actions/checkout@v2 - name: Unshallow run: git fetch --prune --unshallow - name: set up Go uses: actions/setup-go@v2 with: go-version: 1.18 - name: run goreleaser uses: goreleaser/goreleaser-action@v3 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/.github/workflows/test.yml0000644000175000017500000000147714607001502022252 0ustar00maythammaythamname: test on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: name: build runs-on: ubuntu-latest steps: - name: set up Go uses: actions/setup-go@v1 with: go-version: 1.18 id: go - name: set up Go module cache uses: actions/cache@v1 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: setup env run: | echo "name=GOPATH::$(go env GOPATH)" >> $GITHUB_ENV echo "$(go env GOPATH)/bin" >> $GITHUB_PATH shell: bash - name: check out code uses: actions/checkout@v2 - name: install golint run: go install golang.org/x/lint/golint@latest - name: run tests run: make test ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/.gitignore0000644000175000017500000000001414607001502017125 0ustar00maythammaythamdist/ .idea././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/.goreleaser.yml0000644000175000017500000000044014607001502020071 0ustar00maythammaythamenv: - GO111MODULE=on before: hooks: - go mod tidy builds: - env: - CGO_ENABLED=0 main: ./cmd/tengo/main.go goos: - darwin - linux - windows archives: - files: - none* checksum: name_template: 'checksums.txt' changelog: sort: asc ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/LICENSE0000644000175000017500000000205414607001502016150 0ustar00maythammaythamMIT License Copyright (c) 2019 Daniel Kang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/Makefile0000644000175000017500000000027714607001502016610 0ustar00maythammaythamgenerate: go generate ./... lint: golint -set_exit_status ./... test: generate lint go test -race -cover ./... go run ./cmd/tengo -resolve ./testdata/cli/test.tengo fmt: go fmt ./... ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/README.md0000644000175000017500000001164714607001502016432 0ustar00maythammaytham# The Tengo Language [![GoDoc](https://godoc.org/github.com/d5/tengo/v2?status.svg)](https://godoc.org/github.com/d5/tengo/v2) ![test](https://github.com/d5/tengo/workflows/test/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/d5/tengo)](https://goreportcard.com/report/github.com/d5/tengo) **Tengo is a small, dynamic, fast, secure script language for Go.** Tengo is **[fast](#benchmark)** and secure because it's compiled/executed as bytecode on stack-based VM that's written in native Go. ```golang /* The Tengo Language */ fmt := import("fmt") each := func(seq, fn) { for x in seq { fn(x) } } sum := func(init, seq) { each(seq, func(x) { init += x }) return init } fmt.println(sum(0, [1, 2, 3])) // "6" fmt.println(sum("", [1, 2, 3])) // "123" ``` > Test this Tengo code in the > [Tengo Playground](https://tengolang.com/?s=0c8d5d0d88f2795a7093d7f35ae12c3afa17bea3) ## Features - Simple and highly readable [Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md) - Dynamic typing with type coercion - Higher-order functions and closures - Immutable values - [Securely Embeddable](https://github.com/d5/tengo/blob/master/docs/interoperability.md) and [Extensible](https://github.com/d5/tengo/blob/master/docs/objects.md) - Compiler/runtime written in native Go _(no external deps or cgo)_ - Executable as a [standalone](https://github.com/d5/tengo/blob/master/docs/tengo-cli.md) language / REPL - Use cases: rules engine, [state machine](https://github.com/d5/go-fsm), data pipeline, [transpiler](https://github.com/d5/tengo2lua) ## Benchmark | | fib(35) | fibt(35) | Language (Type) | | :--- | ---: | ---: | :---: | | [**Tengo**](https://github.com/d5/tengo) | `2,315ms` | `3ms` | Tengo (VM) | | [go-lua](https://github.com/Shopify/go-lua) | `4,028ms` | `3ms` | Lua (VM) | | [GopherLua](https://github.com/yuin/gopher-lua) | `4,409ms` | `3ms` | Lua (VM) | | [goja](https://github.com/dop251/goja) | `5,194ms` | `4ms` | JavaScript (VM) | | [starlark-go](https://github.com/google/starlark-go) | `6,954ms` | `3ms` | Starlark (Interpreter) | | [gpython](https://github.com/go-python/gpython) | `11,324ms` | `4ms` | Python (Interpreter) | | [Yaegi](https://github.com/containous/yaegi) | `11,715ms` | `10ms` | Yaegi (Interpreter) | | [otto](https://github.com/robertkrimen/otto) | `48,539ms` | `6ms` | JavaScript (Interpreter) | | [Anko](https://github.com/mattn/anko) | `52,821ms` | `6ms` | Anko (Interpreter) | | - | - | - | - | | Go | `47ms` | `2ms` | Go (Native) | | Lua | `756ms` | `2ms` | Lua (Native) | | Python | `1,907ms` | `14ms` | Python2 (Native) | _* [fib(35)](https://github.com/d5/tengobench/blob/master/code/fib.tengo): Fibonacci(35)_ _* [fibt(35)](https://github.com/d5/tengobench/blob/master/code/fibtc.tengo): [tail-call](https://en.wikipedia.org/wiki/Tail_call) version of Fibonacci(35)_ _* **Go** does not read the source code from file, while all other cases do_ _* See [here](https://github.com/d5/tengobench) for commands/codes used_ ## Quick Start ``` go get github.com/d5/tengo/v2 ``` A simple Go example code that compiles/runs Tengo script code with some input/output values: ```golang package main import ( "context" "fmt" "github.com/d5/tengo/v2" ) func main() { // create a new Script instance script := tengo.NewScript([]byte( `each := func(seq, fn) { for x in seq { fn(x) } } sum := 0 mul := 1 each([a, b, c, d], func(x) { sum += x mul *= x })`)) // set values _ = script.Add("a", 1) _ = script.Add("b", 9) _ = script.Add("c", 8) _ = script.Add("d", 4) // run the script compiled, err := script.RunContext(context.Background()) if err != nil { panic(err) } // retrieve values sum := compiled.Get("sum") mul := compiled.Get("mul") fmt.Println(sum, mul) // "22 288" } ``` Or, if you need to evaluate a simple expression, you can use [Eval](https://pkg.go.dev/github.com/d5/tengo/v2#Eval) function instead: ```golang res, err := tengo.Eval(ctx, `input ? "success" : "fail"`, map[string]interface{}{"input": 1}) if err != nil { panic(err) } fmt.Println(res) // "success" ``` ## References - [Language Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md) - [Object Types](https://github.com/d5/tengo/blob/master/docs/objects.md) - [Runtime Types](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) and [Operators](https://github.com/d5/tengo/blob/master/docs/operators.md) - [Builtin Functions](https://github.com/d5/tengo/blob/master/docs/builtins.md) - [Interoperability](https://github.com/d5/tengo/blob/master/docs/interoperability.md) - [Tengo CLI](https://github.com/d5/tengo/blob/master/docs/tengo-cli.md) - [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md) - Syntax Highlighters: [VSCode](https://github.com/lissein/vscode-tengo), [Atom](https://github.com/d5/tengo-atom), [Vim](https://github.com/geseq/tengo-vim) - **Why the name Tengo?** It's from [1Q84](https://en.wikipedia.org/wiki/1Q84). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/builtins.go0000644000175000017500000003171414607001502017330 0ustar00maythammaythampackage tengo var builtinFuncs = []*BuiltinFunction{ { Name: "len", Value: builtinLen, }, { Name: "copy", Value: builtinCopy, }, { Name: "append", Value: builtinAppend, }, { Name: "delete", Value: builtinDelete, }, { Name: "splice", Value: builtinSplice, }, { Name: "string", Value: builtinString, }, { Name: "int", Value: builtinInt, }, { Name: "bool", Value: builtinBool, }, { Name: "float", Value: builtinFloat, }, { Name: "char", Value: builtinChar, }, { Name: "bytes", Value: builtinBytes, }, { Name: "time", Value: builtinTime, }, { Name: "is_int", Value: builtinIsInt, }, { Name: "is_float", Value: builtinIsFloat, }, { Name: "is_string", Value: builtinIsString, }, { Name: "is_bool", Value: builtinIsBool, }, { Name: "is_char", Value: builtinIsChar, }, { Name: "is_bytes", Value: builtinIsBytes, }, { Name: "is_array", Value: builtinIsArray, }, { Name: "is_immutable_array", Value: builtinIsImmutableArray, }, { Name: "is_map", Value: builtinIsMap, }, { Name: "is_immutable_map", Value: builtinIsImmutableMap, }, { Name: "is_iterable", Value: builtinIsIterable, }, { Name: "is_time", Value: builtinIsTime, }, { Name: "is_error", Value: builtinIsError, }, { Name: "is_undefined", Value: builtinIsUndefined, }, { Name: "is_function", Value: builtinIsFunction, }, { Name: "is_callable", Value: builtinIsCallable, }, { Name: "type_name", Value: builtinTypeName, }, { Name: "format", Value: builtinFormat, }, { Name: "range", Value: builtinRange, }, } // GetAllBuiltinFunctions returns all builtin function objects. func GetAllBuiltinFunctions() []*BuiltinFunction { return append([]*BuiltinFunction{}, builtinFuncs...) } func builtinTypeName(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } return &String{Value: args[0].TypeName()}, nil } func builtinIsString(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*String); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsInt(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Int); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsFloat(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Float); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsBool(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Bool); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsChar(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Char); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsBytes(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Bytes); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsArray(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Array); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsImmutableArray(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*ImmutableArray); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsMap(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Map); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsImmutableMap(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*ImmutableMap); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsTime(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Time); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsError(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Error); ok { return TrueValue, nil } return FalseValue, nil } func builtinIsUndefined(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if args[0] == UndefinedValue { return TrueValue, nil } return FalseValue, nil } func builtinIsFunction(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } switch args[0].(type) { case *CompiledFunction: return TrueValue, nil } return FalseValue, nil } func builtinIsCallable(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if args[0].CanCall() { return TrueValue, nil } return FalseValue, nil } func builtinIsIterable(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if args[0].CanIterate() { return TrueValue, nil } return FalseValue, nil } // len(obj object) => int func builtinLen(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } switch arg := args[0].(type) { case *Array: return &Int{Value: int64(len(arg.Value))}, nil case *ImmutableArray: return &Int{Value: int64(len(arg.Value))}, nil case *String: return &Int{Value: int64(len(arg.Value))}, nil case *Bytes: return &Int{Value: int64(len(arg.Value))}, nil case *Map: return &Int{Value: int64(len(arg.Value))}, nil case *ImmutableMap: return &Int{Value: int64(len(arg.Value))}, nil default: return nil, ErrInvalidArgumentType{ Name: "first", Expected: "array/string/bytes/map", Found: arg.TypeName(), } } } //range(start, stop[, step]) func builtinRange(args ...Object) (Object, error) { numArgs := len(args) if numArgs < 2 || numArgs > 3 { return nil, ErrWrongNumArguments } var start, stop, step *Int for i, arg := range args { v, ok := args[i].(*Int) if !ok { var name string switch i { case 0: name = "start" case 1: name = "stop" case 2: name = "step" } return nil, ErrInvalidArgumentType{ Name: name, Expected: "int", Found: arg.TypeName(), } } if i == 2 && v.Value <= 0 { return nil, ErrInvalidRangeStep } switch i { case 0: start = v case 1: stop = v case 2: step = v } } if step == nil { step = &Int{Value: int64(1)} } return buildRange(start.Value, stop.Value, step.Value), nil } func buildRange(start, stop, step int64) *Array { array := &Array{} if start <= stop { for i := start; i < stop; i += step { array.Value = append(array.Value, &Int{ Value: i, }) } } else { for i := start; i > stop; i -= step { array.Value = append(array.Value, &Int{ Value: i, }) } } return array } func builtinFormat(args ...Object) (Object, error) { numArgs := len(args) if numArgs == 0 { return nil, ErrWrongNumArguments } format, ok := args[0].(*String) if !ok { return nil, ErrInvalidArgumentType{ Name: "format", Expected: "string", Found: 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:]...) if err != nil { return nil, err } return &String{Value: s}, nil } func builtinCopy(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } return args[0].Copy(), nil } func builtinString(args ...Object) (Object, error) { argsLen := len(args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } if _, ok := args[0].(*String); ok { return args[0], nil } v, ok := ToString(args[0]) if ok { if len(v) > MaxStringLen { return nil, ErrStringLimit } return &String{Value: v}, nil } if argsLen == 2 { return args[1], nil } return UndefinedValue, nil } func builtinInt(args ...Object) (Object, error) { argsLen := len(args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Int); ok { return args[0], nil } v, ok := ToInt64(args[0]) if ok { return &Int{Value: v}, nil } if argsLen == 2 { return args[1], nil } return UndefinedValue, nil } func builtinFloat(args ...Object) (Object, error) { argsLen := len(args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Float); ok { return args[0], nil } v, ok := ToFloat64(args[0]) if ok { return &Float{Value: v}, nil } if argsLen == 2 { return args[1], nil } return UndefinedValue, nil } func builtinBool(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Bool); ok { return args[0], nil } v, ok := ToBool(args[0]) if ok { if v { return TrueValue, nil } return FalseValue, nil } return UndefinedValue, nil } func builtinChar(args ...Object) (Object, error) { argsLen := len(args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Char); ok { return args[0], nil } v, ok := ToRune(args[0]) if ok { return &Char{Value: v}, nil } if argsLen == 2 { return args[1], nil } return UndefinedValue, nil } func builtinBytes(args ...Object) (Object, error) { argsLen := len(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.Value > int64(MaxBytesLen) { return nil, ErrBytesLimit } return &Bytes{Value: make([]byte, int(n.Value))}, nil } v, ok := ToByteSlice(args[0]) if ok { if len(v) > MaxBytesLen { return nil, ErrBytesLimit } return &Bytes{Value: v}, nil } if argsLen == 2 { return args[1], nil } return UndefinedValue, nil } func builtinTime(args ...Object) (Object, error) { argsLen := len(args) if !(argsLen == 1 || argsLen == 2) { return nil, ErrWrongNumArguments } if _, ok := args[0].(*Time); ok { return args[0], nil } v, ok := ToTime(args[0]) if ok { return &Time{Value: v}, nil } if argsLen == 2 { return args[1], nil } return UndefinedValue, nil } // append(arr, items...) func builtinAppend(args ...Object) (Object, error) { if len(args) < 2 { return nil, ErrWrongNumArguments } switch arg := args[0].(type) { case *Array: return &Array{Value: append(arg.Value, args[1:]...)}, nil case *ImmutableArray: return &Array{Value: append(arg.Value, args[1:]...)}, nil default: return nil, ErrInvalidArgumentType{ Name: "first", Expected: "array", Found: arg.TypeName(), } } } // builtinDelete deletes Map keys // usage: delete(map, "key") // key must be a string func builtinDelete(args ...Object) (Object, error) { argsLen := len(args) if argsLen != 2 { return nil, ErrWrongNumArguments } switch arg := args[0].(type) { case *Map: if key, ok := args[1].(*String); ok { delete(arg.Value, key.Value) return UndefinedValue, nil } return nil, ErrInvalidArgumentType{ Name: "second", Expected: "string", Found: args[1].TypeName(), } default: return nil, ErrInvalidArgumentType{ Name: "first", Expected: "map", Found: arg.TypeName(), } } } // 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) if argsLen == 0 { return nil, ErrWrongNumArguments } array, ok := args[0].(*Array) if !ok { return nil, ErrInvalidArgumentType{ Name: "first", Expected: "array", Found: args[0].TypeName(), } } arrayLen := len(array.Value) var startIdx int if argsLen > 1 { arg1, ok := args[1].(*Int) if !ok { return nil, ErrInvalidArgumentType{ Name: "second", Expected: "int", Found: args[1].TypeName(), } } startIdx = int(arg1.Value) if startIdx < 0 || startIdx > arrayLen { return nil, ErrIndexOutOfBounds } } delCount := len(array.Value) if argsLen > 2 { arg2, ok := args[2].(*Int) if !ok { return nil, ErrInvalidArgumentType{ Name: "third", Expected: "int", Found: args[2].TypeName(), } } delCount = int(arg2.Value) if delCount < 0 { return nil, ErrIndexOutOfBounds } } // if count of to be deleted items is bigger than expected, truncate it if startIdx+delCount > arrayLen { delCount = arrayLen - startIdx } // delete items endIdx := startIdx + delCount deleted := append([]Object{}, array.Value[startIdx:endIdx]...) head := array.Value[:startIdx] var items []Object if argsLen > 3 { items = make([]Object, 0, argsLen-3) for i := 3; i < argsLen; i++ { items = append(items, args[i]) } } items = append(items, array.Value[endIdx:]...) array.Value = append(head, items...) // return deleted items return &Array{Value: deleted}, nil } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/builtins_test.go0000644000175000017500000003354114607001502020367 0ustar00maythammaythampackage tengo_test import ( "errors" "reflect" "testing" "github.com/d5/tengo/v2" ) func Test_builtinDelete(t *testing.T) { var builtinDelete func(args ...tengo.Object) (tengo.Object, error) for _, f := range tengo.GetAllBuiltinFunctions() { if f.Name == "delete" { builtinDelete = f.Value break } } if builtinDelete == nil { t.Fatal("builtin delete not found") } type args struct { args []tengo.Object } tests := []struct { name string args args want tengo.Object wantErr bool wantedErr error target interface{} }{ {name: "invalid-arg", args: args{[]tengo.Object{&tengo.String{}, &tengo.String{}}}, wantErr: true, wantedErr: tengo.ErrInvalidArgumentType{ Name: "first", Expected: "map", Found: "string"}, }, {name: "no-args", wantErr: true, wantedErr: tengo.ErrWrongNumArguments}, {name: "empty-args", args: args{[]tengo.Object{}}, wantErr: true, wantedErr: tengo.ErrWrongNumArguments, }, {name: "3-args", args: args{[]tengo.Object{ (*tengo.Map)(nil), (*tengo.String)(nil), (*tengo.String)(nil)}}, wantErr: true, wantedErr: tengo.ErrWrongNumArguments, }, {name: "nil-map-empty-key", args: args{[]tengo.Object{&tengo.Map{}, &tengo.String{}}}, want: tengo.UndefinedValue, }, {name: "nil-map-nonstr-key", args: args{[]tengo.Object{ &tengo.Map{}, &tengo.Int{}}}, wantErr: true, wantedErr: tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string", Found: "int"}, }, {name: "nil-map-no-key", args: args{[]tengo.Object{&tengo.Map{}}}, wantErr: true, wantedErr: tengo.ErrWrongNumArguments, }, {name: "map-missing-key", args: args{ []tengo.Object{ &tengo.Map{Value: map[string]tengo.Object{ "key": &tengo.String{Value: "value"}, }}, &tengo.String{Value: "key1"}}}, want: tengo.UndefinedValue, target: &tengo.Map{ Value: map[string]tengo.Object{ "key": &tengo.String{ Value: "value"}}}, }, {name: "map-emptied", args: args{ []tengo.Object{ &tengo.Map{Value: map[string]tengo.Object{ "key": &tengo.String{Value: "value"}, }}, &tengo.String{Value: "key"}}}, want: tengo.UndefinedValue, target: &tengo.Map{Value: map[string]tengo.Object{}}, }, {name: "map-multi-keys", args: args{ []tengo.Object{ &tengo.Map{Value: map[string]tengo.Object{ "key1": &tengo.String{Value: "value1"}, "key2": &tengo.Int{Value: 10}, }}, &tengo.String{Value: "key1"}}}, want: tengo.UndefinedValue, target: &tengo.Map{Value: map[string]tengo.Object{ "key2": &tengo.Int{Value: 10}}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := builtinDelete(tt.args.args...) if (err != nil) != tt.wantErr { t.Errorf("builtinDelete() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr && !errors.Is(err, tt.wantedErr) { if err.Error() != tt.wantedErr.Error() { t.Errorf("builtinDelete() error = %v, wantedErr %v", err, tt.wantedErr) return } } if got != tt.want { t.Errorf("builtinDelete() = %v, want %v", got, tt.want) return } if !tt.wantErr && tt.target != nil { switch v := tt.args.args[0].(type) { case *tengo.Map, *tengo.Array: if !reflect.DeepEqual(tt.target, tt.args.args[0]) { t.Errorf("builtinDelete() objects are not equal "+ "got: %+v, want: %+v", tt.args.args[0], tt.target) } default: t.Errorf("builtinDelete() unsuporrted arg[0] type %s", v.TypeName()) return } } }) } } func Test_builtinSplice(t *testing.T) { var builtinSplice func(args ...tengo.Object) (tengo.Object, error) for _, f := range tengo.GetAllBuiltinFunctions() { if f.Name == "splice" { builtinSplice = f.Value break } } if builtinSplice == nil { t.Fatal("builtin splice not found") } tests := []struct { name string args []tengo.Object deleted tengo.Object Array *tengo.Array wantErr bool wantedErr error }{ {name: "no args", args: []tengo.Object{}, wantErr: true, wantedErr: tengo.ErrWrongNumArguments, }, {name: "invalid args", args: []tengo.Object{&tengo.Map{}}, wantErr: true, wantedErr: tengo.ErrInvalidArgumentType{ Name: "first", Expected: "array", Found: "map"}, }, {name: "invalid args", args: []tengo.Object{&tengo.Array{}, &tengo.String{}}, wantErr: true, wantedErr: tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int", Found: "string"}, }, {name: "negative index", args: []tengo.Object{&tengo.Array{}, &tengo.Int{Value: -1}}, wantErr: true, wantedErr: tengo.ErrIndexOutOfBounds}, {name: "non int count", args: []tengo.Object{ &tengo.Array{}, &tengo.Int{Value: 0}, &tengo.String{Value: ""}}, wantErr: true, wantedErr: tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int", Found: "string"}, }, {name: "negative count", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, &tengo.Int{Value: 0}, &tengo.Int{Value: -1}}, wantErr: true, wantedErr: tengo.ErrIndexOutOfBounds, }, {name: "insert with zero count", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, &tengo.Int{Value: 0}, &tengo.Int{Value: 0}, &tengo.String{Value: "b"}}, deleted: &tengo.Array{Value: []tengo.Object{}}, Array: &tengo.Array{Value: []tengo.Object{ &tengo.String{Value: "b"}, &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, }, {name: "insert", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, &tengo.Int{Value: 1}, &tengo.Int{Value: 0}, &tengo.String{Value: "c"}, &tengo.String{Value: "d"}}, deleted: &tengo.Array{Value: []tengo.Object{}}, Array: &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.String{Value: "c"}, &tengo.String{Value: "d"}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, }, {name: "insert with zero count", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, &tengo.Int{Value: 1}, &tengo.Int{Value: 0}, &tengo.String{Value: "c"}, &tengo.String{Value: "d"}}, deleted: &tengo.Array{Value: []tengo.Object{}}, Array: &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.String{Value: "c"}, &tengo.String{Value: "d"}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, }, {name: "insert with delete", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, &tengo.Int{Value: 1}, &tengo.Int{Value: 1}, &tengo.String{Value: "c"}, &tengo.String{Value: "d"}}, deleted: &tengo.Array{ Value: []tengo.Object{&tengo.Int{Value: 1}}}, Array: &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.String{Value: "c"}, &tengo.String{Value: "d"}, &tengo.Int{Value: 2}}}, }, {name: "insert with delete multi", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.String{Value: "c"}, &tengo.String{Value: "d"}}, deleted: &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, Array: &tengo.Array{ Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.String{Value: "c"}, &tengo.String{Value: "d"}}}, }, {name: "delete all with positive count", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, &tengo.Int{Value: 0}, &tengo.Int{Value: 3}}, deleted: &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, Array: &tengo.Array{Value: []tengo.Object{}}, }, {name: "delete all with big count", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, &tengo.Int{Value: 0}, &tengo.Int{Value: 5}}, deleted: &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, Array: &tengo.Array{Value: []tengo.Object{}}, }, {name: "nothing2", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}}, Array: &tengo.Array{Value: []tengo.Object{}}, deleted: &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, }, {name: "pop without count", args: []tengo.Object{ &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}, &tengo.Int{Value: 2}}}, &tengo.Int{Value: 2}}, deleted: &tengo.Array{Value: []tengo.Object{&tengo.Int{Value: 2}}}, Array: &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 0}, &tengo.Int{Value: 1}}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := builtinSplice(tt.args...) if (err != nil) != tt.wantErr { t.Errorf("builtinSplice() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.deleted) { t.Errorf("builtinSplice() = %v, want %v", got, tt.deleted) } if tt.wantErr && tt.wantedErr.Error() != err.Error() { t.Errorf("builtinSplice() error = %v, wantedErr %v", err, tt.wantedErr) } if tt.Array != nil && !reflect.DeepEqual(tt.Array, tt.args[0]) { t.Errorf("builtinSplice() arrays are not equal expected"+ " %s, got %s", tt.Array, tt.args[0].(*tengo.Array)) } }) } } func Test_builtinRange(t *testing.T) { var builtinRange func(args ...tengo.Object) (tengo.Object, error) for _, f := range tengo.GetAllBuiltinFunctions() { if f.Name == "range" { builtinRange = f.Value break } } if builtinRange == nil { t.Fatal("builtin range not found") } tests := []struct { name string args []tengo.Object result *tengo.Array wantErr bool wantedErr error }{ {name: "no args", args: []tengo.Object{}, wantErr: true, wantedErr: tengo.ErrWrongNumArguments, }, {name: "single args", args: []tengo.Object{&tengo.Map{}}, wantErr: true, wantedErr: tengo.ErrWrongNumArguments, }, {name: "4 args", args: []tengo.Object{&tengo.Map{}, &tengo.String{}, &tengo.String{}, &tengo.String{}}, wantErr: true, wantedErr: tengo.ErrWrongNumArguments, }, {name: "invalid start", args: []tengo.Object{&tengo.String{}, &tengo.String{}}, wantErr: true, wantedErr: tengo.ErrInvalidArgumentType{ Name: "start", Expected: "int", Found: "string"}, }, {name: "invalid stop", args: []tengo.Object{&tengo.Int{}, &tengo.String{}}, wantErr: true, wantedErr: tengo.ErrInvalidArgumentType{ Name: "stop", Expected: "int", Found: "string"}, }, {name: "invalid step", args: []tengo.Object{&tengo.Int{}, &tengo.Int{}, &tengo.String{}}, wantErr: true, wantedErr: tengo.ErrInvalidArgumentType{ Name: "step", Expected: "int", Found: "string"}, }, {name: "zero step", 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 wantErr: true, wantedErr: tengo.ErrInvalidRangeStep, }, {name: "same bound", args: []tengo.Object{&tengo.Int{}, &tengo.Int{}}, wantErr: false, result: &tengo.Array{ Value: nil, }, }, {name: "positive range", args: []tengo.Object{&tengo.Int{}, &tengo.Int{Value: 5}}, wantErr: false, result: &tengo.Array{ Value: []tengo.Object{ intObject(0), intObject(1), intObject(2), intObject(3), intObject(4), }, }, }, {name: "negative range", args: []tengo.Object{&tengo.Int{}, &tengo.Int{Value: -5}}, wantErr: false, result: &tengo.Array{ Value: []tengo.Object{ intObject(0), intObject(-1), intObject(-2), intObject(-3), intObject(-4), }, }, }, {name: "positive with step", args: []tengo.Object{&tengo.Int{}, &tengo.Int{Value: 5}, &tengo.Int{Value: 2}}, wantErr: false, result: &tengo.Array{ Value: []tengo.Object{ intObject(0), intObject(2), intObject(4), }, }, }, {name: "negative with step", args: []tengo.Object{&tengo.Int{}, &tengo.Int{Value: -10}, &tengo.Int{Value: 2}}, wantErr: false, result: &tengo.Array{ Value: []tengo.Object{ intObject(0), intObject(-2), intObject(-4), intObject(-6), intObject(-8), }, }, }, {name: "large range", args: []tengo.Object{intObject(-10), intObject(10), &tengo.Int{Value: 3}}, wantErr: false, result: &tengo.Array{ Value: []tengo.Object{ intObject(-10), intObject(-7), intObject(-4), intObject(-1), intObject(2), intObject(5), intObject(8), }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := builtinRange(tt.args...) if (err != nil) != tt.wantErr { t.Errorf("builtinRange() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr && tt.wantedErr.Error() != err.Error() { t.Errorf("builtinRange() error = %v, wantedErr %v", err, tt.wantedErr) } if tt.result != nil && !reflect.DeepEqual(tt.result, got) { t.Errorf("builtinRange() arrays are not equal expected"+ " %s, got %s", tt.result, got.(*tengo.Array)) } }) } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/bytecode.go0000644000175000017500000001612514607001502017274 0ustar00maythammaythampackage tengo import ( "encoding/gob" "fmt" "io" "reflect" "github.com/d5/tengo/v2/parser" ) // Bytecode is a compiled instructions and constants. type Bytecode struct { FileSet *parser.SourceFileSet MainFunction *CompiledFunction Constants []Object } // Encode writes Bytecode data to the writer. func (b *Bytecode) Encode(w io.Writer) error { enc := gob.NewEncoder(w) if err := enc.Encode(b.FileSet); err != nil { return err } if err := enc.Encode(b.MainFunction); err != nil { return err } return enc.Encode(b.Constants) } // CountObjects returns the number of objects found in Constants. func (b *Bytecode) CountObjects() int { n := 0 for _, c := range b.Constants { n += CountObjects(c) } return n } // FormatInstructions returns human readable string representations of // compiled instructions. func (b *Bytecode) FormatInstructions() []string { return FormatInstructions(b.MainFunction.Instructions, 0) } // FormatConstants returns human readable string representations of // compiled constants. func (b *Bytecode) FormatConstants() (output []string) { for cidx, cn := range b.Constants { switch cn := cn.(type) { case *CompiledFunction: output = append(output, fmt.Sprintf( "[% 3d] (Compiled Function|%p)", cidx, &cn)) for _, l := range FormatInstructions(cn.Instructions, 0) { output = append(output, fmt.Sprintf(" %s", l)) } default: output = append(output, fmt.Sprintf("[% 3d] %s (%s|%p)", cidx, cn, reflect.TypeOf(cn).Elem().Name(), &cn)) } } return } // Decode reads Bytecode data from the reader. func (b *Bytecode) Decode(r io.Reader, modules *ModuleMap) error { if modules == nil { modules = NewModuleMap() } dec := gob.NewDecoder(r) if err := dec.Decode(&b.FileSet); err != nil { return err } // TODO: files in b.FileSet.File does not have their 'set' field properly // set to b.FileSet as it's private field and not serialized by gob // encoder/decoder. if err := dec.Decode(&b.MainFunction); err != nil { return err } if err := dec.Decode(&b.Constants); err != nil { return err } for i, v := range b.Constants { fv, err := fixDecodedObject(v, modules) if err != nil { return err } b.Constants[i] = fv } return nil } // RemoveDuplicates finds and remove the duplicate values in Constants. // Note this function mutates Bytecode. func (b *Bytecode) RemoveDuplicates() { var deduped []Object indexMap := make(map[int]int) // mapping from old constant index to new index fns := make(map[*CompiledFunction]int) ints := make(map[int64]int) strings := make(map[string]int) floats := make(map[float64]int) chars := make(map[rune]int) immutableMaps := make(map[string]int) // for modules for curIdx, c := range b.Constants { switch c := c.(type) { case *CompiledFunction: if newIdx, ok := fns[c]; ok { indexMap[curIdx] = newIdx } else { newIdx = len(deduped) fns[c] = newIdx indexMap[curIdx] = newIdx deduped = append(deduped, c) } case *ImmutableMap: modName := inferModuleName(c) newIdx, ok := immutableMaps[modName] if modName != "" && ok { indexMap[curIdx] = newIdx } else { newIdx = len(deduped) immutableMaps[modName] = newIdx indexMap[curIdx] = newIdx deduped = append(deduped, c) } case *Int: if newIdx, ok := ints[c.Value]; ok { indexMap[curIdx] = newIdx } else { newIdx = len(deduped) ints[c.Value] = newIdx indexMap[curIdx] = newIdx deduped = append(deduped, c) } case *String: if newIdx, ok := strings[c.Value]; ok { indexMap[curIdx] = newIdx } else { newIdx = len(deduped) strings[c.Value] = newIdx indexMap[curIdx] = newIdx deduped = append(deduped, c) } case *Float: if newIdx, ok := floats[c.Value]; ok { indexMap[curIdx] = newIdx } else { newIdx = len(deduped) floats[c.Value] = newIdx indexMap[curIdx] = newIdx deduped = append(deduped, c) } case *Char: if newIdx, ok := chars[c.Value]; ok { indexMap[curIdx] = newIdx } else { newIdx = len(deduped) chars[c.Value] = newIdx indexMap[curIdx] = newIdx deduped = append(deduped, c) } default: panic(fmt.Errorf("unsupported top-level constant type: %s", c.TypeName())) } } // replace with de-duplicated constants b.Constants = deduped // update CONST instructions with new indexes // main function updateConstIndexes(b.MainFunction.Instructions, indexMap) // other compiled functions in constants for _, c := range b.Constants { switch c := c.(type) { case *CompiledFunction: updateConstIndexes(c.Instructions, indexMap) } } } func fixDecodedObject( o Object, modules *ModuleMap, ) (Object, error) { switch o := o.(type) { case *Bool: if o.IsFalsy() { return FalseValue, nil } return TrueValue, nil case *Undefined: return UndefinedValue, nil case *Array: for i, v := range o.Value { fv, err := fixDecodedObject(v, modules) if err != nil { return nil, err } o.Value[i] = fv } case *ImmutableArray: for i, v := range o.Value { fv, err := fixDecodedObject(v, modules) if err != nil { return nil, err } o.Value[i] = fv } case *Map: for k, v := range o.Value { fv, err := fixDecodedObject(v, modules) if err != nil { return nil, err } o.Value[k] = fv } case *ImmutableMap: modName := inferModuleName(o) if mod := modules.GetBuiltinModule(modName); mod != nil { return mod.AsImmutableMap(modName), nil } for k, v := range o.Value { // encoding of user function not supported if _, isUserFunction := v.(*UserFunction); isUserFunction { return nil, fmt.Errorf("user function not decodable") } fv, err := fixDecodedObject(v, modules) if err != nil { return nil, err } o.Value[k] = fv } } return o, nil } func updateConstIndexes(insts []byte, indexMap map[int]int) { i := 0 for i < len(insts) { op := insts[i] numOperands := parser.OpcodeOperands[op] _, read := parser.ReadOperands(numOperands, insts[i+1:]) switch op { case parser.OpConstant: curIdx := int(insts[i+2]) | int(insts[i+1])<<8 newIdx, ok := indexMap[curIdx] if !ok { panic(fmt.Errorf("constant index not found: %d", curIdx)) } copy(insts[i:], MakeInstruction(op, newIdx)) case parser.OpClosure: curIdx := int(insts[i+2]) | int(insts[i+1])<<8 numFree := int(insts[i+3]) newIdx, ok := indexMap[curIdx] if !ok { panic(fmt.Errorf("constant index not found: %d", curIdx)) } copy(insts[i:], MakeInstruction(op, newIdx, numFree)) } i += 1 + read } } func inferModuleName(mod *ImmutableMap) string { if modName, ok := mod.Value["__module_name__"].(*String); ok { return modName.Value } return "" } func init() { gob.Register(&parser.SourceFileSet{}) gob.Register(&parser.SourceFile{}) gob.Register(&Array{}) gob.Register(&Bool{}) gob.Register(&Bytes{}) gob.Register(&Char{}) gob.Register(&CompiledFunction{}) gob.Register(&Error{}) gob.Register(&Float{}) gob.Register(&ImmutableArray{}) gob.Register(&ImmutableMap{}) gob.Register(&Int{}) gob.Register(&Map{}) gob.Register(&String{}) gob.Register(&Time{}) gob.Register(&Undefined{}) gob.Register(&UserFunction{}) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/bytecode_test.go0000644000175000017500000002234014607001502020327 0ustar00maythammaythampackage tengo_test import ( "bytes" "testing" "time" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/require" ) type srcfile struct { name string size int } func TestBytecode(t *testing.T) { testBytecodeSerialization(t, bytecode(concatInsts(), objectsArray())) testBytecodeSerialization(t, bytecode( concatInsts(), objectsArray( &tengo.Char{Value: 'y'}, &tengo.Float{Value: 93.11}, compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpGetFree, 0)), &tengo.Float{Value: 39.2}, &tengo.Int{Value: 192}, &tengo.String{Value: "bar"}))) testBytecodeSerialization(t, bytecodeFileSet( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 6), tengo.MakeInstruction(parser.OpPop)), objectsArray( &tengo.Int{Value: 55}, &tengo.Int{Value: 66}, &tengo.Int{Value: 77}, &tengo.Int{Value: 88}, &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "array": &tengo.ImmutableArray{ Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}, tengo.TrueValue, tengo.FalseValue, tengo.UndefinedValue, }, }, "true": tengo.TrueValue, "false": tengo.FalseValue, "bytes": &tengo.Bytes{Value: make([]byte, 16)}, "char": &tengo.Char{Value: 'Y'}, "error": &tengo.Error{Value: &tengo.String{ Value: "some error", }}, "float": &tengo.Float{Value: -19.84}, "immutable_array": &tengo.ImmutableArray{ Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}, tengo.TrueValue, tengo.FalseValue, tengo.UndefinedValue, }, }, "immutable_map": &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "a": &tengo.Int{Value: 1}, "b": &tengo.Int{Value: 2}, "c": &tengo.Int{Value: 3}, "d": tengo.TrueValue, "e": tengo.FalseValue, "f": tengo.UndefinedValue, }, }, "int": &tengo.Int{Value: 91}, "map": &tengo.Map{ Value: map[string]tengo.Object{ "a": &tengo.Int{Value: 1}, "b": &tengo.Int{Value: 2}, "c": &tengo.Int{Value: 3}, "d": tengo.TrueValue, "e": tengo.FalseValue, "f": tengo.UndefinedValue, }, }, "string": &tengo.String{Value: "foo bar"}, "time": &tengo.Time{Value: time.Now()}, "undefined": tengo.UndefinedValue, }, }, compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpGetFree, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpGetFree, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetFree, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpClosure, 4, 2), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpClosure, 5, 1), tengo.MakeInstruction(parser.OpReturn, 1))), fileSet(srcfile{name: "file1", size: 100}, srcfile{name: "file2", size: 200}))) } func TestBytecode_RemoveDuplicates(t *testing.T) { testBytecodeRemoveDuplicates(t, bytecode( concatInsts(), objectsArray( &tengo.Char{Value: 'y'}, &tengo.Float{Value: 93.11}, compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpGetFree, 0)), &tengo.Float{Value: 39.2}, &tengo.Int{Value: 192}, &tengo.String{Value: "bar"})), bytecode( concatInsts(), objectsArray( &tengo.Char{Value: 'y'}, &tengo.Float{Value: 93.11}, compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpGetFree, 0)), &tengo.Float{Value: 39.2}, &tengo.Int{Value: 192}, &tengo.String{Value: "bar"}))) testBytecodeRemoveDuplicates(t, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpConstant, 4), tengo.MakeInstruction(parser.OpConstant, 5), tengo.MakeInstruction(parser.OpConstant, 6), tengo.MakeInstruction(parser.OpConstant, 7), tengo.MakeInstruction(parser.OpConstant, 8), tengo.MakeInstruction(parser.OpClosure, 4, 1)), objectsArray( &tengo.Int{Value: 1}, &tengo.Float{Value: 2.0}, &tengo.Char{Value: '3'}, &tengo.String{Value: "four"}, compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpConstant, 7), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpGetFree, 0)), &tengo.Int{Value: 1}, &tengo.Float{Value: 2.0}, &tengo.Char{Value: '3'}, &tengo.String{Value: "four"})), bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpConstant, 4), tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpClosure, 4, 1)), objectsArray( &tengo.Int{Value: 1}, &tengo.Float{Value: 2.0}, &tengo.Char{Value: '3'}, &tengo.String{Value: "four"}, compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpGetFree, 0))))) testBytecodeRemoveDuplicates(t, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpConstant, 4)), objectsArray( &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}, &tengo.Int{Value: 1}, &tengo.Int{Value: 3})), bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 2)), objectsArray( &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}))) } func TestBytecode_CountObjects(t *testing.T) { b := bytecode( concatInsts(), objectsArray( &tengo.Int{Value: 55}, &tengo.Int{Value: 66}, &tengo.Int{Value: 77}, &tengo.Int{Value: 88}, compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpReturn, 1)))) require.Equal(t, 7, b.CountObjects()) } func fileSet(files ...srcfile) *parser.SourceFileSet { fileSet := parser.NewFileSet() for _, f := range files { fileSet.AddFile(f.name, -1, f.size) } return fileSet } func bytecodeFileSet( instructions []byte, constants []tengo.Object, fileSet *parser.SourceFileSet, ) *tengo.Bytecode { return &tengo.Bytecode{ FileSet: fileSet, MainFunction: &tengo.CompiledFunction{Instructions: instructions}, Constants: constants, } } func testBytecodeRemoveDuplicates( t *testing.T, input, expected *tengo.Bytecode, ) { input.RemoveDuplicates() require.Equal(t, expected.FileSet, input.FileSet) require.Equal(t, expected.MainFunction, input.MainFunction) require.Equal(t, expected.Constants, input.Constants) } func testBytecodeSerialization(t *testing.T, b *tengo.Bytecode) { var buf bytes.Buffer err := b.Encode(&buf) require.NoError(t, err) r := &tengo.Bytecode{} err = r.Decode(bytes.NewReader(buf.Bytes()), nil) require.NoError(t, err) require.Equal(t, b.FileSet, r.FileSet) require.Equal(t, b.MainFunction, r.MainFunction) require.Equal(t, b.Constants, r.Constants) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/cmd/0000755000175000017500000000000014607001502015705 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/cmd/bench/0000755000175000017500000000000014607001502016764 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/cmd/bench/main.go0000644000175000017500000001112114607001502020233 0ustar00maythammaythampackage main import ( "fmt" "time" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/parser" ) func main() { runFib(35) runFibTC1(35) runFibTC2(35) } func runFib(n int) { start := time.Now() nativeResult := fib(n) nativeTime := time.Since(start) input := ` fib := func(x) { if x == 0 { return 0 } else if x == 1 { return 1 } return fib(x-1) + fib(x-2) } ` + fmt.Sprintf("out = fib(%d)", n) parseTime, compileTime, runTime, result, err := runBench([]byte(input)) if err != nil { panic(err) } if nativeResult != int(result.(*tengo.Int).Value) { panic(fmt.Errorf("wrong result: %d != %d", nativeResult, int(result.(*tengo.Int).Value))) } fmt.Println("-------------------------------------") fmt.Printf("fibonacci(%d)\n", n) fmt.Println("-------------------------------------") fmt.Printf("Result: %d\n", nativeResult) fmt.Printf("Go: %s\n", nativeTime) fmt.Printf("Parser: %s\n", parseTime) fmt.Printf("Compile: %s\n", compileTime) fmt.Printf("VM: %s\n", runTime) } func runFibTC1(n int) { start := time.Now() nativeResult := fibTC1(n, 0) nativeTime := time.Since(start) input := ` fib := func(x, s) { if x == 0 { return 0 + s } else if x == 1 { return 1 + s } return fib(x-1, fib(x-2, s)) } ` + fmt.Sprintf("out = fib(%d, 0)", n) parseTime, compileTime, runTime, result, err := runBench([]byte(input)) if err != nil { panic(err) } if nativeResult != int(result.(*tengo.Int).Value) { panic(fmt.Errorf("wrong result: %d != %d", nativeResult, int(result.(*tengo.Int).Value))) } fmt.Println("-------------------------------------") fmt.Printf("fibonacci(%d) (tail-call #1)\n", n) fmt.Println("-------------------------------------") fmt.Printf("Result: %d\n", nativeResult) fmt.Printf("Go: %s\n", nativeTime) fmt.Printf("Parser: %s\n", parseTime) fmt.Printf("Compile: %s\n", compileTime) fmt.Printf("VM: %s\n", runTime) } func runFibTC2(n int) { start := time.Now() nativeResult := fibTC2(n, 0, 1) nativeTime := time.Since(start) input := ` fib := func(x, a, b) { if x == 0 { return a } else if x == 1 { return b } return fib(x-1, b, a+b) } ` + fmt.Sprintf("out = fib(%d, 0, 1)", n) parseTime, compileTime, runTime, result, err := runBench([]byte(input)) if err != nil { panic(err) } if nativeResult != int(result.(*tengo.Int).Value) { panic(fmt.Errorf("wrong result: %d != %d", nativeResult, int(result.(*tengo.Int).Value))) } fmt.Println("-------------------------------------") fmt.Printf("fibonacci(%d) (tail-call #2)\n", n) fmt.Println("-------------------------------------") fmt.Printf("Result: %d\n", nativeResult) fmt.Printf("Go: %s\n", nativeTime) fmt.Printf("Parser: %s\n", parseTime) fmt.Printf("Compile: %s\n", compileTime) fmt.Printf("VM: %s\n", runTime) } func fib(n int) int { if n == 0 { return 0 } else if n == 1 { return 1 } else { return fib(n-1) + fib(n-2) } } func fibTC1(n, s int) int { if n == 0 { return 0 + s } else if n == 1 { return 1 + s } return fibTC1(n-1, fibTC1(n-2, s)) } func fibTC2(n, a, b int) int { if n == 0 { return a } else if n == 1 { return b } else { return fibTC2(n-1, b, a+b) } } func runBench( input []byte, ) ( parseTime time.Duration, compileTime time.Duration, runTime time.Duration, result tengo.Object, err error, ) { var astFile *parser.File parseTime, astFile, err = parse(input) if err != nil { return } var bytecode *tengo.Bytecode compileTime, bytecode, err = compileFile(astFile) if err != nil { return } runTime, result, err = runVM(bytecode) return } func parse(input []byte) (time.Duration, *parser.File, error) { fileSet := parser.NewFileSet() inputFile := fileSet.AddFile("bench", -1, len(input)) start := time.Now() p := parser.NewParser(inputFile, input, nil) file, err := p.ParseFile() if err != nil { return time.Since(start), nil, err } return time.Since(start), file, nil } func compileFile(file *parser.File) (time.Duration, *tengo.Bytecode, error) { symTable := tengo.NewSymbolTable() symTable.Define("out") start := time.Now() c := tengo.NewCompiler(file.InputFile, symTable, nil, nil, nil) if err := c.Compile(file); err != nil { return time.Since(start), nil, err } bytecode := c.Bytecode() bytecode.RemoveDuplicates() return time.Since(start), bytecode, nil } func runVM( bytecode *tengo.Bytecode, ) (time.Duration, tengo.Object, error) { globals := make([]tengo.Object, tengo.GlobalsSize) start := time.Now() v := tengo.NewVM(bytecode, globals, -1) if err := v.Run(); err != nil { return time.Since(start), nil, err } return time.Since(start), globals[0], nil } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/cmd/tengo/0000755000175000017500000000000014607001502017021 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8800416 golang-github-d5-tengo/cmd/tengo/main.go0000644000175000017500000001577214607001502020310 0ustar00maythammaythampackage main import ( "bufio" "bytes" "flag" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/stdlib" ) const ( sourceFileExt = ".tengo" replPrompt = ">> " ) var ( compileOutput string showHelp bool showVersion bool resolvePath bool // TODO Remove this flag at version 3 version = "dev" ) func init() { flag.BoolVar(&showHelp, "help", false, "Show help") flag.StringVar(&compileOutput, "o", "", "Compile output file") flag.BoolVar(&showVersion, "version", false, "Show version") flag.BoolVar(&resolvePath, "resolve", false, "Resolve relative import paths") flag.Parse() } func main() { if showHelp { doHelp() os.Exit(2) } else if showVersion { fmt.Println(version) return } modules := stdlib.GetModuleMap(stdlib.AllModuleNames()...) inputFile := flag.Arg(0) if inputFile == "" { // REPL RunREPL(modules, os.Stdin, os.Stdout) return } inputData, err := ioutil.ReadFile(inputFile) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Error reading input file: %s\n", err.Error()) os.Exit(1) } inputFile, err = filepath.Abs(inputFile) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Error file path: %s\n", err) os.Exit(1) } if len(inputData) > 1 && string(inputData[:2]) == "#!" { copy(inputData, "//") } if compileOutput != "" { err := CompileOnly(modules, inputData, inputFile, compileOutput) if err != nil { _, _ = fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } } else if filepath.Ext(inputFile) == sourceFileExt { err := CompileAndRun(modules, inputData, inputFile) if err != nil { _, _ = fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } } else { if err := RunCompiled(modules, inputData); err != nil { _, _ = fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } } } // CompileOnly compiles the source code and writes the compiled binary into // outputFile. func CompileOnly( modules *tengo.ModuleMap, data []byte, inputFile, outputFile string, ) (err error) { bytecode, err := compileSrc(modules, data, inputFile) if err != nil { return } if outputFile == "" { outputFile = basename(inputFile) + ".out" } out, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY, os.ModePerm) if err != nil { return } defer func() { if err != nil { _ = out.Close() } else { err = out.Close() } }() err = bytecode.Encode(out) if err != nil { return } fmt.Println(outputFile) return } // CompileAndRun compiles the source code and executes it. func CompileAndRun( modules *tengo.ModuleMap, data []byte, inputFile string, ) (err error) { bytecode, err := compileSrc(modules, data, inputFile) if err != nil { return } machine := tengo.NewVM(bytecode, nil, -1) err = machine.Run() return } // RunCompiled reads the compiled binary from file and executes it. func RunCompiled(modules *tengo.ModuleMap, data []byte) (err error) { bytecode := &tengo.Bytecode{} err = bytecode.Decode(bytes.NewReader(data), modules) if err != nil { return } machine := tengo.NewVM(bytecode, nil, -1) err = machine.Run() return } // RunREPL starts REPL. func RunREPL(modules *tengo.ModuleMap, in io.Reader, out io.Writer) { stdin := bufio.NewScanner(in) fileSet := parser.NewFileSet() globals := make([]tengo.Object, tengo.GlobalsSize) symbolTable := tengo.NewSymbolTable() for idx, fn := range tengo.GetAllBuiltinFunctions() { symbolTable.DefineBuiltin(idx, fn.Name) } // embed println function symbol := symbolTable.Define("__repl_println__") globals[symbol.Index] = &tengo.UserFunction{ Name: "println", Value: func(args ...tengo.Object) (ret tengo.Object, err error) { var printArgs []interface{} for _, arg := range args { if _, isUndefined := arg.(*tengo.Undefined); isUndefined { printArgs = append(printArgs, "") } else { s, _ := tengo.ToString(arg) printArgs = append(printArgs, s) } } printArgs = append(printArgs, "\n") _, _ = fmt.Print(printArgs...) return }, } var constants []tengo.Object for { _, _ = fmt.Fprint(out, replPrompt) scanned := stdin.Scan() if !scanned { return } line := stdin.Text() srcFile := fileSet.AddFile("repl", -1, len(line)) p := parser.NewParser(srcFile, []byte(line), nil) file, err := p.ParseFile() if err != nil { _, _ = fmt.Fprintln(out, err.Error()) continue } file = addPrints(file) c := tengo.NewCompiler(srcFile, symbolTable, constants, modules, nil) if err := c.Compile(file); err != nil { _, _ = fmt.Fprintln(out, err.Error()) continue } bytecode := c.Bytecode() machine := tengo.NewVM(bytecode, globals, -1) if err := machine.Run(); err != nil { _, _ = fmt.Fprintln(out, err.Error()) continue } constants = bytecode.Constants } } func compileSrc( modules *tengo.ModuleMap, src []byte, inputFile string, ) (*tengo.Bytecode, error) { fileSet := parser.NewFileSet() srcFile := fileSet.AddFile(filepath.Base(inputFile), -1, len(src)) p := parser.NewParser(srcFile, src, nil) file, err := p.ParseFile() if err != nil { return nil, err } c := tengo.NewCompiler(srcFile, nil, nil, modules, nil) c.EnableFileImport(true) if resolvePath { c.SetImportDir(filepath.Dir(inputFile)) } if err := c.Compile(file); err != nil { return nil, err } bytecode := c.Bytecode() bytecode.RemoveDuplicates() return bytecode, nil } func doHelp() { fmt.Println("Usage:") fmt.Println() fmt.Println(" tengo [flags] {input-file}") fmt.Println() fmt.Println("Flags:") fmt.Println() fmt.Println(" -o compile output file") fmt.Println(" -version show version") fmt.Println() fmt.Println("Examples:") fmt.Println() fmt.Println(" tengo") fmt.Println() fmt.Println(" Start Tengo REPL") fmt.Println() fmt.Println(" tengo myapp.tengo") fmt.Println() fmt.Println(" Compile and run source file (myapp.tengo)") fmt.Println(" Source file must have .tengo extension") fmt.Println() fmt.Println(" tengo -o myapp myapp.tengo") fmt.Println() fmt.Println(" Compile source file (myapp.tengo) into bytecode file (myapp)") fmt.Println() fmt.Println(" tengo myapp") fmt.Println() fmt.Println(" Run bytecode file (myapp)") fmt.Println() fmt.Println() } func addPrints(file *parser.File) *parser.File { var stmts []parser.Stmt for _, s := range file.Stmts { switch s := s.(type) { case *parser.ExprStmt: stmts = append(stmts, &parser.ExprStmt{ Expr: &parser.CallExpr{ Func: &parser.Ident{Name: "__repl_println__"}, Args: []parser.Expr{s.Expr}, }, }) case *parser.AssignStmt: stmts = append(stmts, s) stmts = append(stmts, &parser.ExprStmt{ Expr: &parser.CallExpr{ Func: &parser.Ident{ Name: "__repl_println__", }, Args: s.LHS, }, }) default: stmts = append(stmts, s) } } return &parser.File{ InputFile: file.InputFile, Stmts: stmts, } } func basename(s string) string { s = filepath.Base(s) n := strings.LastIndexByte(s, '.') if n > 0 { return s[:n] } return s } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/compiler.go0000644000175000017500000010265414607001502017313 0ustar00maythammaythampackage tengo import ( "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "reflect" "strings" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/token" ) // compilationScope represents a compiled instructions and the last two // instructions that were emitted. type compilationScope struct { Instructions []byte SymbolInit map[string]bool SourceMap map[int]parser.Pos } // loop represents a loop construct that the compiler uses to track the current // loop. type loop struct { Continues []int Breaks []int } // CompilerError represents a compiler error. type CompilerError struct { FileSet *parser.SourceFileSet Node parser.Node Err error } func (e *CompilerError) Error() string { filePos := e.FileSet.Position(e.Node.Pos()) return fmt.Sprintf("Compile Error: %s\n\tat %s", e.Err.Error(), filePos) } // Compiler compiles the AST into a bytecode. type Compiler struct { file *parser.SourceFile parent *Compiler modulePath string importDir string importFileExt []string constants []Object symbolTable *SymbolTable scopes []compilationScope scopeIndex int modules ModuleGetter compiledModules map[string]*CompiledFunction allowFileImport bool loops []*loop loopIndex int trace io.Writer indent int } // NewCompiler creates a Compiler. func NewCompiler( file *parser.SourceFile, symbolTable *SymbolTable, constants []Object, modules ModuleGetter, trace io.Writer, ) *Compiler { mainScope := compilationScope{ SymbolInit: make(map[string]bool), SourceMap: make(map[int]parser.Pos), } // symbol table if symbolTable == nil { symbolTable = NewSymbolTable() } // add builtin functions to the symbol table for idx, fn := range builtinFuncs { symbolTable.DefineBuiltin(idx, fn.Name) } // builtin modules if modules == nil { modules = NewModuleMap() } return &Compiler{ file: file, symbolTable: symbolTable, constants: constants, scopes: []compilationScope{mainScope}, scopeIndex: 0, loopIndex: -1, trace: trace, modules: modules, compiledModules: make(map[string]*CompiledFunction), importFileExt: []string{SourceFileExtDefault}, } } // Compile compiles the AST node. func (c *Compiler) Compile(node parser.Node) error { if c.trace != nil { if node != nil { defer untracec(tracec(c, fmt.Sprintf("%s (%s)", node.String(), reflect.TypeOf(node).Elem().Name()))) } else { defer untracec(tracec(c, "")) } } switch node := node.(type) { case *parser.File: for _, stmt := range node.Stmts { if err := c.Compile(stmt); err != nil { return err } } case *parser.ExprStmt: if err := c.Compile(node.Expr); err != nil { return err } c.emit(node, parser.OpPop) case *parser.IncDecStmt: op := token.AddAssign if node.Token == token.Dec { op = token.SubAssign } return c.compileAssign(node, []parser.Expr{node.Expr}, []parser.Expr{&parser.IntLit{Value: 1}}, op) case *parser.ParenExpr: if err := c.Compile(node.Expr); err != nil { return err } case *parser.BinaryExpr: if node.Token == token.LAnd || node.Token == token.LOr { return c.compileLogical(node) } if err := c.Compile(node.LHS); err != nil { return err } if err := c.Compile(node.RHS); err != nil { return err } switch node.Token { case token.Add: c.emit(node, parser.OpBinaryOp, int(token.Add)) case token.Sub: c.emit(node, parser.OpBinaryOp, int(token.Sub)) case token.Mul: c.emit(node, parser.OpBinaryOp, int(token.Mul)) case token.Quo: c.emit(node, parser.OpBinaryOp, int(token.Quo)) case token.Rem: c.emit(node, parser.OpBinaryOp, int(token.Rem)) case token.Greater: c.emit(node, parser.OpBinaryOp, int(token.Greater)) case token.GreaterEq: c.emit(node, parser.OpBinaryOp, int(token.GreaterEq)) case token.Less: c.emit(node, parser.OpBinaryOp, int(token.Less)) case token.LessEq: c.emit(node, parser.OpBinaryOp, int(token.LessEq)) case token.Equal: c.emit(node, parser.OpEqual) case token.NotEqual: c.emit(node, parser.OpNotEqual) case token.And: c.emit(node, parser.OpBinaryOp, int(token.And)) case token.Or: c.emit(node, parser.OpBinaryOp, int(token.Or)) case token.Xor: c.emit(node, parser.OpBinaryOp, int(token.Xor)) case token.AndNot: c.emit(node, parser.OpBinaryOp, int(token.AndNot)) case token.Shl: c.emit(node, parser.OpBinaryOp, int(token.Shl)) case token.Shr: c.emit(node, parser.OpBinaryOp, int(token.Shr)) default: return c.errorf(node, "invalid binary operator: %s", node.Token.String()) } case *parser.IntLit: c.emit(node, parser.OpConstant, c.addConstant(&Int{Value: node.Value})) case *parser.FloatLit: c.emit(node, parser.OpConstant, c.addConstant(&Float{Value: node.Value})) case *parser.BoolLit: if node.Value { c.emit(node, parser.OpTrue) } else { c.emit(node, parser.OpFalse) } case *parser.StringLit: if len(node.Value) > MaxStringLen { return c.error(node, ErrStringLimit) } c.emit(node, parser.OpConstant, c.addConstant(&String{Value: node.Value})) case *parser.CharLit: c.emit(node, parser.OpConstant, c.addConstant(&Char{Value: node.Value})) case *parser.UndefinedLit: c.emit(node, parser.OpNull) case *parser.UnaryExpr: if err := c.Compile(node.Expr); err != nil { return err } switch node.Token { case token.Not: c.emit(node, parser.OpLNot) case token.Sub: c.emit(node, parser.OpMinus) case token.Xor: c.emit(node, parser.OpBComplement) case token.Add: // do nothing? default: return c.errorf(node, "invalid unary operator: %s", node.Token.String()) } case *parser.IfStmt: // open new symbol table for the statement c.symbolTable = c.symbolTable.Fork(true) defer func() { c.symbolTable = c.symbolTable.Parent(false) }() if node.Init != nil { if err := c.Compile(node.Init); err != nil { return err } } if err := c.Compile(node.Cond); err != nil { return err } // first jump placeholder jumpPos1 := c.emit(node, parser.OpJumpFalsy, 0) if err := c.Compile(node.Body); err != nil { return err } if node.Else != nil { // second jump placeholder jumpPos2 := c.emit(node, parser.OpJump, 0) // update first jump offset curPos := len(c.currentInstructions()) c.changeOperand(jumpPos1, curPos) if err := c.Compile(node.Else); err != nil { return err } // update second jump offset curPos = len(c.currentInstructions()) c.changeOperand(jumpPos2, curPos) } else { // update first jump offset curPos := len(c.currentInstructions()) c.changeOperand(jumpPos1, curPos) } case *parser.ForStmt: return c.compileForStmt(node) case *parser.ForInStmt: return c.compileForInStmt(node) case *parser.BranchStmt: if node.Token == token.Break { curLoop := c.currentLoop() if curLoop == nil { return c.errorf(node, "break not allowed outside loop") } pos := c.emit(node, parser.OpJump, 0) curLoop.Breaks = append(curLoop.Breaks, pos) } else if node.Token == token.Continue { curLoop := c.currentLoop() if curLoop == nil { return c.errorf(node, "continue not allowed outside loop") } pos := c.emit(node, parser.OpJump, 0) curLoop.Continues = append(curLoop.Continues, pos) } else { panic(fmt.Errorf("invalid branch statement: %s", node.Token.String())) } case *parser.BlockStmt: if len(node.Stmts) == 0 { return nil } c.symbolTable = c.symbolTable.Fork(true) defer func() { c.symbolTable = c.symbolTable.Parent(false) }() for _, stmt := range node.Stmts { if err := c.Compile(stmt); err != nil { return err } } case *parser.AssignStmt: err := c.compileAssign(node, node.LHS, node.RHS, node.Token) if err != nil { return err } case *parser.Ident: symbol, _, ok := c.symbolTable.Resolve(node.Name, false) if !ok { return c.errorf(node, "unresolved reference '%s'", node.Name) } switch symbol.Scope { case ScopeGlobal: c.emit(node, parser.OpGetGlobal, symbol.Index) case ScopeLocal: c.emit(node, parser.OpGetLocal, symbol.Index) case ScopeBuiltin: c.emit(node, parser.OpGetBuiltin, symbol.Index) case ScopeFree: c.emit(node, parser.OpGetFree, symbol.Index) } case *parser.ArrayLit: for _, elem := range node.Elements { if err := c.Compile(elem); err != nil { return err } } c.emit(node, parser.OpArray, len(node.Elements)) case *parser.MapLit: for _, elt := range node.Elements { // key if len(elt.Key) > MaxStringLen { return c.error(node, ErrStringLimit) } c.emit(node, parser.OpConstant, c.addConstant(&String{Value: elt.Key})) // value if err := c.Compile(elt.Value); err != nil { return err } } c.emit(node, parser.OpMap, len(node.Elements)*2) case *parser.SelectorExpr: // selector on RHS side if err := c.Compile(node.Expr); err != nil { return err } if err := c.Compile(node.Sel); err != nil { return err } c.emit(node, parser.OpIndex) case *parser.IndexExpr: if err := c.Compile(node.Expr); err != nil { return err } if err := c.Compile(node.Index); err != nil { return err } c.emit(node, parser.OpIndex) case *parser.SliceExpr: if err := c.Compile(node.Expr); err != nil { return err } if node.Low != nil { if err := c.Compile(node.Low); err != nil { return err } } else { c.emit(node, parser.OpNull) } if node.High != nil { if err := c.Compile(node.High); err != nil { return err } } else { c.emit(node, parser.OpNull) } c.emit(node, parser.OpSliceIndex) case *parser.FuncLit: c.enterScope() for _, p := range node.Type.Params.List { s := c.symbolTable.Define(p.Name) // function arguments is not assigned directly. s.LocalAssigned = true } if err := c.Compile(node.Body); err != nil { return err } // code optimization c.optimizeFunc(node) freeSymbols := c.symbolTable.FreeSymbols() numLocals := c.symbolTable.MaxSymbols() instructions, sourceMap := c.leaveScope() for _, s := range freeSymbols { switch s.Scope { case ScopeLocal: if !s.LocalAssigned { // Here, the closure is capturing a local variable that's // not yet assigned its value. One example is a local // recursive function: // // func() { // foo := func(x) { // // .. // return foo(x-1) // } // } // // which translate into // // 0000 GETL 0 // 0002 CLOSURE ? 1 // 0006 DEFL 0 // // . So the local variable (0) is being captured before // it's assigned the value. // // Solution is to transform the code into something like // this: // // func() { // foo := undefined // foo = func(x) { // // .. // return foo(x-1) // } // } // // that is equivalent to // // 0000 NULL // 0001 DEFL 0 // 0003 GETL 0 // 0005 CLOSURE ? 1 // 0009 SETL 0 // c.emit(node, parser.OpNull) c.emit(node, parser.OpDefineLocal, s.Index) s.LocalAssigned = true } c.emit(node, parser.OpGetLocalPtr, s.Index) case ScopeFree: c.emit(node, parser.OpGetFreePtr, s.Index) } } compiledFunction := &CompiledFunction{ Instructions: instructions, NumLocals: numLocals, NumParameters: len(node.Type.Params.List), VarArgs: node.Type.Params.VarArgs, SourceMap: sourceMap, } if len(freeSymbols) > 0 { c.emit(node, parser.OpClosure, c.addConstant(compiledFunction), len(freeSymbols)) } else { c.emit(node, parser.OpConstant, c.addConstant(compiledFunction)) } case *parser.ReturnStmt: if c.symbolTable.Parent(true) == nil { // outside the function return c.errorf(node, "return not allowed outside function") } if node.Result == nil { c.emit(node, parser.OpReturn, 0) } else { if err := c.Compile(node.Result); err != nil { return err } c.emit(node, parser.OpReturn, 1) } case *parser.CallExpr: if err := c.Compile(node.Func); err != nil { return err } for _, arg := range node.Args { if err := c.Compile(arg); err != nil { return err } } ellipsis := 0 if node.Ellipsis.IsValid() { ellipsis = 1 } c.emit(node, parser.OpCall, len(node.Args), ellipsis) case *parser.ImportExpr: if node.ModuleName == "" { return c.errorf(node, "empty module name") } if mod := c.modules.Get(node.ModuleName); mod != nil { v, err := mod.Import(node.ModuleName) if err != nil { return err } switch v := v.(type) { case []byte: // module written in Tengo compiled, err := c.compileModule(node, node.ModuleName, v, false) if err != nil { return err } c.emit(node, parser.OpConstant, c.addConstant(compiled)) c.emit(node, parser.OpCall, 0, 0) case Object: // builtin module c.emit(node, parser.OpConstant, c.addConstant(v)) default: panic(fmt.Errorf("invalid import value type: %T", v)) } } else if c.allowFileImport { moduleName := node.ModuleName modulePath, err := c.getPathModule(moduleName) if err != nil { return c.errorf(node, "module file path error: %s", err.Error()) } moduleSrc, err := ioutil.ReadFile(modulePath) if err != nil { return c.errorf(node, "module file read error: %s", err.Error()) } compiled, err := c.compileModule(node, modulePath, moduleSrc, true) if err != nil { return err } c.emit(node, parser.OpConstant, c.addConstant(compiled)) c.emit(node, parser.OpCall, 0, 0) } else { return c.errorf(node, "module '%s' not found", node.ModuleName) } case *parser.ExportStmt: // export statement must be in top-level scope if c.scopeIndex != 0 { return c.errorf(node, "export not allowed inside function") } // export statement is simply ignore when compiling non-module code if c.parent == nil { break } if err := c.Compile(node.Result); err != nil { return err } c.emit(node, parser.OpImmutable) c.emit(node, parser.OpReturn, 1) case *parser.ErrorExpr: if err := c.Compile(node.Expr); err != nil { return err } c.emit(node, parser.OpError) case *parser.ImmutableExpr: if err := c.Compile(node.Expr); err != nil { return err } c.emit(node, parser.OpImmutable) case *parser.CondExpr: if err := c.Compile(node.Cond); err != nil { return err } // first jump placeholder jumpPos1 := c.emit(node, parser.OpJumpFalsy, 0) if err := c.Compile(node.True); err != nil { return err } // second jump placeholder jumpPos2 := c.emit(node, parser.OpJump, 0) // update first jump offset curPos := len(c.currentInstructions()) c.changeOperand(jumpPos1, curPos) if err := c.Compile(node.False); err != nil { return err } // update second jump offset curPos = len(c.currentInstructions()) c.changeOperand(jumpPos2, curPos) } return nil } // Bytecode returns a compiled bytecode. func (c *Compiler) Bytecode() *Bytecode { return &Bytecode{ FileSet: c.file.Set(), MainFunction: &CompiledFunction{ Instructions: append(c.currentInstructions(), parser.OpSuspend), SourceMap: c.currentSourceMap(), }, Constants: c.constants, } } // EnableFileImport enables or disables module loading from local files. // Local file modules are disabled by default. func (c *Compiler) EnableFileImport(enable bool) { c.allowFileImport = enable } // SetImportDir sets the initial import directory path for file imports. func (c *Compiler) SetImportDir(dir string) { c.importDir = dir } // SetImportFileExt sets the extension name of the source file for loading // local module files. // // Use this method if you want other source file extension than ".tengo". // // // this will search for *.tengo, *.foo, *.bar // err := c.SetImportFileExt(".tengo", ".foo", ".bar") // // This function requires at least one argument, since it will replace the // current list of extension name. func (c *Compiler) SetImportFileExt(exts ...string) error { if len(exts) == 0 { return fmt.Errorf("missing arg: at least one argument is required") } for _, ext := range exts { if ext != filepath.Ext(ext) || ext == "" { return fmt.Errorf("invalid file extension: %s", ext) } } c.importFileExt = exts // Replace the hole current extension list return nil } // GetImportFileExt returns the current list of extension name. // Thease are the complementary suffix of the source file to search and load // local module files. func (c *Compiler) GetImportFileExt() []string { return c.importFileExt } func (c *Compiler) compileAssign( node parser.Node, lhs, rhs []parser.Expr, op token.Token, ) error { numLHS, numRHS := len(lhs), len(rhs) if numLHS > 1 || numRHS > 1 { return c.errorf(node, "tuple assignment not allowed") } // resolve and compile left-hand side ident, selectors := resolveAssignLHS(lhs[0]) numSel := len(selectors) if op == token.Define && numSel > 0 { // using selector on new variable does not make sense return c.errorf(node, "operator ':=' not allowed with selector") } _, isFunc := rhs[0].(*parser.FuncLit) symbol, depth, exists := c.symbolTable.Resolve(ident, false) if op == token.Define { if depth == 0 && exists { return c.errorf(node, "'%s' redeclared in this block", ident) } if isFunc { symbol = c.symbolTable.Define(ident) } } else { if !exists { return c.errorf(node, "unresolved reference '%s'", ident) } } // +=, -=, *=, /= if op != token.Assign && op != token.Define { if err := c.Compile(lhs[0]); err != nil { return err } } // compile RHSs for _, expr := range rhs { if err := c.Compile(expr); err != nil { return err } } if op == token.Define && !isFunc { symbol = c.symbolTable.Define(ident) } switch op { case token.AddAssign: c.emit(node, parser.OpBinaryOp, int(token.Add)) case token.SubAssign: c.emit(node, parser.OpBinaryOp, int(token.Sub)) case token.MulAssign: c.emit(node, parser.OpBinaryOp, int(token.Mul)) case token.QuoAssign: c.emit(node, parser.OpBinaryOp, int(token.Quo)) case token.RemAssign: c.emit(node, parser.OpBinaryOp, int(token.Rem)) case token.AndAssign: c.emit(node, parser.OpBinaryOp, int(token.And)) case token.OrAssign: c.emit(node, parser.OpBinaryOp, int(token.Or)) case token.AndNotAssign: c.emit(node, parser.OpBinaryOp, int(token.AndNot)) case token.XorAssign: c.emit(node, parser.OpBinaryOp, int(token.Xor)) case token.ShlAssign: c.emit(node, parser.OpBinaryOp, int(token.Shl)) case token.ShrAssign: c.emit(node, parser.OpBinaryOp, int(token.Shr)) } // compile selector expressions (right to left) for i := numSel - 1; i >= 0; i-- { if err := c.Compile(selectors[i]); err != nil { return err } } switch symbol.Scope { case ScopeGlobal: if numSel > 0 { c.emit(node, parser.OpSetSelGlobal, symbol.Index, numSel) } else { c.emit(node, parser.OpSetGlobal, symbol.Index) } case ScopeLocal: if numSel > 0 { c.emit(node, parser.OpSetSelLocal, symbol.Index, numSel) } else { if op == token.Define && !symbol.LocalAssigned { c.emit(node, parser.OpDefineLocal, symbol.Index) } else { c.emit(node, parser.OpSetLocal, symbol.Index) } } // mark the symbol as local-assigned symbol.LocalAssigned = true case ScopeFree: if numSel > 0 { c.emit(node, parser.OpSetSelFree, symbol.Index, numSel) } else { c.emit(node, parser.OpSetFree, symbol.Index) } default: panic(fmt.Errorf("invalid assignment variable scope: %s", symbol.Scope)) } return nil } func (c *Compiler) compileLogical(node *parser.BinaryExpr) error { // left side term if err := c.Compile(node.LHS); err != nil { return err } // jump position var jumpPos int if node.Token == token.LAnd { jumpPos = c.emit(node, parser.OpAndJump, 0) } else { jumpPos = c.emit(node, parser.OpOrJump, 0) } // right side term if err := c.Compile(node.RHS); err != nil { return err } c.changeOperand(jumpPos, len(c.currentInstructions())) return nil } func (c *Compiler) compileForStmt(stmt *parser.ForStmt) error { c.symbolTable = c.symbolTable.Fork(true) defer func() { c.symbolTable = c.symbolTable.Parent(false) }() // init statement if stmt.Init != nil { if err := c.Compile(stmt.Init); err != nil { return err } } // pre-condition position preCondPos := len(c.currentInstructions()) // condition expression postCondPos := -1 if stmt.Cond != nil { if err := c.Compile(stmt.Cond); err != nil { return err } // condition jump position postCondPos = c.emit(stmt, parser.OpJumpFalsy, 0) } // enter loop loop := c.enterLoop() // body statement if err := c.Compile(stmt.Body); err != nil { c.leaveLoop() return err } c.leaveLoop() // post-body position postBodyPos := len(c.currentInstructions()) // post statement if stmt.Post != nil { if err := c.Compile(stmt.Post); err != nil { return err } } // back to condition c.emit(stmt, parser.OpJump, preCondPos) // post-statement position postStmtPos := len(c.currentInstructions()) if postCondPos >= 0 { c.changeOperand(postCondPos, postStmtPos) } // update all break/continue jump positions for _, pos := range loop.Breaks { c.changeOperand(pos, postStmtPos) } for _, pos := range loop.Continues { c.changeOperand(pos, postBodyPos) } return nil } func (c *Compiler) compileForInStmt(stmt *parser.ForInStmt) error { c.symbolTable = c.symbolTable.Fork(true) defer func() { c.symbolTable = c.symbolTable.Parent(false) }() // for-in statement is compiled like following: // // for :it := iterator(iterable); :it.next(); { // k, v := :it.get() // DEFINE operator // // ... body ... // } // // ":it" is a local variable but it will not conflict with other user variables // because character ":" is not allowed in the variable names. // init // :it = iterator(iterable) itSymbol := c.symbolTable.Define(":it") if err := c.Compile(stmt.Iterable); err != nil { return err } c.emit(stmt, parser.OpIteratorInit) if itSymbol.Scope == ScopeGlobal { c.emit(stmt, parser.OpSetGlobal, itSymbol.Index) } else { c.emit(stmt, parser.OpDefineLocal, itSymbol.Index) } // pre-condition position preCondPos := len(c.currentInstructions()) // condition // :it.HasMore() if itSymbol.Scope == ScopeGlobal { c.emit(stmt, parser.OpGetGlobal, itSymbol.Index) } else { c.emit(stmt, parser.OpGetLocal, itSymbol.Index) } c.emit(stmt, parser.OpIteratorNext) // condition jump position postCondPos := c.emit(stmt, parser.OpJumpFalsy, 0) // enter loop loop := c.enterLoop() // assign key variable if stmt.Key.Name != "_" { keySymbol := c.symbolTable.Define(stmt.Key.Name) if itSymbol.Scope == ScopeGlobal { c.emit(stmt, parser.OpGetGlobal, itSymbol.Index) } else { c.emit(stmt, parser.OpGetLocal, itSymbol.Index) } c.emit(stmt, parser.OpIteratorKey) if keySymbol.Scope == ScopeGlobal { c.emit(stmt, parser.OpSetGlobal, keySymbol.Index) } else { keySymbol.LocalAssigned = true c.emit(stmt, parser.OpDefineLocal, keySymbol.Index) } } // assign value variable if stmt.Value.Name != "_" { valueSymbol := c.symbolTable.Define(stmt.Value.Name) if itSymbol.Scope == ScopeGlobal { c.emit(stmt, parser.OpGetGlobal, itSymbol.Index) } else { c.emit(stmt, parser.OpGetLocal, itSymbol.Index) } c.emit(stmt, parser.OpIteratorValue) if valueSymbol.Scope == ScopeGlobal { c.emit(stmt, parser.OpSetGlobal, valueSymbol.Index) } else { valueSymbol.LocalAssigned = true c.emit(stmt, parser.OpDefineLocal, valueSymbol.Index) } } // body statement if err := c.Compile(stmt.Body); err != nil { c.leaveLoop() return err } c.leaveLoop() // post-body position postBodyPos := len(c.currentInstructions()) // back to condition c.emit(stmt, parser.OpJump, preCondPos) // post-statement position postStmtPos := len(c.currentInstructions()) c.changeOperand(postCondPos, postStmtPos) // update all break/continue jump positions for _, pos := range loop.Breaks { c.changeOperand(pos, postStmtPos) } for _, pos := range loop.Continues { c.changeOperand(pos, postBodyPos) } return nil } func (c *Compiler) checkCyclicImports( node parser.Node, modulePath string, ) error { if c.modulePath == modulePath { return c.errorf(node, "cyclic module import: %s", modulePath) } else if c.parent != nil { return c.parent.checkCyclicImports(node, modulePath) } return nil } func (c *Compiler) compileModule( node parser.Node, modulePath string, src []byte, isFile bool, ) (*CompiledFunction, error) { if err := c.checkCyclicImports(node, modulePath); err != nil { return nil, err } compiledModule, exists := c.loadCompiledModule(modulePath) if exists { return compiledModule, nil } modFile := c.file.Set().AddFile(modulePath, -1, len(src)) p := parser.NewParser(modFile, src, nil) file, err := p.ParseFile() if err != nil { return nil, err } // inherit builtin functions symbolTable := NewSymbolTable() for _, sym := range c.symbolTable.BuiltinSymbols() { symbolTable.DefineBuiltin(sym.Index, sym.Name) } // no global scope for the module symbolTable = symbolTable.Fork(false) // compile module moduleCompiler := c.fork(modFile, modulePath, symbolTable, isFile) if err := moduleCompiler.Compile(file); err != nil { return nil, err } // code optimization moduleCompiler.optimizeFunc(node) compiledFunc := moduleCompiler.Bytecode().MainFunction compiledFunc.NumLocals = symbolTable.MaxSymbols() c.storeCompiledModule(modulePath, compiledFunc) return compiledFunc, nil } func (c *Compiler) loadCompiledModule( modulePath string, ) (mod *CompiledFunction, ok bool) { if c.parent != nil { return c.parent.loadCompiledModule(modulePath) } mod, ok = c.compiledModules[modulePath] return } func (c *Compiler) storeCompiledModule( modulePath string, module *CompiledFunction, ) { if c.parent != nil { c.parent.storeCompiledModule(modulePath, module) } c.compiledModules[modulePath] = module } func (c *Compiler) enterLoop() *loop { loop := &loop{} c.loops = append(c.loops, loop) c.loopIndex++ if c.trace != nil { c.printTrace("LOOPE", c.loopIndex) } return loop } func (c *Compiler) leaveLoop() { if c.trace != nil { c.printTrace("LOOPL", c.loopIndex) } c.loops = c.loops[:len(c.loops)-1] c.loopIndex-- } func (c *Compiler) currentLoop() *loop { if c.loopIndex >= 0 { return c.loops[c.loopIndex] } return nil } func (c *Compiler) currentInstructions() []byte { return c.scopes[c.scopeIndex].Instructions } func (c *Compiler) currentSourceMap() map[int]parser.Pos { return c.scopes[c.scopeIndex].SourceMap } func (c *Compiler) enterScope() { scope := compilationScope{ SymbolInit: make(map[string]bool), SourceMap: make(map[int]parser.Pos), } c.scopes = append(c.scopes, scope) c.scopeIndex++ c.symbolTable = c.symbolTable.Fork(false) if c.trace != nil { c.printTrace("SCOPE", c.scopeIndex) } } func (c *Compiler) leaveScope() ( instructions []byte, sourceMap map[int]parser.Pos, ) { instructions = c.currentInstructions() sourceMap = c.currentSourceMap() c.scopes = c.scopes[:len(c.scopes)-1] c.scopeIndex-- c.symbolTable = c.symbolTable.Parent(true) if c.trace != nil { c.printTrace("SCOPL", c.scopeIndex) } return } func (c *Compiler) fork( file *parser.SourceFile, modulePath string, symbolTable *SymbolTable, isFile bool, ) *Compiler { child := NewCompiler(file, symbolTable, nil, c.modules, c.trace) child.modulePath = modulePath // module file path child.parent = c // parent to set to current compiler child.allowFileImport = c.allowFileImport child.importDir = c.importDir child.importFileExt = c.importFileExt if isFile && c.importDir != "" { child.importDir = filepath.Dir(modulePath) } return child } func (c *Compiler) error(node parser.Node, err error) error { return &CompilerError{ FileSet: c.file.Set(), Node: node, Err: err, } } func (c *Compiler) errorf( node parser.Node, format string, args ...interface{}, ) error { return &CompilerError{ FileSet: c.file.Set(), Node: node, Err: fmt.Errorf(format, args...), } } func (c *Compiler) addConstant(o Object) int { if c.parent != nil { // module compilers will use their parent's constants array return c.parent.addConstant(o) } c.constants = append(c.constants, o) if c.trace != nil { c.printTrace(fmt.Sprintf("CONST %04d %s", len(c.constants)-1, o)) } return len(c.constants) - 1 } func (c *Compiler) addInstruction(b []byte) int { posNewIns := len(c.currentInstructions()) c.scopes[c.scopeIndex].Instructions = append( c.currentInstructions(), b...) return posNewIns } func (c *Compiler) replaceInstruction(pos int, inst []byte) { copy(c.currentInstructions()[pos:], inst) if c.trace != nil { c.printTrace(fmt.Sprintf("REPLC %s", FormatInstructions( c.scopes[c.scopeIndex].Instructions[pos:], pos)[0])) } } func (c *Compiler) changeOperand(opPos int, operand ...int) { op := c.currentInstructions()[opPos] inst := MakeInstruction(op, operand...) c.replaceInstruction(opPos, inst) } // optimizeFunc performs some code-level optimization for the current function // instructions. It also removes unreachable (dead code) instructions and adds // "returns" instruction if needed. func (c *Compiler) optimizeFunc(node parser.Node) { // any instructions between RETURN and the function end // or instructions between RETURN and jump target position // are considered as unreachable. // pass 1. identify all jump destinations dsts := make(map[int]bool) iterateInstructions(c.scopes[c.scopeIndex].Instructions, func(pos int, opcode parser.Opcode, operands []int) bool { switch opcode { case parser.OpJump, parser.OpJumpFalsy, parser.OpAndJump, parser.OpOrJump: dsts[operands[0]] = true } return true }) // pass 2. eliminate dead code var newInsts []byte posMap := make(map[int]int) // old position to new position var dstIdx int var deadCode bool iterateInstructions(c.scopes[c.scopeIndex].Instructions, func(pos int, opcode parser.Opcode, operands []int) bool { switch { case dsts[pos]: dstIdx++ deadCode = false case opcode == parser.OpReturn: if deadCode { return true } deadCode = true case deadCode: return true } posMap[pos] = len(newInsts) newInsts = append(newInsts, MakeInstruction(opcode, operands...)...) return true }) // pass 3. update jump positions var lastOp parser.Opcode var appendReturn bool endPos := len(c.scopes[c.scopeIndex].Instructions) newEndPost := len(newInsts) iterateInstructions(newInsts, func(pos int, opcode parser.Opcode, operands []int) bool { switch opcode { case parser.OpJump, parser.OpJumpFalsy, parser.OpAndJump, parser.OpOrJump: newDst, ok := posMap[operands[0]] if ok { copy(newInsts[pos:], MakeInstruction(opcode, newDst)) } else if endPos == operands[0] { // there's a jump instruction that jumps to the end of // function compiler should append "return". copy(newInsts[pos:], MakeInstruction(opcode, newEndPost)) appendReturn = true } else { panic(fmt.Errorf("invalid jump position: %d", newDst)) } } lastOp = opcode return true }) if lastOp != parser.OpReturn { appendReturn = true } // pass 4. update source map newSourceMap := make(map[int]parser.Pos) for pos, srcPos := range c.scopes[c.scopeIndex].SourceMap { newPos, ok := posMap[pos] if ok { newSourceMap[newPos] = srcPos } } c.scopes[c.scopeIndex].Instructions = newInsts c.scopes[c.scopeIndex].SourceMap = newSourceMap // append "return" if appendReturn { c.emit(node, parser.OpReturn, 0) } } func (c *Compiler) emit( node parser.Node, opcode parser.Opcode, operands ...int, ) int { filePos := parser.NoPos if node != nil { filePos = node.Pos() } inst := MakeInstruction(opcode, operands...) pos := c.addInstruction(inst) c.scopes[c.scopeIndex].SourceMap[pos] = filePos if c.trace != nil { c.printTrace(fmt.Sprintf("EMIT %s", FormatInstructions( c.scopes[c.scopeIndex].Instructions[pos:], pos)[0])) } return pos } func (c *Compiler) printTrace(a ...interface{}) { const ( dots = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . " n = len(dots) ) i := 2 * c.indent for i > n { _, _ = fmt.Fprint(c.trace, dots) i -= n } _, _ = fmt.Fprint(c.trace, dots[0:i]) _, _ = fmt.Fprintln(c.trace, a...) } func (c *Compiler) getPathModule(moduleName string) (pathFile string, err error) { for _, ext := range c.importFileExt { nameFile := moduleName if !strings.HasSuffix(nameFile, ext) { nameFile += ext } pathFile, err = filepath.Abs(filepath.Join(c.importDir, nameFile)) if err != nil { continue } // Check if file exists if _, err := os.Stat(pathFile); !errors.Is(err, os.ErrNotExist) { return pathFile, nil } } return "", fmt.Errorf("module '%s' not found at: %s", moduleName, pathFile) } func resolveAssignLHS( expr parser.Expr, ) (name string, selectors []parser.Expr) { switch term := expr.(type) { case *parser.SelectorExpr: name, selectors = resolveAssignLHS(term.Expr) selectors = append(selectors, term.Sel) return case *parser.IndexExpr: name, selectors = resolveAssignLHS(term.Expr) selectors = append(selectors, term.Index) case *parser.Ident: name = term.Name } return } func iterateInstructions( b []byte, fn func(pos int, opcode parser.Opcode, operands []int) bool, ) { for i := 0; i < len(b); i++ { numOperands := parser.OpcodeOperands[b[i]] operands, read := parser.ReadOperands(numOperands, b[i+1:]) if !fn(i, b[i], operands) { break } i += read } } func tracec(c *Compiler, msg string) *Compiler { c.printTrace(msg, "{") c.indent++ return c } func untracec(c *Compiler) { c.indent-- c.printTrace("}") } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/compiler_test.go0000644000175000017500000013131114607001502020342 0ustar00maythammaythampackage tengo_test import ( "fmt" "io/ioutil" "path/filepath" "strings" "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/require" "github.com/d5/tengo/v2/stdlib" ) func TestCompiler_Compile(t *testing.T) { expectCompile(t, `1 + 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `1; 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `1 - 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 12), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `1 * 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 13), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `2 / 1`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 14), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(2), intObject(1)))) expectCompile(t, `true`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpTrue), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) expectCompile(t, `false`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpFalse), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) expectCompile(t, `1 > 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 39), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `1 < 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 38), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `1 >= 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 44), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `1 <= 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 43), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `1 == 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpEqual), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `1 != 2`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpNotEqual), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `true == false`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpTrue), tengo.MakeInstruction(parser.OpFalse), tengo.MakeInstruction(parser.OpEqual), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) expectCompile(t, `true != false`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpTrue), tengo.MakeInstruction(parser.OpFalse), tengo.MakeInstruction(parser.OpNotEqual), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) expectCompile(t, `-1`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpMinus), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1)))) expectCompile(t, `!true`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpTrue), tengo.MakeInstruction(parser.OpLNot), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) expectCompile(t, `if true { 10 }; 3333`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpTrue), // 0000 tengo.MakeInstruction(parser.OpJumpFalsy, 10), // 0001 tengo.MakeInstruction(parser.OpConstant, 0), // 0004 tengo.MakeInstruction(parser.OpPop), // 0007 tengo.MakeInstruction(parser.OpConstant, 1), // 0008 tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), // 0011 objectsArray( intObject(10), intObject(3333)))) expectCompile(t, `if (true) { 10 } else { 20 }; 3333;`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpTrue), // 0000 tengo.MakeInstruction(parser.OpJumpFalsy, 15), // 0001 tengo.MakeInstruction(parser.OpConstant, 0), // 0004 tengo.MakeInstruction(parser.OpPop), // 0007 tengo.MakeInstruction(parser.OpJump, 19), // 0008 tengo.MakeInstruction(parser.OpConstant, 1), // 0011 tengo.MakeInstruction(parser.OpPop), // 0014 tengo.MakeInstruction(parser.OpConstant, 2), // 0015 tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), // 0018 objectsArray( intObject(10), intObject(20), intObject(3333)))) expectCompile(t, `"kami"`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( stringObject("kami")))) expectCompile(t, `"ka" + "mi"`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( stringObject("ka"), stringObject("mi")))) expectCompile(t, `a := 1; b := 2; a += b`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetGlobal, 1), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `a := 1; b := 2; a /= b`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetGlobal, 1), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 1), tengo.MakeInstruction(parser.OpBinaryOp, 14), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2)))) expectCompile(t, `[]`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpArray, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) expectCompile(t, `[1, 2, 3]`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpArray, 3), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3)))) expectCompile(t, `[1 + 2, 3 - 4, 5 * 6]`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpBinaryOp, 12), tengo.MakeInstruction(parser.OpConstant, 4), tengo.MakeInstruction(parser.OpConstant, 5), tengo.MakeInstruction(parser.OpBinaryOp, 13), tengo.MakeInstruction(parser.OpArray, 3), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3), intObject(4), intObject(5), intObject(6)))) expectCompile(t, `{}`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpMap, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) expectCompile(t, `{a: 2, b: 4, c: 6}`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpConstant, 4), tengo.MakeInstruction(parser.OpConstant, 5), tengo.MakeInstruction(parser.OpMap, 6), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( stringObject("a"), intObject(2), stringObject("b"), intObject(4), stringObject("c"), intObject(6)))) expectCompile(t, `{a: 2 + 3, b: 5 * 6}`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpConstant, 4), tengo.MakeInstruction(parser.OpConstant, 5), tengo.MakeInstruction(parser.OpBinaryOp, 13), tengo.MakeInstruction(parser.OpMap, 4), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( stringObject("a"), intObject(2), intObject(3), stringObject("b"), intObject(5), intObject(6)))) expectCompile(t, `[1, 2, 3][1 + 1]`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpArray, 3), tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpIndex), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3)))) expectCompile(t, `{a: 2}[2 - 1]`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpMap, 2), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpBinaryOp, 12), tengo.MakeInstruction(parser.OpIndex), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( stringObject("a"), intObject(2), intObject(1)))) expectCompile(t, `[1, 2, 3][:]`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpArray, 3), tengo.MakeInstruction(parser.OpNull), tengo.MakeInstruction(parser.OpNull), tengo.MakeInstruction(parser.OpSliceIndex), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3)))) expectCompile(t, `[1, 2, 3][0 : 2]`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpArray, 3), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSliceIndex), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3), intObject(0)))) expectCompile(t, `[1, 2, 3][:2]`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpArray, 3), tengo.MakeInstruction(parser.OpNull), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSliceIndex), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3)))) expectCompile(t, `[1, 2, 3][0:]`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpArray, 3), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpNull), tengo.MakeInstruction(parser.OpSliceIndex), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3), intObject(0)))) expectCompile(t, `f1 := func(a) { return a }; f1([1, 2]...);`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpArray, 2), tengo.MakeInstruction(parser.OpCall, 1, 1), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(1, 1, tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpReturn, 1)), intObject(1), intObject(2)))) expectCompile(t, `func() { return 5 + 10 }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(5), intObject(10), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `func() { 5 + 10 }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(5), intObject(10), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpReturn, 0))))) expectCompile(t, `func() { 1; 2 }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpReturn, 0))))) expectCompile(t, `func() { 1; return 2 }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `func() { if(true) { return 1 } else { return 2 } }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpTrue), // 0000 tengo.MakeInstruction(parser.OpJumpFalsy, 11), // 0001 tengo.MakeInstruction(parser.OpConstant, 0), // 0004 tengo.MakeInstruction(parser.OpReturn, 1), // 0007 tengo.MakeInstruction(parser.OpConstant, 1), // 0009 tengo.MakeInstruction(parser.OpReturn, 1))))) // 0012 expectCompile(t, `func() { 1; if(true) { 2 } else { 3 }; 4 }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 4), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3), intObject(4), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), // 0000 tengo.MakeInstruction(parser.OpPop), // 0003 tengo.MakeInstruction(parser.OpTrue), // 0004 tengo.MakeInstruction(parser.OpJumpFalsy, 19), // 0005 tengo.MakeInstruction(parser.OpConstant, 1), // 0008 tengo.MakeInstruction(parser.OpPop), // 0011 tengo.MakeInstruction(parser.OpJump, 23), // 0012 tengo.MakeInstruction(parser.OpConstant, 2), // 0015 tengo.MakeInstruction(parser.OpPop), // 0018 tengo.MakeInstruction(parser.OpConstant, 3), // 0019 tengo.MakeInstruction(parser.OpPop), // 0022 tengo.MakeInstruction(parser.OpReturn, 0))))) // 0023 expectCompile(t, `func() { }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(0, 0, tengo.MakeInstruction(parser.OpReturn, 0))))) expectCompile(t, `func() { 24 }()`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpCall, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(24), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpReturn, 0))))) expectCompile(t, `func() { return 24 }()`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpCall, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(24), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `noArg := func() { 24 }; noArg();`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpCall, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(24), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpReturn, 0))))) expectCompile(t, `noArg := func() { return 24 }; noArg();`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpCall, 0, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(24), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `n := 55; func() { n };`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(55), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpReturn, 0))))) expectCompile(t, `func() { n := 55; return n }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(55), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `func() { a := 55; b := 77; return a + b }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(55), intObject(77), compiledFunction(2, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpDefineLocal, 1), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpGetLocal, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `f1 := func(a) { return a }; f1(24);`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpCall, 1, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(1, 1, tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpReturn, 1)), intObject(24)))) expectCompile(t, `varTest := func(...a) { return a }; varTest(1,2,3);`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpCall, 3, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(1, 1, tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpReturn, 1)), intObject(1), intObject(2), intObject(3)))) expectCompile(t, `f1 := func(a, b, c) { a; b; return c; }; f1(24, 25, 26);`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpCall, 3, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(3, 3, tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpGetLocal, 1), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpGetLocal, 2), tengo.MakeInstruction(parser.OpReturn, 1)), intObject(24), intObject(25), intObject(26)))) expectCompile(t, `func() { n := 55; n = 23; return n }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(55), intObject(23), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `len([]);`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpGetBuiltin, 0), tengo.MakeInstruction(parser.OpArray, 0), tengo.MakeInstruction(parser.OpCall, 1, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) expectCompile(t, `func() { return len([]) }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(0, 0, tengo.MakeInstruction(parser.OpGetBuiltin, 0), tengo.MakeInstruction(parser.OpArray, 0), tengo.MakeInstruction(parser.OpCall, 1, 0), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `func(a) { func(b) { return a + b } }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(1, 1, tengo.MakeInstruction(parser.OpGetFree, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 1, tengo.MakeInstruction(parser.OpGetLocalPtr, 0), tengo.MakeInstruction(parser.OpClosure, 0, 1), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpReturn, 0))))) expectCompile(t, ` func(a) { return func(b) { return func(c) { return a + b + c } } }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( compiledFunction(1, 1, tengo.MakeInstruction(parser.OpGetFree, 0), tengo.MakeInstruction(parser.OpGetFree, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 1, tengo.MakeInstruction(parser.OpGetFreePtr, 0), tengo.MakeInstruction(parser.OpGetLocalPtr, 0), tengo.MakeInstruction(parser.OpClosure, 0, 2), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 1, tengo.MakeInstruction(parser.OpGetLocalPtr, 0), tengo.MakeInstruction(parser.OpClosure, 1, 1), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, ` g := 55; func() { a := 66; return func() { b := 77; return func() { c := 88; return g + a + b + c; } } }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 6), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(55), intObject(66), intObject(77), intObject(88), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpGetFree, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpGetFree, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpGetFreePtr, 0), tengo.MakeInstruction(parser.OpGetLocalPtr, 0), tengo.MakeInstruction(parser.OpClosure, 4, 2), tengo.MakeInstruction(parser.OpReturn, 1)), compiledFunction(1, 0, tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpGetLocalPtr, 0), tengo.MakeInstruction(parser.OpClosure, 5, 1), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, `for i:=0; i<10; i++ {}`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 38), tengo.MakeInstruction(parser.OpJumpFalsy, 35), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpJump, 6), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(0), intObject(10), intObject(1)))) expectCompile(t, `m := {}; for k, v in m {}`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpMap, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpIteratorInit), tengo.MakeInstruction(parser.OpSetGlobal, 1), tengo.MakeInstruction(parser.OpGetGlobal, 1), tengo.MakeInstruction(parser.OpIteratorNext), tengo.MakeInstruction(parser.OpJumpFalsy, 41), tengo.MakeInstruction(parser.OpGetGlobal, 1), tengo.MakeInstruction(parser.OpIteratorKey), tengo.MakeInstruction(parser.OpSetGlobal, 2), tengo.MakeInstruction(parser.OpGetGlobal, 1), tengo.MakeInstruction(parser.OpIteratorValue), tengo.MakeInstruction(parser.OpSetGlobal, 3), tengo.MakeInstruction(parser.OpJump, 13), tengo.MakeInstruction(parser.OpSuspend)), objectsArray())) expectCompile(t, `a := 0; a == 0 && a != 1 || a < 1`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpEqual), tengo.MakeInstruction(parser.OpAndJump, 25), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpNotEqual), tengo.MakeInstruction(parser.OpOrJump, 38), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 38), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(0), intObject(1)))) // unknown module name expectCompileError(t, `import("user1")`, "module 'user1' not found") // too many errors expectCompileError(t, ` r["x"] = { @a:1, @b:1, @c:1, @d:1, @e:1, @f:1, @g:1, @h:1, @i:1, @j:1, @k:1 } `, "Parse Error: illegal character U+0040 '@'\n\tat test:3:5 (and 10 more errors)") expectCompileError(t, `import("")`, "empty module name") // https://github.com/d5/tengo/issues/314 expectCompileError(t, ` (func() { fn := fn() })() `, "unresolved reference 'fn") } func TestCompilerErrorReport(t *testing.T) { expectCompileError(t, `import("user1")`, "Compile Error: module 'user1' not found\n\tat test:1:1") expectCompileError(t, `a = 1`, "Compile Error: unresolved reference 'a'\n\tat test:1:1") expectCompileError(t, `a := a`, "Compile Error: unresolved reference 'a'\n\tat test:1:6") expectCompileError(t, `a, b := 1, 2`, "Compile Error: tuple assignment not allowed\n\tat test:1:1") expectCompileError(t, `a.b := 1`, "not allowed with selector") expectCompileError(t, `a:=1; a:=3`, "Compile Error: 'a' redeclared in this block\n\tat test:1:7") expectCompileError(t, `return 5`, "Compile Error: return not allowed outside function\n\tat test:1:1") expectCompileError(t, `func() { break }`, "Compile Error: break not allowed outside loop\n\tat test:1:10") expectCompileError(t, `func() { continue }`, "Compile Error: continue not allowed outside loop\n\tat test:1:10") expectCompileError(t, `func() { export 5 }`, "Compile Error: export not allowed inside function\n\tat test:1:10") } func TestCompilerDeadCode(t *testing.T) { expectCompile(t, ` func() { a := 4 return a b := 5 // dead code from here c := a return b }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(4), intObject(5), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, ` func() { if true { return 5 a := 4 // dead code from here b := a return b } else { return 4 c := 5 // dead code from here d := c return d } }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(5), intObject(4), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpTrue), tengo.MakeInstruction(parser.OpJumpFalsy, 11), tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpReturn, 1), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, ` func() { a := 1 for { if a == 5 { return 10 } 5 + 5 return 20 b := a return b } }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 4), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(5), intObject(10), intObject(20), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpEqual), tengo.MakeInstruction(parser.OpJumpFalsy, 21), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpReturn, 1), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, ` func() { if true { return 5 a := 4 // dead code from here b := a return b } else { return 4 c := 5 // dead code from here d := c return d } }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(5), intObject(4), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpTrue), tengo.MakeInstruction(parser.OpJumpFalsy, 11), tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpReturn, 1), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpReturn, 1))))) expectCompile(t, ` func() { if true { return } return return 123 }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(123), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpTrue), tengo.MakeInstruction(parser.OpJumpFalsy, 8), tengo.MakeInstruction(parser.OpReturn, 0), tengo.MakeInstruction(parser.OpReturn, 0), tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpReturn, 1))))) } func TestCompilerScopes(t *testing.T) { expectCompile(t, ` if a := 1; a { a = 2 b := a } else { a = 3 b := a }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpJumpFalsy, 31), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpSetGlobal, 1), tengo.MakeInstruction(parser.OpJump, 43), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpSetGlobal, 0), tengo.MakeInstruction(parser.OpGetGlobal, 0), tengo.MakeInstruction(parser.OpSetGlobal, 2), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3)))) expectCompile(t, ` func() { if a := 1; a { a = 2 b := a } else { a = 3 b := a } }`, bytecode( concatInsts( tengo.MakeInstruction(parser.OpConstant, 3), tengo.MakeInstruction(parser.OpPop), tengo.MakeInstruction(parser.OpSuspend)), objectsArray( intObject(1), intObject(2), intObject(3), compiledFunction(0, 0, tengo.MakeInstruction(parser.OpConstant, 0), tengo.MakeInstruction(parser.OpDefineLocal, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpJumpFalsy, 26), tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpDefineLocal, 1), tengo.MakeInstruction(parser.OpJump, 35), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpSetLocal, 0), tengo.MakeInstruction(parser.OpGetLocal, 0), tengo.MakeInstruction(parser.OpDefineLocal, 1), tengo.MakeInstruction(parser.OpReturn, 0))))) } func TestCompiler_custom_extension(t *testing.T) { pathFileSource := "./testdata/issue286/test.mshk" modules := stdlib.GetModuleMap(stdlib.AllModuleNames()...) src, err := ioutil.ReadFile(pathFileSource) require.NoError(t, err) // Escape shegang if len(src) > 1 && string(src[:2]) == "#!" { copy(src, "//") } fileSet := parser.NewFileSet() srcFile := fileSet.AddFile(filepath.Base(pathFileSource), -1, len(src)) p := parser.NewParser(srcFile, src, nil) file, err := p.ParseFile() require.NoError(t, err) c := tengo.NewCompiler(srcFile, nil, nil, modules, nil) c.EnableFileImport(true) c.SetImportDir(filepath.Dir(pathFileSource)) // Search for "*.tengo" and ".mshk"(custom extension) c.SetImportFileExt(".tengo", ".mshk") err = c.Compile(file) require.NoError(t, err) } func TestCompilerNewCompiler_default_file_extension(t *testing.T) { modules := stdlib.GetModuleMap(stdlib.AllModuleNames()...) input := "{}" fileSet := parser.NewFileSet() file := fileSet.AddFile("test", -1, len(input)) c := tengo.NewCompiler(file, nil, nil, modules, nil) c.EnableFileImport(true) require.Equal(t, []string{".tengo"}, c.GetImportFileExt(), "newly created compiler object must contain the default extension") } func TestCompilerSetImportExt_extension_name_validation(t *testing.T) { c := new(tengo.Compiler) // Instantiate a new compiler object with no initialization // Test of empty arg err := c.SetImportFileExt() require.Error(t, err, "empty arg should return an error") // Test of various arg types for _, test := range []struct { extensions []string expect []string requireErr bool msgFail string }{ {[]string{".tengo"}, []string{".tengo"}, false, "well-formed extension should not return an error"}, {[]string{""}, []string{".tengo"}, true, "empty extension name should return an error"}, {[]string{"foo"}, []string{".tengo"}, true, "name without dot prefix should return an error"}, {[]string{"foo.bar"}, []string{".tengo"}, true, "malformed extension should return an error"}, {[]string{"foo."}, []string{".tengo"}, true, "malformed extension should return an error"}, {[]string{".mshk"}, []string{".mshk"}, false, "name with dot prefix should be added"}, {[]string{".foo", ".bar"}, []string{".foo", ".bar"}, false, "it should replace instead of appending"}, } { err := c.SetImportFileExt(test.extensions...) if test.requireErr { require.Error(t, err, test.msgFail) } expect := test.expect actual := c.GetImportFileExt() require.Equal(t, expect, actual, test.msgFail) } } func concatInsts(instructions ...[]byte) []byte { var concat []byte for _, i := range instructions { concat = append(concat, i...) } return concat } func bytecode( instructions []byte, constants []tengo.Object, ) *tengo.Bytecode { return &tengo.Bytecode{ FileSet: parser.NewFileSet(), MainFunction: &tengo.CompiledFunction{Instructions: instructions}, Constants: constants, } } func expectCompile( t *testing.T, input string, expected *tengo.Bytecode, ) { actual, trace, err := traceCompile(input, nil) var ok bool defer func() { if !ok { for _, tr := range trace { t.Log(tr) } } }() require.NoError(t, err) equalBytecode(t, expected, actual) ok = true } func expectCompileError(t *testing.T, input, expected string) { _, trace, err := traceCompile(input, nil) var ok bool defer func() { if !ok { for _, tr := range trace { t.Log(tr) } } }() require.Error(t, err) require.True(t, strings.Contains(err.Error(), expected), "expected error string: %s, got: %s", expected, err.Error()) ok = true } func equalBytecode(t *testing.T, expected, actual *tengo.Bytecode) { require.Equal(t, expected.MainFunction, actual.MainFunction) equalConstants(t, expected.Constants, actual.Constants) } func equalConstants(t *testing.T, expected, actual []tengo.Object) { require.Equal(t, len(expected), len(actual)) for i := 0; i < len(expected); i++ { require.Equal(t, expected[i], actual[i]) } } type compileTracer struct { Out []string } func (o *compileTracer) Write(p []byte) (n int, err error) { o.Out = append(o.Out, string(p)) return len(p), nil } func traceCompile( input string, symbols map[string]tengo.Object, ) (res *tengo.Bytecode, trace []string, err error) { fileSet := parser.NewFileSet() file := fileSet.AddFile("test", -1, len(input)) p := parser.NewParser(file, []byte(input), nil) symTable := tengo.NewSymbolTable() for name := range symbols { symTable.Define(name) } for idx, fn := range tengo.GetAllBuiltinFunctions() { symTable.DefineBuiltin(idx, fn.Name) } tr := &compileTracer{} c := tengo.NewCompiler(file, symTable, nil, nil, tr) parsed, err := p.ParseFile() if err != nil { return } err = c.Compile(parsed) res = c.Bytecode() res.RemoveDuplicates() { trace = append(trace, fmt.Sprintf("Compiler Trace:\n%s", strings.Join(tr.Out, ""))) trace = append(trace, fmt.Sprintf("Compiled Constants:\n%s", strings.Join(res.FormatConstants(), "\n"))) trace = append(trace, fmt.Sprintf("Compiled Instructions:\n%s\n", strings.Join(res.FormatInstructions(), "\n"))) } if err != nil { return } return } func objectsArray(o ...tengo.Object) []tengo.Object { return o } func intObject(v int64) *tengo.Int { return &tengo.Int{Value: v} } func stringObject(v string) *tengo.String { return &tengo.String{Value: v} } func compiledFunction( numLocals, numParams int, insts ...[]byte, ) *tengo.CompiledFunction { return &tengo.CompiledFunction{ Instructions: concatInsts(insts...), NumLocals: numLocals, NumParameters: numParams, } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/doc.go0000644000175000017500000000012214607001502016231 0ustar00maythammaytham// tengo is a small, dynamic, fast, secure script language for Go. package tengo ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/0000755000175000017500000000000014607001502016072 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/builtins.md0000644000175000017500000001702214607001502020247 0ustar00maythammaytham# Builtin Functions ## format Returns a formatted string. The first argument must be a String object. See [this](https://github.com/d5/tengo/blob/master/docs/formatting.md) for more details on formatting. ```golang a := [1, 2, 3] s := format("Foo: %v", a) // s == "Foo: [1, 2, 3]" ``` ## len Returns the number of elements if the given variable is array, string, map, or module map. ```golang v := [1, 2, 3] l := len(v) // l == 3 ``` ## copy Creates a copy of the given variable. `copy` function calls `Object.Copy` interface method, which is expected to return a deep-copy of the value it holds. ```golang v1 := [1, 2, 3] v2 := v1 v3 := copy(v1) v1[1] = 0 print(v2[1]) // "0"; 'v1' and 'v2' referencing the same array print(v3[1]) // "2"; 'v3' not affected by 'v1' ``` ## append Appends object(s) to an array (first argument) and returns a new array object. (Like Go's `append` builtin.) Currently, this function takes array type only. ```golang v := [1] v = append(v, 2, 3) // v == [1, 2, 3] ``` ## delete Deletes the element with the specified key from the map type. First argument must be a map type and second argument must be a string type. (Like Go's `delete` builtin except keys are always string). `delete` returns `undefined` value if successful and it mutates given map. ```golang v := {key: "value"} delete(v, "key") // v == {} ``` ```golang v := {key: "value"} delete(v, "missing") // v == {"key": "value"} ``` ```golang delete({}) // runtime error, second argument is missing delete({}, 1) // runtime error, second argument must be a string type ``` ## splice Deletes and/or changes the contents of a given array and returns deleted items as a new array. `splice` is similar to JS `Array.prototype.splice()` except splice is a builtin function and first argument must an array. First argument must be an array, and if second and third arguments are provided those must be integers otherwise runtime error is returned. Usage: `deleted_items := splice(array[, start[, delete_count[, item1[, item2[, ...]]]])` ```golang v := [1, 2, 3] items := splice(v, 0) // items == [1, 2, 3], v == [] ``` ```golang v := [1, 2, 3] items := splice(v, 1) // items == [2, 3], v == [1] ``` ```golang v := [1, 2, 3] items := splice(v, 0, 1) // items == [1], v == [2, 3] ``` ```golang // deleting v := ["a", "b", "c"] items := splice(v, 1, 2) // items == ["b", "c"], v == ["a"] // splice(v, 1, 3) or splice(v, 1, 99) has same effect for this example ``` ```golang // appending v := ["a", "b", "c"] items := splice(v, 3, 0, "d", "e") // items == [], v == ["a", "b", "c", "d", "e"] ``` ```golang // replacing v := ["a", "b", "c"] items := splice(v, 2, 1, "d") // items == ["c"], v == ["a", "b", "d"] ``` ```golang // inserting v := ["a", "b", "c"] items := splice(v, 0, 0, "d", "e") // items == [], v == ["d", "e", "a", "b", "c"] ``` ```golang // deleting and inserting v := ["a", "b", "c"] items := splice(v, 1, 1, "d", "e") // items == ["b"], v == ["a", "d", "e", "c"] ``` ## type_name Returns the type_name of an object. ```golang type_name(1) // int type_name("str") // string type_name([1, 2, 3]) // array ``` ## string Tries to convert an object to string object. See [Runtime Types](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) for more details on type conversion. ```golang x := string(123) // x == "123" ``` Optionally it can take the second argument, which will be returned if the first argument cannot be converted to string. Note that the second argument does not have to be string. ```golang v = string(undefined, "foo") // v == "foo" v = string(undefined, false) // v == false ``` ## int Tries to convert an object to int object. See [this](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) for more details on type conversion. ```golang v := int("123") // v == 123 ``` Optionally it can take the second argument, which will be returned if the first argument cannot be converted to int. Note that the second argument does not have to be int. ```golang v = int(undefined, 10) // v == 10 v = int(undefined, false) // v == false ``` ## bool Tries to convert an object to bool object. See [this](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) for more details on type conversion. ```golang v := bool(1) // v == true ``` ## float Tries to convert an object to float object. See [this](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) for more details on type conversion. ```golang v := float("19.84") // v == 19.84 ``` Optionally it can take the second argument, which will be returned if the first argument cannot be converted to float. Note that the second argument does not have to be float. ```golang v = float(undefined, 19.84) // v == 19.84 v = float(undefined, false) // v == false ``` ## char Tries to convert an object to char object. See [this](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) for more details on type conversion. ```golang v := char(89) // v == 'Y' ``` Optionally it can take the second argument, which will be returned if the first argument cannot be converted to float. Note that the second argument does not have to be float. ```golang v = char(undefined, 'X') // v == 'X' v = char(undefined, false) // v == false ``` ## bytes Tries to convert an object to bytes object. See [this](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) for more details on type conversion. ```golang v := bytes("foo") // v == [102 111 111] ``` Optionally it can take the second argument, which will be returned if the first argument cannot be converted to float. Note that the second argument does not have to be float. ```golang v = bytes(undefined, bytes("foo")) // v == bytes("foo") v = bytes(undefined, false) // v == false ``` If you pass an int to `bytes()` function, it will create a new byte object with the given size. ```golang v := bytes(100) ``` ## time Tries to convert an object to time value. ```golang v := time(1257894000) // 2009-11-10 23:00:00 +0000 UTC ``` ## is_string Returns `true` if the object's type is string. Or it returns `false`. ## is_int Returns `true` if the object's type is int. Or it returns `false`. ## is_bool Returns `true` if the object's type is bool. Or it returns `false`. ## is_float Returns `true` if the object's type is float. Or it returns `false`. ## is_char Returns `true` if the object's type is char. Or it returns `false`. ## is_bytes Returns `true` if the object's type is bytes. Or it returns `false`. ## is_error Returns `true` if the object's type is error. Or it returns `false`. ## is_undefined Returns `true` if the object's type is undefined. Or it returns `false`. ## is_function Returns `true` if the object's type is function or closure. Or it returns `false`. Note that `is_function` returns `false` for builtin functions and user-provided callable objects. ## is_callable Returns `true` if the object is callable (e.g. function, closure, builtin function, or user-provided callable objects). Or it returns `false`. ## is_array Returns `true` if the object's type is array. Or it returns `false`. ## is_immutable_array Returns `true` if the object's type is immutable array. Or it returns `false`. ## is_map Returns `true` if the object's type is map. Or it returns `false`. ## is_immutable_map Returns `true` if the object's type is immutable map. Or it returns `false`. ## is_iterable Returns `true` if the object's type is iterable: array, immutable array, map, 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`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/formatting.md0000644000175000017500000001062614607001502020573 0ustar00maythammaytham# Formatting The format 'verbs' are derived from Go's but are simpler. ## The verbs ## General ``` %v the value in a default format %T a Go-syntax representation of the type of the value %% a literal percent sign; consumes no value ``` ## Boolean ``` %t the word true or false ``` ## Integer ``` %b base 2 %c the character represented by the corresponding Unicode code point %d base 10 %o base 8 %O base 8 with 0o prefix %q a single-quoted character literal safely escaped with Go syntax. %x base 16, with lower-case letters for a-f %X base 16, with upper-case letters for A-F %U Unicode format: U+1234; same as "U+%04X" ``` ## Float ``` %b decimalless scientific notation with exponent a power of two, in the manner of Go's strconv.FormatFloat with the 'b' format, e.g. -123456p-78 %e scientific notation, e.g. -1.234456e+78 %E scientific notation, e.g. -1.234456E+78 %f decimal point but no exponent, e.g. 123.456 %F synonym for %f %g %e for large exponents, %f otherwise. Precision is discussed below. %G %E for large exponents, %F otherwise %x hexadecimal notation (with decimal power of two exponent), e.g. -0x1.23abcp+20 %X upper-case hexadecimal notation, e.g. -0X1.23ABCP+20 ``` ## String and Bytes ``` %s the uninterpreted bytes of the string or slice %q a double-quoted string safely escaped with Go syntax %x base 16, lower-case, two characters per byte %X base 16, upper-case, two characters per byte ``` ## Default format for %v ``` Bool: %t Int: %d Float: %g String: %s ``` ## Compound Objects ``` Array: [elem0 elem1 ...] Maps: {key1:value1 key2:value2 ...} ``` ## Width and Precision Width is specified by an optional decimal number immediately preceding the verb. If absent, the width is whatever is necessary to represent the value. Precision is specified after the (optional) width by a period followed by a decimal number. If no period is present, a default precision is used. A period with no following number specifies a precision of zero. Examples: ``` %f default width, default precision %9f width 9, default precision %.2f default width, precision 2 %9.2f width 9, precision 2 %9.f width 9, precision 0 ``` Width and precision are measured in units of Unicode code points. Either or both of the flags may be replaced with the character '*', causing their values to be obtained from the next operand (preceding the one to format), which must be of type Int. For most values, width is the minimum number of runes to output, padding the formatted form with spaces if necessary. For Strings and Bytes, however, precision limits the length of the input to be formatted (not the size of the output), truncating if necessary. Normally it is measured in units of Unicode code points, but for these types when formatted with the %x or %X format it is measured in bytes. For floating-point values, width sets the minimum width of the field and precision sets the number of places after the decimal, if appropriate, except that for %g/%G precision sets the maximum number of significant digits (trailing zeros are removed). For example, given 12.345 the format %6.3f prints 12.345 while %.3g prints 12.3. The default precision for %e, %f and %#g is 6; for %g it is the smallest number of digits necessary to identify the value uniquely. For complex numbers, the width and precision apply to the two components independently and the result is parenthesized, so %f applied to 1.2+3.4i produces (1.200000+3.400000i). ## Other flags ``` + always print a sign for numeric values; guarantee ASCII-only output for %q (%+q) - pad with spaces on the right rather than the left (left-justify the field) # alternate format: add leading 0b for binary (%#b), 0 for octal (%#o), 0x or 0X for hex (%#x or %#X); for %q, print a raw (backquoted) string if strconv.CanBackquote returns true; always print a decimal point for %e, %E, %f, %F, %g and %G; do not remove trailing zeros for %g and %G; write e.g. U+0078 'x' if the character is printable for %U (%#U). ' ' (space) leave a space for elided sign in numbers (% d); put spaces between bytes printing strings or slices in hex (% x, % X) 0 pad with leading zeros rather than spaces; for numbers, this moves the padding after the sign ``` Flags are ignored by verbs that do not expect them. For example there is no alternate decimal format, so %#d and %d behave identically. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/interoperability.md0000644000175000017500000001722314607001502022006 0ustar00maythammaytham# Interoperability ## Table of Contents - [Using Scripts](#using-scripts) - [Type Conversion Table](#type-conversion-table) - [User Types](#user-types) - [Sandbox Environments](#sandbox-environments) - [Concurrency](#concurrency) - [Compiler and VM](#compiler-and-vm) ## Using Scripts Embedding and executing the Tengo code in Go is very easy. At a high level, this process is like: - create a [Script](https://godoc.org/github.com/d5/tengo#Script) instance with your code, - _optionally_ add some [Script Variables](https://godoc.org/github.com/d5/tengo#Variable) to Script, - compile or directly run the script, - retrieve _output_ values from the [Compiled](https://godoc.org/github.com/d5/tengo#Compiled) instance. The following is an example where a Tengo script is compiled and run with no input/output variables. ```golang import "github.com/d5/tengo/v2" var code = ` reduce := func(seq, fn) { s := 0 for x in seq { fn(x, s) } return s } print(reduce([1, 2, 3], func(x, s) { s += x })) ` func main() { s := tengo.NewScript([]byte(code)) if _, err := s.Run(); err != nil { panic(err) } } ``` Here's another example where an input variable is added to the script, and, an output variable is accessed through [Variable.Int](https://godoc.org/github.com/d5/tengo#Variable.Int) function: ```golang import ( "fmt" "github.com/d5/tengo/v2" ) func main() { s := tengo.NewScript([]byte(`a := b + 20`)) // define variable 'b' _ = s.Add("b", 10) // compile the source c, err := s.Compile() if err != nil { panic(err) } // run the compiled bytecode // a compiled bytecode 'c' can be executed multiple times without re-compiling it if err := c.Run(); err != nil { panic(err) } // retrieve value of 'a' a := c.Get("a") fmt.Println(a.Int()) // prints "30" // re-run after replacing value of 'b' if err := c.Set("b", 20); err != nil { panic(err) } if err := c.Run(); err != nil { panic(err) } fmt.Println(c.Get("a").Int()) // prints "40" } ``` A variable `b` is defined by the user before compilation using [Script.Add](https://godoc.org/github.com/d5/tengo#Script.Add) function. Then a compiled bytecode `c` is used to execute the bytecode and get the value of global variables. In this example, the value of global variable `a` is read using [Compiled.Get](https://godoc.org/github.com/d5/tengo#Compiled.Get) function. See [documentation](https://godoc.org/github.com/d5/tengo#Variable) for the full list of variable value functions. Value of the global variables can be replaced using [Compiled.Set](https://godoc.org/github.com/d5/tengo#Compiled.Set) function. But it will return an error if you try to set the value of un-defined global variables _(e.g. trying to set the value of `x` in the example)_. ### Type Conversion Table When adding a Variable _([Script.Add](https://godoc.org/github.com/d5/tengo#Script.Add))_, Script converts Go values into Tengo values based on the following conversion table. | Go Type | Tengo Type | Note | | :--- | :--- | :--- | |`nil`|`Undefined`|| |`string`|`String`|| |`int64`|`Int`|| |`int`|`Int`|| |`bool`|`Bool`|| |`rune`|`Char`|| |`byte`|`Char`|| |`float64`|`Float`|| |`[]byte`|`Bytes`|| |`time.Time`|`Time`|| |`error`|`Error{String}`|use `error.Error()` as String value| |`map[string]Object`|`Map`|| |`map[string]interface{}`|`Map`|individual elements converted to Tengo objects| |`[]Object`|`Array`|| |`[]interface{}`|`Array`|individual elements converted to Tengo objects| |`Object`|`Object`|_(no type conversion performed)_| ### User Types Users can add and use a custom user type in Tengo code by implementing [Object](https://godoc.org/github.com/d5/tengo#Object) interface. Tengo runtime will treat the user types in the same way it does to the runtime types with no performance overhead. See [Object Types](https://github.com/d5/tengo/blob/master/docs/objects.md) for more details. ## Sandbox Environments To securely compile and execute _potentially_ unsafe script code, you can use the following Script functions. ### Script.SetImports(modules *objects.ModuleMap) SetImports sets the import modules with corresponding names. Script **does not** include any modules by default. You can use this function to include the [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md). ```golang s := tengo.NewScript([]byte(`math := import("math"); a := math.abs(-19.84)`)) s.SetImports(stdlib.GetModuleMap("math")) // or, to include all stdlib at once s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) ``` You can also include Tengo's written module using `objects.SourceModule` (which implements `objects.Importable`). ```golang s := tengo.NewScript([]byte(`double := import("double"); a := double(20)`)) mods := tengo.NewModuleMap() mods.AddSourceModule("double", []byte(`export func(x) { return x * 2 }`)) s.SetImports(mods) ``` To dynamically load or generate code for imported modules, implement and provide a `tengo.ModuleGetter`. ```golang type DynamicModules struct { mods tengo.ModuleGetter fallback func (name string) tengo.Importable } func (dm *DynamicModules) Get(name string) tengo.Importable { if mod := dm.mods.Get(name); mod != nil { return mod } return dm.fallback() } // ... mods := &DynamicModules{ mods: stdlib.GetModuleMap("math"), fallback: func(name string) tengo.Importable { src := ... // load or generate src for `name` return &tengo.SourceModule{Src: src} }, } s := tengo.NewScript(`foo := import("foo")`) s.SetImports(mods) ``` ### Script.SetMaxAllocs(n int64) SetMaxAllocs sets the maximum number of object allocations. Note this is a cumulative metric that tracks only the object creations. Set this to a negative number (e.g. `-1`) if you don't need to limit the number of allocations. ### Script.EnableFileImport(enable bool) EnableFileImport enables or disables module loading from the local files. It's disabled by default. ### tengo.MaxStringLen Sets the maximum byte-length of string values. This limit applies to all running VM instances in the process. Also it's not recommended to set or update this value while any VM is executing. ### tengo.MaxBytesLen Sets the maximum length of bytes values. This limit applies to all running VM instances in the process. Also it's not recommended to set or update this value while any VM is executing. ## Concurrency A compiled script (`Compiled`) can be used to run the code multiple times by a goroutine. If you want to run the compiled script by multiple goroutine, you should use `Compiled.Clone` function to make a copy of Compiled instances. ### Compiled.Clone() Clone creates a new copy of Compiled instance. Cloned copies are safe for concurrent use by multiple goroutines. ```golang for i := 0; i < concurrency; i++ { go func(compiled *tengo.Compiled) { // inputs _ = compiled.Set("a", rand.Intn(10)) _ = compiled.Set("b", rand.Intn(10)) _ = compiled.Set("c", rand.Intn(10)) if err := compiled.Run(); err != nil { panic(err) } // outputs d = compiled.Get("d").Int() e = compiled.Get("e").Int() }(compiled.Clone()) // Pass the cloned copy of Compiled } ``` ## Compiler and VM Although it's not recommended, you can directly create and run the Tengo [Compiler](https://godoc.org/github.com/d5/tengo#Compiler), and [VM](https://godoc.org/github.com/d5/tengo#VM) for yourself instead of using Scripts and Script Variables. It's a bit more involved as you have to manage the symbol tables and global variables between them, but, basically that's what Script and Script Variable is doing internally. _TODO: add more information here_ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/objects.md0000644000175000017500000003302514607001502020050 0ustar00maythammaytham# Object Types ## Table of Contents - [Tengo Objects](#tengo-objects) - [Runtime Object Types](#runtime-object-types) - [User Object Types](#user-object-types) ## Tengo Objects In Tengo, all object types _(both [runtime types](#runtime-object-types) and [user types](#user-object-types))_ must implement [Object](https://godoc.org/github.com/d5/tengo#Object) interface. ### Object Interface ```golang TypeName() string ``` TypeName method should return the name of the type. Type names are not directly used by the runtime _(except when it reports a run-time error)_, but, it is generally a good idea to keep it short but distinguishable from other types. ```golang String() string ``` String method should return a string representation of the underlying value. The value returned by String method will be used whenever string formatting for the value is required, most commonly when being converted into String value. ```golang BinaryOp(op token.Token, rhs Object) (res Object, err error) ``` In Tengo, a type can overload binary operators (`+`, `-`, `*`, `/`, `%`, `&`, `|`, `^`, `&^`, `>>`, `<<`, `>`, `>=`; _note that `<` and `<=` operators are not overloadable as they're simply implemented by switching left-hand side and right-hand side of `>`/`>=` operator_) by implementing BinaryOp method. BinaryOp method takes the operator `op` and the right-hand side object `rhs`, and, should return a resulting value `res`. **Error value vs runtime error** If BinaryOp method returns an error `err` (the second return value), it will be treated as a run-time error, which will halt the execution (`VM.Run() error`) and will return the error to the user. All runtime type implementations, for example, will return an `ErrInvalidOperator` error when the given operator is not supported by the type. Alternatively the method can return an `Error` value as its result `res` (the first return value), which will not halt the runtime and will be treated like any other values. As a dynamically typed language, the receiver (another expression or statement) can determine how to translate `Error` value returned from binary operator expression. ```golang IsFalsy() bool ``` IsFalsy method should return true if the underlying value is considered to be [falsy](https://github.com/d5/tengo/blob/master/docs/runtime-types.md#objectisfalsy). ```golang Equals(o Object) bool ``` Equals method should return true if the underlying value is considered to be equal to the underlying value of another object `o`. When comparing values of different types, the runtime does not guarantee or force anything, but, it's generally a good idea to make the result consistent. For example, a custom integer type may return true when comparing against String value, but, it should return the same result for the same inputs. ```golang Copy() Object ``` Copy method should return a _new_ copy of the object. Builtin function `copy` uses this method to copy values. Default implementation of all runtime types return a deep-copy values, but, it's not a requirement by the runtime. ```golang IndexGet(index Object) (value Object, err error) ``` IndexGet should take an index Object and return a result Object or an error for indexable objects. Indexable is an object that can take an index and return an object. If a type is indexable, its values support dot selector (value = object.index) and indexer (value = object[index]) syntax. If Object is not indexable, ErrNotIndexable should be returned as error. If nil is returned as value, it will be converted to Undefined value by the runtime. If `IndexGet` returns an error (`err`), the VM will treat it as a run-time error and ignore the returned value. Array and Map implementation forces the type of index Object to be Int and String respectively, but, it's not a required behavior of the VM. It is completely okay to take various index types as long as it is consistent. By convention, Array or Array-like types and Map or Map-like types return `Undefined` value when the key does not exist. But, again, this is not a required behavior. ```golang IndexSet(index, value Object) error ``` IndexSet should take an index Object and a value Object for index assignable objects. Index assignable is an object that can take an index and a value on the left-hand side of the assignment statement. If a type is index assignable, its values support assignment using dot selector (`object.index = value`) and indexer (`object[index] = value`) in the assignment statements. If Object is not index assignable, ErrNotIndexAssignable should be returned as error. If an error is returned, it will be treated as a run-time error. Array and Map implementation forces the type of index Object to be Int and String respectively, but, it's not a required behavior of the VM. It is completely okay to take various index types as long as it is consistent. #### Callable Objects If the type is Callable, its values can be invoked as if they were functions. Two functions need to be implemented for Callable objects. ```golang CanCall() bool ``` 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 should take an arbitrary number of arguments and return a return value and/or an error, which the VM will consider as a run-time error. #### Iterable Objects If a type is iterable, its values can be used in `for-in` statements (`for key, value in object { ... }`). Two functions need to be implemented for Iterable Objects ```golang CanIterate() bool ``` CanIterate should return whether the Object can be Iterated. ```golang Iterate() Iterator ``` The Iterate method should return another object that implements [Iterator](https://godoc.org/github.com/d5/tengo#Iterator) interface. ### Iterator Interface ```golang Next() bool ``` Next method should return true if there are more elements to iterate. When used with `for-in` statements, the compiler uses Key and Value methods to populate the current element's key (or index) and value from the object that this iterator represents. The runtime will stop iterating in `for-in` statement when this method returns false. ```golang Key() Object ``` Key method should return a key (or an index) Object for the current element of the underlying object. It should return the same value until Next method is called again. By convention, iterators for the map or map-like objects returns the String key, and, iterators for array or array-like objects returns the Int ndex. But, it's not a requirement by the VM. ```golang Value() Object ``` Value method should return a value Object for the current element of the underlying object. It should return the same value until Next method is called again. ## Runtime Object Types These are the basic types Tengo runtime supports out of the box: - Primitive value types: [Int](https://godoc.org/github.com/d5/tengo#Int), [String](https://godoc.org/github.com/d5/tengo#String), [Float](https://godoc.org/github.com/d5/tengo#Float), [Bool](https://godoc.org/github.com/d5/tengo#ArrayIterator), [Char](https://godoc.org/github.com/d5/tengo#Char), [Bytes](https://godoc.org/github.com/d5/tengo#Bytes), [Time](https://godoc.org/github.com/d5/tengo#Time) - Composite value types: [Array](https://godoc.org/github.com/d5/tengo#Array), [ImmutableArray](https://godoc.org/github.com/d5/tengo#ImmutableArray), [Map](https://godoc.org/github.com/d5/tengo#Map), [ImmutableMap](https://godoc.org/github.com/d5/tengo#ImmutableMap) - Functions: [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) - [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), [MapIterator](https://godoc.org/github.com/d5/tengo#MapIterator), [ImmutableMapIterator](https://godoc.org/github.com/d5/tengo#ImmutableMapIterator) - [Error](https://godoc.org/github.com/d5/tengo#Error) - [Undefined](https://godoc.org/github.com/d5/tengo#Undefined) - 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) See [Runtime Types](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) for more details on these runtime types. ## User Object Types Users can easily extend and add their own types by implementing the same [Object](https://godoc.org/github.com/d5/tengo#Object) interface and the default `ObjectImpl` implementation. Tengo runtime will treat them in the same way as its runtime types with no performance overhead. Here's an example user type implementation, `StringArray`: ```golang type StringArray struct { tengo.ObjectImpl Value []string } func (o *StringArray) String() string { return strings.Join(o.Value, ", ") } func (o *StringArray) BinaryOp(op token.Token, rhs tengo.Object) (tengo.Object, error) { if rhs, ok := rhs.(*StringArray); ok { switch op { case token.Add: if len(rhs.Value) == 0 { return o, nil } return &StringArray{Value: append(o.Value, rhs.Value...)}, nil } } return nil, tengo.ErrInvalidOperator } func (o *StringArray) IsFalsy() bool { return len(o.Value) == 0 } func (o *StringArray) Equals(x tengo.Object) bool { if x, ok := x.(*StringArray); ok { if len(o.Value) != len(x.Value) { return false } for i, v := range o.Value { if v != x.Value[i] { return false } } return true } return false } func (o *StringArray) Copy() tengo.Object { return &StringArray{ Value: append([]string{}, o.Value...), } } func (o *StringArray) TypeName() string { return "string-array" } ``` You can use a user type via either [Script.Add](https://godoc.org/github.com/d5/tengo#Script.Add) or by directly manipulating the symbol table and the global variables. Here's an example code to add `StringArray` to the script: ```golang // script that uses 'my_list' s := tengo.NewScript([]byte(` fmt := import("fmt") fmt.println(my_list, ", three") `)) s.SetImports(stdlib.GetModuleMap("fmt")) myList := &StringArray{Value: []string{"one", "two"}} s.Add("my_list", myList) // add StringArray value 'my_list' s.Run() // prints "one, two, three" ``` It can also implement `IndexGet` and `IndexSet`: ```golang func (o *StringArray) IndexGet(index tengo.Object) (tengo.Object, error) { intIdx, ok := index.(*tengo.Int) if ok { if intIdx.Value >= 0 && intIdx.Value < int64(len(o.Value)) { return &tengo.String{Value: o.Value[intIdx.Value]}, nil } return nil, tengo.ErrIndexOutOfBounds } strIdx, ok := index.(*tengo.String) if ok { for vidx, str := range o.Value { if strIdx.Value == str { return &tengo.Int{Value: int64(vidx)}, nil } } return tengo.UndefinedValue, nil } return nil, tengo.ErrInvalidIndexType } func (o *StringArray) IndexSet(index, value tengo.Object) error { strVal, ok := tengo.ToString(value) if !ok { return tengo.ErrInvalidIndexValueType } intIdx, ok := index.(*tengo.Int) if ok { if intIdx.Value >= 0 && intIdx.Value < int64(len(o.Value)) { o.Value[intIdx.Value] = strVal return nil } return tengo.ErrIndexOutOfBounds } return tengo.ErrInvalidIndexType } ``` If we implement `CanCall` and `Call`: ```golang func (o *StringArray) CanCall() bool { return true } func (o *StringArray) Call(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string", Found: args[0].TypeName(), } } for i, v := range o.Value { if v == s1 { return &tengo.Int{Value: int64(i)}, nil } } return tengo.UndefinedValue, nil } ``` Then it can be "invoked": ```golang s := tengo.NewScript([]byte(` print(my_list("two")) `)) myList := &StringArray{Value: []string{"one", "two", "three"}} s.Add("my_list", myList) // add StringArray value 'my_list' s.Run() // prints "1" (index of "two") ``` We can also make `StringArray` iterable: ```golang func (o *StringArray) CanIterate() bool { return true } func (o *StringArray) Iterate() tengo.Iterator { return &StringArrayIterator{ strArr: o, } } type StringArrayIterator struct { tengo.ObjectImpl strArr *StringArray idx int } func (i *StringArrayIterator) TypeName() string { return "string-array-iterator" } func (i *StringArrayIterator) Next() bool { i.idx++ return i.idx <= len(i.strArr.Value) } func (i *StringArrayIterator) Key() tengo.Object { return &tengo.Int{Value: int64(i.idx - 1)} } func (i *StringArrayIterator) Value() tengo.Object { return &tengo.String{Value: i.strArr.Value[i.idx-1]} } ``` ### ObjectImpl ObjectImpl represents a default Object Implementation. To defined a new value type, one can embed ObjectImpl in their type declarations to avoid implementing all non-significant methods. TypeName() and String() methods still need to be implemented. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/operators.md0000644000175000017500000001270714607001502020441 0ustar00maythammaytham# Operators ## Int ### Equality - `(int) == (int) = (bool)`: equality - `(int) != (int) = (bool)`: inequality ### Arithmetic Operators - `(int) + (int) = (int)`: sum - `(int) - (int) = (int)`: difference - `(int) * (int) = (int)`: product - `(int) / (int) = (int)`: quotient - `(int) % (int) = (int)`: remainder - `(int) + (float) = (float)`: sum - `(int) - (float) = (float)`: difference - `(int) * (float) = (float)`: product - `(int) / (float) = (float)`: quotient - `(int) + (char) = (char)`: sum - `(int) - (char) = (char)`: difference ### Bitwise Operators - `(int) & (int) = (int)`: bitwise AND - `(int) | (int) = (int)`: bitwise OR - `(int) ^ (int) = (int)`: bitwise XOR - `(int) &^ (int) = (int)`: bitclear (AND NOT) - `(int) << (int) = (int)`: left shift - `(int) >> (int) = (int)`: right shift ### Comparison Operators - `(int) < (int) = (bool)`: less than - `(int) > (int) = (bool)`: greater than - `(int) <= (int) = (bool)`: less than or equal to - `(int) >= (int) = (bool)`: greater than or equal to - `(int) < (float) = (bool)`: less than - `(int) > (float) = (bool)`: greater than - `(int) <= (float) = (bool)`: less than or equal to - `(int) >= (float) = (bool)`: greater than or equal to - `(int) < (char) = (bool)`: less than - `(int) > (char) = (bool)`: greater than - `(int) <= (char) = (bool)`: less than or equal to - `(int) >= (char) = (bool)`: greater than or equal to ## Float ### Equality - `(float) == (float) = (bool)`: equality - `(float) != (float) = (bool)`: inequality ### Arithmetic Operators - `(float) + (float) = (float)`: sum - `(float) - (float) = (float)`: difference - `(float) * (float) = (float)`: product - `(float) / (float) = (float)`: quotient - `(float) + (int) = (int)`: sum - `(float) - (int) = (int)`: difference - `(float) * (int) = (int)`: product - `(float) / (int) = (int)`: quotient ### Comparison Operators - `(float) < (float) = (bool)`: less than - `(float) > (float) = (bool)`: greater than - `(float) <= (float) = (bool)`: less than or equal to - `(float) >= (float) = (bool)`: greater than or equal to - `(float) < (int) = (bool)`: less than - `(float) > (int) = (bool)`: greater than - `(float) <= (int) = (bool)`: less than or equal to - `(float) >= (int) = (bool)`: greater than or equal to ## String ### Equality - `(string) == (string) = (bool)`: equality - `(string) != (string) = (bool)`: inequality ### Concatenation - `(string) + (string) = (string)`: concatenation - `(string) + (other types) = (string)`: concatenation (after string-converted) ### Comparison Operators - `(string) < (string) = (bool)`: less than - `(string) > (string) = (bool)`: greater than - `(string) <= (string) = (bool)`: less than or equal to - `(string) >= (string) = (bool)`: greater than or equal to ## Char ### Equality - `(char) == (char) = (bool)`: equality - `(char) != (char) = (bool)`: inequality ### Arithmetic Operators - `(char) + (char) = (char)`: sum - `(char) - (char) = (char)`: difference - `(char) + (int) = (char)`: sum - `(char) - (int) = (char)`: difference ### Comparison Operators - `(char) < (char) = (bool)`: less than - `(char) > (char) = (bool)`: greater than - `(char) <= (char) = (bool)`: less than or equal to - `(char) >= (char) = (bool)`: greater than or equal to - `(char) < (int) = (bool)`: less than - `(char) > (int) = (bool)`: greater than - `(char) <= (int) = (bool)`: less than or equal to - `(char) >= (int) = (bool)`: greater than or equal to ## Bool ### Equality - `(bool) == (bool) = (bool)`: equality - `(bool) != (bool) = (bool)`: inequality ## Bytes ### Equality Test whether two byte array contain the same data. Uses [bytes.Compare](https://golang.org/pkg/bytes/#Compare) internally. - `(bytes) == (bytes) = (bool)`: equality - `(bytes) != (bytes) = (bool)`: inequality ## Time ### Equality Tests whether two times represent the same time instance. Uses [Time.Equal](https://golang.org/pkg/time/#Time.Equal) internally. - `(time) == (time) = (bool)`: equality - `(time) != (time) = (bool)`: inequality ### Arithmetic Operators - `(time) - (time) = (int)`: difference in nanoseconds (duration) - `(time) + (int) = (time)`: time + duration (nanoseconds) - `(time) - (int) = (time)`: time - duration (nanoseconds) ### Comparison Operators - `(time) < (time) = (bool)`: less than - `(time) > (time) = (bool)`: greater than - `(time) <= (time) = (bool)`: less than or equal to - `(time) >= (time) = (bool)`: greater than or equal to ## Array and ImmutableArray ### Equality Tests whether two _(immutable)_ arrays contain the same objects. - `(array) == (array) = (bool)`: equality - `(array) != (array) = (bool)`: inequality - `(array) == (immutable-array) = (bool)`: equality - `(array) != (immutable-array) = (bool)`: inequality - `(immutable-array) == (immutable-array) = (bool)`: equality - `(immutable-array) != (immutable-array) = (bool)`: inequality - `(immutable-array) == (array) = (bool)`: equality - `(immutable-array) != (array) = (bool)`: inequality ### Concatenation - `(array) + (array)`: return a concatenated array ## Map and ImmutableMap ### Equality Tests whether two _(immutable)_ maps contain the same key-objects. - `(map) == (map) = (bool)`: equality - `(map) != (map) = (bool)`: inequality - `(map) == (immutable-map) = (bool)`: equality - `(map) != (immutable-map) = (bool)`: inequality - `(immutable-map) == (immutable-map) = (bool)`: equality - `(immutable-map) != (immutable-map) = (bool)`: inequality - `(immutable-map) == (map) = (bool)`: equality - `(immutable-map) != (map) = (bool)`: inequality ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/runtime-types.md0000644000175000017500000001107514607001502021245 0ustar00maythammaytham# Tengo Runtime Types - **Int**: signed 64bit integer - **String**: string - **Float**: 64bit floating point - **Bool**: boolean - **Char**: character (`rune` in Go) - **Bytes**: byte array (`[]byte` in Go) - **Array**: objects array (`[]Object` in Go) - **ImmutableArray**: immutable object array (`[]Object` in Go) - **Map**: objects map with string keys (`map[string]Object` in Go) - **ImmutableMap**: immutable object map with string keys (`map[string]Object` in Go) - **Time**: time (`time.Time` in Go) - **Error**: an error with underlying Object value of any type - **Undefined**: undefined ## Type Conversion/Coercion Table |src\dst |Int |String |Float |Bool |Char |Bytes |Array |Map |Time |Error |Undefined| | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | |Int | - |_strconv_ |float64(v)|!IsFalsy()| rune(v)|**X**|**X**|**X**|_time.Unix()_|**X**|**X**| |String |_strconv_| - |_strconv_|!IsFalsy()|**X**|[]byte(s)|**X**|**X**|**X**|**X**|**X**| |Float |int64(f) |_strconv_ | - |!IsFalsy()|**X**|**X**|**X**|**X**|**X**|**X**|**X**| |Bool |1 / 0 |"true" / "false"|**X** | - |**X**|**X**|**X**|**X**|**X**|**X**|**X**| |Char |int64(c) |string(c) |**X** |!IsFalsy()| - |**X**|**X**|**X**|**X**|**X**|**X**| |Bytes |**X** |string(y)|**X** |!IsFalsy()|**X**| - |**X**|**X**|**X**|**X**|**X**| |Array |**X** |"[...]" |**X** |!IsFalsy()|**X**|**X**| - |**X**|**X**|**X**|**X**| |Map |**X** |"{...}" |**X** |!IsFalsy()|**X**|**X**|**X**| - |**X**|**X**|**X**| |Time |**X** |String() |**X** |!IsFalsy()|**X**|**X**|**X**|**X**| - |**X**|**X**| |Error |**X** |"error: ..." |**X** |false|**X**|**X**|**X**|**X**|**X**| - |**X**| |Undefined|**X** |**X**|**X** |false|**X**|**X**|**X**|**X**|**X**|**X**| - | _* **X**: No conversion; Typed value functions for `Variable` will return zero values._ _* strconv: converted using Go's conversion functions from `strconv` package._ _* IsFalsy(): use [Object.IsFalsy()](#objectisfalsy) function_ _* String(): use `Object.String()` function_ _* time.Unix(): use `time.Unix(v, 0)` to convert to Time_ ## Object.IsFalsy() `Object.IsFalsy()` interface method is used to determine if a given value should evaluate to `false` (e.g. for condition expression of `if` statement). - **Int**: `n == 0` - **String**: `len(s) == 0` - **Float**: `isNaN(f)` - **Bool**: `!b` - **Char**: `c == 0` - **Bytes**: `len(bytes) == 0` - **Array**: `len(arr) == 0` - **Map**: `len(map) == 0` - **Time**: `Time.IsZero()` - **Error**: `true` _(Error is always falsy)_ - **Undefined**: `true` _(Undefined is always falsy)_ ## Type Conversion Builtin Functions - `string(x)`: tries to convert `x` into string; returns `undefined` if failed - `int(x)`: tries to convert `x` into int; returns `undefined` if failed - `bool(x)`: tries to convert `x` into bool; returns `undefined` if failed - `float(x)`: tries to convert `x` into float; returns `undefined` if failed - `char(x)`: tries to convert `x` into char; returns `undefined` if failed - `bytes(x)`: tries to convert `x` into bytes; returns `undefined` if failed - `bytes(N)`: as a special case this will create a Bytes variable with the given size `N` (only if `N` is int) - `time(x)`: tries to convert `x` into time; returns `undefined` if failed - See [Builtins](https://github.com/d5/tengo/blob/master/docs/builtins.md) for the full list of builtin functions. ## Type Checking Builtin Functions - `is_string(x)`: returns `true` if `x` is string; `false` otherwise - `is_int(x)`: returns `true` if `x` is int; `false` otherwise - `is_bool(x)`: returns `true` if `x` is bool; `false` otherwise - `is_float(x)`: returns `true` if `x` is float; `false` otherwise - `is_char(x)`: returns `true` if `x` is char; `false` otherwise - `is_bytes(x)`: returns `true` if `x` is bytes; `false` otherwise - `is_array(x)`: return `true` if `x` is array; `false` otherwise - `is_immutable_array(x)`: return `true` if `x` is immutable array; `false` otherwise - `is_map(x)`: return `true` if `x` is map; `false` otherwise - `is_immutable_map(x)`: return `true` if `x` is immutable map; `false` otherwise - `is_time(x)`: return `true` if `x` is time; `false` otherwise - `is_error(x)`: returns `true` if `x` is error; `false` otherwise - `is_undefined(x)`: returns `true` if `x` is undefined; `false` otherwise - See [Builtins](https://github.com/d5/tengo/blob/master/docs/builtins.md) for the full list of builtin functions. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-base64.md0000644000175000017500000000130714607001502020760 0ustar00maythammaytham# Module - "base64" ```golang base64 := import("base64") ``` ## Functions - `encode(src)`: returns the base64 encoding of src. - `decode(s)`: returns the bytes represented by the base64 string s. - `raw_encode(src)`: returns the base64 encoding of src but omits the padding. - `raw_decode(s)`: returns the bytes represented by the base64 string s which omits the padding. - `url_encode(src)`: returns the url-base64 encoding of src. - `url_decode(s)`: returns the bytes represented by the url-base64 string s. - `raw_url_encode(src)`: returns the url-base64 encoding of src but omits the padding. - `raw_url_decode(s)`: returns the bytes represented by the url-base64 string s which omits the padding. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-enum.md0000644000175000017500000000447114607001502020645 0ustar00maythammaytham# Module - "enum" ```golang enum := import("enum") ``` ## Functions - `all(x, fn) => bool`: returns true if the given function `fn` evaluates to a truthy value on all of the items in `x`. It returns undefined if `x` is not enumerable. - `any(x, fn) => bool`: returns true if the given function `fn` evaluates to a truthy value on any of the items in `x`. It returns undefined if `x` is not enumerable. - `chunk(x, size) => [object]`: returns an array of elements split into groups the length of size. If `x` can't be split evenly, the final chunk will be the remaining elements. It returns undefined if `x` is not array. - `at(x, key) => object`: returns an element at the given index (if `x` is array) or key (if `x` is map). It returns undefined if `x` is not enumerable. - `each(x, fn)`: iterates over elements of `x` and invokes `fn` for each element. `fn` is invoked with two arguments: `key` and `value`. `key` is an int index if `x` is array. `key` is a string key if `x` is map. It does not iterate and returns undefined if `x` is not enumerable.` - `filter(x, fn) => [object]`: iterates over elements of `x`, returning an array of all elements `fn` returns truthy for. `fn` is invoked with two arguments: `key` and `value`. `key` is an int index if `x` is array. It returns undefined if `x` is not array. - `find(x, fn) => object`: iterates over elements of `x`, returning value of the first element `fn` returns truthy for. `fn` is invoked with two arguments: `key` and `value`. `key` is an int index if `x` is array. `key` is a string key if `x` is map. It returns undefined if `x` is not enumerable. - `find_key(x, fn) => int/string`: iterates over elements of `x`, returning key or index of the first element `fn` returns truthy for. `fn` is invoked with two arguments: `key` and `value`. `key` is an int index if `x` is array. `key` is a string key if `x` is map. It returns undefined if `x` is not enumerable. - `map(x, fn) => [object]`: creates an array of values by running each element in `x` through `fn`. `fn` is invoked with two arguments: `key` and `value`. `key` is an int index if `x` is array. `key` is a string key if `x` is map. It returns undefined if `x` is not enumerable. - `key(k, _) => object`: returns the first argument. - `value(_, v) => object`: returns the second argument. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-fmt.md0000644000175000017500000000173114607001502020463 0ustar00maythammaytham# Module - "fmt" ```golang fmt := import("fmt") ``` ## Functions - `print(args...)`: Prints a string representation of the given variable to the standard output. Unlike Go's `fmt.Print` function, no spaces are added between the operands. - `println(args...)`: Prints a string representation of the given variable to the standard output with a newline appended. Unlike Go's `fmt.Println` function, no spaces are added between the operands. - `printf(format, args...)`: Prints a formatted string to the standard output. It does not append the newline character at the end. The first argument must a String object. See [this](https://github.com/d5/tengo/blob/master/docs/formatting.md) for more details on formatting. - `sprintf(format, args...)`: Returns a formatted string. Alias of the builtin function `format`. The first argument must be a String object. See [this](https://github.com/d5/tengo/blob/master/docs/formatting.md) for more details on formatting. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-hex.md0000644000175000017500000000031014607001502020451 0ustar00maythammaytham# Module - "hex" ```golang hex := import("hex") ``` ## Functions - `encode(src)`: returns the hexadecimal encoding of src. - `decode(s)`: returns the bytes represented by the hexadecimal string s. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-json.md0000644000175000017500000000161114607001502020643 0ustar00maythammaytham# Module - "json" ```golang json := import("json") ``` ## Functions - `decode(b string/bytes) => object`: Parses the JSON string and returns an object. - `encode(o object) => bytes`: Returns the JSON string (bytes) of the object. Unlike Go's JSON package, this function does not HTML-escape texts, but, one can use `html_escape` function if needed. - `indent(b string/bytes, prefix string, indent string) => bytes`: Returns an indented form of input JSON bytes string. - `html_escape(b string/bytes) => bytes`: Return an HTML-safe form of input JSON bytes string. ## Examples ```golang json := import("json") encoded := json.encode({a: 1, b: [2, 3, 4]}) // JSON-encoded bytes string indentded := json.indent(encoded, "", " ") // indented form html_safe := json.html_escape(encoded) // HTML escaped form decoded := json.decode(encoded) // {a: 1, b: [2, 3, 4]} ``` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-math.md0000644000175000017500000001170614607001502020631 0ustar00maythammaytham# Module - "math" ```golang math := import("math") ``` ## Constants - `e` - `pi` - `phi` - `sqrt2` - `sqrtE` - `sprtPi` - `sqrtPhi` - `ln2` - `log2E` - `ln10` - `ln10E` Mathematical constants. - `maxFloat32` - `smallestNonzeroFloat32` - `maxFloat64` - `smallestNonzeroFloat64` Floating-point limit values. Max is the largest finite value representable by the type. SmallestNonzero is the smallest positive, non-zero value representable by the type. - `maxInt` - `minInt` - `maxInt8` - `minInt8` - `maxInt16` - `minInt16` - `maxInt32` - `minInt32` - `maxInt64` - `minInt64` Integer limit values. ## Functions - `abs(x float) => float`: returns the absolute value of x. - `acos(x float) => float`: returns the arccosine, in radians, of x. - `acosh(x float) => float`: returns the inverse hyperbolic cosine of x. - `asin(x float) => float`: returns the arcsine, in radians, of x. - `asinh(x float) => float`: returns the inverse hyperbolic sine of x. - `atan(x float) => float`: returns the arctangent, in radians, of x. - `atan2(y float, xfloat) => float`: returns the arc tangent of y/x, using the signs of the two to determine the quadrant of the return value. - `atanh(x float) => float`: returns the inverse hyperbolic tangent of x. - `cbrt(x float) => float`: returns the cube root of x. - `ceil(x float) => float`: returns the least integer value greater than or equal to x. - `copysign(x float, y float) => float`: returns a value with the magnitude of x and the sign of y. - `cos(x float) => float`: returns the cosine of the radian argument x. - `cosh(x float) => float`: returns the hyperbolic cosine of x. - `dim(x float, y float) => float`: returns the maximum of x-y or 0. - `erf(x float) => float`: returns the error function of x. - `erfc(x float) => float`: returns the complementary error function of x. - `exp(x float) => float`: returns e**x, the base-e exponential of x. - `exp2(x float) => float`: returns 2**x, the base-2 exponential of x. - `expm1(x float) => float`: returns e**x - 1, the base-e exponential of x minus 1. It is more accurate than Exp(x) - 1 when x is near zero. - `floor(x float) => float`: returns the greatest integer value less than or equal to x. - `gamma(x float) => float`: returns the Gamma function of x. - `hypot(p float, q float) => float`: returns `Sqrt(p * p + q * q)`, taking care to avoid unnecessary overflow and underflow. - `ilogb(x float) => float`: returns the binary exponent of x as an integer. - `inf(sign int) => float`: returns positive infinity if sign >= 0, negative infinity if sign < 0. - `is_inf(f float, sign int) => float`: reports whether f is an infinity, according to sign. If sign > 0, IsInf reports whether f is positive infinity. If sign < 0, IsInf reports whether f is negative infinity. If sign == 0, IsInf reports whether f is either infinity. - `is_nan(f float) => float`: reports whether f is an IEEE 754 ``not-a-number'' value. - `j0(x float) => float`: returns the order-zero Bessel function of the first kind. - `j1(x float) => float`: returns the order-one Bessel function of the first kind. - `jn(n int, x float) => float`: returns the order-n Bessel function of the first kind. - `ldexp(frac float, exp int) => float`: is the inverse of frexp. It returns frac × 2**exp. - `log(x float) => float`: returns the natural logarithm of x. - `log10(x float) => float`: returns the decimal logarithm of x. - `log1p(x float) => float`: returns the natural logarithm of 1 plus its argument x. It is more accurate than Log(1 + x) when x is near zero. - `log2(x float) => float`: returns the binary logarithm of x. - `logb(x float) => float`: returns the binary exponent of x. - `max(x float, y float) => float`: returns the larger of x or y. - `min(x float, y float) => float`: returns the smaller of x or y. - `mod(x float, y float) => float`: returns the floating-point remainder of x/y. - `nan() => float`: returns an IEEE 754 ``not-a-number'' value. - `nextafter(x float, y float) => float`: returns the next representable float64 value after x towards y. - `pow(x float, y float) => float`: returns x**y, the base-x exponential of y. - `pow10(n int) => float`: returns 10**n, the base-10 exponential of n. - `remainder(x float, y float) => float`: returns the IEEE 754 floating-point remainder of x/y. - `signbit(x float) => float`: returns true if x is negative or negative zero. - `sin(x float) => float`: returns the sine of the radian argument x. - `sinh(x float) => float`: returns the hyperbolic sine of x. - `sqrt(x float) => float`: returns the square root of x. - `tan(x float) => float`: returns the tangent of the radian argument x. - `tanh(x float) => float`: returns the hyperbolic tangent of x. - `trunc(x float) => float`: returns the integer value of x. - `y0(x float) => float`: returns the order-zero Bessel function of the second kind. - `y1(x float) => float`: returns the order-one Bessel function of the second kind. - `yn(n int, x float) => float`: returns the order-n Bessel function of the second kind. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-os.md0000644000175000017500000002013714607001502020317 0ustar00maythammaytham# Module - "os" ```golang os := import("os") ``` ## Constants - `platform` - `arch` - `o_rdonly` - `o_wronly` - `o_rdwr` - `o_append` - `o_create` - `o_excl` - `o_sync` - `o_trunc` - `mode_dir` - `mode_append` - `mode_exclusive` - `mode_temporary` - `mode_symlink` - `mode_device` - `mode_named_pipe` - `mode_socket` - `mode_setuid` - `mode_setgui` - `mode_char_device` - `mode_sticky` - `mode_irregular` - `mode_type` - `mode_perm` - `seek_set` - `seek_cur` - `seek_end` - `path_separator` - `path_list_separator` - `dev_null` ## Functions - `args() => [string]`: returns command-line arguments, starting with the program name. - `chdir(dir string) => error`: changes the current working directory to the named directory. - `chmod(name string, mode int) => error`: changes the mode of the named file to mode. - `chown(name string, uid int, gid int) => error`: changes the numeric uid and gid of the named file. - `clearenv()`: deletes all environment variables. - `environ() => [string]`: returns a copy of strings representing the environment. - `exit(code int)`: causes the current program to exit with the given status code. - `expand_env(s string) => string`: replaces ${var} or $var in the string according to the values of the current environment variables. - `getegid() => int`: returns the numeric effective group id of the caller. - `getenv(key string) => string`: retrieves the value of the environment variable named by the key. - `geteuid() => int`: returns the numeric effective user id of the caller. - `getgid() => int`: returns the numeric group id of the caller. - `getgroups() => [int]/error`: returns a list of the numeric ids of groups that the caller belongs to. - `getpagesize() => int`: returns the underlying system's memory page size. - `getpid() => int`: returns the process id of the caller. - `getppid() => int`: returns the process id of the caller's parent. - `getuid() => int`: returns the numeric user id of the caller. - `getwd() => string/error`: returns a rooted path name corresponding to the current directory. - `hostname() => string/error`: returns the host name reported by the kernel. - `lchown(name string, uid int, gid int) => error`: changes the numeric uid and gid of the named file. - `link(oldname string, newname string) => error`: creates newname as a hard link to the oldname file. - `lookup_env(key string) => string/false`: retrieves the value of the environment variable named by the key. - `mkdir(name string, perm int) => error`: creates a new directory with the specified name and permission bits (before umask). - `mkdir_all(name string, perm int) => error`: creates a directory named path, along with any necessary parents, and returns nil, or else returns an error. - `read_file(name string) => bytes/error`: reads the contents of a file into a byte array - `readlink(name string) => string/error`: returns the destination of the named symbolic link. - `remove(name string) => error`: removes the named file or (empty) directory. - `remove_all(name string) => error`: removes path and any children it contains. - `rename(oldpath string, newpath string) => error`: renames (moves) oldpath to newpath. - `setenv(key string, value string) => error`: sets the value of the environment variable named by the key. - `stat(filename string) => FileInfo/error`: returns a file info structure describing the file - `symlink(oldname string newname string) => error`: creates newname as a symbolic link to oldname. - `temp_dir() => string`: returns the default directory to use for temporary files. - `truncate(name string, size int) => error`: changes the size of the named file. - `unsetenv(key string) => error`: unsets a single environment variable. - `create(name string) => File/error`: creates the named file with mode 0666 (before umask), truncating it if it already exists. - `open(name string) => File/error`: opens the named file for reading. If successful, methods on the returned file can be used for reading; the associated file descriptor has mode O_RDONLY. - `open_file(name string, flag int, perm int) => File/error`: is the generalized open call; most users will use Open or Create instead. It opens the named file with specified flag (O_RDONLY etc.) and perm (before umask), if applicable. - `find_process(pid int) => Process/error`: looks for a running process by its pid. - `start_process(name string, argv [string], dir string, env [string]) => Process/error`: starts a new process with the program, arguments and attributes specified by name, argv and attr. The argv slice will become os.Args in the new process, so it normally starts with the program name. - `exec_look_path(file string) => string/error`: searches for an executable named file in the directories named by the PATH environment variable. - `exec(name string, args...) => Command/error`: returns the Command to execute the named program with the given arguments. ## File ```golang file := os.create("myfile") file.write_string("some data") file.close() ``` - `chdir() => true/error`: changes the current working directory to the file, - `chown(uid int, gid int) => true/error`: changes the numeric uid and gid of the named file. - `close() => error`: closes the File, rendering it unusable for I/O. - `name() => string`: returns the name of the file as presented to Open. - `readdirnames(n int) => [string]/error`: reads and returns a slice of names from the directory. - `sync() => error`: commits the current contents of the file to stable storage. - `write(bytes) => int/error`: writes len(b) bytes to the File. - `write_string(string) => int/error`: is like 'write', but writes the contents of string s rather than a slice of bytes. - `read(bytes) => int/error`: reads up to len(b) bytes from the File. - `stat() => FileInfo/error`: returns a file info structure describing the file - `chmod(mode int) => error`: changes the mode of the file to mode. - `seek(offset int, whence int) => int/error`: sets the offset for the next Read or Write on file to offset, interpreted according to whence: 0 means relative to the origin of the file, 1 means relative to the current offset, and 2 means relative to the end. ## Process ```golang proc := start_process("app", ["arg1", "arg2"], "dir", []) proc.wait() ``` - `kill() => error`: causes the Process to exit immediately. - `release() => error`: releases any resources associated with the process, rendering it unusable in the future. - `signal(signal int) => error`: sends a signal to the Process. - `wait() => ProcessState/error`: waits for the Process to exit, and then returns a ProcessState describing its status and an error, if any. ## ProcessState ```golang proc := start_process("app", ["arg1", "arg2"], "dir", []) stat := proc.wait() pid := stat.pid() ``` - `exited() => bool`: reports whether the program has exited. - `pid() => int`: returns the process id of the exited process. - `string() => string`: returns a string representation of the process. - `success() => bool`: reports whether the program exited successfully, such as with exit status 0 on Unix. ```golang cmd := exec.command("echo", ["foo", "bar"]) output := cmd.output() ``` ## FileInfo - `name`: name of the file the info describes - `mtime`: time the file was last modified - `size`: file size in bytes - `mode`: file permissions as in int, comparable to octal permissions - `directory`: boolean indicating if the file is a directory ## Command - `combined_output() => bytes/error`: runs the command and returns its combined standard output and standard error. - `output() => bytes/error`: runs the command and returns its standard output. - `run() => error`: starts the specified command and waits for it to complete. - `start() => error`: starts the specified command but does not wait for it to complete. - `wait() => error`: waits for the command to exit and waits for any copying to stdin or copying from stdout or stderr to complete. - `set_path(path string)`: sets the path of the command to run. - `set_dir(dir string)`: sets the working directory of the process. - `set_env(env [string])`: sets the environment of the process. - `process() => Process`: returns the underlying process, once started. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-rand.md0000644000175000017500000000502614607001502020622 0ustar00maythammaytham# Module - "rand" ```golang rand := import("rand") ``` ## Functions - `seed(seed int)`: uses the provided seed value to initialize the default Source to a deterministic state. - `exp_float() => float`: returns an exponentially distributed float64 in the range (0, +math.MaxFloat64] with an exponential distribution whose rate parameter (lambda) is 1 and whose mean is 1/lambda (1) from the default Source. - `float() => float`: returns, as a float64, a pseudo-random number in [0.0,1.0) from the default Source. - `int() => int`: returns a non-negative pseudo-random 63-bit integer as an int64 from the default Source. - `intn(n int) => int`: returns, as an int64, a non-negative pseudo-random number in [0,n) from the default Source. It panics if n <= 0. - `norm_float) => float`: returns a normally distributed float64 in the range [-math.MaxFloat64, +math.MaxFloat64] with standard normal distribution (mean = 0, stddev = 1) from the default Source. - `perm(n int) => [int]`: returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n) from the default Source. - `read(p bytes) => int/error`: generates len(p) random bytes from the default Source and writes them into p. It always returns len(p) and a nil error. - `rand(src_seed int) => Rand`: returns a new Rand that uses random values from src to generate other random values. ## Rand - `seed(seed int)`: uses the provided seed value to initialize the default Source to a deterministic state. - `exp_float() => float`: returns an exponentially distributed float64 in the range (0, +math.MaxFloat64] with an exponential distribution whose rate parameter (lambda) is 1 and whose mean is 1/lambda (1) from the default Source. - `float() => float`: returns, as a float64, a pseudo-random number in [0.0,1.0) from the default Source. - `int() => int`: returns a non-negative pseudo-random 63-bit integer as an int64 from the default Source. - `intn(n int) => int`: returns, as an int64, a non-negative pseudo-random number in [0,n) from the default Source. It panics if n <= 0. - `norm_float) => float`: returns a normally distributed float64 in the range [-math.MaxFloat64, +math.MaxFloat64] with standard normal distribution (mean = 0, stddev = 1) from the default Source. - `perm(n int) => [int]`: returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n) from the default Source. - `read(p bytes) => int/error`: generates len(p) random bytes from the default Source and writes them into p. It always returns len(p) and a nil error. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-text.md0000644000175000017500000002007014607001502020656 0ustar00maythammaytham# Module - "text" ```golang text := import("text") ``` ## Functions - `re_match(pattern string, text string) => bool/error`: reports whether the string s contains any match of the regular expression pattern. - `re_find(pattern string, text string, count int) => [[{text: string, begin: int, end: int}]]/undefined`: returns an array holding all matches, each of which is an array of map object that contains matching text, begin and end (exclusive) index. - `re_replace(pattern string, text string, repl string) => string/error`: returns a copy of src, replacing matches of the pattern with the replacement string repl. - `re_split(pattern string, text string, count int) => [string]/error`: slices s into substrings separated by the expression and returns a slice of the substrings between those expression matches. - `re_compile(pattern string) => Regexp/error`: parses a regular expression and returns, if successful, a Regexp object that can be used to match against text. - `compare(a string, b string) => int`: returns an integer comparing two strings lexicographically. The result will be 0 if a==b, -1 if a < b, and +1 if a > b. - `contains(s string, substr string) => bool`: reports whether substr is within s. - `contains_any(s string, chars string) => bool`: reports whether any Unicode code points in chars are within s. - `count(s string, substr string) => int`: counts the number of non-overlapping instances of substr in s. - `equal_fold(s string, t string) => bool`: reports whether s and t, interpreted as UTF-8 strings, - `fields(s string) => [string]`: splits the string s around each instance of one or more consecutive white space characters, as defined by unicode.IsSpace, returning a slice of substrings of s or an empty slice if s contains only white space. - `has_prefix(s string, prefix string) => bool`: tests whether the string s begins with prefix. - `has_suffix(s string, suffix string) => bool`: tests whether the string s ends with suffix. - `index(s string, substr string) => int`: returns the index of the first instance of substr in s, or -1 if substr is not present in s. - `index_any(s string, chars string) => int`: returns the index of the first instance of any Unicode code point from chars in s, or -1 if no Unicode code point from chars is present in s. - `join(arr string, sep string) => string`: concatenates the elements of a to create a single string. The separator string sep is placed between elements in the resulting string. - `last_index(s string, substr string) => int`: returns the index of the last instance of substr in s, or -1 if substr is not present in s. - `last_index_any(s string, chars string) => int`: returns the index of the last instance of any Unicode code point from chars in s, or -1 if no Unicode code point from chars is present in s. - `repeat(s string, count int) => string`: returns a new string consisting of count copies of the string s. - `replace(s string, old string, new string, n int) => string`: returns a copy of the string s with the first n non-overlapping instances of old replaced by new. - `substr(s string, lower int, upper int) => string => string`: returns a substring of the string s specified by the lower and upper parameters. - `split(s string, sep string) => [string]`: slices s into all substrings separated by sep and returns a slice of the substrings between those separators. - `split_after(s string, sep string) => [string]`: slices s into all substrings after each instance of sep and returns a slice of those substrings. - `split_after_n(s string, sep string, n int) => [string]`: slices s into substrings after each instance of sep and returns a slice of those substrings. - `split_n(s string, sep string, n int) => [string]`: slices s into substrings separated by sep and returns a slice of the substrings between those separators. - `title(s string) => string`: returns a copy of the string s with all Unicode letters that begin words mapped to their title case. - `to_lower(s string) => string`: returns a copy of the string s with all Unicode letters mapped to their lower case. - `to_title(s string) => string`: returns a copy of the string s with all Unicode letters mapped to their title case. - `to_upper(s string) => string`: returns a copy of the string s with all Unicode letters mapped to their upper case. - `pad_left(s string, pad_len int, pad_with string) => string`: returns a copy of the string s padded on the left with the contents of the string pad_with to length pad_len. If pad_with is not specified, white space is used as the default padding. - `pad_right(s string, pad_len int, pad_with string) => string`: returns a copy of the string s padded on the right with the contents of the string pad_with to length pad_len. If pad_with is not specified, white space is used as the default padding. - `trim(s string, cutset string) => string`: returns a slice of the string s with all leading and trailing Unicode code points contained in cutset removed. - `trim_left(s string, cutset string) => string`: returns a slice of the string s with all leading Unicode code points contained in cutset removed. - `trim_prefix(s string, prefix string) => string`: returns s without the provided leading prefix string. - `trim_right(s string, cutset string) => string`: returns a slice of the string s, with all trailing Unicode code points contained in cutset removed. - `trim_space(s string) => string`: returns a slice of the string s, with all leading and trailing white space removed, as defined by Unicode. - `trim_suffix(s string, suffix string) => string`: returns s without the provided trailing suffix string. - `atoi(str string) => int/error`: returns the result of ParseInt(s, 10, 0) converted to type int. - `format_bool(b bool) => string`: returns "true" or "false" according to the value of b. - `format_float(f float, fmt string, prec int, bits int) => string`: converts the floating-point number f to a string, according to the format fmt and precision prec. - `format_int(i int, base int) => string`: returns the string representation of i in the given base, for 2 <= base <= 36. The result uses the lower-case letters 'a' to 'z' for digit values >= 10. - `itoa(i int) => string`: is shorthand for format_int(i, 10). - `parse_bool(s string) => bool/error`: returns the boolean value represented by the string. It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False. Any other value returns an error. - `parse_float(s string, bits int) => float/error`: converts the string s to a floating-point number with the precision specified by bitSize: 32 for float32, or 64 for float64. When bitSize=32, the result still has type float64, but it will be convertible to float32 without changing its value. - `parse_int(s string, base int, bits int) => int/error`: interprets a string s in the given base (0, 2 to 36) and bit size (0 to 64) and returns the corresponding value i. - `quote(s string) => string`: returns a double-quoted Go string literal representing s. The returned string uses Go escape sequences (\t, \n, \xFF, \u0100) for control characters and non-printable characters as defined by IsPrint. - `unquote(s string) => string/error`: interprets s as a single-quoted, double-quoted, or backquoted Go string literal, returning the string value that s quotes. (If s is single-quoted, it would be a Go character literal; Unquote returns the corresponding one-character string.) ## Regexp - `match(text string) => bool`: reports whether the string s contains any match of the regular expression pattern. - `find(text string, count int) => [[{text: string, begin: int, end: int}]]/undefined`: returns an array holding all matches, each of which is an array of map object that contains matching text, begin and end (exclusive) index. - `replace(src string, repl string) => string`: returns a copy of src, replacing matches of the pattern with the replacement string repl. - `split(text string, count int) => [string]`: slices s into substrings separated by the expression and returns a slice of the substrings between those expression matches. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib-times.md0000644000175000017500000001411514607001502021016 0ustar00maythammaytham# Module - "times" ```golang times := import("times") ``` ## Constants - `format_ansic`: time format "Mon Jan _2 15:04:05 2006" - `format_unix_date`: time format "Mon Jan _2 15:04:05 MST 2006" - `format_ruby_date`: time format "Mon Jan 02 15:04:05 -0700 2006" - `format_rfc822`: time format "02 Jan 06 15:04 MST" - `format_rfc822z`: time format "02 Jan 06 15:04 -0700" - `format_rfc850`: time format "Monday, 02-Jan-06 15:04:05 MST" - `format_rfc1123`: time format "Mon, 02 Jan 2006 15:04:05 MST" - `format_rfc1123z`: time format "Mon, 02 Jan 2006 15:04:05 -0700" - `format_rfc3339`: time format "2006-01-02T15:04:05Z07:00" - `format_rfc3339_nano`: time format "2006-01-02T15:04:05.999999999Z07:00" - `format_kitchen`: time format "3:04PM" - `format_stamp`: time format "Jan _2 15:04:05" - `format_stamp_milli`: time format "Jan _2 15:04:05.000" - `format_stamp_micro`: time format "Jan _2 15:04:05.000000" - `format_stamp_nano`: time format "Jan _2 15:04:05.000000000" - `nanosecond` - `microsecond` - `millisecond` - `second` - `minute` - `hour` - `january` - `february` - `march` - `april` - `may` - `june` - `july` - `august` - `september` - `october` - `november` - `december` ## Functions - `sleep(duration int)`: pauses the current goroutine for at least the duration d. A negative or zero duration causes Sleep to return immediately. - `parse_duration(s string) => int`: parses a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". - `since(t time) => int`: returns the time elapsed since t. - `until(t time) => int`: returns the duration until t. - `duration_hours(duration int) => float`: returns the duration as a floating point number of hours. - `duration_minutes(duration int) => float`: returns the duration as a floating point number of minutes. - `duration_nanoseconds(duration int) => int`: returns the duration as an integer of nanoseconds. - `duration_seconds(duration int) => float`: returns the duration as a floating point number of seconds. - `duration_string(duration int) => string`: returns a string representation of duration. - `month_string(month int) => string`: returns the English name of the month ("January", "February", ...). - `date(year int, month int, day int, hour int, min int, sec int, nsec int, loc string) => time`: returns the Time corresponding to "yyyy-mm-dd hh:mm:ss + nsec nanoseconds" in the appropriate zone for that Time in the given (optional) location. The Local time zone will be used if executed without specifying a location. - `now() => time`: returns the current local time. - `parse(format string, s string) => time`: parses a formatted string and returns the time value it represents. The layout defines the format by showing how the reference time, defined to be "Mon Jan 2 15:04:05 -0700 MST 2006" would be interpreted if it were the value; it serves as an example of the input format. The same interpretation will then be made to the input string. - `unix(sec int, nsec int) => time`: returns the local Time corresponding to the given Unix time, sec seconds and nsec nanoseconds since January 1, 1970 UTC. - `add(t time, duration int) => time`: returns the time t+d. - `add_date(t time, years int, months int, days int) => time`: returns the time corresponding to adding the given number of years, months, and days to t. For example, AddDate(-1, 2, 3) applied to January 1, 2011 returns March 4, 2010. - `sub(t time, u time) => int`: returns the duration t-u. - `after(t time, u time) => bool`: reports whether the time instant t is after u. - `before(t time, u time) => bool`: reports whether the time instant t is before u. - `time_year(t time) => int`: returns the year in which t occurs. - `time_month(t time) => int`: returns the month of the year specified by t. - `time_day(t time) => int`: returns the day of the month specified by t. - `time_weekday(t time) => int`: returns the day of the week specified by t. - `time_hour(t time) => int`: returns the hour within the day specified by t, in the range [0, 23]. - `time_minute(t time) => int`: returns the minute offset within the hour specified by t, in the range [0, 59]. - `time_second(t time) => int`: returns the second offset within the minute specified by t, in the range [0, 59]. - `time_nanosecond(t time) => int`: returns the nanosecond offset within the second specified by t, in the range [0, 999999999]. - `time_unix(t time) => int`: returns t as a Unix time, the number of seconds elapsed since January 1, 1970 UTC. The result does not depend on the location associated with t. - `time_unix_nano(t time) => int`: returns t as a Unix time, the number of nanoseconds elapsed since January 1, 1970 UTC. The result is undefined if the Unix time in nanoseconds cannot be represented by an int64 (a date before the year 1678 or after 2262). Note that this means the result of calling UnixNano on the zero Time is undefined. The result does not depend on the location associated with t. - `time_format(t time, format) => string`: returns a textual representation of he time value formatted according to layout, which defines the format by showing how the reference time, defined to be "Mon Jan 2 15:04:05 -0700 MST 2006" would be displayed if it were the value; it serves as an example of the desired output. The same display rules will then be applied to the time value. - `time_location(t time) => string`: returns the time zone name associated with t. - `time_string(t time) => string`: returns the time formatted using the format string "2006-01-02 15:04:05.999999999 -0700 MST". - `is_zero(t time) => bool`: reports whether t represents the zero time instant, January 1, year 1, 00:00:00 UTC. - `in_location(t time, l string) => time`: returns a copy of t representing the same time instant, but with the copy's location information set to l for display purposes. - `to_local(t time) => time`: returns t with the location set to local time. - `to_utc(t time) => time`: returns t with the location set to UTC. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/stdlib.md0000644000175000017500000000207114607001502017675 0ustar00maythammaytham# Standard Library - [os](https://github.com/d5/tengo/blob/master/docs/stdlib-os.md): platform-independent interface to operating system functionality. - [text](https://github.com/d5/tengo/blob/master/docs/stdlib-text.md): regular expressions, string conversion, and manipulation - [math](https://github.com/d5/tengo/blob/master/docs/stdlib-math.md): mathematical constants and functions - [times](https://github.com/d5/tengo/blob/master/docs/stdlib-times.md): time-related functions - [rand](https://github.com/d5/tengo/blob/master/docs/stdlib-rand.md): random functions - [fmt](https://github.com/d5/tengo/blob/master/docs/stdlib-fmt.md): formatting functions - [json](https://github.com/d5/tengo/blob/master/docs/stdlib-json.md): JSON functions - [enum](https://github.com/d5/tengo/blob/master/docs/stdlib-enum.md): Enumeration functions - [hex](https://github.com/d5/tengo/blob/master/docs/stdlib-hex.md): hex encoding and decoding functions - [base64](https://github.com/d5/tengo/blob/master/docs/stdlib-base64.md): base64 encoding and decoding functions ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/tengo-cli.md0000644000175000017500000000313714607001502020301 0ustar00maythammaytham# Tengo CLI Tool Tengo is designed as an embedding script language for Go, but, it can also be compiled and executed as native binary using `tengo` CLI tool. ## Installing Tengo CLI To install `tengo` tool, run: ```bash go get github.com/d5/tengo/cmd/tengo ``` Or, you can download the precompiled binaries from [here](https://github.com/d5/tengo/releases/latest). ## Compiling and Executing Tengo Code You can directly execute the Tengo source code by running `tengo` tool with your Tengo source file (`*.tengo`). ```bash tengo myapp.tengo ``` Or, you can compile the code into a binary file and execute it later. ```bash tengo -o myapp myapp.tengo # compile 'myapp.tengo' into binary file 'myapp' tengo myapp # execute the compiled binary `myapp` ``` Or, you can make tengo source file executable ```bash # copy tengo executable to a dir where PATH environment variable includes cp tengo /usr/local/bin/ # add shebang line to source file cat > myapp.tengo << EOF #!/usr/local/bin/tengo fmt := import("fmt") fmt.println("Hello World!") EOF # make myapp.tengo file executable chmod +x myapp.tengo # run your script ./myapp.tengo ``` **Note: Your source file must have `.tengo` extension.** ## Resolving Relative Import Paths If there are tengo source module files which are imported with relative import paths, CLI has `-resolve` flag. Flag enables to import a module relative to importing file. This behavior will be default at version 3. ## Tengo REPL You can run Tengo [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) if you run `tengo` with no arguments. ```bash tengo ``` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/docs/tutorial.md0000644000175000017500000003605714607001502020272 0ustar00maythammaytham# Tengo Language Syntax Tengo's syntax is designed to be familiar to Go developers while being a bit simpler and more streamlined. **You can test the Tengo code in online [Playground](https://tengolang.com).** ## Values and Value Types In Tengo, everything is a value, and, all values are associated with a type. ```golang 19 + 84 // int values "aomame" + `kawa` // string values -9.22 + 1e10 // float values true || false // bool values '九' > '9' // char values [1, false, "foo"] // array value {a: 12.34, b: "bar"} // map value func() { /*...*/ } // function value ``` Here's a list of all available value types in Tengo. | 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` | | error | [error](#error-values) value | - | | time | time value | `time.Time` | | array | value array _(mutable)_ | `[]any` | | immutable array | [immutable](#immutable-values) array | - | | map | value map with string keys _(mutable)_ | `map[string]any` | | immutable map | [immutable](#immutable-values) map | - | | undefined | [undefined](#undefined-values) value | - | | function | [function](#function-values) value | - | | _user-defined_ | value of [user-defined types](https://github.com/d5/tengo/blob/master/docs/objects.md) | - | ### Error Values In Tengo, an error can be represented using "error" typed values. An error value is created using `error` expression, and, it must have an underlying value. The underlying value of an error value can be access using `.value` selector. ```golang err1 := error("oops") // error with string value err2 := error(1+2+3) // error with int value if is_error(err1) { // 'is_error' builtin function err_val := err1.value // get underlying value } ``` ### Immutable Values In Tengo, basically all values (except for array and map) are immutable. ```golang s := "12345" s[1] = 'b' // illegal: String is immutable a := [1, 2, 3] a[1] = "two" // ok: a is now [1, "two", 3] ``` An array or map value can be made immutable using `immutable` expression. ```golang b := immutable([1, 2, 3]) b[1] = "foo" // illegal: 'b' references to an immutable array. ``` Note that re-assigning a new value to the variable has nothing to do with the value immutability. ```golang s := "abc" s = "foo" // ok a := immutable([1, 2, 3]) a = false // ok ``` Note that, if you copy (using `copy` builtin function) an immutable value, it will return a "mutable" copy. Also, immutability is not applied to the individual elements of the array or map value, unless they are explicitly made immutable. ```golang a := immutable({b: 4, c: [1, 2, 3]}) a.b = 5 // illegal a.c[1] = 5 // ok: because 'a.c' is not immutable a = immutable({b: 4, c: immutable([1, 2, 3])}) a.c[1] = 5 // illegal ``` ### Undefined Values In Tengo, an "undefined" value can be used to represent an unexpected or non-existing value: - A function that does not return a value explicitly considered to return `undefined` value. - Indexer or selector on composite value types may return `undefined` if the key or index does not exist. - Type conversion builtin functions without a default value will return `undefined` if conversion fails. ```golang a := func() { b := 4 }() // a == undefined b := [1, 2, 3][10] // b == undefined c := {a: "foo"}["b"] // c == undefined d := int("foo") // d == undefined ``` ### Array Values In Tengo, array is an ordered list of values of any types. Elements of an array can be accessed using indexer `[]`. ```golang [1, 2, 3][0] // == 1 [1, 2, 3][2] // == 3 [1, 2, 3][3] // == undefined ["foo", "bar", [1, 2, 3]] // ok: array with an array element ``` ### Map Values In Tengo, map is a set of key-value pairs where key is string and the value is of any value types. Value of a map can be accessed using indexer `[]` or selector '.' operators. ```golang m := { a: 1, b: false, c: "foo" } 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 ``` ### Function Values 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. ```golang my_func := func(arg1, arg2) { return arg1 + arg2 } adder := func(base) { return func(x) { return base + x } // capturing 'base' } add5 := adder(5) nine := add5(4) // == 9 ``` Unlike Go, Tengo does not have declarations. So the following code is illegal: ```golang func my_func(arg1, arg2) { // illegal return arg1 + arg2 } ``` Tengo also supports variadic functions/closures: ```golang variadic := func (a, b, ...c) { return [a, b, c] } variadic(1, 2, 3, 4) // [1, 2, [3, 4]] variadicClosure := func(a) { return func(b, ...c) { return [a, b, c] } } variadicClosure(1)(2, 3, 4) // [1, 2, [3, 4]] ``` Only the last parameter can be variadic. The following code is also illegal: ```golang // illegal, because a is variadic and is not the last parameter illegal := func(a..., b) { /*... */ } ``` When calling a function, the number of passing arguments must match that of function definition. ```golang f := func(a, b) {} f(1, 2, 3) // Runtime Error: wrong number of arguments: want=2, got=3 ``` Like Go, you can use ellipsis `...` to pass array-type value as its last parameter: ```golang f1 := func(a, b, c) { return a + b + c } f1([1, 2, 3]...) // => 6 f1(1, [2, 3]...) // => 6 f1(1, 2, [3]...) // => 6 f1([1, 2]...) // Runtime Error: wrong number of arguments: want=3, got=2 f2 := func(a, ...b) {} f2(1) // valid; a = 1, b = [] f2(1, 2) // valid; a = 1, b = [2] f2(1, 2, 3) // valid; a = 1, b = [2, 3] f2([1, 2, 3]...) // valid; a = 1, b = [2, 3] ``` ## Variables and Scopes A value can be assigned to a variable using assignment operator `:=` and `=`. - `:=` operator defines a new variable in the scope and assigns a value. - `=` operator assigns a new value to an existing variable in the scope. Variables are defined either in global scope (defined outside function) or in local scope (defined inside function). ```golang a := "foo" // define 'a' in global scope func() { // function scope A b := 52 // define 'b' in function scope A func() { // function scope B c := 19.84 // define 'c' in function scope B a = "bee" // ok: assign new value to 'a' from global scope b = 20 // ok: assign new value to 'b' from function scope A b := true // ok: define new 'b' in function scope B // (shadowing 'b' from function scope A) } a = "bar" // ok: assigne new value to 'a' from global scope b = 10 // ok: assigne new value to 'b' a := -100 // ok: define new 'a' in function scope A // (shadowing 'a' from global scope) c = -9.1 // illegal: 'c' is not defined b := [1, 2] // illegal: 'b' is already defined in the same scope } b = 25 // illegal: 'b' is not defined a := {d: 2} // illegal: 'a' is already defined in the same scope ``` Unlike Go, a variable can be assigned a value of different types. ```golang a := 123 // assigned 'int' a = "123" // re-assigned 'string' a = [1, 2, 3] // re-assigned 'array' ``` ## Type Conversions Although the type is not directly specified in Tengo, one can use type conversion [builtin functions](https://github.com/d5/tengo/blob/master/docs/builtins.md) to convert between value types. ```golang s1 := string(1984) // "1984" i2 := int("-999") // -999 f3 := float(-51) // -51.0 b4 := bool(1) // true c5 := char("X") // 'X' ``` See [Operators](https://github.com/d5/tengo/blob/master/docs/operators.md) for more details on type coercions. ## Operators ### Unary Operators | Operator | Usage | Types | | :---: | :---: | :---: | | `+` | same as `0 + x` | int, float | | `-` | same as `0 - x` | int, float | | `!` | logical NOT | all types* | | `^` | bitwise complement | int | _In Tengo, all values can be either [truthy or falsy](https://github.com/d5/tengo/blob/d5-patch-1/docs/runtime-types.md#objectisfalsy)._ ### Binary Operators | Operator | Usage | Types | | :---: | :---: | :---: | | `==` | equal | all types | | `!=` | not equal | all types | | `&&` | logical AND | all types | | `\|\|` | logical OR | all types | | `+` | add/concat | int, float, string, char, time, array | | `-` | subtract | int, float, char, time | | `*` | multiply | int, float | | `/` | divide | int, float | | `&` | bitwise AND | int | | `\|` | bitwise OR | int | | `^` | bitwise XOR | int | | `&^` | bitclear (AND NOT) | int | | `<<` | shift left | int | | `>>` | shift right | int | | `<` | less than | int, float, char, time, string | | `<=` | less than or equal to | int, float, char, time, string | | `>` | greater than | int, float, char, time, string | | `>=` | greater than or equal to | int, float, char, time, string | _See [Operators](https://github.com/d5/tengo/blob/master/docs/operators.md) for more details._ ### Ternary Operators Tengo has a ternary conditional operator `(condition expression) ? (true expression) : (false expression)`. ```golang a := true ? 1 : -1 // a == 1 min := func(a, b) { return a < b ? a : b } b := min(5, 10) // b == 5 ``` ### Assignment and Increment Operators | Operator | Usage | | :---: | :---: | | `+=` | `(lhs) = (lhs) + (rhs)` | | `-=` | `(lhs) = (lhs) - (rhs)` | | `*=` | `(lhs) = (lhs) * (rhs)` | | `/=` | `(lhs) = (lhs) / (rhs)` | | `%=` | `(lhs) = (lhs) % (rhs)` | | `&=` | `(lhs) = (lhs) & (rhs)` | | `\|=` | `(lhs) = (lhs) \| (rhs)` | | `&^=` | `(lhs) = (lhs) &^ (rhs)` | | `^=` | `(lhs) = (lhs) ^ (rhs)` | | `<<=` | `(lhs) = (lhs) << (rhs)` | | `>>=` | `(lhs) = (lhs) >> (rhs)` | | `++` | `(lhs) = (lhs) + 1` | | `--` | `(lhs) = (lhs) - 1` | ### Operator Precedences Unary operators have the highest precedence, and, ternary operator has the lowest precedence. There are five precedence levels for binary operators. Multiplication operators bind strongest, followed by addition operators, comparison operators, `&&` (logical AND), and finally `||` (logical OR): | Precedence | Operator | | :---: | :---: | | 5 | `*` `/` `%` `<<` `>>` `&` `&^` | | 4 | `+` `-` `\|` `^` | | 3 | `==` `!=` `<` `<=` `>` `>=` | | 2 | `&&` | | 1 | `\|\|` | Like Go, `++` and `--` operators form statements, not expressions, they fall outside the operator hierarchy. ### Selector and Indexer One can use selector (`.`) and indexer (`[]`) operators to read or write elements of composite types (array, map, string, bytes). ```golang ["one", "two", "three"][1] // == "two" m := { a: 1, b: [2, 3, 4], c: func() { return 10 } } m.a // == 1 m["b"][1] // == 3 m.c() // == 10 m.x = 5 // add 'x' to map 'm' m["b"][5] // == undefined m["b"][5].d // == undefined m.b[5] = 0 // == undefined m.x.y.z // == undefined ``` Like Go, one can use slice operator `[:]` for sequence value types such as array, string, bytes. ```golang a := [1, 2, 3, 4, 5][1:3] // == [2, 3] b := [1, 2, 3, 4, 5][3:] // == [4, 5] c := [1, 2, 3, 4, 5][:3] // == [1, 2, 3] d := "hello world"[2:10] // == "llo worl" c := [1, 2, 3, 4, 5][-1:10] // == [1, 2, 3, 4, 5] ``` **Note: Keywords cannot be used as selectors.** ```golang a := {in: true} // Parse Error: expected map key, found 'in' a.func = "" // Parse Error: expected selector, found 'func' ``` Use double quotes and indexer to use keywords with maps. ```golang a := {"in": true} a["func"] = "" ``` ## Statements ### If Statement "If" statement is very similar to Go. ```golang if a < 0 { // execute if 'a' is negative } else if a == 0 { // execute if 'a' is zero } else { // execute if 'a' is positive } ``` Like Go, the condition expression may be preceded by a simple statement, which executes before the expression is evaluated. ```golang if a := foo(); a < 0 { // execute if 'a' is negative } ``` ### For Statement "For" statement is very similar to Go. ```golang // for (init); (condition); (post) {} for a:=0; a<10; a++ { // ... } // for (condition) {} for a < 10 { // ... } // for {} for { // ... } ``` ### For-In Statement "For-In" statement is new in Tengo. It's similar to Go's `for range` statement. "For-In" statement can iterate any iterable value types (array, map, bytes, string, undefined). ```golang for v in [1, 2, 3] { // array: element // 'v' is value } for i, v in [1, 2, 3] { // array: index and element // 'i' is index // 'v' is value } for k, v in {k1: 1, k2: 2} { // map: key and value // 'k' is key // 'v' is value } ``` ## Modules Module is the basic compilation unit in Tengo. A module can import another module using `import` expression. Main module: ```golang sum := import("./sum") // load module from a local file fmt.print(sum(10)) // module function ``` Another module in `sum.tengo` file: ```golang base := 5 export func(x) { return x + base } ``` By default, `import` solves the missing extension name of a module file as "`.tengo`"[^note]. Thus, `sum := import("./sum")` is equivalent to `sum := import("./sum.tengo")`. [^note]: If using Tengo as a library in Go, the file extension name "`.tengo`" can be customized. In that case, use the `SetImportFileExt` function of the `Compiler` type. See the [Go reference](https://pkg.go.dev/github.com/d5/tengo/v2) for details. In Tengo, modules are very similar to functions. - `import` expression loads the module code and execute it like a function. - Module should return a value using `export` statement. - Module can return `export` a value of any types: int, map, function, etc. - `export` in a module is like `return` in a function: it stops execution and return a value to the importing code. - `export`-ed values are always immutable. - If the module does not have any `export` statement, `import` expression simply returns `undefined`. _(Just like the function that has no `return`.)_ - Note that `export` statement is completely ignored and not evaluated if the code is executed as a main module. Also, you can use `import` expression to load the [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md) as well. ```golang math := import("math") a := math.abs(-19.84) // == 19.84 ``` ## Comments Like Go, Tengo supports line comments (`//...`) and block comments (`/* ... */`). ```golang /* multi-line block comments */ a := 5 // line comments ``` ## Differences from Go Unlike Go, Tengo does not have the following: - Declarations - Imaginary values - Structs - Pointers - Channels - Goroutines - Tuple assignment - Variable parameters - Switch statement - Goto statement - Defer statement - Panic - Type assertion ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/errors.go0000644000175000017500000000436414607001502017014 0ustar00maythammaythampackage tengo import ( "errors" "fmt" ) var ( // ErrStackOverflow is a stack overflow error. ErrStackOverflow = errors.New("stack overflow") // ErrObjectAllocLimit is an objects allocation limit error. ErrObjectAllocLimit = errors.New("object allocation limit exceeded") // ErrIndexOutOfBounds is an error where a given index is out of the // bounds. ErrIndexOutOfBounds = errors.New("index out of bounds") // ErrInvalidIndexType represents an invalid index type. ErrInvalidIndexType = errors.New("invalid index type") // ErrInvalidIndexValueType represents an invalid index value type. ErrInvalidIndexValueType = errors.New("invalid index value type") // ErrInvalidIndexOnError represents an invalid index on error. ErrInvalidIndexOnError = errors.New("invalid index on error") // ErrInvalidOperator represents an error for invalid operator usage. ErrInvalidOperator = errors.New("invalid operator") // ErrWrongNumArguments represents a wrong number of arguments error. ErrWrongNumArguments = errors.New("wrong number of arguments") // ErrBytesLimit represents an error where the size of bytes value exceeds // the limit. ErrBytesLimit = errors.New("exceeding bytes size limit") // ErrStringLimit represents an error where the size of string value // exceeds the limit. ErrStringLimit = errors.New("exceeding string size limit") // ErrNotIndexable is an error where an Object is not indexable. ErrNotIndexable = errors.New("not indexable") // ErrNotIndexAssignable is an error where an Object is not index // assignable. ErrNotIndexAssignable = errors.New("not index-assignable") // ErrNotImplemented is an error where an Object has not implemented a // required method. ErrNotImplemented = errors.New("not implemented") // ErrInvalidRangeStep is an error where the step parameter is less than or equal to 0 when using builtin range function. ErrInvalidRangeStep = errors.New("range step must be greater than 0") ) // ErrInvalidArgumentType represents an invalid argument value type error. type ErrInvalidArgumentType struct { Name string Expected string Found string } func (e ErrInvalidArgumentType) Error() string { return fmt.Sprintf("invalid type for argument '%s': expected %s, found %s", e.Name, e.Expected, e.Found) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/eval.go0000644000175000017500000000154114607001502016421 0ustar00maythammaythampackage tengo import ( "context" "fmt" "strings" ) // Eval compiles and executes given expr with params, and returns an // evaluated value. expr must be an expression. Otherwise it will fail to // compile. Expression must not use or define variable "__res__" as it's // reserved for the internal usage. func Eval( ctx context.Context, expr string, params map[string]interface{}, ) (interface{}, error) { expr = strings.TrimSpace(expr) if expr == "" { return nil, fmt.Errorf("empty expression") } script := NewScript([]byte(fmt.Sprintf("__res__ := (%s)", expr))) for pk, pv := range params { err := script.Add(pk, pv) if err != nil { return nil, fmt.Errorf("script add: %w", err) } } compiled, err := script.RunContext(ctx) if err != nil { return nil, fmt.Errorf("script run: %w", err) } return compiled.Get("__res__").Value(), nil } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/eval_test.go0000644000175000017500000000161414607001502017461 0ustar00maythammaythampackage tengo_test import ( "context" "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" ) func TestEval(t *testing.T) { eval := func( expr string, params map[string]interface{}, expected interface{}, ) { ctx := context.Background() actual, err := tengo.Eval(ctx, expr, params) require.NoError(t, err) require.Equal(t, expected, actual) } eval(`undefined`, nil, nil) eval(`1`, nil, int64(1)) eval(`19 + 23`, nil, int64(42)) eval(`"foo bar"`, nil, "foo bar") eval(`[1, 2, 3][1]`, nil, int64(2)) eval( `5 + p`, map[string]interface{}{ "p": 7, }, int64(12), ) eval( `"seven is " + p`, map[string]interface{}{ "p": 7, }, "seven is 7", ) eval( `"" + a + b`, map[string]interface{}{ "a": 7, "b": " is seven", }, "7 is seven", ) eval( `a ? "success" : "fail"`, map[string]interface{}{ "a": 1, }, "success", ) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/example_test.go0000644000175000017500000000124214607001502020162 0ustar00maythammaythampackage tengo_test import ( "context" "fmt" "github.com/d5/tengo/v2" ) func Example() { // Tengo script code src := ` each := func(seq, fn) { for x in seq { fn(x) } } sum := 0 mul := 1 each([a, b, c, d], func(x) { sum += x mul *= x })` // create a new Script instance script := tengo.NewScript([]byte(src)) // set values _ = script.Add("a", 1) _ = script.Add("b", 9) _ = script.Add("c", 8) _ = script.Add("d", 4) // run the script compiled, err := script.RunContext(context.Background()) if err != nil { panic(err) } // retrieve values sum := compiled.Get("sum") mul := compiled.Get("mul") fmt.Println(sum, mul) // Output: // 22 288 } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/examples/0000755000175000017500000000000014607001502016760 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/examples/interoperability/0000755000175000017500000000000014607001502022345 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/examples/interoperability/main.go0000644000175000017500000001517214607001502023626 0ustar00maythammaytham/*An example to demonstrate an alternative way to run tengo functions from go. */ package main import ( "container/list" "context" "errors" "fmt" "math/rand" "sync" "time" "github.com/d5/tengo/v2" ) // CallArgs holds function name to be executed and its required parameters with // a channel to listen result of function. type CallArgs struct { Func string Params []tengo.Object Result chan<- tengo.Object } // NewGoProxy creates GoProxy object. func NewGoProxy(ctx context.Context) *GoProxy { mod := new(GoProxy) mod.ctx = ctx 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}, } mod.tasks = list.New() return mod } // GoProxy is a builtin tengo module to register tengo functions and run them. type GoProxy struct { tengo.ObjectImpl ctx context.Context moduleMap map[string]tengo.Object callbacks map[string]tengo.Object callChan chan *CallArgs tasks *list.List mtx sync.Mutex } // TypeName returns type name. func (mod *GoProxy) TypeName() string { return "GoProxy" } func (mod *GoProxy) String() string { m := tengo.ImmutableMap{Value: mod.moduleMap} return m.String() } // ModuleMap returns a map to add a builtin tengo module. func (mod *GoProxy) ModuleMap() map[string]tengo.Object { return mod.moduleMap } // CallChan returns call channel which expects arguments to run a tengo // function. func (mod *GoProxy) CallChan() chan<- *CallArgs { return mod.callChan } func (mod *GoProxy) next(args ...tengo.Object) (tengo.Object, error) { mod.mtx.Lock() defer mod.mtx.Unlock() select { case <-mod.ctx.Done(): return tengo.FalseValue, nil case args := <-mod.callChan: if args != nil { mod.tasks.PushBack(args) } return tengo.TrueValue, nil } } func (mod *GoProxy) register(args ...tengo.Object) (tengo.Object, error) { if len(args) == 0 { return nil, tengo.ErrWrongNumArguments } mod.mtx.Lock() defer mod.mtx.Unlock() switch v := args[0].(type) { case *tengo.Map: mod.callbacks = v.Value case *tengo.ImmutableMap: mod.callbacks = v.Value default: return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "map", Found: args[0].TypeName(), } } return tengo.UndefinedValue, nil } func (mod *GoProxy) args(args ...tengo.Object) (tengo.Object, error) { mod.mtx.Lock() defer mod.mtx.Unlock() if mod.tasks.Len() == 0 { return tengo.UndefinedValue, nil } el := mod.tasks.Front() callArgs, ok := el.Value.(*CallArgs) if !ok || callArgs == nil { return nil, errors.New("invalid call arguments") } mod.tasks.Remove(el) f, ok := mod.callbacks[callArgs.Func] if !ok { return tengo.UndefinedValue, nil } compiledFunc, ok := f.(*tengo.CompiledFunction) if !ok { return tengo.UndefinedValue, nil } params := callArgs.Params if params == nil { params = make([]tengo.Object, 0) } // callable.VarArgs implementation is omitted. return &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "result": &tengo.UserFunction{ Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) > 0 { callArgs.Result <- args[0] return tengo.UndefinedValue, nil } callArgs.Result <- &tengo.Error{ Value: &tengo.String{ Value: tengo.ErrWrongNumArguments.Error()}, } return tengo.UndefinedValue, nil }}, "num_params": &tengo.Int{Value: int64(compiledFunc.NumParameters)}, "callable": compiledFunc, "params": &tengo.Array{Value: params}, }, }, nil } // ProxySource is a tengo script to handle bidirectional arguments flow between // go and pure tengo functions. Note: you should add more if conditions for // different number of parameters. // TODO: handle variadic functions. var ProxySource = ` export func(args) { if is_undefined(args) { return } callable := args.callable if is_undefined(callable) { 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) } ` func main() { src := ` // goproxy and proxy must be imported. goproxy := import("goproxy") proxy := import("proxy") global := 0 callbacks := { sum: func(a, b) { return a + b }, multiply: func(a, b) { return a * b }, increment: func() { global++ return global } } // Register callbacks to call them in goproxy loop. goproxy.register(callbacks) // goproxy loop waits for new call requests and run them with the help of // "proxy" source module. Cancelling the context breaks the loop. for goproxy.next() { proxy(goproxy.args()) } ` // 5 seconds context timeout is enough for an example. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() script := tengo.NewScript([]byte(src)) moduleMap := tengo.NewModuleMap() goproxy := NewGoProxy(ctx) // register modules moduleMap.AddBuiltinModule("goproxy", goproxy.ModuleMap()) moduleMap.AddSourceModule("proxy", []byte(ProxySource)) script.SetImports(moduleMap) compiled, err := script.Compile() if err != nil { panic(err) } // call "sum", "multiply", "increment" functions from tengo in a new goroutine go func() { callChan := goproxy.CallChan() result := make(chan tengo.Object, 1) // TODO: check tengo error from result channel. loop: for { select { case <-ctx.Done(): break loop default: } 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}, &tengo.Int{Value: i2}}, Result: result, } v := <-result fmt.Printf("%d + %d = %v\n", i1, i2, v) 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}, &tengo.Int{Value: i2}}, Result: result, } v = <-result fmt.Printf("%d * %d = %v\n", i1, i2, v) fmt.Println("Calling tengo increment function") callChan <- &CallArgs{Func: "increment", Result: result} v = <-result fmt.Printf("increment = %v\n", v) time.Sleep(1 * time.Second) } }() if err := compiled.RunContext(ctx); err != nil { fmt.Println(err) } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/formatter.go0000644000175000017500000006765414607001502017516 0ustar00maythammaythampackage tengo import ( "strconv" "sync" "unicode/utf8" ) // Strings for use with fmtbuf.WriteString. This is less overhead than using // fmtbuf.Write with byte arrays. const ( commaSpaceString = ", " nilParenString = "(nil)" percentBangString = "%!" missingString = "(MISSING)" badIndexString = "(BADINDEX)" extraString = "%!(EXTRA " badWidthString = "%!(BADWIDTH)" badPrecString = "%!(BADPREC)" noVerbString = "%!(NOVERB)" ) const ( ldigits = "0123456789abcdefx" udigits = "0123456789ABCDEFX" ) const ( signed = true unsigned = false ) // flags placed in a separate struct for easy clearing. type fmtFlags struct { widPresent bool precPresent bool minus bool plus bool sharp bool space bool zero bool // For the formats %+v %#v, we set the plusV/sharpV flags // and clear the plus/sharp flags since %+v and %#v are in effect // different, flagless formats set at the top level. plusV bool sharpV bool // error-related flags. inDetail bool needNewline bool needColon bool } // A formatter is the raw formatter used by Printf etc. // It prints into a fmtbuf that must be set up separately. type formatter struct { buf *fmtbuf fmtFlags wid int // width prec int // precision // intbuf is large enough to store %b of an int64 with a sign and // avoids padding at the end of the struct on 32 bit architectures. intbuf [68]byte } func (f *formatter) clearFlags() { f.fmtFlags = fmtFlags{} } func (f *formatter) init(buf *fmtbuf) { f.buf = buf f.clearFlags() } // writePadding generates n bytes of padding. func (f *formatter) writePadding(n int) { if n <= 0 { // No padding bytes needed. return } buf := *f.buf oldLen := len(buf) newLen := oldLen + n if newLen > MaxStringLen { panic(ErrStringLimit) } // Make enough room for padding. if newLen > cap(buf) { buf = make(fmtbuf, cap(buf)*2+n) copy(buf, *f.buf) } // Decide which byte the padding should be filled with. padByte := byte(' ') if f.zero { padByte = byte('0') } // Fill padding with padByte. padding := buf[oldLen:newLen] for i := range padding { padding[i] = padByte } *f.buf = buf[:newLen] } // pad appends b to f.buf, padded on left (!f.minus) or right (f.minus). func (f *formatter) pad(b []byte) { if !f.widPresent || f.wid == 0 { f.buf.Write(b) return } width := f.wid - utf8.RuneCount(b) if !f.minus { // left padding f.writePadding(width) f.buf.Write(b) } else { // right padding f.buf.Write(b) f.writePadding(width) } } // padString appends s to f.buf, padded on left (!f.minus) or right (f.minus). func (f *formatter) padString(s string) { if !f.widPresent || f.wid == 0 { f.buf.WriteString(s) return } width := f.wid - utf8.RuneCountInString(s) if !f.minus { // left padding f.writePadding(width) f.buf.WriteString(s) } else { // right padding f.buf.WriteString(s) f.writePadding(width) } } // fmtBoolean formats a boolean. func (f *formatter) fmtBoolean(v bool) { if v { f.padString("true") } else { f.padString("false") } } // fmtUnicode formats a uint64 as "U+0078" or with f.sharp set as "U+0078 'x'". func (f *formatter) fmtUnicode(u uint64) { buf := f.intbuf[0:] // With default precision set the maximum needed buf length is 18 // for formatting -1 with %#U ("U+FFFFFFFFFFFFFFFF") which fits // into the already allocated intbuf with a capacity of 68 bytes. prec := 4 if f.precPresent && f.prec > 4 { prec = f.prec // Compute space needed for "U+" , number, " '", character, "'". width := 2 + prec + 2 + utf8.UTFMax + 1 if width > len(buf) { buf = make([]byte, width) } } // Format into buf, ending at buf[i]. Formatting numbers is easier // right-to-left. i := len(buf) // For %#U we want to add a space and a quoted character at the end of // the fmtbuf. if f.sharp && u <= utf8.MaxRune && strconv.IsPrint(rune(u)) { i-- buf[i] = '\'' i -= utf8.RuneLen(rune(u)) utf8.EncodeRune(buf[i:], rune(u)) i-- buf[i] = '\'' i-- buf[i] = ' ' } // Format the Unicode code point u as a hexadecimal number. for u >= 16 { i-- buf[i] = udigits[u&0xF] prec-- u >>= 4 } i-- buf[i] = udigits[u] prec-- // Add zeros in front of the number until requested precision is reached. for prec > 0 { i-- buf[i] = '0' prec-- } // Add a leading "U+". i-- buf[i] = '+' i-- buf[i] = 'U' oldZero := f.zero f.zero = false f.pad(buf[i:]) f.zero = oldZero } // fmtInteger formats signed and unsigned integers. func (f *formatter) fmtInteger( u uint64, base int, isSigned bool, verb rune, digits string, ) { negative := isSigned && int64(u) < 0 if negative { u = -u } buf := f.intbuf[0:] // The already allocated f.intbuf with a capacity of 68 bytes // is large enough for integer formatting when no precision or width is set. if f.widPresent || f.precPresent { // Account 3 extra bytes for possible addition of a sign and "0x". width := 3 + f.wid + f.prec // wid and prec are always positive. if width > len(buf) { // We're going to need a bigger boat. buf = make([]byte, width) } } // Two ways to ask for extra leading zero digits: %.3d or %03d. // If both are specified the f.zero flag is ignored and // padding with spaces is used instead. prec := 0 if f.precPresent { prec = f.prec // Precision of 0 and value of 0 means "print nothing" but padding. if prec == 0 && u == 0 { oldZero := f.zero f.zero = false f.writePadding(f.wid) f.zero = oldZero return } } else if f.zero && f.widPresent { prec = f.wid if negative || f.plus || f.space { prec-- // leave room for sign } } // Because printing is easier right-to-left: format u into buf, ending at // buf[i]. We could make things marginally faster by splitting the 32-bit // case out into a separate block but it's not worth the duplication, so // u has 64 bits. i := len(buf) // Use constants for the division and modulo for more efficient code. // Switch cases ordered by popularity. switch base { case 10: for u >= 10 { i-- next := u / 10 buf[i] = byte('0' + u - next*10) u = next } case 16: for u >= 16 { i-- buf[i] = digits[u&0xF] u >>= 4 } case 8: for u >= 8 { i-- buf[i] = byte('0' + u&7) u >>= 3 } case 2: for u >= 2 { i-- buf[i] = byte('0' + u&1) u >>= 1 } default: panic("fmt: unknown base; can't happen") } i-- buf[i] = digits[u] for i > 0 && prec > len(buf)-i { i-- buf[i] = '0' } // Various prefixes: 0x, -, etc. if f.sharp { switch base { case 2: // Add a leading 0b. i-- buf[i] = 'b' i-- buf[i] = '0' case 8: if buf[i] != '0' { i-- buf[i] = '0' } case 16: // Add a leading 0x or 0X. i-- buf[i] = digits[16] i-- buf[i] = '0' } } if verb == 'O' { i-- buf[i] = 'o' i-- buf[i] = '0' } if negative { i-- buf[i] = '-' } else if f.plus { i-- buf[i] = '+' } else if f.space { i-- buf[i] = ' ' } // Left padding with zeros has already been handled like precision earlier // or the f.zero flag is ignored due to an explicitly set precision. oldZero := f.zero f.zero = false f.pad(buf[i:]) f.zero = oldZero } // truncate truncates the string s to the specified precision, if present. func (f *formatter) truncateString(s string) string { if f.precPresent { n := f.prec for i := range s { n-- if n < 0 { return s[:i] } } } return s } // truncate truncates the byte slice b as a string of the specified precision, // if present. func (f *formatter) truncate(b []byte) []byte { if f.precPresent { n := f.prec for i := 0; i < len(b); { n-- if n < 0 { return b[:i] } wid := 1 if b[i] >= utf8.RuneSelf { _, wid = utf8.DecodeRune(b[i:]) } i += wid } } return b } // fmtS formats a string. func (f *formatter) fmtS(s string) { s = f.truncateString(s) f.padString(s) } // fmtBs formats the byte slice b as if it was formatted as string with fmtS. func (f *formatter) fmtBs(b []byte) { b = f.truncate(b) f.pad(b) } // fmtSbx formats a string or byte slice as a hexadecimal encoding of its bytes. func (f *formatter) fmtSbx(s string, b []byte, digits string) { length := len(b) if b == nil { // No byte slice present. Assume string s should be encoded. length = len(s) } // Set length to not process more bytes than the precision demands. if f.precPresent && f.prec < length { length = f.prec } // Compute width of the encoding taking into account the f.sharp and // f.space flag. width := 2 * length if width > 0 { if f.space { // Each element encoded by two hexadecimals will get a leading // 0x or 0X. if f.sharp { width *= 2 } // Elements will be separated by a space. width += length - 1 } else if f.sharp { // Only a leading 0x or 0X will be added for the whole string. width += 2 } } else { // The byte slice or string that should be encoded is empty. if f.widPresent { f.writePadding(f.wid) } return } // Handle padding to the left. if f.widPresent && f.wid > width && !f.minus { f.writePadding(f.wid - width) } // Write the encoding directly into the output fmtbuf. buf := *f.buf if f.sharp { // Add leading 0x or 0X. buf = append(buf, '0', digits[16]) } var c byte for i := 0; i < length; i++ { if f.space && i > 0 { // Separate elements with a space. buf = append(buf, ' ') if f.sharp { // Add leading 0x or 0X for each element. buf = append(buf, '0', digits[16]) } } if b != nil { c = b[i] // Take a byte from the input byte slice. } else { c = s[i] // Take a byte from the input string. } // Encode each byte as two hexadecimal digits. buf = append(buf, digits[c>>4], digits[c&0xF]) } *f.buf = buf // Handle padding to the right. if f.widPresent && f.wid > width && f.minus { f.writePadding(f.wid - width) } } // fmtSx formats a string as a hexadecimal encoding of its bytes. func (f *formatter) fmtSx(s, digits string) { f.fmtSbx(s, nil, digits) } // fmtBx formats a byte slice as a hexadecimal encoding of its bytes. func (f *formatter) fmtBx(b []byte, digits string) { f.fmtSbx("", b, digits) } // fmtQ formats a string as a double-quoted, escaped Go string constant. // If f.sharp is set a raw (backquoted) string may be returned instead // if the string does not contain any control characters other than tab. func (f *formatter) fmtQ(s string) { s = f.truncateString(s) if f.sharp && strconv.CanBackquote(s) { f.padString("`" + s + "`") return } buf := f.intbuf[:0] if f.plus { f.pad(strconv.AppendQuoteToASCII(buf, s)) } else { f.pad(strconv.AppendQuote(buf, s)) } } // fmtC formats an integer as a Unicode character. // If the character is not valid Unicode, it will print '\ufffd'. func (f *formatter) fmtC(c uint64) { r := rune(c) if c > utf8.MaxRune { r = utf8.RuneError } buf := f.intbuf[:0] w := utf8.EncodeRune(buf[:utf8.UTFMax], r) f.pad(buf[:w]) } // fmtQc formats an integer as a single-quoted, escaped Go character constant. // If the character is not valid Unicode, it will print '\ufffd'. func (f *formatter) fmtQc(c uint64) { r := rune(c) if c > utf8.MaxRune { r = utf8.RuneError } buf := f.intbuf[:0] if f.plus { f.pad(strconv.AppendQuoteRuneToASCII(buf, r)) } else { f.pad(strconv.AppendQuoteRune(buf, r)) } } // fmtFloat formats a float64. It assumes that verb is a valid format specifier // for strconv.AppendFloat and therefore fits into a byte. func (f *formatter) fmtFloat(v float64, size int, verb rune, prec int) { // Explicit precision in format specifier overrules default precision. if f.precPresent { prec = f.prec } // Format number, reserving space for leading + sign if needed. num := strconv.AppendFloat(f.intbuf[:1], v, byte(verb), prec, size) if num[1] == '-' || num[1] == '+' { num = num[1:] } else { num[0] = '+' } // f.space means to add a leading space instead of a "+" sign unless // the sign is explicitly asked for by f.plus. if f.space && num[0] == '+' && !f.plus { num[0] = ' ' } // Special handling for infinities and NaN, // which don't look like a number so shouldn't be padded with zeros. if num[1] == 'I' || num[1] == 'N' { oldZero := f.zero f.zero = false // Remove sign before NaN if not asked for. if num[1] == 'N' && !f.space && !f.plus { num = num[1:] } f.pad(num) f.zero = oldZero return } // The sharp flag forces printing a decimal point for non-binary formats // and retains trailing zeros, which we may need to restore. if f.sharp && verb != 'b' { digits := 0 switch verb { case 'v', 'g', 'G', 'x': digits = prec // If no precision is set explicitly use a precision of 6. if digits == -1 { digits = 6 } } // Buffer pre-allocated with enough room for // exponent notations of the form "e+123" or "p-1023". var tailBuf [6]byte tail := tailBuf[:0] hasDecimalPoint := false // Starting from i = 1 to skip sign at num[0]. for i := 1; i < len(num); i++ { switch num[i] { case '.': hasDecimalPoint = true case 'p', 'P': tail = append(tail, num[i:]...) num = num[:i] case 'e', 'E': if verb != 'x' && verb != 'X' { tail = append(tail, num[i:]...) num = num[:i] break } fallthrough default: digits-- } } if !hasDecimalPoint { num = append(num, '.') } for digits > 0 { num = append(num, '0') digits-- } num = append(num, tail...) } // We want a sign if asked for and if the sign is not positive. if f.plus || num[0] != '+' { // If we're zero padding to the left we want the sign before the // leading zeros. Achieve this by writing the sign out and then padding // the unsigned number. if f.zero && f.widPresent && f.wid > len(num) { f.buf.WriteSingleByte(num[0]) f.writePadding(f.wid - len(num)) f.buf.Write(num[1:]) return } f.pad(num) return } // No sign to show and the number is positive; just print the unsigned // number. f.pad(num[1:]) } // Use simple []byte instead of bytes.Buffer to avoid large dependency. type fmtbuf []byte func (b *fmtbuf) Write(p []byte) { if len(*b)+len(p) > MaxStringLen { panic(ErrStringLimit) } *b = append(*b, p...) } func (b *fmtbuf) WriteString(s string) { if len(*b)+len(s) > MaxStringLen { panic(ErrStringLimit) } *b = append(*b, s...) } func (b *fmtbuf) WriteSingleByte(c byte) { if len(*b) >= MaxStringLen { panic(ErrStringLimit) } *b = append(*b, c) } func (b *fmtbuf) WriteRune(r rune) { if len(*b)+utf8.RuneLen(r) > MaxStringLen { panic(ErrStringLimit) } if r < utf8.RuneSelf { *b = append(*b, byte(r)) return } b2 := *b n := len(b2) for n+utf8.UTFMax > cap(b2) { b2 = append(b2, 0) } w := utf8.EncodeRune(b2[n:n+utf8.UTFMax], r) *b = b2[:n+w] } // pp is used to store a printer's state and is reused with sync.Pool to avoid // allocations. type pp struct { buf fmtbuf // arg holds the current item. arg Object // fmt is used to format basic items such as integers or strings. fmt formatter // reordered records whether the format string used argument reordering. reordered bool // goodArgNum records whether the most recent reordering directive was // valid. goodArgNum bool // erroring is set when printing an error string to guard against calling // handleMethods. erroring bool } var ppFree = sync.Pool{ New: func() interface{} { return new(pp) }, } // newPrinter allocates a new pp struct or grabs a cached one. func newPrinter() *pp { p := ppFree.Get().(*pp) p.erroring = false p.fmt.init(&p.buf) return p } // free saves used pp structs in ppFree; avoids an allocation per invocation. func (p *pp) free() { // Proper usage of a sync.Pool requires each entry to have approximately // the same memory cost. To obtain this property when the stored type // contains a variably-sized fmtbuf, we add a hard limit on the maximum // fmtbuf to place back in the pool. // // See https://golang.org/issue/23199 if cap(p.buf) > 64<<10 { return } p.buf = p.buf[:0] p.arg = nil ppFree.Put(p) } func (p *pp) Width() (wid int, ok bool) { return p.fmt.wid, p.fmt.widPresent } func (p *pp) Precision() (prec int, ok bool) { return p.fmt.prec, p.fmt.precPresent } func (p *pp) Flag(b int) bool { switch b { case '-': return p.fmt.minus case '+': return p.fmt.plus || p.fmt.plusV case '#': return p.fmt.sharp || p.fmt.sharpV case ' ': return p.fmt.space case '0': return p.fmt.zero } return false } // Implement Write so we can call Fprintf on a pp (through State), for // recursive use in custom verbs. func (p *pp) Write(b []byte) (ret int, err error) { p.buf.Write(b) return len(b), nil } // Implement WriteString so that we can call io.WriteString // on a pp (through state), for efficiency. func (p *pp) WriteString(s string) (ret int, err error) { p.buf.WriteString(s) return len(s), nil } func (p *pp) WriteRune(r rune) (ret int, err error) { p.buf.WriteRune(r) return utf8.RuneLen(r), nil } func (p *pp) WriteSingleByte(c byte) (ret int, err error) { p.buf.WriteSingleByte(c) return 1, nil } // tooLarge reports whether the magnitude of the integer is // too large to be used as a formatting width or precision. func tooLarge(x int) bool { const max int = 1e6 return x > max || x < -max } // parsenum converts ASCII to integer. num is 0 (and isnum is false) if no // number present. func parsenum(s string, start, end int) (num int, isnum bool, newi int) { if start >= end { return 0, false, end } for newi = start; newi < end && '0' <= s[newi] && s[newi] <= '9'; newi++ { if tooLarge(num) { return 0, false, end // Overflow; crazy long number most likely. } num = num*10 + int(s[newi]-'0') isnum = true } return } func (p *pp) badVerb(verb rune) { p.erroring = true _, _ = p.WriteString(percentBangString) _, _ = p.WriteRune(verb) _, _ = p.WriteSingleByte('(') switch { case p.arg != nil: _, _ = p.WriteString(p.arg.String()) _, _ = p.WriteSingleByte('=') p.printArg(p.arg, 'v') default: _, _ = p.WriteString(UndefinedValue.String()) } _, _ = p.WriteSingleByte(')') p.erroring = false } func (p *pp) fmtBool(v bool, verb rune) { switch verb { case 't', 'v': p.fmt.fmtBoolean(v) default: p.badVerb(verb) } } // fmt0x64 formats a uint64 in hexadecimal and prefixes it with 0x or // not, as requested, by temporarily setting the sharp flag. func (p *pp) fmt0x64(v uint64, leading0x bool) { sharp := p.fmt.sharp p.fmt.sharp = leading0x p.fmt.fmtInteger(v, 16, unsigned, 'v', ldigits) p.fmt.sharp = sharp } // fmtInteger formats a signed or unsigned integer. func (p *pp) fmtInteger(v uint64, isSigned bool, verb rune) { switch verb { case 'v': if p.fmt.sharpV && !isSigned { p.fmt0x64(v, true) } else { p.fmt.fmtInteger(v, 10, isSigned, verb, ldigits) } case 'd': p.fmt.fmtInteger(v, 10, isSigned, verb, ldigits) case 'b': p.fmt.fmtInteger(v, 2, isSigned, verb, ldigits) case 'o', 'O': p.fmt.fmtInteger(v, 8, isSigned, verb, ldigits) case 'x': p.fmt.fmtInteger(v, 16, isSigned, verb, ldigits) case 'X': p.fmt.fmtInteger(v, 16, isSigned, verb, udigits) case 'c': p.fmt.fmtC(v) case 'q': if v <= utf8.MaxRune { p.fmt.fmtQc(v) } else { p.badVerb(verb) } case 'U': p.fmt.fmtUnicode(v) default: p.badVerb(verb) } } // fmtFloat formats a float. The default precision for each verb // is specified as last argument in the call to fmt_float. func (p *pp) fmtFloat(v float64, size int, verb rune) { switch verb { case 'v': p.fmt.fmtFloat(v, size, 'g', -1) case 'b', 'g', 'G', 'x', 'X': p.fmt.fmtFloat(v, size, verb, -1) case 'f', 'e', 'E': p.fmt.fmtFloat(v, size, verb, 6) case 'F': p.fmt.fmtFloat(v, size, 'f', 6) default: p.badVerb(verb) } } func (p *pp) fmtString(v string, verb rune) { switch verb { case 'v': if p.fmt.sharpV { p.fmt.fmtQ(v) } else { p.fmt.fmtS(v) } case 's': p.fmt.fmtS(v) case 'x': p.fmt.fmtSx(v, ldigits) case 'X': p.fmt.fmtSx(v, udigits) case 'q': p.fmt.fmtQ(v) default: p.badVerb(verb) } } func (p *pp) fmtBytes(v []byte, verb rune, typeString string) { switch verb { case 'v', 'd': if p.fmt.sharpV { _, _ = p.WriteString(typeString) if v == nil { _, _ = p.WriteString(nilParenString) return } _, _ = p.WriteSingleByte('{') for i, c := range v { if i > 0 { _, _ = p.WriteString(commaSpaceString) } p.fmt0x64(uint64(c), true) } _, _ = p.WriteSingleByte('}') } else { _, _ = p.WriteSingleByte('[') for i, c := range v { if i > 0 { _, _ = p.WriteSingleByte(' ') } p.fmt.fmtInteger(uint64(c), 10, unsigned, verb, ldigits) } _, _ = p.WriteSingleByte(']') } case 's': p.fmt.fmtBs(v) case 'x': p.fmt.fmtBx(v, ldigits) case 'X': p.fmt.fmtBx(v, udigits) case 'q': p.fmt.fmtQ(string(v)) } } func (p *pp) printArg(arg Object, verb rune) { p.arg = arg if arg == nil { arg = UndefinedValue } // Special processing considerations. // %T (the value's type) and %p (its address) are special; we always do // them first. switch verb { case 'T': p.fmt.fmtS(arg.TypeName()) return case 'v': p.fmt.fmtS(arg.String()) return } // Some types can be done without reflection. switch f := arg.(type) { case *Bool: p.fmtBool(!f.IsFalsy(), verb) case *Float: p.fmtFloat(f.Value, 64, verb) case *Int: p.fmtInteger(uint64(f.Value), signed, verb) case *String: p.fmtString(f.Value, verb) case *Bytes: p.fmtBytes(f.Value, verb, "[]byte") default: p.fmtString(f.String(), verb) } } // intFromArg gets the argNumth element of a. On return, isInt reports whether // the argument has integer type. func intFromArg(a []Object, argNum int) (num int, isInt bool, newArgNum int) { newArgNum = argNum if argNum < len(a) { var num64 int64 num64, isInt = ToInt64(a[argNum]) num = int(num64) newArgNum = argNum + 1 if tooLarge(num) { num = 0 isInt = false } } return } // parseArgNumber returns the value of the bracketed number, minus 1 // (explicit argument numbers are one-indexed but we want zero-indexed). // The opening bracket is known to be present at format[0]. // The returned values are the index, the number of bytes to consume // up to the closing paren, if present, and whether the number parsed // ok. The bytes to consume will be 1 if no closing paren is present. func parseArgNumber(format string) (index int, wid int, ok bool) { // There must be at least 3 bytes: [n]. if len(format) < 3 { return 0, 1, false } // Find closing bracket. for i := 1; i < len(format); i++ { if format[i] == ']' { width, ok, newi := parsenum(format, 1, i) if !ok || newi != i { return 0, i + 1, false } // arg numbers are one-indexed andskip paren. return width - 1, i + 1, true } } return 0, 1, false } // argNumber returns the next argument to evaluate, which is either the value // of the passed-in argNum or the value of the bracketed integer that begins // format[i:]. It also returns the new value of i, that is, the index of the // next byte of the format to process. func (p *pp) argNumber( argNum int, format string, i int, numArgs int, ) (newArgNum, newi int, found bool) { if len(format) <= i || format[i] != '[' { return argNum, i, false } p.reordered = true index, wid, ok := parseArgNumber(format[i:]) if ok && 0 <= index && index < numArgs { return index, i + wid, true } p.goodArgNum = false return argNum, i + wid, ok } func (p *pp) badArgNum(verb rune) { _, _ = p.WriteString(percentBangString) _, _ = p.WriteRune(verb) _, _ = p.WriteString(badIndexString) } func (p *pp) missingArg(verb rune) { _, _ = p.WriteString(percentBangString) _, _ = p.WriteRune(verb) _, _ = p.WriteString(missingString) } func (p *pp) doFormat(format string, a []Object) (err error) { defer func() { if r := recover(); r != nil { if e, ok := r.(error); ok && e == ErrStringLimit { err = e return } panic(r) } }() end := len(format) argNum := 0 // we process one argument per non-trivial format afterIndex := false // previous item in format was an index like [3]. p.reordered = false formatLoop: for i := 0; i < end; { p.goodArgNum = true lasti := i for i < end && format[i] != '%' { i++ } if i > lasti { _, _ = p.WriteString(format[lasti:i]) } if i >= end { // done processing format string break } // Process one verb i++ // Do we have flags? p.fmt.clearFlags() simpleFormat: for ; i < end; i++ { c := format[i] switch c { case '#': p.fmt.sharp = true case '0': // Only allow zero padding to the left. p.fmt.zero = !p.fmt.minus case '+': p.fmt.plus = true case '-': p.fmt.minus = true p.fmt.zero = false // Do not pad with zeros to the right. case ' ': p.fmt.space = true default: // Fast path for common case of ascii lower case simple verbs // without precision or width or argument indices. if 'a' <= c && c <= 'z' && argNum < len(a) { if c == 'v' { // Go syntax p.fmt.sharpV = p.fmt.sharp p.fmt.sharp = false // Struct-field syntax p.fmt.plusV = p.fmt.plus p.fmt.plus = false } p.printArg(a[argNum], rune(c)) argNum++ i++ continue formatLoop } // Format is more complex than simple flags and a verb or is // malformed. break simpleFormat } } // Do we have an explicit argument index? argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a)) // Do we have width? if i < end && format[i] == '*' { i++ p.fmt.wid, p.fmt.widPresent, argNum = intFromArg(a, argNum) if !p.fmt.widPresent { _, _ = p.WriteString(badWidthString) } // We have a negative width, so take its value and ensure // that the minus flag is set if p.fmt.wid < 0 { p.fmt.wid = -p.fmt.wid p.fmt.minus = true p.fmt.zero = false // Do not pad with zeros to the right. } afterIndex = false } else { p.fmt.wid, p.fmt.widPresent, i = parsenum(format, i, end) if afterIndex && p.fmt.widPresent { // "%[3]2d" p.goodArgNum = false } } // Do we have precision? if i+1 < end && format[i] == '.' { i++ if afterIndex { // "%[3].2d" p.goodArgNum = false } argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a)) if i < end && format[i] == '*' { i++ p.fmt.prec, p.fmt.precPresent, argNum = intFromArg(a, argNum) // Negative precision arguments don't make sense if p.fmt.prec < 0 { p.fmt.prec = 0 p.fmt.precPresent = false } if !p.fmt.precPresent { _, _ = p.WriteString(badPrecString) } afterIndex = false } else { p.fmt.prec, p.fmt.precPresent, i = parsenum(format, i, end) if !p.fmt.precPresent { p.fmt.prec = 0 p.fmt.precPresent = true } } } if !afterIndex { argNum, i, afterIndex = p.argNumber(argNum, format, i, len(a)) } if i >= end { _, _ = p.WriteString(noVerbString) break } verb, size := rune(format[i]), 1 if verb >= utf8.RuneSelf { verb, size = utf8.DecodeRuneInString(format[i:]) } i += size switch { case verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec. _, _ = p.WriteSingleByte('%') case !p.goodArgNum: p.badArgNum(verb) case argNum >= len(a): // No argument left over to print for the current verb. p.missingArg(verb) case verb == 'v': // Go syntax p.fmt.sharpV = p.fmt.sharp p.fmt.sharp = false // Struct-field syntax p.fmt.plusV = p.fmt.plus p.fmt.plus = false fallthrough default: p.printArg(a[argNum], verb) argNum++ } } // Check for extra arguments unless the call accessed the arguments // out of order, in which case it's too expensive to detect if they've all // been used and arguably OK if they're not. if !p.reordered && argNum < len(a) { p.fmt.clearFlags() _, _ = p.WriteString(extraString) for i, arg := range a[argNum:] { if i > 0 { _, _ = p.WriteString(commaSpaceString) } if arg == nil { _, _ = p.WriteString(UndefinedValue.String()) } else { _, _ = p.WriteString(arg.TypeName()) _, _ = p.WriteSingleByte('=') p.printArg(arg, 'v') } } _, _ = p.WriteSingleByte(')') } return nil } // Format is like fmt.Sprintf but using Objects. func Format(format string, a ...Object) (string, error) { p := newPrinter() err := p.doFormat(format, a) s := string(p.buf) p.free() return s, err } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/go.mod0000644000175000017500000000004714607001502016251 0ustar00maythammaythammodule github.com/d5/tengo/v2 go 1.13 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/go.sum0000644000175000017500000000000014607001502016263 0ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/instructions.go0000644000175000017500000000303214607001502020233 0ustar00maythammaythampackage tengo import ( "fmt" "github.com/d5/tengo/v2/parser" ) // MakeInstruction returns a bytecode for an opcode and the operands. func MakeInstruction(opcode parser.Opcode, operands ...int) []byte { numOperands := parser.OpcodeOperands[opcode] totalLen := 1 for _, w := range numOperands { totalLen += w } instruction := make([]byte, totalLen) instruction[0] = opcode offset := 1 for i, o := range operands { width := numOperands[i] switch width { case 1: instruction[offset] = byte(o) case 2: n := uint16(o) instruction[offset] = byte(n >> 8) instruction[offset+1] = byte(n) case 4: n := uint32(o) instruction[offset] = byte(n >> 24) instruction[offset+1] = byte(n >> 16) instruction[offset+2] = byte(n >> 8) instruction[offset+3] = byte(n) } offset += width } return instruction } // FormatInstructions returns string representation of bytecode instructions. func FormatInstructions(b []byte, posOffset int) []string { var out []string i := 0 for i < len(b) { numOperands := parser.OpcodeOperands[b[i]] operands, read := parser.ReadOperands(numOperands, b[i+1:]) switch len(numOperands) { case 0: out = append(out, fmt.Sprintf("%04d %-7s", posOffset+i, parser.OpcodeNames[b[i]])) case 1: out = append(out, fmt.Sprintf("%04d %-7s %-5d", posOffset+i, parser.OpcodeNames[b[i]], operands[0])) case 2: out = append(out, fmt.Sprintf("%04d %-7s %-5d %-5d", posOffset+i, parser.OpcodeNames[b[i]], operands[0], operands[1])) } i += 1 + read } return out } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/iterator.go0000644000175000017500000001101714607001502017322 0ustar00maythammaythampackage tengo // Iterator represents an iterator for underlying data type. type Iterator interface { Object // Next returns true if there are more elements to iterate. Next() bool // Key returns the key or index value of the current element. Key() Object // Value returns the value of the current element. Value() Object } // ArrayIterator is an iterator for an array. type ArrayIterator struct { ObjectImpl v []Object i int l int } // TypeName returns the name of the type. func (i *ArrayIterator) TypeName() string { return "array-iterator" } func (i *ArrayIterator) String() string { return "" } // IsFalsy returns true if the value of the type is falsy. func (i *ArrayIterator) IsFalsy() bool { return true } // Equals returns true if the value of the type is equal to the value of // another object. func (i *ArrayIterator) Equals(Object) bool { return false } // Copy returns a copy of the type. func (i *ArrayIterator) Copy() Object { return &ArrayIterator{v: i.v, i: i.i, l: i.l} } // Next returns true if there are more elements to iterate. func (i *ArrayIterator) Next() bool { i.i++ return i.i <= i.l } // Key returns the key or index value of the current element. func (i *ArrayIterator) Key() Object { return &Int{Value: int64(i.i - 1)} } // Value returns the value of the current element. func (i *ArrayIterator) Value() Object { return i.v[i.i-1] } // BytesIterator represents an iterator for a string. type BytesIterator struct { ObjectImpl v []byte i int l int } // TypeName returns the name of the type. func (i *BytesIterator) TypeName() string { return "bytes-iterator" } func (i *BytesIterator) String() string { return "" } // Equals returns true if the value of the type is equal to the value of // another object. func (i *BytesIterator) Equals(Object) bool { return false } // Copy returns a copy of the type. func (i *BytesIterator) Copy() Object { return &BytesIterator{v: i.v, i: i.i, l: i.l} } // Next returns true if there are more elements to iterate. func (i *BytesIterator) Next() bool { i.i++ return i.i <= i.l } // Key returns the key or index value of the current element. func (i *BytesIterator) Key() Object { return &Int{Value: int64(i.i - 1)} } // Value returns the value of the current element. func (i *BytesIterator) Value() Object { return &Int{Value: int64(i.v[i.i-1])} } // MapIterator represents an iterator for the map. type MapIterator struct { ObjectImpl v map[string]Object k []string i int l int } // TypeName returns the name of the type. func (i *MapIterator) TypeName() string { return "map-iterator" } func (i *MapIterator) String() string { return "" } // IsFalsy returns true if the value of the type is falsy. func (i *MapIterator) IsFalsy() bool { return true } // Equals returns true if the value of the type is equal to the value of // another object. func (i *MapIterator) Equals(Object) bool { return false } // Copy returns a copy of the type. func (i *MapIterator) Copy() Object { return &MapIterator{v: i.v, k: i.k, i: i.i, l: i.l} } // Next returns true if there are more elements to iterate. func (i *MapIterator) Next() bool { i.i++ return i.i <= i.l } // Key returns the key or index value of the current element. func (i *MapIterator) Key() Object { k := i.k[i.i-1] return &String{Value: k} } // Value returns the value of the current element. func (i *MapIterator) Value() Object { k := i.k[i.i-1] return i.v[k] } // StringIterator represents an iterator for a string. type StringIterator struct { ObjectImpl v []rune i int l int } // TypeName returns the name of the type. func (i *StringIterator) TypeName() string { return "string-iterator" } func (i *StringIterator) String() string { return "" } // IsFalsy returns true if the value of the type is falsy. func (i *StringIterator) IsFalsy() bool { return true } // Equals returns true if the value of the type is equal to the value of // another object. func (i *StringIterator) Equals(Object) bool { return false } // Copy returns a copy of the type. func (i *StringIterator) Copy() Object { return &StringIterator{v: i.v, i: i.i, l: i.l} } // Next returns true if there are more elements to iterate. func (i *StringIterator) Next() bool { i.i++ return i.i <= i.l } // Key returns the key or index value of the current element. func (i *StringIterator) Key() Object { return &Int{Value: int64(i.i - 1)} } // Value returns the value of the current element. func (i *StringIterator) Value() Object { return &Char{Value: i.v[i.i-1]} } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/modules.go0000644000175000017500000000472514607001502017151 0ustar00maythammaythampackage tengo // Importable interface represents importable module instance. type Importable interface { // Import should return either an Object or module source code ([]byte). Import(moduleName string) (interface{}, error) } // ModuleGetter enables implementing dynamic module loading. type ModuleGetter interface { Get(name string) Importable } // ModuleMap represents a set of named modules. Use NewModuleMap to create a // new module map. type ModuleMap struct { m map[string]Importable } // NewModuleMap creates a new module map. func NewModuleMap() *ModuleMap { return &ModuleMap{ m: make(map[string]Importable), } } // Add adds an import module. func (m *ModuleMap) Add(name string, module Importable) { m.m[name] = module } // AddBuiltinModule adds a builtin module. func (m *ModuleMap) AddBuiltinModule(name string, attrs map[string]Object) { m.m[name] = &BuiltinModule{Attrs: attrs} } // AddSourceModule adds a source module. func (m *ModuleMap) AddSourceModule(name string, src []byte) { m.m[name] = &SourceModule{Src: src} } // Remove removes a named module. func (m *ModuleMap) Remove(name string) { delete(m.m, name) } // Get returns an import module identified by name. It returns if the name is // not found. func (m *ModuleMap) Get(name string) Importable { return m.m[name] } // GetBuiltinModule returns a builtin module identified by name. It returns // if the name is not found or the module is not a builtin module. func (m *ModuleMap) GetBuiltinModule(name string) *BuiltinModule { mod, _ := m.m[name].(*BuiltinModule) return mod } // GetSourceModule returns a source module identified by name. It returns if // the name is not found or the module is not a source module. func (m *ModuleMap) GetSourceModule(name string) *SourceModule { mod, _ := m.m[name].(*SourceModule) return mod } // Copy creates a copy of the module map. func (m *ModuleMap) Copy() *ModuleMap { c := &ModuleMap{ m: make(map[string]Importable), } for name, mod := range m.m { c.m[name] = mod } return c } // Len returns the number of named modules. func (m *ModuleMap) Len() int { return len(m.m) } // AddMap adds named modules from another module map. func (m *ModuleMap) AddMap(o *ModuleMap) { for name, mod := range o.m { m.m[name] = mod } } // SourceModule is an importable module that's written in Tengo. type SourceModule struct { Src []byte } // Import returns a module source code. func (m *SourceModule) Import(_ string) (interface{}, error) { return m.Src, nil } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/objects.go0000644000175000017500000010641414607001502017130 0ustar00maythammaythampackage tengo import ( "bytes" "fmt" "math" "strconv" "strings" "time" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/token" ) var ( // TrueValue represents a true value. TrueValue Object = &Bool{value: true} // FalseValue represents a false value. FalseValue Object = &Bool{value: false} // UndefinedValue represents an undefined value. UndefinedValue Object = &Undefined{} ) // Object represents an object in the VM. type Object interface { // TypeName should return the name of the type. TypeName() string // String should return a string representation of the type's value. String() string // BinaryOp should return another object that is the result of a given // binary operator and a right-hand side object. If BinaryOp returns an // error, the VM will treat it as a run-time error. BinaryOp(op token.Token, rhs Object) (Object, error) // IsFalsy should return true if the value of the type should be considered // as falsy. IsFalsy() bool // Equals should return true if the value of the type should be considered // as equal to the value of another object. Equals(another Object) bool // Copy should return a copy of the type (and its value). Copy function // will be used for copy() builtin function which is expected to deep-copy // the values generally. Copy() Object // IndexGet should take an index Object and return a result Object or an // error for indexable objects. Indexable is an object that can take an // index and return an object. If error is returned, the runtime will treat // it as a run-time error and ignore returned value. If Object is not // indexable, ErrNotIndexable should be returned as error. If nil is // returned as value, it will be converted to UndefinedToken value by the // runtime. IndexGet(index Object) (value Object, err error) // IndexSet should take an index Object and a value Object for index // assignable objects. Index assignable is an object that can take an index // and a value on the left-hand side of the assignment statement. If Object // is not index assignable, ErrNotIndexAssignable should be returned as // error. If an error is returned, it will be treated as a run-time error. IndexSet(index, value Object) error // Iterate should return an Iterator for the type. Iterate() Iterator // CanIterate should return whether the Object can be Iterated. CanIterate() bool // 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) // CanCall should return whether the Object can be Called. CanCall() bool } // ObjectImpl represents a default Object Implementation. To defined a new // value type, one can embed ObjectImpl in their type declarations to avoid // implementing all non-significant methods. TypeName() and String() methods // still need to be implemented. type ObjectImpl struct { } // TypeName returns the name of the type. func (o *ObjectImpl) TypeName() string { panic(ErrNotImplemented) } func (o *ObjectImpl) String() string { panic(ErrNotImplemented) } // BinaryOp returns another object that is the result of a given binary // operator and a right-hand side object. func (o *ObjectImpl) BinaryOp(_ token.Token, _ Object) (Object, error) { return nil, ErrInvalidOperator } // Copy returns a copy of the type. func (o *ObjectImpl) Copy() Object { return nil } // IsFalsy returns true if the value of the type is falsy. func (o *ObjectImpl) IsFalsy() bool { return false } // Equals returns true if the value of the type is equal to the value of // another object. func (o *ObjectImpl) Equals(x Object) bool { return o == x } // IndexGet returns an element at a given index. func (o *ObjectImpl) IndexGet(_ Object) (res Object, err error) { return nil, ErrNotIndexable } // IndexSet sets an element at a given index. func (o *ObjectImpl) IndexSet(_, _ Object) (err error) { return ErrNotIndexAssignable } // Iterate returns an iterator. func (o *ObjectImpl) Iterate() Iterator { return nil } // CanIterate returns whether the Object can be Iterated. func (o *ObjectImpl) CanIterate() bool { return false } // 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) { return nil, nil } // CanCall returns whether the Object can be Called. func (o *ObjectImpl) CanCall() bool { return false } // Array represents an array of objects. type Array struct { ObjectImpl Value []Object } // TypeName returns the name of the type. func (o *Array) TypeName() string { return "array" } func (o *Array) String() string { var elements []string for _, e := range o.Value { elements = append(elements, e.String()) } return fmt.Sprintf("[%s]", strings.Join(elements, ", ")) } // BinaryOp returns another object that is the result of a given binary // operator and a right-hand side object. func (o *Array) BinaryOp(op token.Token, rhs Object) (Object, error) { if rhs, ok := rhs.(*Array); ok { switch op { case token.Add: if len(rhs.Value) == 0 { return o, nil } return &Array{Value: append(o.Value, rhs.Value...)}, nil } } return nil, ErrInvalidOperator } // Copy returns a copy of the type. func (o *Array) Copy() Object { var c []Object for _, elem := range o.Value { c = append(c, elem.Copy()) } return &Array{Value: c} } // IsFalsy returns true if the value of the type is falsy. func (o *Array) IsFalsy() bool { return len(o.Value) == 0 } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Array) Equals(x Object) bool { var xVal []Object switch x := x.(type) { case *Array: xVal = x.Value case *ImmutableArray: xVal = x.Value default: return false } if len(o.Value) != len(xVal) { return false } for i, e := range o.Value { if !e.Equals(xVal[i]) { return false } } return true } // IndexGet returns an element at a given index. func (o *Array) IndexGet(index Object) (res Object, err error) { intIdx, ok := index.(*Int) if !ok { err = ErrInvalidIndexType return } idxVal := int(intIdx.Value) if idxVal < 0 || idxVal >= len(o.Value) { res = UndefinedValue return } res = o.Value[idxVal] return } // IndexSet sets an element at a given index. func (o *Array) IndexSet(index, value Object) (err error) { intIdx, ok := ToInt(index) if !ok { err = ErrInvalidIndexType return } if intIdx < 0 || intIdx >= len(o.Value) { err = ErrIndexOutOfBounds return } o.Value[intIdx] = value return nil } // Iterate creates an array iterator. func (o *Array) Iterate() Iterator { return &ArrayIterator{ v: o.Value, l: len(o.Value), } } // CanIterate returns whether the Object can be Iterated. func (o *Array) CanIterate() bool { return true } // Bool represents a boolean value. type Bool struct { ObjectImpl // this is intentionally non-public to force using objects.TrueValue and // FalseValue always value bool } func (o *Bool) String() string { if o.value { return "true" } return "false" } // TypeName returns the name of the type. func (o *Bool) TypeName() string { return "bool" } // Copy returns a copy of the type. func (o *Bool) Copy() Object { return o } // IsFalsy returns true if the value of the type is falsy. func (o *Bool) IsFalsy() bool { return !o.value } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Bool) Equals(x Object) bool { return o == x } // GobDecode decodes bool value from input bytes. func (o *Bool) GobDecode(b []byte) (err error) { o.value = b[0] == 1 return } // GobEncode encodes bool values into bytes. func (o *Bool) GobEncode() (b []byte, err error) { if o.value { b = []byte{1} } else { b = []byte{0} } return } // BuiltinFunction represents a builtin function. type BuiltinFunction struct { ObjectImpl Name string Value CallableFunc } // TypeName returns the name of the type. func (o *BuiltinFunction) TypeName() string { return "builtin-function:" + o.Name } func (o *BuiltinFunction) String() string { return "" } // Copy returns a copy of the type. func (o *BuiltinFunction) Copy() Object { return &BuiltinFunction{Value: o.Value} } // Equals returns true if the value of the type is equal to the value of // another object. func (o *BuiltinFunction) Equals(_ Object) bool { return false } // Call executes a builtin function. func (o *BuiltinFunction) Call(args ...Object) (Object, error) { return o.Value(args...) } // CanCall returns whether the Object can be Called. func (o *BuiltinFunction) CanCall() bool { return true } // BuiltinModule is an importable module that's written in Go. type BuiltinModule struct { Attrs map[string]Object } // Import returns an immutable map for the module. func (m *BuiltinModule) Import(moduleName string) (interface{}, error) { return m.AsImmutableMap(moduleName), nil } // AsImmutableMap converts builtin module into an immutable map. func (m *BuiltinModule) AsImmutableMap(moduleName string) *ImmutableMap { attrs := make(map[string]Object, len(m.Attrs)) for k, v := range m.Attrs { attrs[k] = v.Copy() } attrs["__module_name__"] = &String{Value: moduleName} return &ImmutableMap{Value: attrs} } // Bytes represents a byte array. type Bytes struct { ObjectImpl Value []byte } func (o *Bytes) String() string { return string(o.Value) } // TypeName returns the name of the type. func (o *Bytes) TypeName() string { return "bytes" } // BinaryOp returns another object that is the result of a given binary // operator and a right-hand side object. func (o *Bytes) BinaryOp(op token.Token, rhs Object) (Object, error) { switch op { case token.Add: switch rhs := rhs.(type) { case *Bytes: if len(o.Value)+len(rhs.Value) > MaxBytesLen { return nil, ErrBytesLimit } return &Bytes{Value: append(o.Value, rhs.Value...)}, nil } } return nil, ErrInvalidOperator } // Copy returns a copy of the type. func (o *Bytes) Copy() Object { return &Bytes{Value: append([]byte{}, o.Value...)} } // IsFalsy returns true if the value of the type is falsy. func (o *Bytes) IsFalsy() bool { return len(o.Value) == 0 } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Bytes) Equals(x Object) bool { t, ok := x.(*Bytes) if !ok { return false } return bytes.Equal(o.Value, t.Value) } // IndexGet returns an element (as Int) at a given index. func (o *Bytes) IndexGet(index Object) (res Object, err error) { intIdx, ok := index.(*Int) if !ok { err = ErrInvalidIndexType return } idxVal := int(intIdx.Value) if idxVal < 0 || idxVal >= len(o.Value) { res = UndefinedValue return } res = &Int{Value: int64(o.Value[idxVal])} return } // Iterate creates a bytes iterator. func (o *Bytes) Iterate() Iterator { return &BytesIterator{ v: o.Value, l: len(o.Value), } } // CanIterate returns whether the Object can be Iterated. func (o *Bytes) CanIterate() bool { return true } // Char represents a character value. type Char struct { ObjectImpl Value rune } func (o *Char) String() string { return string(o.Value) } // TypeName returns the name of the type. func (o *Char) TypeName() string { return "char" } // BinaryOp returns another object that is the result of a given binary // operator and a right-hand side object. func (o *Char) BinaryOp(op token.Token, rhs Object) (Object, error) { switch rhs := rhs.(type) { case *Char: switch op { case token.Add: r := o.Value + rhs.Value if r == o.Value { return o, nil } return &Char{Value: r}, nil case token.Sub: r := o.Value - rhs.Value if r == o.Value { return o, nil } return &Char{Value: r}, nil case token.Less: if o.Value < rhs.Value { return TrueValue, nil } return FalseValue, nil case token.Greater: if o.Value > rhs.Value { return TrueValue, nil } return FalseValue, nil case token.LessEq: if o.Value <= rhs.Value { return TrueValue, nil } return FalseValue, nil case token.GreaterEq: if o.Value >= rhs.Value { return TrueValue, nil } return FalseValue, nil } case *Int: switch op { case token.Add: r := o.Value + rune(rhs.Value) if r == o.Value { return o, nil } return &Char{Value: r}, nil case token.Sub: r := o.Value - rune(rhs.Value) if r == o.Value { return o, nil } return &Char{Value: r}, nil case token.Less: if int64(o.Value) < rhs.Value { return TrueValue, nil } return FalseValue, nil case token.Greater: if int64(o.Value) > rhs.Value { return TrueValue, nil } return FalseValue, nil case token.LessEq: if int64(o.Value) <= rhs.Value { return TrueValue, nil } return FalseValue, nil case token.GreaterEq: if int64(o.Value) >= rhs.Value { return TrueValue, nil } return FalseValue, nil } } return nil, ErrInvalidOperator } // Copy returns a copy of the type. func (o *Char) Copy() Object { return &Char{Value: o.Value} } // IsFalsy returns true if the value of the type is falsy. func (o *Char) IsFalsy() bool { return o.Value == 0 } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Char) Equals(x Object) bool { t, ok := x.(*Char) if !ok { return false } return o.Value == t.Value } // 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 } // TypeName returns the name of the type. func (o *CompiledFunction) TypeName() string { return "compiled-function" } func (o *CompiledFunction) String() string { return "" } // 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 } } // Equals returns true if the value of the type is equal to the value of // another object. func (o *CompiledFunction) Equals(_ Object) bool { return false } // SourcePos returns the source position of the instruction at ip. func (o *CompiledFunction) SourcePos(ip int) parser.Pos { for ip >= 0 { if p, ok := o.SourceMap[ip]; ok { return p } ip-- } return parser.NoPos } // CanCall returns whether the Object can be Called. func (o *CompiledFunction) CanCall() bool { return true } // Error represents an error value. type Error struct { ObjectImpl Value Object } // TypeName returns the name of the type. func (o *Error) TypeName() string { return "error" } func (o *Error) String() string { if o.Value != nil { return fmt.Sprintf("error: %s", o.Value.String()) } return "error" } // IsFalsy returns true if the value of the type is falsy. func (o *Error) IsFalsy() bool { return true // error is always false. } // Copy returns a copy of the type. func (o *Error) Copy() Object { return &Error{Value: o.Value.Copy()} } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Error) Equals(x Object) bool { return o == x // pointer equality } // IndexGet returns an element at a given index. func (o *Error) IndexGet(index Object) (res Object, err error) { if strIdx, _ := ToString(index); strIdx != "value" { err = ErrInvalidIndexOnError return } res = o.Value return } // Float represents a floating point number value. type Float struct { ObjectImpl Value float64 } func (o *Float) String() string { return strconv.FormatFloat(o.Value, 'f', -1, 64) } // TypeName returns the name of the type. func (o *Float) TypeName() string { return "float" } // BinaryOp returns another object that is the result of a given binary // operator and a right-hand side object. func (o *Float) BinaryOp(op token.Token, rhs Object) (Object, error) { switch rhs := rhs.(type) { case *Float: switch op { case token.Add: r := o.Value + rhs.Value if r == o.Value { return o, nil } return &Float{Value: r}, nil case token.Sub: r := o.Value - rhs.Value if r == o.Value { return o, nil } return &Float{Value: r}, nil case token.Mul: r := o.Value * rhs.Value if r == o.Value { return o, nil } return &Float{Value: r}, nil case token.Quo: r := o.Value / rhs.Value if r == o.Value { return o, nil } return &Float{Value: r}, nil case token.Less: if o.Value < rhs.Value { return TrueValue, nil } return FalseValue, nil case token.Greater: if o.Value > rhs.Value { return TrueValue, nil } return FalseValue, nil case token.LessEq: if o.Value <= rhs.Value { return TrueValue, nil } return FalseValue, nil case token.GreaterEq: if o.Value >= rhs.Value { return TrueValue, nil } return FalseValue, nil } case *Int: switch op { case token.Add: r := o.Value + float64(rhs.Value) if r == o.Value { return o, nil } return &Float{Value: r}, nil case token.Sub: r := o.Value - float64(rhs.Value) if r == o.Value { return o, nil } return &Float{Value: r}, nil case token.Mul: r := o.Value * float64(rhs.Value) if r == o.Value { return o, nil } return &Float{Value: r}, nil case token.Quo: r := o.Value / float64(rhs.Value) if r == o.Value { return o, nil } return &Float{Value: r}, nil case token.Less: if o.Value < float64(rhs.Value) { return TrueValue, nil } return FalseValue, nil case token.Greater: if o.Value > float64(rhs.Value) { return TrueValue, nil } return FalseValue, nil case token.LessEq: if o.Value <= float64(rhs.Value) { return TrueValue, nil } return FalseValue, nil case token.GreaterEq: if o.Value >= float64(rhs.Value) { return TrueValue, nil } return FalseValue, nil } } return nil, ErrInvalidOperator } // Copy returns a copy of the type. func (o *Float) Copy() Object { return &Float{Value: o.Value} } // IsFalsy returns true if the value of the type is falsy. func (o *Float) IsFalsy() bool { return math.IsNaN(o.Value) } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Float) Equals(x Object) bool { t, ok := x.(*Float) if !ok { return false } return o.Value == t.Value } // ImmutableArray represents an immutable array of objects. type ImmutableArray struct { ObjectImpl Value []Object } // TypeName returns the name of the type. func (o *ImmutableArray) TypeName() string { return "immutable-array" } func (o *ImmutableArray) String() string { var elements []string for _, e := range o.Value { elements = append(elements, e.String()) } return fmt.Sprintf("[%s]", strings.Join(elements, ", ")) } // BinaryOp returns another object that is the result of a given binary // operator and a right-hand side object. func (o *ImmutableArray) BinaryOp(op token.Token, rhs Object) (Object, error) { if rhs, ok := rhs.(*ImmutableArray); ok { switch op { case token.Add: return &Array{Value: append(o.Value, rhs.Value...)}, nil } } return nil, ErrInvalidOperator } // Copy returns a copy of the type. func (o *ImmutableArray) Copy() Object { var c []Object for _, elem := range o.Value { c = append(c, elem.Copy()) } return &Array{Value: c} } // IsFalsy returns true if the value of the type is falsy. func (o *ImmutableArray) IsFalsy() bool { return len(o.Value) == 0 } // Equals returns true if the value of the type is equal to the value of // another object. func (o *ImmutableArray) Equals(x Object) bool { var xVal []Object switch x := x.(type) { case *Array: xVal = x.Value case *ImmutableArray: xVal = x.Value default: return false } if len(o.Value) != len(xVal) { return false } for i, e := range o.Value { if !e.Equals(xVal[i]) { return false } } return true } // IndexGet returns an element at a given index. func (o *ImmutableArray) IndexGet(index Object) (res Object, err error) { intIdx, ok := index.(*Int) if !ok { err = ErrInvalidIndexType return } idxVal := int(intIdx.Value) if idxVal < 0 || idxVal >= len(o.Value) { res = UndefinedValue return } res = o.Value[idxVal] return } // Iterate creates an array iterator. func (o *ImmutableArray) Iterate() Iterator { return &ArrayIterator{ v: o.Value, l: len(o.Value), } } // CanIterate returns whether the Object can be Iterated. func (o *ImmutableArray) CanIterate() bool { return true } // ImmutableMap represents an immutable map object. type ImmutableMap struct { ObjectImpl Value map[string]Object } // TypeName returns the name of the type. func (o *ImmutableMap) TypeName() string { return "immutable-map" } func (o *ImmutableMap) String() string { var pairs []string for k, v := range o.Value { pairs = append(pairs, fmt.Sprintf("%s: %s", k, v.String())) } return fmt.Sprintf("{%s}", strings.Join(pairs, ", ")) } // Copy returns a copy of the type. func (o *ImmutableMap) Copy() Object { c := make(map[string]Object) for k, v := range o.Value { c[k] = v.Copy() } return &Map{Value: c} } // IsFalsy returns true if the value of the type is falsy. func (o *ImmutableMap) IsFalsy() bool { return len(o.Value) == 0 } // IndexGet returns the value for the given key. func (o *ImmutableMap) IndexGet(index Object) (res Object, err error) { strIdx, ok := ToString(index) if !ok { err = ErrInvalidIndexType return } res, ok = o.Value[strIdx] if !ok { res = UndefinedValue } return } // Equals returns true if the value of the type is equal to the value of // another object. func (o *ImmutableMap) Equals(x Object) bool { var xVal map[string]Object switch x := x.(type) { case *Map: xVal = x.Value case *ImmutableMap: xVal = x.Value default: return false } if len(o.Value) != len(xVal) { return false } for k, v := range o.Value { tv := xVal[k] if !v.Equals(tv) { return false } } return true } // Iterate creates an immutable map iterator. func (o *ImmutableMap) Iterate() Iterator { var keys []string for k := range o.Value { keys = append(keys, k) } return &MapIterator{ v: o.Value, k: keys, l: len(keys), } } // CanIterate returns whether the Object can be Iterated. func (o *ImmutableMap) CanIterate() bool { return true } // Int represents an integer value. type Int struct { ObjectImpl Value int64 } func (o *Int) String() string { return strconv.FormatInt(o.Value, 10) } // TypeName returns the name of the type. func (o *Int) TypeName() string { return "int" } // BinaryOp returns another object that is the result of a given binary // operator and a right-hand side object. func (o *Int) BinaryOp(op token.Token, rhs Object) (Object, error) { switch rhs := rhs.(type) { case *Int: switch op { case token.Add: r := o.Value + rhs.Value if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.Sub: r := o.Value - rhs.Value if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.Mul: r := o.Value * rhs.Value if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.Quo: r := o.Value / rhs.Value if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.Rem: r := o.Value % rhs.Value if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.And: r := o.Value & rhs.Value if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.Or: r := o.Value | rhs.Value if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.Xor: r := o.Value ^ rhs.Value if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.AndNot: r := o.Value &^ rhs.Value if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.Shl: r := o.Value << uint64(rhs.Value) if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.Shr: r := o.Value >> uint64(rhs.Value) if r == o.Value { return o, nil } return &Int{Value: r}, nil case token.Less: if o.Value < rhs.Value { return TrueValue, nil } return FalseValue, nil case token.Greater: if o.Value > rhs.Value { return TrueValue, nil } return FalseValue, nil case token.LessEq: if o.Value <= rhs.Value { return TrueValue, nil } return FalseValue, nil case token.GreaterEq: if o.Value >= rhs.Value { return TrueValue, nil } return FalseValue, nil } case *Float: switch op { case token.Add: return &Float{Value: float64(o.Value) + rhs.Value}, nil case token.Sub: return &Float{Value: float64(o.Value) - rhs.Value}, nil case token.Mul: return &Float{Value: float64(o.Value) * rhs.Value}, nil case token.Quo: return &Float{Value: float64(o.Value) / rhs.Value}, nil case token.Less: if float64(o.Value) < rhs.Value { return TrueValue, nil } return FalseValue, nil case token.Greater: if float64(o.Value) > rhs.Value { return TrueValue, nil } return FalseValue, nil case token.LessEq: if float64(o.Value) <= rhs.Value { return TrueValue, nil } return FalseValue, nil case token.GreaterEq: if float64(o.Value) >= rhs.Value { return TrueValue, nil } return FalseValue, nil } case *Char: switch op { case token.Add: return &Char{Value: rune(o.Value) + rhs.Value}, nil case token.Sub: return &Char{Value: rune(o.Value) - rhs.Value}, nil case token.Less: if o.Value < int64(rhs.Value) { return TrueValue, nil } return FalseValue, nil case token.Greater: if o.Value > int64(rhs.Value) { return TrueValue, nil } return FalseValue, nil case token.LessEq: if o.Value <= int64(rhs.Value) { return TrueValue, nil } return FalseValue, nil case token.GreaterEq: if o.Value >= int64(rhs.Value) { return TrueValue, nil } return FalseValue, nil } } return nil, ErrInvalidOperator } // Copy returns a copy of the type. func (o *Int) Copy() Object { return &Int{Value: o.Value} } // IsFalsy returns true if the value of the type is falsy. func (o *Int) IsFalsy() bool { return o.Value == 0 } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Int) Equals(x Object) bool { t, ok := x.(*Int) if !ok { return false } return o.Value == t.Value } // Map represents a map of objects. type Map struct { ObjectImpl Value map[string]Object } // TypeName returns the name of the type. func (o *Map) TypeName() string { return "map" } func (o *Map) String() string { var pairs []string for k, v := range o.Value { pairs = append(pairs, fmt.Sprintf("%s: %s", k, v.String())) } return fmt.Sprintf("{%s}", strings.Join(pairs, ", ")) } // Copy returns a copy of the type. func (o *Map) Copy() Object { c := make(map[string]Object) for k, v := range o.Value { c[k] = v.Copy() } return &Map{Value: c} } // IsFalsy returns true if the value of the type is falsy. func (o *Map) IsFalsy() bool { return len(o.Value) == 0 } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Map) Equals(x Object) bool { var xVal map[string]Object switch x := x.(type) { case *Map: xVal = x.Value case *ImmutableMap: xVal = x.Value default: return false } if len(o.Value) != len(xVal) { return false } for k, v := range o.Value { tv := xVal[k] if !v.Equals(tv) { return false } } return true } // IndexGet returns the value for the given key. func (o *Map) IndexGet(index Object) (res Object, err error) { strIdx, ok := ToString(index) if !ok { err = ErrInvalidIndexType return } res, ok = o.Value[strIdx] if !ok { res = UndefinedValue } return } // IndexSet sets the value for the given key. func (o *Map) IndexSet(index, value Object) (err error) { strIdx, ok := ToString(index) if !ok { err = ErrInvalidIndexType return } o.Value[strIdx] = value return nil } // Iterate creates a map iterator. func (o *Map) Iterate() Iterator { var keys []string for k := range o.Value { keys = append(keys, k) } return &MapIterator{ v: o.Value, k: keys, l: len(keys), } } // CanIterate returns whether the Object can be Iterated. func (o *Map) CanIterate() bool { return true } // ObjectPtr represents a free variable. type ObjectPtr struct { ObjectImpl Value *Object } func (o *ObjectPtr) String() string { return "free-var" } // TypeName returns the name of the type. func (o *ObjectPtr) TypeName() string { return "" } // Copy returns a copy of the type. func (o *ObjectPtr) Copy() Object { return o } // IsFalsy returns true if the value of the type is falsy. func (o *ObjectPtr) IsFalsy() bool { return o.Value == nil } // Equals returns true if the value of the type is equal to the value of // another object. func (o *ObjectPtr) Equals(x Object) bool { return o == x } // String represents a string value. type String struct { ObjectImpl Value string runeStr []rune } // TypeName returns the name of the type. func (o *String) TypeName() string { return "string" } func (o *String) String() string { return strconv.Quote(o.Value) } // BinaryOp returns another object that is the result of a given binary // operator and a right-hand side object. func (o *String) BinaryOp(op token.Token, rhs Object) (Object, error) { switch op { case token.Add: switch rhs := rhs.(type) { case *String: if len(o.Value)+len(rhs.Value) > MaxStringLen { return nil, ErrStringLimit } return &String{Value: o.Value + rhs.Value}, nil default: rhsStr := rhs.String() if len(o.Value)+len(rhsStr) > MaxStringLen { return nil, ErrStringLimit } return &String{Value: o.Value + rhsStr}, nil } case token.Less: switch rhs := rhs.(type) { case *String: if o.Value < rhs.Value { return TrueValue, nil } return FalseValue, nil } case token.LessEq: switch rhs := rhs.(type) { case *String: if o.Value <= rhs.Value { return TrueValue, nil } return FalseValue, nil } case token.Greater: switch rhs := rhs.(type) { case *String: if o.Value > rhs.Value { return TrueValue, nil } return FalseValue, nil } case token.GreaterEq: switch rhs := rhs.(type) { case *String: if o.Value >= rhs.Value { return TrueValue, nil } return FalseValue, nil } } return nil, ErrInvalidOperator } // IsFalsy returns true if the value of the type is falsy. func (o *String) IsFalsy() bool { return len(o.Value) == 0 } // Copy returns a copy of the type. func (o *String) Copy() Object { return &String{Value: o.Value} } // Equals returns true if the value of the type is equal to the value of // another object. func (o *String) Equals(x Object) bool { t, ok := x.(*String) if !ok { return false } return o.Value == t.Value } // IndexGet returns a character at a given index. func (o *String) IndexGet(index Object) (res Object, err error) { intIdx, ok := index.(*Int) if !ok { err = ErrInvalidIndexType return } idxVal := int(intIdx.Value) if o.runeStr == nil { o.runeStr = []rune(o.Value) } if idxVal < 0 || idxVal >= len(o.runeStr) { res = UndefinedValue return } res = &Char{Value: o.runeStr[idxVal]} return } // Iterate creates a string iterator. func (o *String) Iterate() Iterator { if o.runeStr == nil { o.runeStr = []rune(o.Value) } return &StringIterator{ v: o.runeStr, l: len(o.runeStr), } } // CanIterate returns whether the Object can be Iterated. func (o *String) CanIterate() bool { return true } // Time represents a time value. type Time struct { ObjectImpl Value time.Time } func (o *Time) String() string { return o.Value.String() } // TypeName returns the name of the type. func (o *Time) TypeName() string { return "time" } // BinaryOp returns another object that is the result of a given binary // operator and a right-hand side object. func (o *Time) BinaryOp(op token.Token, rhs Object) (Object, error) { switch rhs := rhs.(type) { case *Int: switch op { case token.Add: // time + int => time if rhs.Value == 0 { return o, nil } return &Time{Value: o.Value.Add(time.Duration(rhs.Value))}, nil case token.Sub: // time - int => time if rhs.Value == 0 { return o, nil } return &Time{Value: o.Value.Add(time.Duration(-rhs.Value))}, nil } case *Time: switch op { case token.Sub: // time - time => int (duration) return &Int{Value: int64(o.Value.Sub(rhs.Value))}, nil case token.Less: // time < time => bool if o.Value.Before(rhs.Value) { return TrueValue, nil } return FalseValue, nil case token.Greater: if o.Value.After(rhs.Value) { return TrueValue, nil } return FalseValue, nil case token.LessEq: if o.Value.Equal(rhs.Value) || o.Value.Before(rhs.Value) { return TrueValue, nil } return FalseValue, nil case token.GreaterEq: if o.Value.Equal(rhs.Value) || o.Value.After(rhs.Value) { return TrueValue, nil } return FalseValue, nil } } return nil, ErrInvalidOperator } // Copy returns a copy of the type. func (o *Time) Copy() Object { return &Time{Value: o.Value} } // IsFalsy returns true if the value of the type is falsy. func (o *Time) IsFalsy() bool { return o.Value.IsZero() } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Time) Equals(x Object) bool { t, ok := x.(*Time) if !ok { return false } return o.Value.Equal(t.Value) } // Undefined represents an undefined value. type Undefined struct { ObjectImpl } // TypeName returns the name of the type. func (o *Undefined) TypeName() string { return "undefined" } func (o *Undefined) String() string { return "" } // Copy returns a copy of the type. func (o *Undefined) Copy() Object { return o } // IsFalsy returns true if the value of the type is falsy. func (o *Undefined) IsFalsy() bool { return true } // Equals returns true if the value of the type is equal to the value of // another object. func (o *Undefined) Equals(x Object) bool { return o == x } // IndexGet returns an element at a given index. func (o *Undefined) IndexGet(_ Object) (Object, error) { return UndefinedValue, nil } // Iterate creates a map iterator. func (o *Undefined) Iterate() Iterator { return o } // CanIterate returns whether the Object can be Iterated. func (o *Undefined) CanIterate() bool { return true } // Next returns true if there are more elements to iterate. func (o *Undefined) Next() bool { return false } // Key returns the key or index value of the current element. func (o *Undefined) Key() Object { return o } // Value returns the value of the current element. func (o *Undefined) Value() Object { return o } // UserFunction represents a user function. type UserFunction struct { ObjectImpl Name string Value CallableFunc } // TypeName returns the name of the type. func (o *UserFunction) TypeName() string { return "user-function:" + o.Name } func (o *UserFunction) String() string { return "" } // Copy returns a copy of the type. func (o *UserFunction) Copy() Object { return &UserFunction{Value: o.Value, Name: o.Name} } // Equals returns true if the value of the type is equal to the value of // another object. func (o *UserFunction) Equals(_ Object) bool { return false } // Call invokes a user function. func (o *UserFunction) Call(args ...Object) (Object, error) { return o.Value(args...) } // CanCall returns whether the Object can be Called. func (o *UserFunction) CanCall() bool { return true } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/objects_test.go0000644000175000017500000005261114607001502020166 0ustar00maythammaythampackage tengo_test import ( "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" "github.com/d5/tengo/v2/token" ) func TestObject_TypeName(t *testing.T) { var o tengo.Object = &tengo.Int{} require.Equal(t, "int", o.TypeName()) o = &tengo.Float{} require.Equal(t, "float", o.TypeName()) o = &tengo.Char{} require.Equal(t, "char", o.TypeName()) o = &tengo.String{} require.Equal(t, "string", o.TypeName()) o = &tengo.Bool{} require.Equal(t, "bool", o.TypeName()) o = &tengo.Array{} require.Equal(t, "array", o.TypeName()) o = &tengo.Map{} require.Equal(t, "map", o.TypeName()) o = &tengo.ArrayIterator{} require.Equal(t, "array-iterator", o.TypeName()) o = &tengo.StringIterator{} require.Equal(t, "string-iterator", o.TypeName()) o = &tengo.MapIterator{} require.Equal(t, "map-iterator", o.TypeName()) o = &tengo.BuiltinFunction{Name: "fn"} require.Equal(t, "builtin-function:fn", o.TypeName()) o = &tengo.UserFunction{Name: "fn"} require.Equal(t, "user-function:fn", o.TypeName()) o = &tengo.CompiledFunction{} require.Equal(t, "compiled-function", o.TypeName()) o = &tengo.Undefined{} require.Equal(t, "undefined", o.TypeName()) o = &tengo.Error{} require.Equal(t, "error", o.TypeName()) o = &tengo.Bytes{} require.Equal(t, "bytes", o.TypeName()) } func TestObject_IsFalsy(t *testing.T) { var o tengo.Object = &tengo.Int{Value: 0} require.True(t, o.IsFalsy()) o = &tengo.Int{Value: 1} require.False(t, o.IsFalsy()) o = &tengo.Float{Value: 0} require.False(t, o.IsFalsy()) o = &tengo.Float{Value: 1} require.False(t, o.IsFalsy()) o = &tengo.Char{Value: ' '} require.False(t, o.IsFalsy()) o = &tengo.Char{Value: 'T'} require.False(t, o.IsFalsy()) o = &tengo.String{Value: ""} require.True(t, o.IsFalsy()) o = &tengo.String{Value: " "} require.False(t, o.IsFalsy()) o = &tengo.Array{Value: nil} require.True(t, o.IsFalsy()) o = &tengo.Array{Value: []tengo.Object{nil}} // nil is not valid but still count as 1 element require.False(t, o.IsFalsy()) o = &tengo.Map{Value: nil} require.True(t, o.IsFalsy()) o = &tengo.Map{Value: map[string]tengo.Object{"a": nil}} // nil is not valid but still count as 1 element require.False(t, o.IsFalsy()) o = &tengo.StringIterator{} require.True(t, o.IsFalsy()) o = &tengo.ArrayIterator{} require.True(t, o.IsFalsy()) o = &tengo.MapIterator{} require.True(t, o.IsFalsy()) o = &tengo.BuiltinFunction{} require.False(t, o.IsFalsy()) o = &tengo.CompiledFunction{} require.False(t, o.IsFalsy()) o = &tengo.Undefined{} require.True(t, o.IsFalsy()) o = &tengo.Error{} require.True(t, o.IsFalsy()) o = &tengo.Bytes{} require.True(t, o.IsFalsy()) o = &tengo.Bytes{Value: []byte{1, 2}} require.False(t, o.IsFalsy()) } func TestObject_String(t *testing.T) { var o tengo.Object = &tengo.Int{Value: 0} require.Equal(t, "0", o.String()) o = &tengo.Int{Value: 1} require.Equal(t, "1", o.String()) o = &tengo.Float{Value: 0} require.Equal(t, "0", o.String()) o = &tengo.Float{Value: 1} require.Equal(t, "1", o.String()) o = &tengo.Char{Value: ' '} require.Equal(t, " ", o.String()) o = &tengo.Char{Value: 'T'} require.Equal(t, "T", o.String()) o = &tengo.String{Value: ""} require.Equal(t, `""`, o.String()) o = &tengo.String{Value: " "} require.Equal(t, `" "`, o.String()) o = &tengo.Array{Value: nil} require.Equal(t, "[]", o.String()) o = &tengo.Map{Value: nil} require.Equal(t, "{}", o.String()) o = &tengo.Error{Value: nil} require.Equal(t, "error", o.String()) o = &tengo.Error{Value: &tengo.String{Value: "error 1"}} require.Equal(t, `error: "error 1"`, o.String()) o = &tengo.StringIterator{} require.Equal(t, "", o.String()) o = &tengo.ArrayIterator{} require.Equal(t, "", o.String()) o = &tengo.MapIterator{} require.Equal(t, "", o.String()) o = &tengo.Undefined{} require.Equal(t, "", o.String()) o = &tengo.Bytes{} require.Equal(t, "", o.String()) o = &tengo.Bytes{Value: []byte("foo")} require.Equal(t, "foo", o.String()) } func TestObject_BinaryOp(t *testing.T) { var o tengo.Object = &tengo.Char{} _, err := o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) o = &tengo.Bool{} _, err = o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) o = &tengo.Map{} _, err = o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) o = &tengo.ArrayIterator{} _, err = o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) o = &tengo.StringIterator{} _, err = o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) o = &tengo.MapIterator{} _, err = o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) o = &tengo.BuiltinFunction{} _, err = o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) o = &tengo.CompiledFunction{} _, err = o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) o = &tengo.Undefined{} _, err = o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) o = &tengo.Error{} _, err = o.BinaryOp(token.Add, tengo.UndefinedValue) require.Error(t, err) } func TestArray_BinaryOp(t *testing.T) { testBinaryOp(t, &tengo.Array{Value: nil}, token.Add, &tengo.Array{Value: nil}, &tengo.Array{Value: nil}) testBinaryOp(t, &tengo.Array{Value: nil}, token.Add, &tengo.Array{Value: []tengo.Object{}}, &tengo.Array{Value: nil}) testBinaryOp(t, &tengo.Array{Value: []tengo.Object{}}, token.Add, &tengo.Array{Value: nil}, &tengo.Array{Value: []tengo.Object{}}) testBinaryOp(t, &tengo.Array{Value: []tengo.Object{}}, token.Add, &tengo.Array{Value: []tengo.Object{}}, &tengo.Array{Value: []tengo.Object{}}) testBinaryOp(t, &tengo.Array{Value: nil}, token.Add, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, }}, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, }}) testBinaryOp(t, &tengo.Array{Value: nil}, token.Add, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}, }}, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}, }}) testBinaryOp(t, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}, }}, token.Add, &tengo.Array{Value: nil}, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}, }}) testBinaryOp(t, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}, }}, token.Add, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 4}, &tengo.Int{Value: 5}, &tengo.Int{Value: 6}, }}, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}, &tengo.Int{Value: 4}, &tengo.Int{Value: 5}, &tengo.Int{Value: 6}, }}) } func TestError_Equals(t *testing.T) { err1 := &tengo.Error{Value: &tengo.String{Value: "some error"}} err2 := err1 require.True(t, err1.Equals(err2)) require.True(t, err2.Equals(err1)) err2 = &tengo.Error{Value: &tengo.String{Value: "some error"}} require.False(t, err1.Equals(err2)) require.False(t, err2.Equals(err1)) } func TestFloat_BinaryOp(t *testing.T) { // float + float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { testBinaryOp(t, &tengo.Float{Value: l}, token.Add, &tengo.Float{Value: r}, &tengo.Float{Value: l + r}) } } // float - float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { testBinaryOp(t, &tengo.Float{Value: l}, token.Sub, &tengo.Float{Value: r}, &tengo.Float{Value: l - r}) } } // float * float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { testBinaryOp(t, &tengo.Float{Value: l}, token.Mul, &tengo.Float{Value: r}, &tengo.Float{Value: l * r}) } } // float / float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { if r != 0 { testBinaryOp(t, &tengo.Float{Value: l}, token.Quo, &tengo.Float{Value: r}, &tengo.Float{Value: l / r}) } } } // float < float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { testBinaryOp(t, &tengo.Float{Value: l}, token.Less, &tengo.Float{Value: r}, boolValue(l < r)) } } // float > float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { testBinaryOp(t, &tengo.Float{Value: l}, token.Greater, &tengo.Float{Value: r}, boolValue(l > r)) } } // float <= float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { testBinaryOp(t, &tengo.Float{Value: l}, token.LessEq, &tengo.Float{Value: r}, boolValue(l <= r)) } } // float >= float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { testBinaryOp(t, &tengo.Float{Value: l}, token.GreaterEq, &tengo.Float{Value: r}, boolValue(l >= r)) } } // float + int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Float{Value: l}, token.Add, &tengo.Int{Value: r}, &tengo.Float{Value: l + float64(r)}) } } // float - int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Float{Value: l}, token.Sub, &tengo.Int{Value: r}, &tengo.Float{Value: l - float64(r)}) } } // float * int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Float{Value: l}, token.Mul, &tengo.Int{Value: r}, &tengo.Float{Value: l * float64(r)}) } } // float / int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { if r != 0 { testBinaryOp(t, &tengo.Float{Value: l}, token.Quo, &tengo.Int{Value: r}, &tengo.Float{Value: l / float64(r)}) } } } // float < int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Float{Value: l}, token.Less, &tengo.Int{Value: r}, boolValue(l < float64(r))) } } // float > int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Float{Value: l}, token.Greater, &tengo.Int{Value: r}, boolValue(l > float64(r))) } } // float <= int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Float{Value: l}, token.LessEq, &tengo.Int{Value: r}, boolValue(l <= float64(r))) } } // float >= int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Float{Value: l}, token.GreaterEq, &tengo.Int{Value: r}, boolValue(l >= float64(r))) } } } func TestInt_BinaryOp(t *testing.T) { // int + int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Int{Value: l}, token.Add, &tengo.Int{Value: r}, &tengo.Int{Value: l + r}) } } // int - int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Int{Value: l}, token.Sub, &tengo.Int{Value: r}, &tengo.Int{Value: l - r}) } } // int * int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Int{Value: l}, token.Mul, &tengo.Int{Value: r}, &tengo.Int{Value: l * r}) } } // int / int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { if r != 0 { testBinaryOp(t, &tengo.Int{Value: l}, token.Quo, &tengo.Int{Value: r}, &tengo.Int{Value: l / r}) } } } // int % int for l := int64(-4); l <= 4; l++ { for r := -int64(-4); r <= 4; r++ { if r == 0 { testBinaryOp(t, &tengo.Int{Value: l}, token.Rem, &tengo.Int{Value: r}, &tengo.Int{Value: l % r}) } } } // int & int testBinaryOp(t, &tengo.Int{Value: 0}, token.And, &tengo.Int{Value: 0}, &tengo.Int{Value: int64(0)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.And, &tengo.Int{Value: 0}, &tengo.Int{Value: int64(1) & int64(0)}) testBinaryOp(t, &tengo.Int{Value: 0}, token.And, &tengo.Int{Value: 1}, &tengo.Int{Value: int64(0) & int64(1)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.And, &tengo.Int{Value: 1}, &tengo.Int{Value: int64(1)}) testBinaryOp(t, &tengo.Int{Value: 0}, token.And, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(0) & int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.And, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(1) & int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: int64(0xffffffff)}, token.And, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: 1984}, token.And, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(1984) & int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: -1984}, token.And, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(-1984) & int64(0xffffffff)}) // int | int testBinaryOp(t, &tengo.Int{Value: 0}, token.Or, &tengo.Int{Value: 0}, &tengo.Int{Value: int64(0)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.Or, &tengo.Int{Value: 0}, &tengo.Int{Value: int64(1) | int64(0)}) testBinaryOp(t, &tengo.Int{Value: 0}, token.Or, &tengo.Int{Value: 1}, &tengo.Int{Value: int64(0) | int64(1)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.Or, &tengo.Int{Value: 1}, &tengo.Int{Value: int64(1)}) testBinaryOp(t, &tengo.Int{Value: 0}, token.Or, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(0) | int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.Or, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(1) | int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: int64(0xffffffff)}, token.Or, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: 1984}, token.Or, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(1984) | int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: -1984}, token.Or, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(-1984) | int64(0xffffffff)}) // int ^ int testBinaryOp(t, &tengo.Int{Value: 0}, token.Xor, &tengo.Int{Value: 0}, &tengo.Int{Value: int64(0)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.Xor, &tengo.Int{Value: 0}, &tengo.Int{Value: int64(1) ^ int64(0)}) testBinaryOp(t, &tengo.Int{Value: 0}, token.Xor, &tengo.Int{Value: 1}, &tengo.Int{Value: int64(0) ^ int64(1)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.Xor, &tengo.Int{Value: 1}, &tengo.Int{Value: int64(0)}) testBinaryOp(t, &tengo.Int{Value: 0}, token.Xor, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(0) ^ int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.Xor, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(1) ^ int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: int64(0xffffffff)}, token.Xor, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(0)}) testBinaryOp(t, &tengo.Int{Value: 1984}, token.Xor, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(1984) ^ int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: -1984}, token.Xor, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(-1984) ^ int64(0xffffffff)}) // int &^ int testBinaryOp(t, &tengo.Int{Value: 0}, token.AndNot, &tengo.Int{Value: 0}, &tengo.Int{Value: int64(0)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.AndNot, &tengo.Int{Value: 0}, &tengo.Int{Value: int64(1) &^ int64(0)}) testBinaryOp(t, &tengo.Int{Value: 0}, token.AndNot, &tengo.Int{Value: 1}, &tengo.Int{Value: int64(0) &^ int64(1)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.AndNot, &tengo.Int{Value: 1}, &tengo.Int{Value: int64(0)}) testBinaryOp(t, &tengo.Int{Value: 0}, token.AndNot, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(0) &^ int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.AndNot, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(1) &^ int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: int64(0xffffffff)}, token.AndNot, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(0)}) testBinaryOp(t, &tengo.Int{Value: 1984}, token.AndNot, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(1984) &^ int64(0xffffffff)}) testBinaryOp(t, &tengo.Int{Value: -1984}, token.AndNot, &tengo.Int{Value: int64(0xffffffff)}, &tengo.Int{Value: int64(-1984) &^ int64(0xffffffff)}) // int << int for s := int64(0); s < 64; s++ { testBinaryOp(t, &tengo.Int{Value: 0}, token.Shl, &tengo.Int{Value: s}, &tengo.Int{Value: int64(0) << uint(s)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.Shl, &tengo.Int{Value: s}, &tengo.Int{Value: int64(1) << uint(s)}) testBinaryOp(t, &tengo.Int{Value: 2}, token.Shl, &tengo.Int{Value: s}, &tengo.Int{Value: int64(2) << uint(s)}) testBinaryOp(t, &tengo.Int{Value: -1}, token.Shl, &tengo.Int{Value: s}, &tengo.Int{Value: int64(-1) << uint(s)}) testBinaryOp(t, &tengo.Int{Value: -2}, token.Shl, &tengo.Int{Value: s}, &tengo.Int{Value: int64(-2) << uint(s)}) testBinaryOp(t, &tengo.Int{Value: int64(0xffffffff)}, token.Shl, &tengo.Int{Value: s}, &tengo.Int{Value: int64(0xffffffff) << uint(s)}) } // int >> int for s := int64(0); s < 64; s++ { testBinaryOp(t, &tengo.Int{Value: 0}, token.Shr, &tengo.Int{Value: s}, &tengo.Int{Value: int64(0) >> uint(s)}) testBinaryOp(t, &tengo.Int{Value: 1}, token.Shr, &tengo.Int{Value: s}, &tengo.Int{Value: int64(1) >> uint(s)}) testBinaryOp(t, &tengo.Int{Value: 2}, token.Shr, &tengo.Int{Value: s}, &tengo.Int{Value: int64(2) >> uint(s)}) testBinaryOp(t, &tengo.Int{Value: -1}, token.Shr, &tengo.Int{Value: s}, &tengo.Int{Value: int64(-1) >> uint(s)}) testBinaryOp(t, &tengo.Int{Value: -2}, token.Shr, &tengo.Int{Value: s}, &tengo.Int{Value: int64(-2) >> uint(s)}) testBinaryOp(t, &tengo.Int{Value: int64(0xffffffff)}, token.Shr, &tengo.Int{Value: s}, &tengo.Int{Value: int64(0xffffffff) >> uint(s)}) } // int < int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Int{Value: l}, token.Less, &tengo.Int{Value: r}, boolValue(l < r)) } } // int > int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Int{Value: l}, token.Greater, &tengo.Int{Value: r}, boolValue(l > r)) } } // int <= int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Int{Value: l}, token.LessEq, &tengo.Int{Value: r}, boolValue(l <= r)) } } // int >= int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { testBinaryOp(t, &tengo.Int{Value: l}, token.GreaterEq, &tengo.Int{Value: r}, boolValue(l >= r)) } } // int + float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { testBinaryOp(t, &tengo.Int{Value: l}, token.Add, &tengo.Float{Value: r}, &tengo.Float{Value: float64(l) + r}) } } // int - float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { testBinaryOp(t, &tengo.Int{Value: l}, token.Sub, &tengo.Float{Value: r}, &tengo.Float{Value: float64(l) - r}) } } // int * float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { testBinaryOp(t, &tengo.Int{Value: l}, token.Mul, &tengo.Float{Value: r}, &tengo.Float{Value: float64(l) * r}) } } // int / float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { if r != 0 { testBinaryOp(t, &tengo.Int{Value: l}, token.Quo, &tengo.Float{Value: r}, &tengo.Float{Value: float64(l) / r}) } } } // int < float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { testBinaryOp(t, &tengo.Int{Value: l}, token.Less, &tengo.Float{Value: r}, boolValue(float64(l) < r)) } } // int > float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { testBinaryOp(t, &tengo.Int{Value: l}, token.Greater, &tengo.Float{Value: r}, boolValue(float64(l) > r)) } } // int <= float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { testBinaryOp(t, &tengo.Int{Value: l}, token.LessEq, &tengo.Float{Value: r}, boolValue(float64(l) <= r)) } } // int >= float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { testBinaryOp(t, &tengo.Int{Value: l}, token.GreaterEq, &tengo.Float{Value: r}, boolValue(float64(l) >= r)) } } } func TestMap_Index(t *testing.T) { m := &tengo.Map{Value: make(map[string]tengo.Object)} k := &tengo.Int{Value: 1} v := &tengo.String{Value: "abcdef"} err := m.IndexSet(k, v) require.NoError(t, err) res, err := m.IndexGet(k) require.NoError(t, err) require.Equal(t, v, res) } func TestString_BinaryOp(t *testing.T) { lstr := "abcde" rstr := "01234" for l := 0; l < len(lstr); l++ { for r := 0; r < len(rstr); r++ { ls := lstr[l:] rs := rstr[r:] testBinaryOp(t, &tengo.String{Value: ls}, token.Add, &tengo.String{Value: rs}, &tengo.String{Value: ls + rs}) rc := []rune(rstr)[r] testBinaryOp(t, &tengo.String{Value: ls}, token.Add, &tengo.Char{Value: rc}, &tengo.String{Value: ls + string(rc)}) } } } func testBinaryOp( t *testing.T, lhs tengo.Object, op token.Token, rhs tengo.Object, expected tengo.Object, ) { t.Helper() actual, err := lhs.BinaryOp(op, rhs) require.NoError(t, err) require.Equal(t, expected, actual) } func boolValue(b bool) tengo.Object { if b { return tengo.TrueValue } return tengo.FalseValue } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/0000755000175000017500000000000014607001502016436 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/ast.go0000644000175000017500000000251214607001502017554 0ustar00maythammaythampackage parser import ( "strings" ) const ( nullRep = "" ) // Node represents a node in the AST. type Node interface { // Pos returns the position of first character belonging to the node. Pos() Pos // End returns the position of first character immediately after the node. End() Pos // String returns a string representation of the node. String() string } // IdentList represents a list of identifiers. type IdentList struct { LParen Pos VarArgs bool List []*Ident RParen Pos } // Pos returns the position of first character belonging to the node. func (n *IdentList) Pos() Pos { if n.LParen.IsValid() { return n.LParen } if len(n.List) > 0 { return n.List[0].Pos() } return NoPos } // End returns the position of first character immediately after the node. func (n *IdentList) End() Pos { if n.RParen.IsValid() { return n.RParen + 1 } if l := len(n.List); l > 0 { return n.List[l-1].End() } return NoPos } // NumFields returns the number of fields. func (n *IdentList) NumFields() int { if n == nil { return 0 } return len(n.List) } func (n *IdentList) String() string { var list []string for i, e := range n.List { if n.VarArgs && i == len(n.List)-1 { list = append(list, "..."+e.String()) } else { list = append(list, e.String()) } } return "(" + strings.Join(list, ", ") + ")" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/ast_test.go0000644000175000017500000000133014607001502020610 0ustar00maythammaythampackage parser_test import ( "testing" "github.com/d5/tengo/v2/parser" ) func TestIdentListString(t *testing.T) { identListVar := &parser.IdentList{ List: []*parser.Ident{ {Name: "a"}, {Name: "b"}, {Name: "c"}, }, VarArgs: true, } expectedVar := "(a, b, ...c)" if str := identListVar.String(); str != expectedVar { t.Fatalf("expected string of %#v to be %s, got %s", identListVar, expectedVar, str) } identList := &parser.IdentList{ List: []*parser.Ident{ {Name: "a"}, {Name: "b"}, {Name: "c"}, }, VarArgs: false, } expected := "(a, b, c)" if str := identList.String(); str != expected { t.Fatalf("expected string of %#v to be %s, got %s", identList, expected, str) } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/expr.go0000644000175000017500000003076114607001502017752 0ustar00maythammaythampackage parser import ( "strings" "github.com/d5/tengo/v2/token" ) // Expr represents an expression node in the AST. type Expr interface { Node exprNode() } // ArrayLit represents an array literal. type ArrayLit struct { Elements []Expr LBrack Pos RBrack Pos } func (e *ArrayLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *ArrayLit) Pos() Pos { return e.LBrack } // End returns the position of first character immediately after the node. func (e *ArrayLit) End() Pos { return e.RBrack + 1 } func (e *ArrayLit) String() string { var elements []string for _, m := range e.Elements { elements = append(elements, m.String()) } return "[" + strings.Join(elements, ", ") + "]" } // BadExpr represents a bad expression. type BadExpr struct { From Pos To Pos } func (e *BadExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *BadExpr) Pos() Pos { return e.From } // End returns the position of first character immediately after the node. func (e *BadExpr) End() Pos { return e.To } func (e *BadExpr) String() string { return "" } // BinaryExpr represents a binary operator expression. type BinaryExpr struct { LHS Expr RHS Expr Token token.Token TokenPos Pos } func (e *BinaryExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *BinaryExpr) Pos() Pos { return e.LHS.Pos() } // End returns the position of first character immediately after the node. func (e *BinaryExpr) End() Pos { return e.RHS.End() } func (e *BinaryExpr) String() string { return "(" + e.LHS.String() + " " + e.Token.String() + " " + e.RHS.String() + ")" } // BoolLit represents a boolean literal. type BoolLit struct { Value bool ValuePos Pos Literal string } func (e *BoolLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *BoolLit) Pos() Pos { return e.ValuePos } // End returns the position of first character immediately after the node. func (e *BoolLit) End() Pos { return Pos(int(e.ValuePos) + len(e.Literal)) } func (e *BoolLit) String() string { return e.Literal } // CallExpr represents a function call expression. type CallExpr struct { Func Expr LParen Pos Args []Expr Ellipsis Pos RParen Pos } func (e *CallExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *CallExpr) Pos() Pos { return e.Func.Pos() } // End returns the position of first character immediately after the node. func (e *CallExpr) End() Pos { return e.RParen + 1 } func (e *CallExpr) String() string { var args []string for _, e := range e.Args { args = append(args, e.String()) } if len(args) > 0 && e.Ellipsis.IsValid() { args[len(args)-1] = args[len(args)-1] + "..." } return e.Func.String() + "(" + strings.Join(args, ", ") + ")" } // CharLit represents a character literal. type CharLit struct { Value rune ValuePos Pos Literal string } func (e *CharLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *CharLit) Pos() Pos { return e.ValuePos } // End returns the position of first character immediately after the node. func (e *CharLit) End() Pos { return Pos(int(e.ValuePos) + len(e.Literal)) } func (e *CharLit) String() string { return e.Literal } // CondExpr represents a ternary conditional expression. type CondExpr struct { Cond Expr True Expr False Expr QuestionPos Pos ColonPos Pos } func (e *CondExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *CondExpr) Pos() Pos { return e.Cond.Pos() } // End returns the position of first character immediately after the node. func (e *CondExpr) End() Pos { return e.False.End() } func (e *CondExpr) String() string { return "(" + e.Cond.String() + " ? " + e.True.String() + " : " + e.False.String() + ")" } // ErrorExpr represents an error expression type ErrorExpr struct { Expr Expr ErrorPos Pos LParen Pos RParen Pos } func (e *ErrorExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *ErrorExpr) Pos() Pos { return e.ErrorPos } // End returns the position of first character immediately after the node. func (e *ErrorExpr) End() Pos { return e.RParen } func (e *ErrorExpr) String() string { return "error(" + e.Expr.String() + ")" } // FloatLit represents a floating point literal. type FloatLit struct { Value float64 ValuePos Pos Literal string } func (e *FloatLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *FloatLit) Pos() Pos { return e.ValuePos } // End returns the position of first character immediately after the node. func (e *FloatLit) End() Pos { return Pos(int(e.ValuePos) + len(e.Literal)) } func (e *FloatLit) String() string { return e.Literal } // FuncLit represents a function literal. type FuncLit struct { Type *FuncType Body *BlockStmt } func (e *FuncLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *FuncLit) Pos() Pos { return e.Type.Pos() } // End returns the position of first character immediately after the node. func (e *FuncLit) End() Pos { return e.Body.End() } func (e *FuncLit) String() string { return "func" + e.Type.Params.String() + " " + e.Body.String() } // FuncType represents a function type definition. type FuncType struct { FuncPos Pos Params *IdentList } func (e *FuncType) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *FuncType) Pos() Pos { return e.FuncPos } // End returns the position of first character immediately after the node. func (e *FuncType) End() Pos { return e.Params.End() } func (e *FuncType) String() string { return "func" + e.Params.String() } // Ident represents an identifier. type Ident struct { Name string NamePos Pos } func (e *Ident) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *Ident) Pos() Pos { return e.NamePos } // End returns the position of first character immediately after the node. func (e *Ident) End() Pos { return Pos(int(e.NamePos) + len(e.Name)) } func (e *Ident) String() string { if e != nil { return e.Name } return nullRep } // ImmutableExpr represents an immutable expression type ImmutableExpr struct { Expr Expr ErrorPos Pos LParen Pos RParen Pos } func (e *ImmutableExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *ImmutableExpr) Pos() Pos { return e.ErrorPos } // End returns the position of first character immediately after the node. func (e *ImmutableExpr) End() Pos { return e.RParen } func (e *ImmutableExpr) String() string { return "immutable(" + e.Expr.String() + ")" } // ImportExpr represents an import expression type ImportExpr struct { ModuleName string Token token.Token TokenPos Pos } func (e *ImportExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *ImportExpr) Pos() Pos { return e.TokenPos } // End returns the position of first character immediately after the node. func (e *ImportExpr) End() Pos { // import("moduleName") return Pos(int(e.TokenPos) + 10 + len(e.ModuleName)) } func (e *ImportExpr) String() string { return `import("` + e.ModuleName + `")` } // IndexExpr represents an index expression. type IndexExpr struct { Expr Expr LBrack Pos Index Expr RBrack Pos } func (e *IndexExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *IndexExpr) Pos() Pos { return e.Expr.Pos() } // End returns the position of first character immediately after the node. func (e *IndexExpr) End() Pos { return e.RBrack + 1 } func (e *IndexExpr) String() string { var index string if e.Index != nil { index = e.Index.String() } return e.Expr.String() + "[" + index + "]" } // IntLit represents an integer literal. type IntLit struct { Value int64 ValuePos Pos Literal string } func (e *IntLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *IntLit) Pos() Pos { return e.ValuePos } // End returns the position of first character immediately after the node. func (e *IntLit) End() Pos { return Pos(int(e.ValuePos) + len(e.Literal)) } func (e *IntLit) String() string { return e.Literal } // MapElementLit represents a map element. type MapElementLit struct { Key string KeyPos Pos ColonPos Pos Value Expr } func (e *MapElementLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *MapElementLit) Pos() Pos { return e.KeyPos } // End returns the position of first character immediately after the node. func (e *MapElementLit) End() Pos { return e.Value.End() } func (e *MapElementLit) String() string { return e.Key + ": " + e.Value.String() } // MapLit represents a map literal. type MapLit struct { LBrace Pos Elements []*MapElementLit RBrace Pos } func (e *MapLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *MapLit) Pos() Pos { return e.LBrace } // End returns the position of first character immediately after the node. func (e *MapLit) End() Pos { return e.RBrace + 1 } func (e *MapLit) String() string { var elements []string for _, m := range e.Elements { elements = append(elements, m.String()) } return "{" + strings.Join(elements, ", ") + "}" } // ParenExpr represents a parenthesis wrapped expression. type ParenExpr struct { Expr Expr LParen Pos RParen Pos } func (e *ParenExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *ParenExpr) Pos() Pos { return e.LParen } // End returns the position of first character immediately after the node. func (e *ParenExpr) End() Pos { return e.RParen + 1 } func (e *ParenExpr) String() string { return "(" + e.Expr.String() + ")" } // SelectorExpr represents a selector expression. type SelectorExpr struct { Expr Expr Sel Expr } func (e *SelectorExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *SelectorExpr) Pos() Pos { return e.Expr.Pos() } // End returns the position of first character immediately after the node. func (e *SelectorExpr) End() Pos { return e.Sel.End() } func (e *SelectorExpr) String() string { return e.Expr.String() + "." + e.Sel.String() } // SliceExpr represents a slice expression. type SliceExpr struct { Expr Expr LBrack Pos Low Expr High Expr RBrack Pos } func (e *SliceExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *SliceExpr) Pos() Pos { return e.Expr.Pos() } // End returns the position of first character immediately after the node. func (e *SliceExpr) End() Pos { return e.RBrack + 1 } func (e *SliceExpr) String() string { var low, high string if e.Low != nil { low = e.Low.String() } if e.High != nil { high = e.High.String() } return e.Expr.String() + "[" + low + ":" + high + "]" } // StringLit represents a string literal. type StringLit struct { Value string ValuePos Pos Literal string } func (e *StringLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *StringLit) Pos() Pos { return e.ValuePos } // End returns the position of first character immediately after the node. func (e *StringLit) End() Pos { return Pos(int(e.ValuePos) + len(e.Literal)) } func (e *StringLit) String() string { return e.Literal } // UnaryExpr represents an unary operator expression. type UnaryExpr struct { Expr Expr Token token.Token TokenPos Pos } func (e *UnaryExpr) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *UnaryExpr) Pos() Pos { return e.Expr.Pos() } // End returns the position of first character immediately after the node. func (e *UnaryExpr) End() Pos { return e.Expr.End() } func (e *UnaryExpr) String() string { return "(" + e.Token.String() + e.Expr.String() + ")" } // UndefinedLit represents an undefined literal. type UndefinedLit struct { TokenPos Pos } func (e *UndefinedLit) exprNode() {} // Pos returns the position of first character belonging to the node. func (e *UndefinedLit) Pos() Pos { return e.TokenPos } // End returns the position of first character immediately after the node. func (e *UndefinedLit) End() Pos { return e.TokenPos + 9 // len(undefined) == 9 } func (e *UndefinedLit) String() string { return "undefined" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/file.go0000644000175000017500000000107714607001502017711 0ustar00maythammaythampackage parser import ( "strings" ) // File represents a file unit. type File struct { InputFile *SourceFile Stmts []Stmt } // Pos returns the position of first character belonging to the node. func (n *File) Pos() Pos { return Pos(n.InputFile.Base) } // End returns the position of first character immediately after the node. func (n *File) End() Pos { return Pos(n.InputFile.Base + n.InputFile.Size) } func (n *File) String() string { var stmts []string for _, e := range n.Stmts { stmts = append(stmts, e.String()) } return strings.Join(stmts, "; ") } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/opcodes.go0000644000175000017500000001155414607001502020427 0ustar00maythammaythampackage parser // Opcode represents a single byte operation code. type Opcode = byte // List of opcodes const ( OpConstant Opcode = iota // Load constant OpBComplement // bitwise complement OpPop // Pop OpTrue // Push true OpFalse // Push false OpEqual // Equal == OpNotEqual // Not equal != OpMinus // Minus - OpLNot // Logical not ! OpJumpFalsy // Jump if falsy OpAndJump // Logical AND jump OpOrJump // Logical OR jump OpJump // Jump OpNull // Push null OpArray // Array object OpMap // Map object OpError // Error object OpImmutable // Immutable object OpIndex // Index operation OpSliceIndex // Slice operation OpCall // Call function OpReturn // Return OpGetGlobal // Get global variable OpSetGlobal // Set global variable OpSetSelGlobal // Set global variable using selectors OpGetLocal // Get local variable OpSetLocal // Set local variable OpDefineLocal // Define local variable OpSetSelLocal // Set local variable using selectors OpGetFreePtr // Get free variable pointer object OpGetFree // Get free variables OpSetFree // Set free variables OpGetLocalPtr // Get local variable as a pointer OpSetSelFree // Set free variables using selectors OpGetBuiltin // Get builtin function OpClosure // Push closure OpIteratorInit // Iterator init OpIteratorNext // Iterator next OpIteratorKey // Iterator key OpIteratorValue // Iterator value OpBinaryOp // Binary operation OpSuspend // Suspend VM ) // OpcodeNames are string representation of opcodes. var OpcodeNames = [...]string{ OpConstant: "CONST", OpPop: "POP", OpTrue: "TRUE", OpFalse: "FALSE", OpBComplement: "NEG", OpEqual: "EQL", OpNotEqual: "NEQ", OpMinus: "NEG", OpLNot: "NOT", OpJumpFalsy: "JMPF", OpAndJump: "ANDJMP", OpOrJump: "ORJMP", OpJump: "JMP", OpNull: "NULL", OpGetGlobal: "GETG", OpSetGlobal: "SETG", OpSetSelGlobal: "SETSG", OpArray: "ARR", OpMap: "MAP", OpError: "ERROR", OpImmutable: "IMMUT", OpIndex: "INDEX", OpSliceIndex: "SLICE", OpCall: "CALL", OpReturn: "RET", OpGetLocal: "GETL", OpSetLocal: "SETL", OpDefineLocal: "DEFL", OpSetSelLocal: "SETSL", OpGetBuiltin: "BUILTIN", OpClosure: "CLOSURE", OpGetFreePtr: "GETFP", OpGetFree: "GETF", OpSetFree: "SETF", OpGetLocalPtr: "GETLP", OpSetSelFree: "SETSF", OpIteratorInit: "ITER", OpIteratorNext: "ITNXT", OpIteratorKey: "ITKEY", OpIteratorValue: "ITVAL", OpBinaryOp: "BINARYOP", OpSuspend: "SUSPEND", } // OpcodeOperands is the number of operands. var OpcodeOperands = [...][]int{ OpConstant: {2}, OpPop: {}, OpTrue: {}, OpFalse: {}, OpBComplement: {}, OpEqual: {}, OpNotEqual: {}, OpMinus: {}, OpLNot: {}, OpJumpFalsy: {4}, OpAndJump: {4}, OpOrJump: {4}, OpJump: {4}, OpNull: {}, OpGetGlobal: {2}, OpSetGlobal: {2}, OpSetSelGlobal: {2, 1}, OpArray: {2}, OpMap: {2}, OpError: {}, OpImmutable: {}, OpIndex: {}, OpSliceIndex: {}, OpCall: {1, 1}, OpReturn: {1}, OpGetLocal: {1}, OpSetLocal: {1}, OpDefineLocal: {1}, OpSetSelLocal: {1, 1}, OpGetBuiltin: {1}, OpClosure: {2, 1}, OpGetFreePtr: {1}, OpGetFree: {1}, OpSetFree: {1}, OpGetLocalPtr: {1}, OpSetSelFree: {1, 1}, OpIteratorInit: {}, OpIteratorNext: {}, OpIteratorKey: {}, OpIteratorValue: {}, OpBinaryOp: {1}, OpSuspend: {}, } // ReadOperands reads operands from the bytecode. func ReadOperands(numOperands []int, ins []byte) (operands []int, offset int) { for _, width := range numOperands { switch width { case 1: operands = append(operands, int(ins[offset])) case 2: operands = append(operands, int(ins[offset+1])|int(ins[offset])<<8) case 4: operands = append(operands, int(ins[offset+3])|int(ins[offset+2])<<8|int(ins[offset+1])<<16|int(ins[offset])<<24) } offset += width } return } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/parser.go0000644000175000017500000005534714607001502020277 0ustar00maythammaythampackage parser import ( "fmt" "io" "sort" "strconv" "github.com/d5/tengo/v2/token" ) type bailout struct{} var stmtStart = map[token.Token]bool{ token.Break: true, token.Continue: true, token.For: true, token.If: true, token.Return: true, token.Export: true, } // Error represents a parser error. type Error struct { Pos SourceFilePos Msg string } func (e Error) Error() string { if e.Pos.Filename != "" || e.Pos.IsValid() { return fmt.Sprintf("Parse Error: %s\n\tat %s", e.Msg, e.Pos) } return fmt.Sprintf("Parse Error: %s", e.Msg) } // ErrorList is a collection of parser errors. type ErrorList []*Error // Add adds a new parser error to the collection. func (p *ErrorList) Add(pos SourceFilePos, msg string) { *p = append(*p, &Error{pos, msg}) } // Len returns the number of elements in the collection. func (p ErrorList) Len() int { return len(p) } func (p ErrorList) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func (p ErrorList) Less(i, j int) bool { e := &p[i].Pos f := &p[j].Pos if e.Filename != f.Filename { return e.Filename < f.Filename } if e.Line != f.Line { return e.Line < f.Line } if e.Column != f.Column { return e.Column < f.Column } return p[i].Msg < p[j].Msg } // Sort sorts the collection. func (p ErrorList) Sort() { sort.Sort(p) } func (p ErrorList) Error() string { switch len(p) { case 0: return "no errors" case 1: return p[0].Error() } return fmt.Sprintf("%s (and %d more errors)", p[0], len(p)-1) } // Err returns an error. func (p ErrorList) Err() error { if len(p) == 0 { return nil } return p } // Parser parses the Tengo source files. It's based on Go's parser // implementation. type Parser struct { file *SourceFile errors ErrorList scanner *Scanner pos Pos token token.Token tokenLit string exprLevel int // < 0: in control clause, >= 0: in expression syncPos Pos // last sync position syncCount int // number of advance calls without progress trace bool indent int traceOut io.Writer } // NewParser creates a Parser. func NewParser(file *SourceFile, src []byte, trace io.Writer) *Parser { p := &Parser{ file: file, trace: trace != nil, traceOut: trace, } p.scanner = NewScanner(p.file, src, func(pos SourceFilePos, msg string) { p.errors.Add(pos, msg) }, 0) p.next() return p } // ParseFile parses the source and returns an AST file unit. func (p *Parser) ParseFile() (file *File, err error) { defer func() { if e := recover(); e != nil { if _, ok := e.(bailout); !ok { panic(e) } } p.errors.Sort() err = p.errors.Err() }() if p.trace { defer untracep(tracep(p, "File")) } if p.errors.Len() > 0 { return nil, p.errors.Err() } stmts := p.parseStmtList() p.expect(token.EOF) if p.errors.Len() > 0 { return nil, p.errors.Err() } file = &File{ InputFile: p.file, Stmts: stmts, } return } func (p *Parser) parseExpr() Expr { if p.trace { defer untracep(tracep(p, "Expression")) } expr := p.parseBinaryExpr(token.LowestPrec + 1) // ternary conditional expression if p.token == token.Question { return p.parseCondExpr(expr) } return expr } func (p *Parser) parseBinaryExpr(prec1 int) Expr { if p.trace { defer untracep(tracep(p, "BinaryExpression")) } x := p.parseUnaryExpr() for { op, prec := p.token, p.token.Precedence() if prec < prec1 { return x } pos := p.expect(op) y := p.parseBinaryExpr(prec + 1) x = &BinaryExpr{ LHS: x, RHS: y, Token: op, TokenPos: pos, } } } func (p *Parser) parseCondExpr(cond Expr) Expr { questionPos := p.expect(token.Question) trueExpr := p.parseExpr() colonPos := p.expect(token.Colon) falseExpr := p.parseExpr() return &CondExpr{ Cond: cond, True: trueExpr, False: falseExpr, QuestionPos: questionPos, ColonPos: colonPos, } } func (p *Parser) parseUnaryExpr() Expr { if p.trace { defer untracep(tracep(p, "UnaryExpression")) } switch p.token { case token.Add, token.Sub, token.Not, token.Xor: pos, op := p.pos, p.token p.next() x := p.parseUnaryExpr() return &UnaryExpr{ Token: op, TokenPos: pos, Expr: x, } } return p.parsePrimaryExpr() } func (p *Parser) parsePrimaryExpr() Expr { if p.trace { defer untracep(tracep(p, "PrimaryExpression")) } x := p.parseOperand() L: for { switch p.token { case token.Period: p.next() switch p.token { case token.Ident: x = p.parseSelector(x) default: pos := p.pos p.errorExpected(pos, "selector") p.advance(stmtStart) return &BadExpr{From: pos, To: p.pos} } case token.LBrack: x = p.parseIndexOrSlice(x) case token.LParen: x = p.parseCall(x) default: break L } } return x } func (p *Parser) parseCall(x Expr) *CallExpr { if p.trace { defer untracep(tracep(p, "Call")) } 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 p.next() } if !p.expectComma(token.RParen, "call argument") { break } } p.exprLevel-- rparen := p.expect(token.RParen) return &CallExpr{ Func: x, LParen: lparen, RParen: rparen, Ellipsis: ellipsis, Args: list, } } func (p *Parser) expectComma(closing token.Token, want string) bool { if p.token == token.Comma { p.next() if p.token == closing { p.errorExpected(p.pos, want) return false } return true } if p.token == token.Semicolon && p.tokenLit == "\n" { p.next() } return false } func (p *Parser) parseIndexOrSlice(x Expr) Expr { if p.trace { defer untracep(tracep(p, "IndexOrSlice")) } lbrack := p.expect(token.LBrack) p.exprLevel++ var index [2]Expr if p.token != token.Colon { index[0] = p.parseExpr() } numColons := 0 if p.token == token.Colon { numColons++ p.next() if p.token != token.RBrack && p.token != token.EOF { index[1] = p.parseExpr() } } p.exprLevel-- rbrack := p.expect(token.RBrack) if numColons > 0 { // slice expression return &SliceExpr{ Expr: x, LBrack: lbrack, RBrack: rbrack, Low: index[0], High: index[1], } } return &IndexExpr{ Expr: x, LBrack: lbrack, RBrack: rbrack, Index: index[0], } } func (p *Parser) parseSelector(x Expr) Expr { if p.trace { defer untracep(tracep(p, "Selector")) } sel := p.parseIdent() return &SelectorExpr{Expr: x, Sel: &StringLit{ Value: sel.Name, ValuePos: sel.NamePos, Literal: sel.Name, }} } func (p *Parser) parseOperand() Expr { if p.trace { defer untracep(tracep(p, "Operand")) } switch p.token { case token.Ident: return p.parseIdent() case token.Int: v, err := strconv.ParseInt(p.tokenLit, 0, 64) if err == strconv.ErrRange { p.error(p.pos, "number out of range") } else if err != nil { p.error(p.pos, "invalid integer") } x := &IntLit{ Value: v, ValuePos: p.pos, Literal: p.tokenLit, } p.next() return x case token.Float: v, err := strconv.ParseFloat(p.tokenLit, 64) if err == strconv.ErrRange { p.error(p.pos, "number out of range") } else if err != nil { p.error(p.pos, "invalid float") } x := &FloatLit{ Value: v, ValuePos: p.pos, Literal: p.tokenLit, } p.next() return x case token.Char: return p.parseCharLit() case token.String: v, _ := strconv.Unquote(p.tokenLit) x := &StringLit{ Value: v, ValuePos: p.pos, Literal: p.tokenLit, } p.next() return x case token.True: x := &BoolLit{ Value: true, ValuePos: p.pos, Literal: p.tokenLit, } p.next() return x case token.False: x := &BoolLit{ Value: false, ValuePos: p.pos, Literal: p.tokenLit, } p.next() return x case token.Undefined: x := &UndefinedLit{TokenPos: p.pos} p.next() return x case token.Import: return p.parseImportExpr() case token.LParen: lparen := p.pos p.next() p.exprLevel++ x := p.parseExpr() p.exprLevel-- rparen := p.expect(token.RParen) return &ParenExpr{ LParen: lparen, Expr: x, RParen: rparen, } case token.LBrack: // array literal return p.parseArrayLit() case token.LBrace: // map literal return p.parseMapLit() case token.Func: // function literal return p.parseFuncLit() case token.Error: // error expression return p.parseErrorExpr() case token.Immutable: // immutable expression return p.parseImmutableExpr() default: p.errorExpected(p.pos, "operand") } pos := p.pos p.advance(stmtStart) return &BadExpr{From: pos, To: p.pos} } func (p *Parser) parseImportExpr() Expr { pos := p.pos p.next() p.expect(token.LParen) if p.token != token.String { p.errorExpected(p.pos, "module name") p.advance(stmtStart) return &BadExpr{From: pos, To: p.pos} } // module name moduleName, _ := strconv.Unquote(p.tokenLit) expr := &ImportExpr{ ModuleName: moduleName, Token: token.Import, TokenPos: pos, } p.next() p.expect(token.RParen) return expr } func (p *Parser) parseCharLit() Expr { if n := len(p.tokenLit); n >= 3 { code, _, _, err := strconv.UnquoteChar(p.tokenLit[1:n-1], '\'') if err == nil { x := &CharLit{ Value: code, ValuePos: p.pos, Literal: p.tokenLit, } p.next() return x } } pos := p.pos p.error(pos, "illegal char literal") p.next() return &BadExpr{ From: pos, To: p.pos, } } func (p *Parser) parseFuncLit() Expr { if p.trace { defer untracep(tracep(p, "FuncLit")) } typ := p.parseFuncType() p.exprLevel++ body := p.parseBody() p.exprLevel-- return &FuncLit{ Type: typ, Body: body, } } func (p *Parser) parseArrayLit() Expr { if p.trace { defer untracep(tracep(p, "ArrayLit")) } lbrack := p.expect(token.LBrack) p.exprLevel++ var elements []Expr for p.token != token.RBrack && p.token != token.EOF { elements = append(elements, p.parseExpr()) if !p.expectComma(token.RBrack, "array element") { break } } p.exprLevel-- rbrack := p.expect(token.RBrack) return &ArrayLit{ Elements: elements, LBrack: lbrack, RBrack: rbrack, } } func (p *Parser) parseErrorExpr() Expr { pos := p.pos p.next() lparen := p.expect(token.LParen) value := p.parseExpr() rparen := p.expect(token.RParen) return &ErrorExpr{ ErrorPos: pos, Expr: value, LParen: lparen, RParen: rparen, } } func (p *Parser) parseImmutableExpr() Expr { pos := p.pos p.next() lparen := p.expect(token.LParen) value := p.parseExpr() rparen := p.expect(token.RParen) return &ImmutableExpr{ ErrorPos: pos, Expr: value, LParen: lparen, RParen: rparen, } } func (p *Parser) parseFuncType() *FuncType { if p.trace { defer untracep(tracep(p, "FuncType")) } pos := p.expect(token.Func) params := p.parseIdentList() return &FuncType{ FuncPos: pos, Params: params, } } func (p *Parser) parseBody() *BlockStmt { if p.trace { defer untracep(tracep(p, "Body")) } lbrace := p.expect(token.LBrace) list := p.parseStmtList() rbrace := p.expect(token.RBrace) return &BlockStmt{ LBrace: lbrace, RBrace: rbrace, Stmts: list, } } func (p *Parser) parseStmtList() (list []Stmt) { if p.trace { defer untracep(tracep(p, "StatementList")) } for p.token != token.RBrace && p.token != token.EOF { list = append(list, p.parseStmt()) } return } func (p *Parser) parseIdent() *Ident { pos := p.pos name := "_" if p.token == token.Ident { name = p.tokenLit p.next() } else { p.expect(token.Ident) } return &Ident{ NamePos: pos, Name: name, } } func (p *Parser) parseIdentList() *IdentList { if p.trace { defer untracep(tracep(p, "IdentList")) } var params []*Ident lparen := p.expect(token.LParen) isVarArgs := false if p.token != token.RParen { if p.token == token.Ellipsis { isVarArgs = true p.next() } params = append(params, p.parseIdent()) for !isVarArgs && p.token == token.Comma { p.next() if p.token == token.Ellipsis { isVarArgs = true p.next() } params = append(params, p.parseIdent()) } } rparen := p.expect(token.RParen) return &IdentList{ LParen: lparen, RParen: rparen, VarArgs: isVarArgs, List: params, } } func (p *Parser) parseStmt() (stmt Stmt) { if p.trace { defer untracep(tracep(p, "Statement")) } switch p.token { 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: s := p.parseSimpleStmt(false) p.expectSemi() return s case token.Return: return p.parseReturnStmt() case token.Export: return p.parseExportStmt() case token.If: return p.parseIfStmt() case token.For: return p.parseForStmt() case token.Break, token.Continue: return p.parseBranchStmt(p.token) case token.Semicolon: s := &EmptyStmt{Semicolon: p.pos, Implicit: p.tokenLit == "\n"} p.next() return s case token.RBrace: // semicolon may be omitted before a closing "}" return &EmptyStmt{Semicolon: p.pos, Implicit: true} default: pos := p.pos p.errorExpected(pos, "statement") p.advance(stmtStart) return &BadStmt{From: pos, To: p.pos} } } func (p *Parser) parseForStmt() Stmt { if p.trace { defer untracep(tracep(p, "ForStmt")) } pos := p.expect(token.For) // for {} if p.token == token.LBrace { body := p.parseBlockStmt() p.expectSemi() return &ForStmt{ ForPos: pos, Body: body, } } prevLevel := p.exprLevel p.exprLevel = -1 var s1 Stmt if p.token != token.Semicolon { // skipping init s1 = p.parseSimpleStmt(true) } // for _ in seq {} or // for value in seq {} or // for key, value in seq {} if forInStmt, isForIn := s1.(*ForInStmt); isForIn { forInStmt.ForPos = pos p.exprLevel = prevLevel forInStmt.Body = p.parseBlockStmt() p.expectSemi() return forInStmt } // for init; cond; post {} var s2, s3 Stmt if p.token == token.Semicolon { p.next() if p.token != token.Semicolon { s2 = p.parseSimpleStmt(false) // cond } p.expect(token.Semicolon) if p.token != token.LBrace { s3 = p.parseSimpleStmt(false) // post } } else { // for cond {} s2 = s1 s1 = nil } // body p.exprLevel = prevLevel body := p.parseBlockStmt() p.expectSemi() cond := p.makeExpr(s2, "condition expression") return &ForStmt{ ForPos: pos, Init: s1, Cond: cond, Post: s3, Body: body, } } func (p *Parser) parseBranchStmt(tok token.Token) Stmt { if p.trace { defer untracep(tracep(p, "BranchStmt")) } pos := p.expect(tok) var label *Ident if p.token == token.Ident { label = p.parseIdent() } p.expectSemi() return &BranchStmt{ Token: tok, TokenPos: pos, Label: label, } } func (p *Parser) parseIfStmt() Stmt { if p.trace { defer untracep(tracep(p, "IfStmt")) } pos := p.expect(token.If) init, cond := p.parseIfHeader() body := p.parseBlockStmt() var elseStmt Stmt if p.token == token.Else { p.next() switch p.token { case token.If: elseStmt = p.parseIfStmt() case token.LBrace: elseStmt = p.parseBlockStmt() p.expectSemi() default: p.errorExpected(p.pos, "if or {") elseStmt = &BadStmt{From: p.pos, To: p.pos} } } else { p.expectSemi() } return &IfStmt{ IfPos: pos, Init: init, Cond: cond, Body: body, Else: elseStmt, } } func (p *Parser) parseBlockStmt() *BlockStmt { if p.trace { defer untracep(tracep(p, "BlockStmt")) } lbrace := p.expect(token.LBrace) list := p.parseStmtList() rbrace := p.expect(token.RBrace) return &BlockStmt{ LBrace: lbrace, RBrace: rbrace, Stmts: list, } } func (p *Parser) parseIfHeader() (init Stmt, cond Expr) { if p.token == token.LBrace { p.error(p.pos, "missing condition in if statement") cond = &BadExpr{From: p.pos, To: p.pos} return } outer := p.exprLevel p.exprLevel = -1 if p.token == token.Semicolon { p.error(p.pos, "missing init in if statement") return } init = p.parseSimpleStmt(false) var condStmt Stmt if p.token == token.LBrace { condStmt = init init = nil } else if p.token == token.Semicolon { p.next() condStmt = p.parseSimpleStmt(false) } else { p.error(p.pos, "missing condition in if statement") } if condStmt != nil { cond = p.makeExpr(condStmt, "boolean expression") } if cond == nil { cond = &BadExpr{From: p.pos, To: p.pos} } p.exprLevel = outer return } func (p *Parser) makeExpr(s Stmt, want string) Expr { if s == nil { return nil } if es, isExpr := s.(*ExprStmt); isExpr { return es.Expr } found := "simple statement" if _, isAss := s.(*AssignStmt); isAss { found = "assignment" } p.error(s.Pos(), fmt.Sprintf("expected %s, found %s", want, found)) return &BadExpr{From: s.Pos(), To: p.safePos(s.End())} } func (p *Parser) parseReturnStmt() Stmt { if p.trace { defer untracep(tracep(p, "ReturnStmt")) } pos := p.pos p.expect(token.Return) var x Expr if p.token != token.Semicolon && p.token != token.RBrace { x = p.parseExpr() } p.expectSemi() return &ReturnStmt{ ReturnPos: pos, Result: x, } } func (p *Parser) parseExportStmt() Stmt { if p.trace { defer untracep(tracep(p, "ExportStmt")) } pos := p.pos p.expect(token.Export) x := p.parseExpr() p.expectSemi() return &ExportStmt{ ExportPos: pos, Result: x, } } func (p *Parser) parseSimpleStmt(forIn bool) Stmt { if p.trace { defer untracep(tracep(p, "SimpleStmt")) } x := p.parseExprList() switch p.token { case token.Assign, token.Define: // assignment statement pos, tok := p.pos, p.token p.next() y := p.parseExprList() return &AssignStmt{ LHS: x, RHS: y, Token: tok, TokenPos: pos, } case token.In: if forIn { p.next() y := p.parseExpr() var key, value *Ident var ok bool switch len(x) { case 1: key = &Ident{Name: "_", NamePos: x[0].Pos()} value, ok = x[0].(*Ident) if !ok { p.errorExpected(x[0].Pos(), "identifier") value = &Ident{Name: "_", NamePos: x[0].Pos()} } case 2: key, ok = x[0].(*Ident) if !ok { p.errorExpected(x[0].Pos(), "identifier") key = &Ident{Name: "_", NamePos: x[0].Pos()} } value, ok = x[1].(*Ident) if !ok { p.errorExpected(x[1].Pos(), "identifier") value = &Ident{Name: "_", NamePos: x[1].Pos()} } } return &ForInStmt{ Key: key, Value: value, Iterable: y, } } } if len(x) > 1 { p.errorExpected(x[0].Pos(), "1 expression") // continue with first expression } switch p.token { case token.Define, token.AddAssign, token.SubAssign, token.MulAssign, token.QuoAssign, token.RemAssign, token.AndAssign, token.OrAssign, token.XorAssign, token.ShlAssign, token.ShrAssign, token.AndNotAssign: pos, tok := p.pos, p.token p.next() y := p.parseExpr() return &AssignStmt{ LHS: []Expr{x[0]}, RHS: []Expr{y}, Token: tok, TokenPos: pos, } case token.Inc, token.Dec: // increment or decrement statement s := &IncDecStmt{Expr: x[0], Token: p.token, TokenPos: p.pos} p.next() return s } return &ExprStmt{Expr: x[0]} } func (p *Parser) parseExprList() (list []Expr) { if p.trace { defer untracep(tracep(p, "ExpressionList")) } list = append(list, p.parseExpr()) for p.token == token.Comma { p.next() list = append(list, p.parseExpr()) } return } func (p *Parser) parseMapElementLit() *MapElementLit { if p.trace { defer untracep(tracep(p, "MapElementLit")) } pos := p.pos name := "_" if p.token == token.Ident { name = p.tokenLit } else if p.token == token.String { v, _ := strconv.Unquote(p.tokenLit) name = v } else { p.errorExpected(pos, "map key") } p.next() colonPos := p.expect(token.Colon) valueExpr := p.parseExpr() return &MapElementLit{ Key: name, KeyPos: pos, ColonPos: colonPos, Value: valueExpr, } } func (p *Parser) parseMapLit() *MapLit { if p.trace { defer untracep(tracep(p, "MapLit")) } lbrace := p.expect(token.LBrace) p.exprLevel++ var elements []*MapElementLit for p.token != token.RBrace && p.token != token.EOF { elements = append(elements, p.parseMapElementLit()) if !p.expectComma(token.RBrace, "map element") { break } } p.exprLevel-- rbrace := p.expect(token.RBrace) return &MapLit{ LBrace: lbrace, RBrace: rbrace, Elements: elements, } } func (p *Parser) expect(token token.Token) Pos { pos := p.pos if p.token != token { p.errorExpected(pos, "'"+token.String()+"'") } p.next() return pos } func (p *Parser) expectSemi() { switch p.token { case token.RParen, token.RBrace: // semicolon is optional before a closing ')' or '}' case token.Comma: // permit a ',' instead of a ';' but complain p.errorExpected(p.pos, "';'") fallthrough case token.Semicolon: p.next() default: p.errorExpected(p.pos, "';'") p.advance(stmtStart) } } func (p *Parser) advance(to map[token.Token]bool) { for ; p.token != token.EOF; p.next() { if to[p.token] { if p.pos == p.syncPos && p.syncCount < 10 { p.syncCount++ return } if p.pos > p.syncPos { p.syncPos = p.pos p.syncCount = 0 return } } } } func (p *Parser) error(pos Pos, msg string) { filePos := p.file.Position(pos) n := len(p.errors) if n > 0 && p.errors[n-1].Pos.Line == filePos.Line { // discard errors reported on the same line return } if n > 10 { // too many errors; terminate early panic(bailout{}) } p.errors.Add(filePos, msg) } func (p *Parser) errorExpected(pos Pos, msg string) { msg = "expected " + msg if pos == p.pos { // error happened at the current position: provide more specific switch { case p.token == token.Semicolon && p.tokenLit == "\n": msg += ", found newline" case p.token.IsLiteral(): msg += ", found " + p.tokenLit default: msg += ", found '" + p.token.String() + "'" } } p.error(pos, msg) } func (p *Parser) next() { if p.trace && p.pos.IsValid() { s := p.token.String() switch { case p.token.IsLiteral(): p.printTrace(s, p.tokenLit) case p.token.IsOperator(), p.token.IsKeyword(): p.printTrace(`"` + s + `"`) default: p.printTrace(s) } } p.token, p.tokenLit, p.pos = p.scanner.Scan() } func (p *Parser) printTrace(a ...interface{}) { const ( dots = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . " n = len(dots) ) filePos := p.file.Position(p.pos) _, _ = fmt.Fprintf(p.traceOut, "%5d: %5d:%3d: ", p.pos, filePos.Line, filePos.Column) i := 2 * p.indent for i > n { _, _ = fmt.Fprint(p.traceOut, dots) i -= n } _, _ = fmt.Fprint(p.traceOut, dots[0:i]) _, _ = fmt.Fprintln(p.traceOut, a...) } func (p *Parser) safePos(pos Pos) Pos { fileBase := p.file.Base fileSize := p.file.Size if int(pos) < fileBase || int(pos) > fileBase+fileSize { return Pos(fileBase + fileSize) } return pos } func tracep(p *Parser, msg string) *Parser { p.printTrace(msg, "(") p.indent++ return p } func untracep(p *Parser) { p.indent-- p.printTrace(")") } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/parser_test.go0000644000175000017500000014500214607001502021322 0ustar00maythammaythampackage parser_test import ( "fmt" "io" "reflect" "strconv" "strings" "testing" . "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/require" "github.com/d5/tengo/v2/token" ) func TestParserError(t *testing.T) { err := &Error{Pos: SourceFilePos{ Offset: 10, Line: 1, Column: 10, }, Msg: "test"} require.Equal(t, "Parse Error: test\n\tat 1:10", err.Error()) } func TestParserErrorList(t *testing.T) { var list ErrorList list.Add(SourceFilePos{Offset: 20, Line: 2, Column: 10}, "error 2") list.Add(SourceFilePos{Offset: 30, Line: 3, Column: 10}, "error 3") list.Add(SourceFilePos{Offset: 10, Line: 1, Column: 10}, "error 1") list.Sort() require.Equal(t, "Parse Error: error 1\n\tat 1:10 (and 2 more errors)", list.Error()) } func TestParseArray(t *testing.T) { expectParse(t, "[1, 2, 3]", func(p pfn) []Stmt { return stmts( exprStmt( arrayLit(p(1, 1), p(1, 9), intLit(1, p(1, 2)), intLit(2, p(1, 5)), intLit(3, p(1, 8))))) }) expectParse(t, ` [ 1, 2, 3 ]`, func(p pfn) []Stmt { return stmts( exprStmt( arrayLit(p(2, 1), p(6, 1), intLit(1, p(3, 2)), intLit(2, p(4, 2)), intLit(3, p(5, 2))))) }) expectParse(t, ` [ 1, 2, 3 ]`, func(p pfn) []Stmt { return stmts( exprStmt( arrayLit(p(2, 1), p(7, 1), intLit(1, p(3, 2)), intLit(2, p(4, 2)), intLit(3, p(5, 2))))) }) expectParse(t, `[1, "foo", 12.34]`, func(p pfn) []Stmt { return stmts( exprStmt( arrayLit(p(1, 1), p(1, 17), intLit(1, p(1, 2)), stringLit("foo", p(1, 5)), floatLit(12.34, p(1, 12))))) }) expectParse(t, "a = [1, 2, 3]", func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(arrayLit(p(1, 5), p(1, 13), intLit(1, p(1, 6)), intLit(2, p(1, 9)), intLit(3, p(1, 12)))), token.Assign, p(1, 3))) }) expectParse(t, "a = [1 + 2, b * 4, [4, c]]", func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(arrayLit(p(1, 5), p(1, 26), binaryExpr( intLit(1, p(1, 6)), intLit(2, p(1, 10)), token.Add, p(1, 8)), binaryExpr( ident("b", p(1, 13)), intLit(4, p(1, 17)), token.Mul, p(1, 15)), arrayLit(p(1, 20), p(1, 25), intLit(4, p(1, 21)), ident("c", p(1, 24))))), token.Assign, p(1, 3))) }) expectParseError(t, `[1, 2, 3,]`) expectParseError(t, ` [ 1, 2, 3, ]`) expectParseError(t, ` [ 1, 2, 3, ]`) expectParseError(t, `[1, 2, 3, ,]`) } func TestParseAssignment(t *testing.T) { expectParse(t, "a = 5", func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(intLit(5, p(1, 5))), token.Assign, p(1, 3))) }) expectParse(t, "a := 5", func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(intLit(5, p(1, 6))), token.Define, p(1, 3))) }) expectParse(t, "a, b = 5, 10", func(p pfn) []Stmt { return stmts( assignStmt( exprs( ident("a", p(1, 1)), ident("b", p(1, 4))), exprs( intLit(5, p(1, 8)), intLit(10, p(1, 11))), token.Assign, p(1, 6))) }) expectParse(t, "a, b := 5, 10", func(p pfn) []Stmt { return stmts( assignStmt( exprs( ident("a", p(1, 1)), ident("b", p(1, 4))), exprs( intLit(5, p(1, 9)), intLit(10, p(1, 12))), token.Define, p(1, 6))) }) expectParse(t, "a, b = a + 2, b - 8", func(p pfn) []Stmt { return stmts( assignStmt( exprs( ident("a", p(1, 1)), ident("b", p(1, 4))), exprs( binaryExpr( ident("a", p(1, 8)), intLit(2, p(1, 12)), token.Add, p(1, 10)), binaryExpr( ident("b", p(1, 15)), intLit(8, p(1, 19)), token.Sub, p(1, 17))), token.Assign, p(1, 6))) }) expectParse(t, "a = [1, 2, 3]", func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(arrayLit(p(1, 5), p(1, 13), intLit(1, p(1, 6)), intLit(2, p(1, 9)), intLit(3, p(1, 12)))), token.Assign, p(1, 3))) }) expectParse(t, "a = [1 + 2, b * 4, [4, c]]", func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(arrayLit(p(1, 5), p(1, 26), binaryExpr( intLit(1, p(1, 6)), intLit(2, p(1, 10)), token.Add, p(1, 8)), binaryExpr( ident("b", p(1, 13)), intLit(4, p(1, 17)), token.Mul, p(1, 15)), arrayLit(p(1, 20), p(1, 25), intLit(4, p(1, 21)), ident("c", p(1, 24))))), token.Assign, p(1, 3))) }) expectParse(t, "a += 5", func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(intLit(5, p(1, 6))), token.AddAssign, p(1, 3))) }) expectParse(t, "a *= 5 + 10", func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs( binaryExpr( intLit(5, p(1, 6)), intLit(10, p(1, 10)), token.Add, p(1, 8))), token.MulAssign, p(1, 3))) }) } func TestParseBoolean(t *testing.T) { expectParse(t, "true", func(p pfn) []Stmt { return stmts( exprStmt( boolLit(true, p(1, 1)))) }) expectParse(t, "false", func(p pfn) []Stmt { return stmts( exprStmt( boolLit(false, p(1, 1)))) }) expectParse(t, "true != false", func(p pfn) []Stmt { return stmts( exprStmt( binaryExpr( boolLit(true, p(1, 1)), boolLit(false, p(1, 9)), token.NotEqual, p(1, 6)))) }) expectParse(t, "!false", func(p pfn) []Stmt { return stmts( exprStmt( unaryExpr( boolLit(false, p(1, 2)), token.Not, p(1, 1)))) }) } func TestParseCall(t *testing.T) { 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))))) }) expectParse(t, "add(1, 2, v...)", func(p pfn) []Stmt { return stmts( exprStmt( callExpr( ident("add", p(1, 1)), p(1, 4), p(1, 15), p(1, 12), intLit(1, p(1, 5)), intLit(2, p(1, 8)), ident("v", p(1, 11))))) }) expectParse(t, "a = add(1, 2, 3)", func(p pfn) []Stmt { return stmts( assignStmt( exprs( ident("a", p(1, 1))), 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)))), token.Assign, p(1, 3))) }) expectParse(t, "a, b = add(1, 2, 3)", func(p pfn) []Stmt { return stmts( assignStmt( exprs( ident("a", p(1, 1)), ident("b", p(1, 4))), 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)))), token.Assign, p(1, 6))) }) expectParse(t, "add(a + 1, 2 * 1, (b + c))", func(p pfn) []Stmt { return stmts( 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( 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)") expectParseString(t, "add(a, b, 1, 2 * 3, 4 + 5, add(6, 7 * 8))", "add(a, b, 1, (2 * 3), (4 + 5), add(6, (7 * 8)))") expectParseString(t, "f1(a) + f2(b) * f3(c)", "(f1(a) + (f2(b) * f3(c)))") expectParseString(t, "(f1(a) + f2(b)) * f3(c)", "(((f1(a) + f2(b))) * f3(c))") expectParse(t, "func(a, b) { a + b }(1, 2)", func(p pfn) []Stmt { return stmts( exprStmt( callExpr( funcLit( funcType( identList( p(1, 5), p(1, 10), false, ident("a", p(1, 6)), ident("b", p(1, 9))), p(1, 1)), blockStmt( p(1, 12), p(1, 20), exprStmt( binaryExpr( ident("a", p(1, 14)), 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))))) }) expectParse(t, `a.b()`, func(p pfn) []Stmt { return stmts( exprStmt( callExpr( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), p(1, 4), p(1, 5), NoPos))) }) expectParse(t, `a.b.c()`, func(p pfn) []Stmt { return stmts( exprStmt( callExpr( selectorExpr( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), stringLit("c", p(1, 5))), p(1, 6), p(1, 7), NoPos))) }) expectParse(t, `a["b"].c()`, func(p pfn) []Stmt { return stmts( exprStmt( callExpr( selectorExpr( indexExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3)), p(1, 2), p(1, 6)), stringLit("c", p(1, 8))), p(1, 9), p(1, 10), NoPos))) }) expectParseError(t, `add(...a, 1)`) expectParseError(t, `add(a..., 1)`) expectParseError(t, `add(a..., b...)`) expectParseError(t, `add(1, a..., b...)`) expectParseError(t, `add(...)`) expectParseError(t, `add(1, ...)`) expectParseError(t, `add(1, ..., )`) expectParseError(t, `add(...a)`) } func TestParseChar(t *testing.T) { expectParse(t, `'A'`, func(p pfn) []Stmt { return stmts( exprStmt( charLit('A', 1))) }) expectParse(t, `'九'`, func(p pfn) []Stmt { return stmts( exprStmt( charLit('九', 1))) }) expectParseError(t, `''`) expectParseError(t, `'AB'`) expectParseError(t, `'A九'`) } func TestParseCondExpr(t *testing.T) { expectParse(t, "a ? b : c", func(p pfn) []Stmt { return stmts( exprStmt( condExpr( ident("a", p(1, 1)), ident("b", p(1, 5)), ident("c", p(1, 9)), p(1, 3), p(1, 7)))) }) expectParse(t, `a ? b : c`, func(p pfn) []Stmt { return stmts( exprStmt( condExpr( ident("a", p(1, 1)), ident("b", p(1, 5)), ident("c", p(1, 9)), p(1, 3), p(1, 7)))) }) expectParseString(t, `a ? b : c`, "(a ? b : c)") expectParseString(t, `a + b ? c - d : e * f`, "((a + b) ? (c - d) : (e * f))") expectParseString(t, `a == b ? c + (d / e) : f ? g : h + i`, "((a == b) ? (c + ((d / e))) : (f ? g : (h + i)))") expectParseString(t, `(a + b) ? (c - d) : (e * f)`, "(((a + b)) ? ((c - d)) : ((e * f)))") expectParseString(t, `a + (b ? c : d) - e`, "((a + ((b ? c : d))) - e)") expectParseString(t, `a ? b ? c : d : e`, "(a ? (b ? c : d) : e)") expectParseString(t, `a := b ? c : d`, "a := (b ? c : d)") expectParseString(t, `x := a ? b ? c : d : e`, "x := (a ? (b ? c : d) : e)") // ? : should be at the end of each line if it's multi-line expectParseError(t, `a ? b : c`) expectParseError(t, `a ? (b : e)`) expectParseError(t, `(a ? b) : e`) } func TestParseError(t *testing.T) { expectParse(t, `error(1234)`, func(p pfn) []Stmt { return stmts( exprStmt( errorExpr(p(1, 1), intLit(1234, p(1, 7)), p(1, 6), p(1, 11)))) }) expectParse(t, `err1 := error("some error")`, func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("err1", p(1, 1))), exprs(errorExpr(p(1, 9), stringLit("some error", p(1, 15)), p(1, 14), p(1, 27))), token.Define, p(1, 6))) }) expectParse(t, `return error("some error")`, func(p pfn) []Stmt { return stmts( returnStmt(p(1, 1), errorExpr(p(1, 8), stringLit("some error", p(1, 14)), p(1, 13), p(1, 26)))) }) expectParse(t, `return error("some" + "error")`, func(p pfn) []Stmt { return stmts( returnStmt(p(1, 1), errorExpr(p(1, 8), binaryExpr( stringLit("some", p(1, 14)), stringLit("error", p(1, 23)), token.Add, p(1, 21)), p(1, 13), p(1, 30)))) }) expectParseError(t, `error()`) // must have a value } func TestParseForIn(t *testing.T) { expectParse(t, "for x in y {}", func(p pfn) []Stmt { return stmts( forInStmt( ident("_", p(1, 5)), ident("x", p(1, 5)), ident("y", p(1, 10)), blockStmt(p(1, 12), p(1, 13)), p(1, 1))) }) expectParse(t, "for _ in y {}", func(p pfn) []Stmt { return stmts( forInStmt( ident("_", p(1, 5)), ident("_", p(1, 5)), ident("y", p(1, 10)), blockStmt(p(1, 12), p(1, 13)), p(1, 1))) }) expectParse(t, "for x in [1, 2, 3] {}", func(p pfn) []Stmt { return stmts( forInStmt( ident("_", p(1, 5)), ident("x", p(1, 5)), arrayLit( p(1, 10), p(1, 18), intLit(1, p(1, 11)), intLit(2, p(1, 14)), intLit(3, p(1, 17))), blockStmt(p(1, 20), p(1, 21)), p(1, 1))) }) expectParse(t, "for x, y in z {}", func(p pfn) []Stmt { return stmts( forInStmt( ident("x", p(1, 5)), ident("y", p(1, 8)), ident("z", p(1, 13)), blockStmt(p(1, 15), p(1, 16)), p(1, 1))) }) expectParse(t, "for x, y in {k1: 1, k2: 2} {}", func(p pfn) []Stmt { return stmts( forInStmt( ident("x", p(1, 5)), ident("y", p(1, 8)), mapLit( p(1, 13), p(1, 26), mapElementLit( "k1", p(1, 14), p(1, 16), intLit(1, p(1, 18))), mapElementLit( "k2", p(1, 21), p(1, 23), intLit(2, p(1, 25)))), blockStmt(p(1, 28), p(1, 29)), p(1, 1))) }) } func TestParseFor(t *testing.T) { expectParse(t, "for {}", func(p pfn) []Stmt { return stmts( forStmt(nil, nil, nil, blockStmt(p(1, 5), p(1, 6)), p(1, 1))) }) expectParse(t, "for a == 5 {}", func(p pfn) []Stmt { return stmts( forStmt( nil, binaryExpr( ident("a", p(1, 5)), intLit(5, p(1, 10)), token.Equal, p(1, 7)), nil, blockStmt(p(1, 12), p(1, 13)), p(1, 1))) }) expectParse(t, "for a := 0; a == 5; {}", func(p pfn) []Stmt { return stmts( forStmt( assignStmt( exprs(ident("a", p(1, 5))), exprs(intLit(0, p(1, 10))), token.Define, p(1, 7)), binaryExpr( ident("a", p(1, 13)), intLit(5, p(1, 18)), token.Equal, p(1, 15)), nil, blockStmt(p(1, 22), p(1, 23)), p(1, 1))) }) expectParse(t, "for a := 0; a < 5; a++ {}", func(p pfn) []Stmt { return stmts( forStmt( assignStmt( exprs(ident("a", p(1, 5))), exprs(intLit(0, p(1, 10))), token.Define, p(1, 7)), binaryExpr( ident("a", p(1, 13)), intLit(5, p(1, 17)), token.Less, p(1, 15)), incDecStmt( ident("a", p(1, 20)), token.Inc, p(1, 21)), blockStmt(p(1, 24), p(1, 25)), p(1, 1))) }) expectParse(t, "for ; a < 5; a++ {}", func(p pfn) []Stmt { return stmts( forStmt( nil, binaryExpr( ident("a", p(1, 7)), intLit(5, p(1, 11)), token.Less, p(1, 9)), incDecStmt( ident("a", p(1, 14)), token.Inc, p(1, 15)), blockStmt(p(1, 18), p(1, 19)), p(1, 1))) }) expectParse(t, "for a := 0; ; a++ {}", func(p pfn) []Stmt { return stmts( forStmt( assignStmt( exprs(ident("a", p(1, 5))), exprs(intLit(0, p(1, 10))), token.Define, p(1, 7)), nil, incDecStmt( ident("a", p(1, 15)), token.Inc, p(1, 16)), blockStmt(p(1, 19), p(1, 20)), p(1, 1))) }) expectParse(t, "for a == 5 && b != 4 {}", func(p pfn) []Stmt { return stmts( forStmt( nil, binaryExpr( binaryExpr( ident("a", p(1, 5)), intLit(5, p(1, 10)), token.Equal, p(1, 7)), binaryExpr( ident("b", p(1, 15)), intLit(4, p(1, 20)), token.NotEqual, p(1, 17)), token.LAnd, p(1, 12)), nil, blockStmt(p(1, 22), p(1, 23)), p(1, 1))) }) } func TestParseFunction(t *testing.T) { expectParse(t, "a = func(b, c, d) { return d }", func(p pfn) []Stmt { return stmts( assignStmt( exprs( ident("a", p(1, 1))), exprs( funcLit( funcType( identList(p(1, 9), p(1, 17), false, ident("b", p(1, 10)), ident("c", p(1, 13)), ident("d", p(1, 16))), p(1, 5)), blockStmt(p(1, 19), p(1, 30), returnStmt(p(1, 21), ident("d", p(1, 28)))))), token.Assign, p(1, 3))) }) } func TestParseVariadicFunction(t *testing.T) { expectParse(t, "a = func(...args) { return args }", func(p pfn) []Stmt { return stmts( assignStmt( exprs( ident("a", p(1, 1))), exprs( funcLit( funcType( identList( p(1, 9), p(1, 17), true, ident("args", p(1, 13)), ), p(1, 5)), blockStmt(p(1, 19), p(1, 33), returnStmt(p(1, 21), ident("args", p(1, 28)), ), ), ), ), token.Assign, p(1, 3))) }) } func TestParseVariadicFunctionWithArgs(t *testing.T) { expectParse(t, "a = func(x, y, ...z) { return z }", func(p pfn) []Stmt { return stmts( assignStmt( exprs( ident("a", p(1, 1))), exprs( funcLit( funcType( identList( p(1, 9), p(1, 20), true, ident("x", p(1, 10)), ident("y", p(1, 13)), ident("z", p(1, 19)), ), p(1, 5)), blockStmt(p(1, 22), p(1, 33), returnStmt(p(1, 24), ident("z", p(1, 31)), ), ), ), ), token.Assign, p(1, 3))) }) expectParseError(t, "a = func(x, y, ...z, invalid) { return z }") expectParseError(t, "a = func(...args, invalid) { return args }") } func TestParseIf(t *testing.T) { expectParse(t, "if a == 5 {}", func(p pfn) []Stmt { return stmts( ifStmt( nil, binaryExpr( ident("a", p(1, 4)), intLit(5, p(1, 9)), token.Equal, p(1, 6)), blockStmt( p(1, 11), p(1, 12)), nil, p(1, 1))) }) expectParse(t, "if a == 5 && b != 3 {}", func(p pfn) []Stmt { return stmts( ifStmt( nil, binaryExpr( binaryExpr( ident("a", p(1, 4)), intLit(5, p(1, 9)), token.Equal, p(1, 6)), binaryExpr( ident("b", p(1, 14)), intLit(3, p(1, 19)), token.NotEqual, p(1, 16)), token.LAnd, p(1, 11)), blockStmt( p(1, 21), p(1, 22)), nil, p(1, 1))) }) expectParse(t, "if a == 5 { a = 3; a = 1 }", func(p pfn) []Stmt { return stmts( ifStmt( nil, binaryExpr( ident("a", p(1, 4)), intLit(5, p(1, 9)), token.Equal, p(1, 6)), blockStmt( p(1, 11), p(1, 26), assignStmt( exprs(ident("a", p(1, 13))), exprs(intLit(3, p(1, 17))), token.Assign, p(1, 15)), assignStmt( exprs(ident("a", p(1, 20))), exprs(intLit(1, p(1, 24))), token.Assign, p(1, 22))), nil, p(1, 1))) }) expectParse(t, "if a == 5 { a = 3; a = 1 } else { a = 2; a = 4 }", func(p pfn) []Stmt { return stmts( ifStmt( nil, binaryExpr( ident("a", p(1, 4)), intLit(5, p(1, 9)), token.Equal, p(1, 6)), blockStmt( p(1, 11), p(1, 26), assignStmt( exprs(ident("a", p(1, 13))), exprs(intLit(3, p(1, 17))), token.Assign, p(1, 15)), assignStmt( exprs(ident("a", p(1, 20))), exprs(intLit(1, p(1, 24))), token.Assign, p(1, 22))), blockStmt( p(1, 33), p(1, 48), assignStmt( exprs(ident("a", p(1, 35))), exprs(intLit(2, p(1, 39))), token.Assign, p(1, 37)), assignStmt( exprs(ident("a", p(1, 42))), exprs(intLit(4, p(1, 46))), token.Assign, p(1, 44))), p(1, 1))) }) expectParse(t, ` if a == 5 { b = 3 c = 1 } else if d == 3 { e = 8 f = 3 } else { g = 2 h = 4 }`, func(p pfn) []Stmt { return stmts( ifStmt( nil, binaryExpr( ident("a", p(2, 4)), intLit(5, p(2, 9)), token.Equal, p(2, 6)), blockStmt( p(2, 11), p(5, 1), assignStmt( exprs(ident("b", p(3, 2))), exprs(intLit(3, p(3, 6))), token.Assign, p(3, 4)), assignStmt( exprs(ident("c", p(4, 2))), exprs(intLit(1, p(4, 6))), token.Assign, p(4, 4))), ifStmt( nil, binaryExpr( ident("d", p(5, 11)), intLit(3, p(5, 16)), token.Equal, p(5, 13)), blockStmt( p(5, 18), p(8, 1), assignStmt( exprs(ident("e", p(6, 2))), exprs(intLit(8, p(6, 6))), token.Assign, p(6, 4)), assignStmt( exprs(ident("f", p(7, 2))), exprs(intLit(3, p(7, 6))), token.Assign, p(7, 4))), blockStmt( p(8, 8), p(11, 1), assignStmt( exprs(ident("g", p(9, 2))), exprs(intLit(2, p(9, 6))), token.Assign, p(9, 4)), assignStmt( exprs(ident("h", p(10, 2))), exprs(intLit(4, p(10, 6))), token.Assign, p(10, 4))), p(5, 8)), p(2, 1))) }) expectParse(t, "if a := 3; a < b {}", func(p pfn) []Stmt { return stmts( ifStmt( assignStmt( exprs(ident("a", p(1, 4))), exprs(intLit(3, p(1, 9))), token.Define, p(1, 6)), binaryExpr( ident("a", p(1, 12)), ident("b", p(1, 16)), token.Less, p(1, 14)), blockStmt( p(1, 18), p(1, 19)), nil, p(1, 1))) }) expectParse(t, "if a++; a < b {}", func(p pfn) []Stmt { return stmts( ifStmt( incDecStmt(ident("a", p(1, 4)), token.Inc, p(1, 5)), binaryExpr( ident("a", p(1, 9)), ident("b", p(1, 13)), token.Less, p(1, 11)), blockStmt( p(1, 15), p(1, 16)), nil, p(1, 1))) }) expectParseError(t, `if {}`) expectParseError(t, `if a == b { } else a != b { }`) expectParseError(t, `if a == b { } else if { }`) expectParseError(t, `else { }`) expectParseError(t, `if ; {}`) expectParseError(t, `if a := 3; {}`) expectParseError(t, `if ; a < 3 {}`) } func TestParseImport(t *testing.T) { expectParse(t, `a := import("mod1")`, func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(importExpr("mod1", p(1, 6))), token.Define, p(1, 3))) }) expectParse(t, `import("mod1").var1`, func(p pfn) []Stmt { return stmts( exprStmt( selectorExpr( importExpr("mod1", p(1, 1)), stringLit("var1", p(1, 16))))) }) expectParse(t, `import("mod1").func1()`, func(p pfn) []Stmt { return stmts( exprStmt( callExpr( selectorExpr( importExpr("mod1", p(1, 1)), stringLit("func1", p(1, 16))), p(1, 21), p(1, 22), NoPos))) }) expectParse(t, `for x, y in import("mod1") {}`, func(p pfn) []Stmt { return stmts( forInStmt( ident("x", p(1, 5)), ident("y", p(1, 8)), importExpr("mod1", p(1, 13)), blockStmt(p(1, 28), p(1, 29)), p(1, 1))) }) } func TestParseIndex(t *testing.T) { expectParse(t, "[1, 2, 3][1]", func(p pfn) []Stmt { return stmts( exprStmt( indexExpr( arrayLit(p(1, 1), p(1, 9), intLit(1, p(1, 2)), intLit(2, p(1, 5)), intLit(3, p(1, 8))), intLit(1, p(1, 11)), p(1, 10), p(1, 12)))) }) expectParse(t, "[1, 2, 3][5 - a]", func(p pfn) []Stmt { return stmts( exprStmt( indexExpr( arrayLit(p(1, 1), p(1, 9), intLit(1, p(1, 2)), intLit(2, p(1, 5)), intLit(3, p(1, 8))), binaryExpr( intLit(5, p(1, 11)), ident("a", p(1, 15)), token.Sub, p(1, 13)), p(1, 10), p(1, 16)))) }) expectParse(t, "[1, 2, 3][5 : a]", func(p pfn) []Stmt { return stmts( exprStmt( sliceExpr( arrayLit(p(1, 1), p(1, 9), intLit(1, p(1, 2)), intLit(2, p(1, 5)), intLit(3, p(1, 8))), intLit(5, p(1, 11)), ident("a", p(1, 15)), p(1, 10), p(1, 16)))) }) expectParse(t, "[1, 2, 3][a + 3 : b - 8]", func(p pfn) []Stmt { return stmts( exprStmt( sliceExpr( arrayLit(p(1, 1), p(1, 9), intLit(1, p(1, 2)), intLit(2, p(1, 5)), intLit(3, p(1, 8))), binaryExpr( ident("a", p(1, 11)), intLit(3, p(1, 15)), token.Add, p(1, 13)), binaryExpr( ident("b", p(1, 19)), intLit(8, p(1, 23)), token.Sub, p(1, 21)), p(1, 10), p(1, 24)))) }) expectParse(t, `{a: 1, b: 2}["b"]`, func(p pfn) []Stmt { return stmts( exprStmt( indexExpr( mapLit(p(1, 1), p(1, 12), mapElementLit( "a", p(1, 2), p(1, 3), intLit(1, p(1, 5))), mapElementLit( "b", p(1, 8), p(1, 9), intLit(2, p(1, 11)))), stringLit("b", p(1, 14)), p(1, 13), p(1, 17)))) }) expectParse(t, `{a: 1, b: 2}[a + b]`, func(p pfn) []Stmt { return stmts( exprStmt( indexExpr( mapLit(p(1, 1), p(1, 12), mapElementLit( "a", p(1, 2), p(1, 3), intLit(1, p(1, 5))), mapElementLit( "b", p(1, 8), p(1, 9), intLit(2, p(1, 11)))), binaryExpr( ident("a", p(1, 14)), ident("b", p(1, 18)), token.Add, p(1, 16)), p(1, 13), p(1, 19)))) }) } func TestParseLogical(t *testing.T) { expectParse(t, "a && 5 || true", func(p pfn) []Stmt { return stmts( exprStmt( binaryExpr( binaryExpr( ident("a", p(1, 1)), intLit(5, p(1, 6)), token.LAnd, p(1, 3)), boolLit(true, p(1, 11)), token.LOr, p(1, 8)))) }) expectParse(t, "a || 5 && true", func(p pfn) []Stmt { return stmts( exprStmt( binaryExpr( ident("a", p(1, 1)), binaryExpr( intLit(5, p(1, 6)), boolLit(true, p(1, 11)), token.LAnd, p(1, 8)), token.LOr, p(1, 3)))) }) expectParse(t, "a && (5 || true)", func(p pfn) []Stmt { return stmts( exprStmt( binaryExpr( ident("a", p(1, 1)), parenExpr( binaryExpr( intLit(5, p(1, 7)), boolLit(true, p(1, 12)), token.LOr, p(1, 9)), p(1, 6), p(1, 16)), token.LAnd, p(1, 3)))) }) } func TestParseMap(t *testing.T) { expectParse(t, "{ key1: 1, key2: \"2\", key3: true }", func(p pfn) []Stmt { return stmts( exprStmt( mapLit(p(1, 1), p(1, 34), mapElementLit( "key1", p(1, 3), p(1, 7), intLit(1, p(1, 9))), mapElementLit( "key2", p(1, 12), p(1, 16), stringLit("2", p(1, 18))), mapElementLit( "key3", p(1, 23), p(1, 27), boolLit(true, p(1, 29)))))) }) expectParse(t, "{ \"key1\": 1 }", func(p pfn) []Stmt { return stmts( exprStmt( mapLit(p(1, 1), p(1, 13), mapElementLit( "key1", p(1, 3), p(1, 9), intLit(1, p(1, 11)))))) }) expectParse(t, "a = { key1: 1, key2: \"2\", key3: true }", func(p pfn) []Stmt { return stmts(assignStmt( exprs(ident("a", p(1, 1))), exprs(mapLit(p(1, 5), p(1, 38), mapElementLit( "key1", p(1, 7), p(1, 11), intLit(1, p(1, 13))), mapElementLit( "key2", p(1, 16), p(1, 20), stringLit("2", p(1, 22))), mapElementLit( "key3", p(1, 27), p(1, 31), boolLit(true, p(1, 33))))), token.Assign, p(1, 3))) }) expectParse(t, "a = { key1: 1, key2: \"2\", key3: { k1: `bar`, k2: 4 } }", func(p pfn) []Stmt { return stmts(assignStmt( exprs(ident("a", p(1, 1))), exprs(mapLit(p(1, 5), p(1, 54), mapElementLit( "key1", p(1, 7), p(1, 11), intLit(1, p(1, 13))), mapElementLit( "key2", p(1, 16), p(1, 20), stringLit("2", p(1, 22))), mapElementLit( "key3", p(1, 27), p(1, 31), mapLit(p(1, 33), p(1, 52), mapElementLit( "k1", p(1, 35), p(1, 37), stringLit("bar", p(1, 39))), mapElementLit( "k2", p(1, 46), p(1, 48), intLit(4, p(1, 50))))))), token.Assign, p(1, 3))) }) expectParse(t, ` { key1: 1, key2: "2", key3: true }`, func(p pfn) []Stmt { return stmts(exprStmt( mapLit(p(2, 1), p(6, 1), mapElementLit( "key1", p(3, 2), p(3, 6), intLit(1, p(3, 8))), mapElementLit( "key2", p(4, 2), p(4, 6), stringLit("2", p(4, 8))), mapElementLit( "key3", p(5, 2), p(5, 6), boolLit(true, p(5, 8)))))) }) expectParseError(t, ` { key1: 1, key2: "2", key3: true, }`) // unlike Go, trailing comma for the last element is illegal expectParseError(t, `{ key1: 1, }`) expectParseError(t, `{ key1: 1, key2: 2, }`) } func TestParsePrecedence(t *testing.T) { expectParseString(t, `a + b + c`, `((a + b) + c)`) expectParseString(t, `a + b * c`, `(a + (b * c))`) expectParseString(t, `x = 2 * 1 + 3 / 4`, `x = ((2 * 1) + (3 / 4))`) } func TestParseSelector(t *testing.T) { expectParse(t, "a.b", func(p pfn) []Stmt { return stmts( exprStmt( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))))) }) expectParse(t, "a.b.c", func(p pfn) []Stmt { return stmts( exprStmt( selectorExpr( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), stringLit("c", p(1, 5))))) }) expectParse(t, "{k1:1}.k1", func(p pfn) []Stmt { return stmts( exprStmt( selectorExpr( mapLit( p(1, 1), p(1, 6), mapElementLit( "k1", p(1, 2), p(1, 4), intLit(1, p(1, 5)))), stringLit("k1", p(1, 8))))) }) expectParse(t, "{k1:{v1:1}}.k1.v1", func(p pfn) []Stmt { return stmts( exprStmt( selectorExpr( selectorExpr( mapLit( p(1, 1), p(1, 11), mapElementLit("k1", p(1, 2), p(1, 4), mapLit(p(1, 5), p(1, 10), mapElementLit( "v1", p(1, 6), p(1, 8), intLit(1, p(1, 9)))))), stringLit("k1", p(1, 13))), stringLit("v1", p(1, 16))))) }) expectParse(t, "a.b = 4", func(p pfn) []Stmt { return stmts( assignStmt( exprs( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3)))), exprs(intLit(4, p(1, 7))), token.Assign, p(1, 5))) }) expectParse(t, "a.b.c = 4", func(p pfn) []Stmt { return stmts( assignStmt( exprs( selectorExpr( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), stringLit("c", p(1, 5)))), exprs(intLit(4, p(1, 9))), token.Assign, p(1, 7))) }) expectParse(t, "a.b.c = 4 + 5", func(p pfn) []Stmt { return stmts( assignStmt( exprs( selectorExpr( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), stringLit("c", p(1, 5)))), exprs( binaryExpr( intLit(4, p(1, 9)), intLit(5, p(1, 13)), token.Add, p(1, 11))), token.Assign, p(1, 7))) }) expectParse(t, "a[0].c = 4", func(p pfn) []Stmt { return stmts( assignStmt( exprs( selectorExpr( indexExpr( ident("a", p(1, 1)), intLit(0, p(1, 3)), p(1, 2), p(1, 4)), stringLit("c", p(1, 6)))), exprs(intLit(4, p(1, 10))), token.Assign, p(1, 8))) }) expectParse(t, "a.b[0].c = 4", func(p pfn) []Stmt { return stmts( assignStmt( exprs( selectorExpr( indexExpr( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), intLit(0, p(1, 5)), p(1, 4), p(1, 6)), stringLit("c", p(1, 8)))), exprs(intLit(4, p(1, 12))), token.Assign, p(1, 10))) }) expectParse(t, "a.b[0][2].c = 4", func(p pfn) []Stmt { return stmts( assignStmt( exprs( selectorExpr( indexExpr( indexExpr( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), intLit(0, p(1, 5)), p(1, 4), p(1, 6)), intLit(2, p(1, 8)), p(1, 7), p(1, 9)), stringLit("c", p(1, 11)))), exprs(intLit(4, p(1, 15))), token.Assign, p(1, 13))) }) expectParse(t, `a.b["key1"][2].c = 4`, func(p pfn) []Stmt { return stmts( assignStmt( exprs( selectorExpr( indexExpr( indexExpr( selectorExpr( ident("a", p(1, 1)), stringLit("b", p(1, 3))), stringLit("key1", p(1, 5)), p(1, 4), p(1, 11)), intLit(2, p(1, 13)), p(1, 12), p(1, 14)), stringLit("c", p(1, 16)))), exprs(intLit(4, p(1, 20))), token.Assign, p(1, 18))) }) expectParse(t, "a[0].b[2].c = 4", func(p pfn) []Stmt { return stmts( assignStmt( exprs( selectorExpr( indexExpr( selectorExpr( indexExpr( ident("a", p(1, 1)), intLit(0, p(1, 3)), p(1, 2), p(1, 4)), stringLit("b", p(1, 6))), intLit(2, p(1, 8)), p(1, 7), p(1, 9)), stringLit("c", p(1, 11)))), exprs(intLit(4, p(1, 15))), token.Assign, p(1, 13))) }) expectParseError(t, `a.(b.c)`) } func TestParseSemicolon(t *testing.T) { expectParse(t, "1", func(p pfn) []Stmt { return stmts( exprStmt(intLit(1, p(1, 1)))) }) expectParse(t, "1;", func(p pfn) []Stmt { return stmts( exprStmt(intLit(1, p(1, 1)))) }) expectParse(t, "1;;", func(p pfn) []Stmt { return stmts( exprStmt(intLit(1, p(1, 1))), emptyStmt(false, p(1, 3))) }) expectParse(t, `1 `, func(p pfn) []Stmt { return stmts( exprStmt(intLit(1, p(1, 1)))) }) expectParse(t, `1 ;`, func(p pfn) []Stmt { return stmts( exprStmt(intLit(1, p(1, 1))), emptyStmt(false, p(2, 1))) }) expectParse(t, `1; ;`, func(p pfn) []Stmt { return stmts( exprStmt(intLit(1, p(1, 1))), emptyStmt(false, p(2, 1))) }) } func TestParseString(t *testing.T) { expectParse(t, `a = "foo\nbar"`, func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(stringLit("foo\nbar", p(1, 5))), token.Assign, p(1, 3))) }) expectParse(t, "a = `raw string`", func(p pfn) []Stmt { return stmts( assignStmt( exprs(ident("a", p(1, 1))), exprs(stringLit("raw string", p(1, 5))), token.Assign, p(1, 3))) }) } func TestParseInt(t *testing.T) { testCases := []string{ // All valid digits "1234567890", "0b10", "0o12345670", "0x123456789abcdef0", "0x123456789ABCDEF0", // Alternative base prefixes "010", "0B10", "0O10", "0X10", // Invalid digits "0b2", "08", "0o8", "1a", "0xg", // Range errors "9223372036854775807", "9223372036854775808", // invalid: range error // Examples from specification (https://go.dev/ref/spec#Integer_literals) "42", "4_2", "0600", "0_600", "0o600", "0O600", // second character is capital letter 'O' "0xBadFace", "0xBad_Face", "0x_67_7a_2f_cc_40_c6", "170141183460469231731687303715884105727", "170_141183_460469_231731_687303_715884_105727", "42_", // invalid: _ must separate successive digits "4__2", // invalid: only one _ at a time "0_xBadFace", // invalid: _ must separate successive digits } for _, num := range testCases { t.Run(num, func(t *testing.T) { expected, err := strconv.ParseInt(num, 0, 64) if err == nil { expectParse(t, num, func(p pfn) []Stmt { return stmts(exprStmt(intLit(expected, p(1, 1)))) }) } else { expectParseError(t, num) } }) } } func TestParseFloat(t *testing.T) { testCases := []string{ // Different placements of decimal point ".0", "0.", "0.0", "00.0", "00.00", "0.0.0", "0..0", // Ignoring leading zeros "010.0", "00010.0", "08.0", "0a.0", // ivalid: hex character // Exponents "1e1", "1E1", "1e1.1", "1e+1", "1e-1", "1e+-1", "0x1p1", "0x10p1", // Examples from language specifcation (https://go.dev/ref/spec#Floating-point_literals) "0.", "72.40", "072.40", // == 72.40 "2.71828", "1.e+0", "6.67428e-11", "1E6", ".25", ".12345E+5", "1_5.", // == 15.0 "0.15e+0_2", // == 15.0 "0x1p-2", // == 0.25 "0x2.p10", // == 2048.0 "0x1.Fp+0", // == 1.9375 "0X.8p-0", // == 0.5 "0X_1FFFP-16", // == 0.1249847412109375 "0x.p1", // invalid: mantissa has no digits "1p-2", // invalid: p exponent requires hexadecimal mantissa "0x1.5e-2", // invalid: hexadecimal mantissa requires p exponent "1_.5", // invalid: _ must separate successive digits "1._5", // invalid: _ must separate successive digits "1.5_e1", // invalid: _ must separate successive digits "1.5e_1", // invalid: _ must separate successive digits "1.5e1_", // invalid: _ must separate successive digits } for _, num := range testCases { t.Run(num, func(t *testing.T) { expected, err := strconv.ParseFloat(num, 64) if err == nil { expectParse(t, num, func(p pfn) []Stmt { return stmts(exprStmt(floatLit(expected, p(1, 1)))) }) } else { expectParseError(t, num) } }) } } func TestMismatchBrace(t *testing.T) { expectParseError(t, ` fmt := import("fmt") out := 0 if 3 == 1 { out = 1 } } else { out = 2 } fmt.println(out) `) } func TestParseNumberExpressions(t *testing.T) { expectParse(t, `0x15e+2`, func(p pfn) []Stmt { return stmts( exprStmt( binaryExpr( intLit(0x15e, p(1, 1)), intLit(2, p(1, 7)), token.Add, p(1, 6)))) }) expectParse(t, `0-_42`, func(p pfn) []Stmt { return stmts( exprStmt( binaryExpr( intLit(0, p(1, 1)), ident("_42", p(1, 3)), token.Sub, p(1, 2)))) }) } type pfn func(int, int) Pos // position conversion function type expectedFn func(pos pfn) []Stmt // callback function to return expected results type parseTracer struct { out []string } func (o *parseTracer) Write(p []byte) (n int, err error) { o.out = append(o.out, string(p)) return len(p), nil } //type slowPrinter struct { //} // //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() testFile := testFileSet.AddFile("test", -1, len(input)) var ok bool defer func() { if !ok { // print trace tr := &parseTracer{} p := NewParser(testFile, []byte(input), tr) actual, _ := p.ParseFile() if actual != nil { t.Logf("Parsed:\n%s", actual.String()) } t.Logf("Trace:\n%s", strings.Join(tr.out, "")) } }() p := NewParser(testFile, []byte(input), nil) actual, err := p.ParseFile() require.NoError(t, err) expected := fn(func(line, column int) Pos { return Pos(int(testFile.LineStart(line)) + (column - 1)) }) require.Equal(t, len(expected), len(actual.Stmts)) for i := 0; i < len(expected); i++ { equalStmt(t, expected[i], actual.Stmts[i]) } ok = true } func expectParseError(t *testing.T, input string) { testFileSet := NewFileSet() testFile := testFileSet.AddFile("test", -1, len(input)) var ok bool defer func() { if !ok { // print trace tr := &parseTracer{} p := NewParser(testFile, []byte(input), tr) _, _ = p.ParseFile() t.Logf("Trace:\n%s", strings.Join(tr.out, "")) } }() p := NewParser(testFile, []byte(input), nil) _, err := p.ParseFile() require.Error(t, err) ok = true } func expectParseString(t *testing.T, input, expected string) { var ok bool defer func() { if !ok { // print trace tr := &parseTracer{} _, _ = parseSource("test", []byte(input), tr) t.Logf("Trace:\n%s", strings.Join(tr.out, "")) } }() actual, err := parseSource("test", []byte(input), nil) require.NoError(t, err) require.Equal(t, expected, actual.String()) ok = true } func stmts(s ...Stmt) []Stmt { return s } func exprStmt(x Expr) *ExprStmt { return &ExprStmt{Expr: x} } func assignStmt( lhs, rhs []Expr, token token.Token, pos Pos, ) *AssignStmt { return &AssignStmt{LHS: lhs, RHS: rhs, Token: token, TokenPos: pos} } func emptyStmt(implicit bool, pos Pos) *EmptyStmt { return &EmptyStmt{Implicit: implicit, Semicolon: pos} } func returnStmt(pos Pos, result Expr) *ReturnStmt { return &ReturnStmt{Result: result, ReturnPos: pos} } func forStmt( init Stmt, cond Expr, post Stmt, body *BlockStmt, pos Pos, ) *ForStmt { return &ForStmt{ Cond: cond, Init: init, Post: post, Body: body, ForPos: pos, } } func forInStmt( key, value *Ident, seq Expr, body *BlockStmt, pos Pos, ) *ForInStmt { return &ForInStmt{ Key: key, Value: value, Iterable: seq, Body: body, ForPos: pos, } } func ifStmt( init Stmt, cond Expr, body *BlockStmt, elseStmt Stmt, pos Pos, ) *IfStmt { return &IfStmt{ Init: init, Cond: cond, Body: body, Else: elseStmt, IfPos: pos, } } func incDecStmt( expr Expr, tok token.Token, pos Pos, ) *IncDecStmt { return &IncDecStmt{Expr: expr, Token: tok, TokenPos: pos} } func funcType(params *IdentList, pos Pos) *FuncType { return &FuncType{Params: params, FuncPos: pos} } func blockStmt(lbrace, rbrace Pos, list ...Stmt) *BlockStmt { return &BlockStmt{Stmts: list, LBrace: lbrace, RBrace: rbrace} } func ident(name string, pos Pos) *Ident { return &Ident{Name: name, NamePos: pos} } func identList( opening, closing Pos, varArgs bool, list ...*Ident, ) *IdentList { return &IdentList{ VarArgs: varArgs, List: list, LParen: opening, RParen: closing, } } func binaryExpr( x, y Expr, op token.Token, pos Pos, ) *BinaryExpr { return &BinaryExpr{LHS: x, RHS: y, Token: op, TokenPos: pos} } func condExpr( cond, trueExpr, falseExpr Expr, questionPos, colonPos Pos, ) *CondExpr { return &CondExpr{ Cond: cond, True: trueExpr, False: falseExpr, QuestionPos: questionPos, ColonPos: colonPos, } } func unaryExpr(x Expr, op token.Token, pos Pos) *UnaryExpr { return &UnaryExpr{Expr: x, Token: op, TokenPos: pos} } func importExpr(moduleName string, pos Pos) *ImportExpr { return &ImportExpr{ ModuleName: moduleName, Token: token.Import, TokenPos: pos, } } func exprs(list ...Expr) []Expr { return list } func intLit(value int64, pos Pos) *IntLit { return &IntLit{Value: value, ValuePos: pos} } func floatLit(value float64, pos Pos) *FloatLit { return &FloatLit{Value: value, ValuePos: pos} } func stringLit(value string, pos Pos) *StringLit { return &StringLit{Value: value, ValuePos: pos} } func charLit(value rune, pos Pos) *CharLit { return &CharLit{ Value: value, ValuePos: pos, Literal: fmt.Sprintf("'%c'", value), } } func boolLit(value bool, pos Pos) *BoolLit { return &BoolLit{Value: value, ValuePos: pos} } func arrayLit(lbracket, rbracket Pos, list ...Expr) *ArrayLit { return &ArrayLit{LBrack: lbracket, RBrack: rbracket, Elements: list} } func mapElementLit( key string, keyPos Pos, colonPos Pos, value Expr, ) *MapElementLit { return &MapElementLit{ Key: key, KeyPos: keyPos, ColonPos: colonPos, Value: value, } } func mapLit( lbrace, rbrace Pos, list ...*MapElementLit, ) *MapLit { return &MapLit{LBrace: lbrace, RBrace: rbrace, Elements: list} } func funcLit(funcType *FuncType, body *BlockStmt) *FuncLit { return &FuncLit{Type: funcType, Body: body} } 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 indexExpr( x, index Expr, lbrack, rbrack Pos, ) *IndexExpr { return &IndexExpr{ Expr: x, Index: index, LBrack: lbrack, RBrack: rbrack, } } func sliceExpr( x, low, high Expr, lbrack, rbrack Pos, ) *SliceExpr { return &SliceExpr{ Expr: x, Low: low, High: high, LBrack: lbrack, RBrack: rbrack, } } func errorExpr( pos Pos, x Expr, lparen, rparen Pos, ) *ErrorExpr { return &ErrorExpr{ Expr: x, ErrorPos: pos, LParen: lparen, RParen: rparen, } } func selectorExpr(x, sel Expr) *SelectorExpr { return &SelectorExpr{Expr: x, Sel: sel} } func equalStmt(t *testing.T, expected, actual Stmt) { if expected == nil || reflect.ValueOf(expected).IsNil() { require.Nil(t, actual, "expected nil, but got not nil") return } require.NotNil(t, actual, "expected not nil, but got nil") require.IsType(t, expected, actual) switch expected := expected.(type) { case *ExprStmt: equalExpr(t, expected.Expr, actual.(*ExprStmt).Expr) case *EmptyStmt: require.Equal(t, expected.Implicit, actual.(*EmptyStmt).Implicit) require.Equal(t, expected.Semicolon, actual.(*EmptyStmt).Semicolon) case *BlockStmt: require.Equal(t, expected.LBrace, actual.(*BlockStmt).LBrace) require.Equal(t, expected.RBrace, actual.(*BlockStmt).RBrace) equalStmts(t, expected.Stmts, actual.(*BlockStmt).Stmts) case *AssignStmt: equalExprs(t, expected.LHS, actual.(*AssignStmt).LHS) equalExprs(t, expected.RHS, actual.(*AssignStmt).RHS) require.Equal(t, int(expected.Token), int(actual.(*AssignStmt).Token)) require.Equal(t, int(expected.TokenPos), int(actual.(*AssignStmt).TokenPos)) case *IfStmt: equalStmt(t, expected.Init, actual.(*IfStmt).Init) equalExpr(t, expected.Cond, actual.(*IfStmt).Cond) equalStmt(t, expected.Body, actual.(*IfStmt).Body) equalStmt(t, expected.Else, actual.(*IfStmt).Else) require.Equal(t, expected.IfPos, actual.(*IfStmt).IfPos) case *IncDecStmt: equalExpr(t, expected.Expr, actual.(*IncDecStmt).Expr) require.Equal(t, expected.Token, actual.(*IncDecStmt).Token) require.Equal(t, expected.TokenPos, actual.(*IncDecStmt).TokenPos) case *ForStmt: equalStmt(t, expected.Init, actual.(*ForStmt).Init) equalExpr(t, expected.Cond, actual.(*ForStmt).Cond) equalStmt(t, expected.Post, actual.(*ForStmt).Post) equalStmt(t, expected.Body, actual.(*ForStmt).Body) require.Equal(t, expected.ForPos, actual.(*ForStmt).ForPos) case *ForInStmt: equalExpr(t, expected.Key, actual.(*ForInStmt).Key) equalExpr(t, expected.Value, actual.(*ForInStmt).Value) equalExpr(t, expected.Iterable, actual.(*ForInStmt).Iterable) equalStmt(t, expected.Body, actual.(*ForInStmt).Body) require.Equal(t, expected.ForPos, actual.(*ForInStmt).ForPos) case *ReturnStmt: equalExpr(t, expected.Result, actual.(*ReturnStmt).Result) require.Equal(t, expected.ReturnPos, actual.(*ReturnStmt).ReturnPos) case *BranchStmt: equalExpr(t, expected.Label, actual.(*BranchStmt).Label) require.Equal(t, expected.Token, actual.(*BranchStmt).Token) require.Equal(t, expected.TokenPos, actual.(*BranchStmt).TokenPos) default: panic(fmt.Errorf("unknown type: %T", expected)) } } func equalExpr(t *testing.T, expected, actual Expr) { if expected == nil || reflect.ValueOf(expected).IsNil() { require.Nil(t, actual, "expected nil, but got not nil") return } require.NotNil(t, actual, "expected not nil, but got nil") require.IsType(t, expected, actual) switch expected := expected.(type) { case *Ident: require.Equal(t, expected.Name, actual.(*Ident).Name) require.Equal(t, int(expected.NamePos), int(actual.(*Ident).NamePos)) case *IntLit: require.Equal(t, expected.Value, actual.(*IntLit).Value) require.Equal(t, int(expected.ValuePos), int(actual.(*IntLit).ValuePos)) case *FloatLit: require.Equal(t, expected.Value, actual.(*FloatLit).Value) require.Equal(t, int(expected.ValuePos), int(actual.(*FloatLit).ValuePos)) case *BoolLit: require.Equal(t, expected.Value, actual.(*BoolLit).Value) require.Equal(t, int(expected.ValuePos), int(actual.(*BoolLit).ValuePos)) case *CharLit: require.Equal(t, expected.Value, actual.(*CharLit).Value) require.Equal(t, int(expected.ValuePos), int(actual.(*CharLit).ValuePos)) case *StringLit: require.Equal(t, expected.Value, actual.(*StringLit).Value) require.Equal(t, int(expected.ValuePos), int(actual.(*StringLit).ValuePos)) case *ArrayLit: require.Equal(t, expected.LBrack, actual.(*ArrayLit).LBrack) require.Equal(t, expected.RBrack, actual.(*ArrayLit).RBrack) equalExprs(t, expected.Elements, actual.(*ArrayLit).Elements) case *MapLit: require.Equal(t, expected.LBrace, actual.(*MapLit).LBrace) require.Equal(t, expected.RBrace, actual.(*MapLit).RBrace) equalMapElements(t, expected.Elements, actual.(*MapLit).Elements) case *BinaryExpr: equalExpr(t, expected.LHS, actual.(*BinaryExpr).LHS) equalExpr(t, expected.RHS, actual.(*BinaryExpr).RHS) require.Equal(t, expected.Token, actual.(*BinaryExpr).Token) require.Equal(t, expected.TokenPos, actual.(*BinaryExpr).TokenPos) case *UnaryExpr: equalExpr(t, expected.Expr, actual.(*UnaryExpr).Expr) require.Equal(t, expected.Token, actual.(*UnaryExpr).Token) require.Equal(t, expected.TokenPos, actual.(*UnaryExpr).TokenPos) case *FuncLit: equalFuncType(t, expected.Type, actual.(*FuncLit).Type) equalStmt(t, expected.Body, actual.(*FuncLit).Body) case *CallExpr: equalExpr(t, expected.Func, actual.(*CallExpr).Func) require.Equal(t, expected.LParen, actual.(*CallExpr).LParen) require.Equal(t, expected.RParen, actual.(*CallExpr).RParen) equalExprs(t, expected.Args, actual.(*CallExpr).Args) case *ParenExpr: equalExpr(t, expected.Expr, actual.(*ParenExpr).Expr) require.Equal(t, expected.LParen, actual.(*ParenExpr).LParen) require.Equal(t, expected.RParen, actual.(*ParenExpr).RParen) case *IndexExpr: equalExpr(t, expected.Expr, actual.(*IndexExpr).Expr) equalExpr(t, expected.Index, actual.(*IndexExpr).Index) require.Equal(t, expected.LBrack, actual.(*IndexExpr).LBrack) require.Equal(t, expected.RBrack, actual.(*IndexExpr).RBrack) case *SliceExpr: equalExpr(t, expected.Expr, actual.(*SliceExpr).Expr) equalExpr(t, expected.Low, actual.(*SliceExpr).Low) equalExpr(t, expected.High, actual.(*SliceExpr).High) require.Equal(t, expected.LBrack, actual.(*SliceExpr).LBrack) require.Equal(t, expected.RBrack, actual.(*SliceExpr).RBrack) case *SelectorExpr: equalExpr(t, expected.Expr, actual.(*SelectorExpr).Expr) equalExpr(t, expected.Sel, actual.(*SelectorExpr).Sel) case *ImportExpr: require.Equal(t, expected.ModuleName, actual.(*ImportExpr).ModuleName) require.Equal(t, int(expected.TokenPos), int(actual.(*ImportExpr).TokenPos)) require.Equal(t, expected.Token, actual.(*ImportExpr).Token) case *ErrorExpr: equalExpr(t, expected.Expr, actual.(*ErrorExpr).Expr) require.Equal(t, int(expected.ErrorPos), int(actual.(*ErrorExpr).ErrorPos)) require.Equal(t, int(expected.LParen), int(actual.(*ErrorExpr).LParen)) require.Equal(t, int(expected.RParen), int(actual.(*ErrorExpr).RParen)) case *CondExpr: equalExpr(t, expected.Cond, actual.(*CondExpr).Cond) equalExpr(t, expected.True, actual.(*CondExpr).True) equalExpr(t, expected.False, actual.(*CondExpr).False) require.Equal(t, expected.QuestionPos, actual.(*CondExpr).QuestionPos) require.Equal(t, expected.ColonPos, actual.(*CondExpr).ColonPos) default: panic(fmt.Errorf("unknown type: %T", expected)) } } 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) } func equalIdents(t *testing.T, expected, actual []*Ident) { require.Equal(t, len(expected), len(actual)) for i := 0; i < len(expected); i++ { equalExpr(t, expected[i], actual[i]) } } func equalExprs(t *testing.T, expected, actual []Expr) { require.Equal(t, len(expected), len(actual)) for i := 0; i < len(expected); i++ { equalExpr(t, expected[i], actual[i]) } } func equalStmts(t *testing.T, expected, actual []Stmt) { require.Equal(t, len(expected), len(actual)) for i := 0; i < len(expected); i++ { equalStmt(t, expected[i], actual[i]) } } func equalMapElements( t *testing.T, expected, actual []*MapElementLit, ) { require.Equal(t, len(expected), len(actual)) for i := 0; i < len(expected); i++ { require.Equal(t, expected[i].Key, actual[i].Key) require.Equal(t, expected[i].KeyPos, actual[i].KeyPos) require.Equal(t, expected[i].ColonPos, actual[i].ColonPos) equalExpr(t, expected[i].Value, actual[i].Value) } } func parseSource( filename string, src []byte, trace io.Writer, ) (res *File, err error) { fileSet := NewFileSet() file := fileSet.AddFile(filename, -1, len(src)) p := NewParser(file, src, trace) return p.ParseFile() } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/pos.go0000644000175000017500000000035714607001502017573 0ustar00maythammaythampackage parser // Pos represents a position in the file set. type Pos int // NoPos represents an invalid position. const NoPos Pos = 0 // IsValid returns true if the position is valid. func (p Pos) IsValid() bool { return p != NoPos } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/scanner.go0000644000175000017500000003267514607001502020433 0ustar00maythammaythampackage parser import ( "fmt" "unicode" "unicode/utf8" "github.com/d5/tengo/v2/token" ) // byte order mark const bom = 0xFEFF // ScanMode represents a scanner mode. type ScanMode int // List of scanner modes. const ( ScanComments ScanMode = 1 << iota DontInsertSemis ) // ScannerErrorHandler is an error handler for the scanner. type ScannerErrorHandler func(pos SourceFilePos, msg string) // Scanner reads the Tengo source text. It's based on Go's scanner // implementation. type Scanner struct { file *SourceFile // source file handle src []byte // source ch rune // current character offset int // character offset readOffset int // reading offset (position after current character) lineOffset int // current line offset insertSemi bool // insert a semicolon before next newline errorHandler ScannerErrorHandler // error reporting; or nil errorCount int // number of errors encountered mode ScanMode } // NewScanner creates a Scanner. func NewScanner( file *SourceFile, src []byte, errorHandler ScannerErrorHandler, mode ScanMode, ) *Scanner { if file.Size != len(src) { panic(fmt.Sprintf("file size (%d) does not match src len (%d)", file.Size, len(src))) } s := &Scanner{ file: file, src: src, errorHandler: errorHandler, ch: ' ', mode: mode, } s.next() if s.ch == bom { s.next() // ignore BOM at file beginning } return s } // ErrorCount returns the number of errors. func (s *Scanner) ErrorCount() int { return s.errorCount } // Scan returns a token, token literal and its position. func (s *Scanner) Scan() ( tok token.Token, literal string, pos Pos, ) { s.skipWhitespace() pos = s.file.FileSetPos(s.offset) insertSemi := false // determine token value switch ch := s.ch; { case isLetter(ch): literal = s.scanIdentifier() tok = token.Lookup(literal) switch tok { case token.Ident, token.Break, token.Continue, token.Return, token.Export, token.True, token.False, token.Undefined: insertSemi = true } case ('0' <= ch && ch <= '9') || (ch == '.' && '0' <= s.peek() && s.peek() <= '9'): insertSemi = true tok, literal = s.scanNumber() default: s.next() // always make progress switch ch { case -1: // EOF if s.insertSemi { s.insertSemi = false // EOF consumed return token.Semicolon, "\n", pos } tok = token.EOF case '\n': // we only reach here if s.insertSemi was set in the first place s.insertSemi = false // newline consumed return token.Semicolon, "\n", pos case '"': insertSemi = true tok = token.String literal = s.scanString() case '\'': insertSemi = true tok = token.Char literal = s.scanRune() case '`': insertSemi = true tok = token.String literal = s.scanRawString() case ':': tok = s.switch2(token.Colon, token.Define) case '.': tok = token.Period if s.ch == '.' && s.peek() == '.' { s.next() s.next() // consume last '.' tok = token.Ellipsis } case ',': tok = token.Comma case '?': tok = token.Question case ';': tok = token.Semicolon literal = ";" case '(': tok = token.LParen case ')': insertSemi = true tok = token.RParen case '[': tok = token.LBrack case ']': insertSemi = true tok = token.RBrack case '{': tok = token.LBrace case '}': insertSemi = true tok = token.RBrace case '+': tok = s.switch3(token.Add, token.AddAssign, '+', token.Inc) if tok == token.Inc { insertSemi = true } case '-': tok = s.switch3(token.Sub, token.SubAssign, '-', token.Dec) if tok == token.Dec { insertSemi = true } case '*': tok = s.switch2(token.Mul, token.MulAssign) case '/': if s.ch == '/' || s.ch == '*' { // comment if s.insertSemi && s.findLineEnd() { // reset position to the beginning of the comment s.ch = '/' s.offset = s.file.Offset(pos) s.readOffset = s.offset + 1 s.insertSemi = false // newline consumed return token.Semicolon, "\n", pos } comment := s.scanComment() if s.mode&ScanComments == 0 { // skip comment s.insertSemi = false // newline consumed return s.Scan() } tok = token.Comment literal = comment } else { tok = s.switch2(token.Quo, token.QuoAssign) } case '%': tok = s.switch2(token.Rem, token.RemAssign) case '^': tok = s.switch2(token.Xor, token.XorAssign) case '<': tok = s.switch4(token.Less, token.LessEq, '<', token.Shl, token.ShlAssign) case '>': tok = s.switch4(token.Greater, token.GreaterEq, '>', token.Shr, token.ShrAssign) case '=': tok = s.switch2(token.Assign, token.Equal) case '!': tok = s.switch2(token.Not, token.NotEqual) case '&': if s.ch == '^' { s.next() tok = s.switch2(token.AndNot, token.AndNotAssign) } else { tok = s.switch3(token.And, token.AndAssign, '&', token.LAnd) } case '|': tok = s.switch3(token.Or, token.OrAssign, '|', token.LOr) default: // next reports unexpected BOMs - don't repeat if ch != bom { s.error(s.file.Offset(pos), fmt.Sprintf("illegal character %#U", ch)) } insertSemi = s.insertSemi // preserve insertSemi info tok = token.Illegal literal = string(ch) } } if s.mode&DontInsertSemis == 0 { s.insertSemi = insertSemi } return } func (s *Scanner) next() { if s.readOffset < len(s.src) { s.offset = s.readOffset if s.ch == '\n' { s.lineOffset = s.offset s.file.AddLine(s.offset) } r, w := rune(s.src[s.readOffset]), 1 switch { case r == 0: s.error(s.offset, "illegal character NUL") case r >= utf8.RuneSelf: // not ASCII r, w = utf8.DecodeRune(s.src[s.readOffset:]) if r == utf8.RuneError && w == 1 { s.error(s.offset, "illegal UTF-8 encoding") } else if r == bom && s.offset > 0 { s.error(s.offset, "illegal byte order mark") } } s.readOffset += w s.ch = r } else { s.offset = len(s.src) if s.ch == '\n' { s.lineOffset = s.offset s.file.AddLine(s.offset) } s.ch = -1 // eof } } func (s *Scanner) peek() byte { if s.readOffset < len(s.src) { return s.src[s.readOffset] } return 0 } func (s *Scanner) error(offset int, msg string) { if s.errorHandler != nil { s.errorHandler(s.file.Position(s.file.FileSetPos(offset)), msg) } s.errorCount++ } func (s *Scanner) scanComment() string { // initial '/' already consumed; s.ch == '/' || s.ch == '*' offs := s.offset - 1 // position of initial '/' var numCR int if s.ch == '/' { //-style comment // (the final '\n' is not considered part of the comment) s.next() for s.ch != '\n' && s.ch >= 0 { if s.ch == '\r' { numCR++ } s.next() } goto exit } /*-style comment */ s.next() for s.ch >= 0 { ch := s.ch if ch == '\r' { numCR++ } s.next() if ch == '*' && s.ch == '/' { s.next() goto exit } } s.error(offs, "comment not terminated") exit: lit := s.src[offs:s.offset] // On Windows, a (//-comment) line may end in "\r\n". // Remove the final '\r' before analyzing the text for line directives (matching the compiler). // Remove any other '\r' afterwards (matching the pre-existing behavior of the scanner). if numCR > 0 && len(lit) >= 2 && lit[1] == '/' && lit[len(lit)-1] == '\r' { lit = lit[:len(lit)-1] numCR-- } if numCR > 0 { lit = StripCR(lit, lit[1] == '*') } return string(lit) } func (s *Scanner) findLineEnd() bool { // initial '/' already consumed defer func(offs int) { // reset scanner state to where it was upon calling findLineEnd s.ch = '/' s.offset = offs s.readOffset = offs + 1 s.next() // consume initial '/' again }(s.offset - 1) // 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 return true } /*-style comment: look for newline */ s.next() for s.ch >= 0 { ch := s.ch if ch == '\n' { return true } s.next() if ch == '*' && s.ch == '/' { s.next() break } } s.skipWhitespace() // s.insertSemi is set if s.ch < 0 || s.ch == '\n' { return true } if s.ch != '/' { // non-comment tok return false } s.next() // consume '/' } return false } func (s *Scanner) scanIdentifier() string { offs := s.offset for isLetter(s.ch) || isDigit(s.ch) { s.next() } return string(s.src[offs:s.offset]) } func (s *Scanner) scanDigits(base int) { for s.ch == '_' || digitVal(s.ch) < base { s.next() } } func (s *Scanner) scanNumber() (token.Token, string) { offs := s.offset tok := token.Int base := 10 // Determine base switch { case s.ch == '0' && lower(s.peek()) == 'b': base = 2 s.next() s.next() case s.ch == '0' && lower(s.peek()) == 'o': base = 8 s.next() s.next() case s.ch == '0' && lower(s.peek()) == 'x': base = 16 s.next() s.next() } // Scan whole number s.scanDigits(base) // Scan fractional part if s.ch == '.' && (base == 10 || base == 16) { tok = token.Float s.next() s.scanDigits(base) } // Scan exponent if s.ch == 'e' || s.ch == 'E' || s.ch == 'p' || s.ch == 'P' { tok = token.Float s.next() if s.ch == '-' || s.ch == '+' { s.next() } offs := s.offset s.scanDigits(10) if offs == s.offset { s.error(offs, "exponent has no digits") } } return tok, string(s.src[offs:s.offset]) } func (s *Scanner) scanEscape(quote rune) bool { offs := s.offset var n int var base, max uint32 switch s.ch { case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote: s.next() return true case '0', '1', '2', '3', '4', '5', '6', '7': n, base, max = 3, 8, 255 case 'x': s.next() n, base, max = 2, 16, 255 case 'u': s.next() n, base, max = 4, 16, unicode.MaxRune case 'U': s.next() n, base, max = 8, 16, unicode.MaxRune default: msg := "unknown escape sequence" if s.ch < 0 { msg = "escape sequence not terminated" } s.error(offs, msg) return false } var x uint32 for n > 0 { d := uint32(digitVal(s.ch)) if d >= base { msg := fmt.Sprintf( "illegal character %#U in escape sequence", s.ch) if s.ch < 0 { msg = "escape sequence not terminated" } s.error(s.offset, msg) return false } x = x*base + d s.next() n-- } if x > max || 0xD800 <= x && x < 0xE000 { s.error(offs, "escape sequence is invalid Unicode code point") return false } return true } func (s *Scanner) scanRune() string { offs := s.offset - 1 // '\'' opening already consumed valid := true n := 0 for { ch := s.ch if ch == '\n' || ch < 0 { // only report error if we don't have one already if valid { s.error(offs, "rune literal not terminated") valid = false } break } s.next() if ch == '\'' { break } n++ if ch == '\\' { if !s.scanEscape('\'') { valid = false } // continue to read to closing quote } } if valid && n != 1 { s.error(offs, "illegal rune literal") } return string(s.src[offs:s.offset]) } func (s *Scanner) scanString() string { offs := s.offset - 1 // '"' opening already consumed for { ch := s.ch if ch == '\n' || ch < 0 { s.error(offs, "string literal not terminated") break } s.next() if ch == '"' { break } if ch == '\\' { s.scanEscape('"') } } return string(s.src[offs:s.offset]) } func (s *Scanner) scanRawString() string { offs := s.offset - 1 // '`' opening already consumed hasCR := false for { ch := s.ch if ch < 0 { s.error(offs, "raw string literal not terminated") break } s.next() if ch == '`' { break } if ch == '\r' { hasCR = true } } lit := s.src[offs:s.offset] if hasCR { lit = StripCR(lit, false) } return string(lit) } // StripCR removes carriage return characters. func StripCR(b []byte, comment bool) []byte { c := make([]byte, len(b)) i := 0 for j, ch := range b { // In a /*-style comment, don't strip \r from *\r/ (incl. sequences of // \r from *\r\r...\r/) since the resulting */ would terminate the // comment too early unless the \r is immediately following the opening // /* in which case it's ok because /*/ is not closed yet. if ch != '\r' || comment && i > len("/*") && c[i-1] == '*' && j+1 < len(b) && b[j+1] == '/' { c[i] = ch i++ } } return c[:i] } func (s *Scanner) skipWhitespace() { for s.ch == ' ' || s.ch == '\t' || s.ch == '\n' && !s.insertSemi || s.ch == '\r' { s.next() } } func (s *Scanner) switch2(tok0, tok1 token.Token) token.Token { if s.ch == '=' { s.next() return tok1 } return tok0 } func (s *Scanner) switch3( tok0, tok1 token.Token, ch2 rune, tok2 token.Token, ) token.Token { if s.ch == '=' { s.next() return tok1 } if s.ch == ch2 { s.next() return tok2 } return tok0 } func (s *Scanner) switch4( tok0, tok1 token.Token, ch2 rune, tok2, tok3 token.Token, ) token.Token { if s.ch == '=' { s.next() return tok1 } if s.ch == ch2 { s.next() if s.ch == '=' { s.next() return tok3 } return tok2 } return tok0 } func isLetter(ch rune) bool { return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch) } func isDigit(ch rune) bool { return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch) } func digitVal(ch rune) int { switch { case '0' <= ch && ch <= '9': return int(ch - '0') case 'a' <= ch && ch <= 'f': return int(ch - 'a' + 10) case 'A' <= ch && ch <= 'F': return int(ch - 'A' + 10) } return 16 // larger than any legal digit val } func lower(c byte) byte { return c | ('x' - 'X') } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/scanner_test.go0000644000175000017500000001406314607001502021461 0ustar00maythammaythampackage parser_test import ( "fmt" "math/rand" "strings" "testing" "time" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/require" "github.com/d5/tengo/v2/token" ) var testFileSet = parser.NewFileSet() type scanResult struct { Token token.Token Literal string Line int Column int } func TestScanner_Scan(t *testing.T) { var testCases = [...]struct { token token.Token literal string }{ {token.Comment, "/* a comment */"}, {token.Comment, "// a comment \n"}, {token.Comment, "/*\r*/"}, {token.Comment, "/**\r/*/"}, {token.Comment, "/**\r\r/*/"}, {token.Comment, "//\r\n"}, {token.Ident, "foobar"}, {token.Ident, "a۰۱۸"}, {token.Ident, "foo६४"}, {token.Ident, "bar9876"}, {token.Ident, "ŝ"}, {token.Ident, "ŝfoo"}, {token.Int, "0"}, {token.Int, "1"}, {token.Int, "123456789012345678890"}, {token.Int, "01234567"}, {token.Int, "0xcafebabe"}, {token.Float, "0."}, {token.Float, ".0"}, {token.Float, "3.14159265"}, {token.Float, "1e0"}, {token.Float, "1e+100"}, {token.Float, "1e-100"}, {token.Float, "2.71828e-1000"}, {token.Char, "'a'"}, {token.Char, "'\\000'"}, {token.Char, "'\\xFF'"}, {token.Char, "'\\uff16'"}, {token.Char, "'\\U0000ff16'"}, {token.String, "`foobar`"}, {token.String, "`" + `foo bar` + "`", }, {token.String, "`\r`"}, {token.String, "`foo\r\nbar`"}, {token.Add, "+"}, {token.Sub, "-"}, {token.Mul, "*"}, {token.Quo, "/"}, {token.Rem, "%"}, {token.And, "&"}, {token.Or, "|"}, {token.Xor, "^"}, {token.Shl, "<<"}, {token.Shr, ">>"}, {token.AndNot, "&^"}, {token.AddAssign, "+="}, {token.SubAssign, "-="}, {token.MulAssign, "*="}, {token.QuoAssign, "/="}, {token.RemAssign, "%="}, {token.AndAssign, "&="}, {token.OrAssign, "|="}, {token.XorAssign, "^="}, {token.ShlAssign, "<<="}, {token.ShrAssign, ">>="}, {token.AndNotAssign, "&^="}, {token.LAnd, "&&"}, {token.LOr, "||"}, {token.Inc, "++"}, {token.Dec, "--"}, {token.Equal, "=="}, {token.Less, "<"}, {token.Greater, ">"}, {token.Assign, "="}, {token.Not, "!"}, {token.NotEqual, "!="}, {token.LessEq, "<="}, {token.GreaterEq, ">="}, {token.Define, ":="}, {token.Ellipsis, "..."}, {token.LParen, "("}, {token.LBrack, "["}, {token.LBrace, "{"}, {token.Comma, ","}, {token.Period, "."}, {token.RParen, ")"}, {token.RBrack, "]"}, {token.RBrace, "}"}, {token.Semicolon, ";"}, {token.Colon, ":"}, {token.Break, "break"}, {token.Continue, "continue"}, {token.Else, "else"}, {token.For, "for"}, {token.Func, "func"}, {token.If, "if"}, {token.Return, "return"}, {token.Export, "export"}, } // combine var lines []string var lineSum int lineNos := make([]int, len(testCases)) columnNos := make([]int, len(testCases)) for i, tc := range testCases { // add 0-2 lines before each test case emptyLines := rand.Intn(3) for j := 0; j < emptyLines; j++ { lines = append(lines, strings.Repeat(" ", rand.Intn(10))) } // add test case line with some whitespaces around it emptyColumns := rand.Intn(10) lines = append(lines, fmt.Sprintf("%s%s%s", strings.Repeat(" ", emptyColumns), tc.literal, strings.Repeat(" ", rand.Intn(10)))) lineNos[i] = lineSum + emptyLines + 1 lineSum += emptyLines + countLines(tc.literal) columnNos[i] = emptyColumns + 1 } // expected results var expected []scanResult var expectedSkipComments []scanResult for i, tc := range testCases { // expected literal var expectedLiteral string switch tc.token { case token.Comment: // strip CRs in comments expectedLiteral = string(parser.StripCR([]byte(tc.literal), tc.literal[1] == '*')) //-style comment literal doesn't contain newline if expectedLiteral[1] == '/' { expectedLiteral = expectedLiteral[:len(expectedLiteral)-1] } case token.Ident: expectedLiteral = tc.literal case token.Semicolon: expectedLiteral = ";" default: if tc.token.IsLiteral() { // strip CRs in raw string expectedLiteral = tc.literal if expectedLiteral[0] == '`' { expectedLiteral = string(parser.StripCR( []byte(expectedLiteral), false)) } } else if tc.token.IsKeyword() { expectedLiteral = tc.literal } } res := scanResult{ Token: tc.token, Literal: expectedLiteral, Line: lineNos[i], Column: columnNos[i], } expected = append(expected, res) if tc.token != token.Comment { expectedSkipComments = append(expectedSkipComments, res) } } scanExpect(t, strings.Join(lines, "\n"), parser.ScanComments|parser.DontInsertSemis, expected...) scanExpect(t, strings.Join(lines, "\n"), parser.DontInsertSemis, expectedSkipComments...) } func TestStripCR(t *testing.T) { for _, tc := range []struct { input string expect string }{ {"//\n", "//\n"}, {"//\r\n", "//\n"}, {"//\r\r\r\n", "//\n"}, {"//\r*\r/\r\n", "//*/\n"}, {"/**/", "/**/"}, {"/*\r/*/", "/*/*/"}, {"/*\r*/", "/**/"}, {"/**\r/*/", "/**\r/*/"}, {"/*\r/\r*\r/*/", "/*/*\r/*/"}, {"/*\r\r\r\r*/", "/**/"}, } { actual := string(parser.StripCR([]byte(tc.input), len(tc.input) >= 2 && tc.input[1] == '*')) require.Equal(t, tc.expect, actual) } } func scanExpect( t *testing.T, input string, mode parser.ScanMode, expected ...scanResult, ) { testFile := testFileSet.AddFile("test", -1, len(input)) s := parser.NewScanner( testFile, []byte(input), func(_ parser.SourceFilePos, msg string) { require.Fail(t, msg) }, mode) for idx, e := range expected { tok, literal, pos := s.Scan() filePos := testFile.Position(pos) require.Equal(t, e.Token, tok, "[%d] expected: %s, actual: %s", idx, e.Token.String(), tok.String()) require.Equal(t, e.Literal, literal) require.Equal(t, e.Line, filePos.Line) require.Equal(t, e.Column, filePos.Column) } tok, _, _ := s.Scan() require.Equal(t, token.EOF, tok, "more tokens left") require.Equal(t, 0, s.ErrorCount()) } func countLines(s string) int { if s == "" { return 0 } n := 1 for i := 0; i < len(s); i++ { if s[i] == '\n' { n++ } } return n } func init() { rand.Seed(time.Now().UnixNano()) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/source_file.go0000644000175000017500000001302114607001502021261 0ustar00maythammaythampackage parser import ( "fmt" "sort" ) // SourceFilePos represents a position information in the file. type SourceFilePos struct { Filename string // filename, if any Offset int // offset, starting at 0 Line int // line number, starting at 1 Column int // column number, starting at 1 (byte count) } // IsValid returns true if the position is valid. func (p SourceFilePos) IsValid() bool { return p.Line > 0 } // String returns a string in one of several forms: // // file:line:column valid position with file name // file:line valid position with file name but no column (column == 0) // line:column valid position without file name // line valid position without file name and no column (column == 0) // file invalid position with file name // - invalid position without file name // func (p SourceFilePos) String() string { s := p.Filename if p.IsValid() { if s != "" { s += ":" } s += fmt.Sprintf("%d", p.Line) if p.Column != 0 { s += fmt.Sprintf(":%d", p.Column) } } if s == "" { s = "-" } return s } // SourceFileSet represents a set of source files. type SourceFileSet struct { Base int // base offset for the next file Files []*SourceFile // list of files in the order added to the set LastFile *SourceFile // cache of last file looked up } // NewFileSet creates a new file set. func NewFileSet() *SourceFileSet { return &SourceFileSet{ Base: 1, // 0 == NoPos } } // AddFile adds a new file in the file set. func (s *SourceFileSet) AddFile(filename string, base, size int) *SourceFile { if base < 0 { base = s.Base } if base < s.Base || size < 0 { panic("illegal base or size") } f := &SourceFile{ set: s, Name: filename, Base: base, Size: size, Lines: []int{0}, } base += size + 1 // +1 because EOF also has a position if base < 0 { panic("offset overflow (> 2G of source code in file set)") } // add the file to the file set s.Base = base s.Files = append(s.Files, f) s.LastFile = f return f } // File returns the file that contains the position p. If no such file is // found (for instance for p == NoPos), the result is nil. func (s *SourceFileSet) File(p Pos) (f *SourceFile) { if p != NoPos { f = s.file(p) } return } // Position converts a SourcePos p in the fileset into a SourceFilePos value. func (s *SourceFileSet) Position(p Pos) (pos SourceFilePos) { if p != NoPos { if f := s.file(p); f != nil { return f.position(p) } } return } func (s *SourceFileSet) file(p Pos) *SourceFile { // common case: p is in last file f := s.LastFile if f != nil && f.Base <= int(p) && int(p) <= f.Base+f.Size { return f } // p is not in last file - search all files if i := searchFiles(s.Files, int(p)); i >= 0 { f := s.Files[i] // f.base <= int(p) by definition of searchFiles if int(p) <= f.Base+f.Size { s.LastFile = f // race is ok - s.last is only a cache return f } } return nil } func searchFiles(a []*SourceFile, x int) int { return sort.Search(len(a), func(i int) bool { return a[i].Base > x }) - 1 } // SourceFile represents a source file. type SourceFile struct { // SourceFile set for the file set *SourceFileSet // SourceFile name as provided to AddFile Name string // SourcePos value range for this file is [base...base+size] Base int // SourceFile size as provided to AddFile Size int // Lines contains the offset of the first character for each line // (the first entry is always 0) Lines []int } // Set returns SourceFileSet. func (f *SourceFile) Set() *SourceFileSet { return f.set } // LineCount returns the current number of lines. func (f *SourceFile) LineCount() int { return len(f.Lines) } // AddLine adds a new line. func (f *SourceFile) AddLine(offset int) { i := len(f.Lines) if (i == 0 || f.Lines[i-1] < offset) && offset < f.Size { f.Lines = append(f.Lines, offset) } } // LineStart returns the position of the first character in the line. func (f *SourceFile) LineStart(line int) Pos { if line < 1 { panic("illegal line number (line numbering starts at 1)") } if line > len(f.Lines) { panic("illegal line number") } return Pos(f.Base + f.Lines[line-1]) } // FileSetPos returns the position in the file set. func (f *SourceFile) FileSetPos(offset int) Pos { if offset > f.Size { panic("illegal file offset") } return Pos(f.Base + offset) } // Offset translates the file set position into the file offset. func (f *SourceFile) Offset(p Pos) int { if int(p) < f.Base || int(p) > f.Base+f.Size { panic("illegal SourcePos value") } return int(p) - f.Base } // Position translates the file set position into the file position. func (f *SourceFile) Position(p Pos) (pos SourceFilePos) { if p != NoPos { if int(p) < f.Base || int(p) > f.Base+f.Size { panic("illegal SourcePos value") } pos = f.position(p) } return } func (f *SourceFile) position(p Pos) (pos SourceFilePos) { offset := int(p) - f.Base pos.Offset = offset pos.Filename, pos.Line, pos.Column = f.unpack(offset) return } func (f *SourceFile) unpack(offset int) (filename string, line, column int) { filename = f.Name if i := searchInts(f.Lines, offset); i >= 0 { line, column = i+1, offset-f.Lines[i]+1 } return } func searchInts(a []int, x int) int { // This function body is a manually inlined version of: // return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1 i, j := 0, len(a) for i < j { h := i + (j-i)/2 // avoid overflow when computing h // i ≤ h < j if a[h] <= x { i = h + 1 } else { j = h } } return i - 1 } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/parser/stmt.go0000644000175000017500000001615714607001502017766 0ustar00maythammaythampackage parser import ( "strings" "github.com/d5/tengo/v2/token" ) // Stmt represents a statement in the AST. type Stmt interface { Node stmtNode() } // AssignStmt represents an assignment statement. type AssignStmt struct { LHS []Expr RHS []Expr Token token.Token TokenPos Pos } func (s *AssignStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *AssignStmt) Pos() Pos { return s.LHS[0].Pos() } // End returns the position of first character immediately after the node. func (s *AssignStmt) End() Pos { return s.RHS[len(s.RHS)-1].End() } func (s *AssignStmt) String() string { var lhs, rhs []string for _, e := range s.LHS { lhs = append(lhs, e.String()) } for _, e := range s.RHS { rhs = append(rhs, e.String()) } return strings.Join(lhs, ", ") + " " + s.Token.String() + " " + strings.Join(rhs, ", ") } // BadStmt represents a bad statement. type BadStmt struct { From Pos To Pos } func (s *BadStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *BadStmt) Pos() Pos { return s.From } // End returns the position of first character immediately after the node. func (s *BadStmt) End() Pos { return s.To } func (s *BadStmt) String() string { return "" } // BlockStmt represents a block statement. type BlockStmt struct { Stmts []Stmt LBrace Pos RBrace Pos } func (s *BlockStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *BlockStmt) Pos() Pos { return s.LBrace } // End returns the position of first character immediately after the node. func (s *BlockStmt) End() Pos { return s.RBrace + 1 } func (s *BlockStmt) String() string { var list []string for _, e := range s.Stmts { list = append(list, e.String()) } return "{" + strings.Join(list, "; ") + "}" } // BranchStmt represents a branch statement. type BranchStmt struct { Token token.Token TokenPos Pos Label *Ident } func (s *BranchStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *BranchStmt) Pos() Pos { return s.TokenPos } // End returns the position of first character immediately after the node. func (s *BranchStmt) End() Pos { if s.Label != nil { return s.Label.End() } return Pos(int(s.TokenPos) + len(s.Token.String())) } func (s *BranchStmt) String() string { var label string if s.Label != nil { label = " " + s.Label.Name } return s.Token.String() + label } // EmptyStmt represents an empty statement. type EmptyStmt struct { Semicolon Pos Implicit bool } func (s *EmptyStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *EmptyStmt) Pos() Pos { return s.Semicolon } // End returns the position of first character immediately after the node. func (s *EmptyStmt) End() Pos { if s.Implicit { return s.Semicolon } return s.Semicolon + 1 } func (s *EmptyStmt) String() string { return ";" } // ExportStmt represents an export statement. type ExportStmt struct { ExportPos Pos Result Expr } func (s *ExportStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *ExportStmt) Pos() Pos { return s.ExportPos } // End returns the position of first character immediately after the node. func (s *ExportStmt) End() Pos { return s.Result.End() } func (s *ExportStmt) String() string { return "export " + s.Result.String() } // ExprStmt represents an expression statement. type ExprStmt struct { Expr Expr } func (s *ExprStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *ExprStmt) Pos() Pos { return s.Expr.Pos() } // End returns the position of first character immediately after the node. func (s *ExprStmt) End() Pos { return s.Expr.End() } func (s *ExprStmt) String() string { return s.Expr.String() } // ForInStmt represents a for-in statement. type ForInStmt struct { ForPos Pos Key *Ident Value *Ident Iterable Expr Body *BlockStmt } func (s *ForInStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *ForInStmt) Pos() Pos { return s.ForPos } // End returns the position of first character immediately after the node. func (s *ForInStmt) End() Pos { return s.Body.End() } func (s *ForInStmt) String() string { if s.Value != nil { return "for " + s.Key.String() + ", " + s.Value.String() + " in " + s.Iterable.String() + " " + s.Body.String() } return "for " + s.Key.String() + " in " + s.Iterable.String() + " " + s.Body.String() } // ForStmt represents a for statement. type ForStmt struct { ForPos Pos Init Stmt Cond Expr Post Stmt Body *BlockStmt } func (s *ForStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *ForStmt) Pos() Pos { return s.ForPos } // End returns the position of first character immediately after the node. func (s *ForStmt) End() Pos { return s.Body.End() } func (s *ForStmt) String() string { var init, cond, post string if s.Init != nil { init = s.Init.String() } if s.Cond != nil { cond = s.Cond.String() + " " } if s.Post != nil { post = s.Post.String() } if init != "" || post != "" { return "for " + init + " ; " + cond + " ; " + post + s.Body.String() } return "for " + cond + s.Body.String() } // IfStmt represents an if statement. type IfStmt struct { IfPos Pos Init Stmt Cond Expr Body *BlockStmt Else Stmt // else branch; or nil } func (s *IfStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *IfStmt) Pos() Pos { return s.IfPos } // End returns the position of first character immediately after the node. func (s *IfStmt) End() Pos { if s.Else != nil { return s.Else.End() } return s.Body.End() } func (s *IfStmt) String() string { var initStmt, elseStmt string if s.Init != nil { initStmt = s.Init.String() + "; " } if s.Else != nil { elseStmt = " else " + s.Else.String() } return "if " + initStmt + s.Cond.String() + " " + s.Body.String() + elseStmt } // IncDecStmt represents increment or decrement statement. type IncDecStmt struct { Expr Expr Token token.Token TokenPos Pos } func (s *IncDecStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *IncDecStmt) Pos() Pos { return s.Expr.Pos() } // End returns the position of first character immediately after the node. func (s *IncDecStmt) End() Pos { return Pos(int(s.TokenPos) + 2) } func (s *IncDecStmt) String() string { return s.Expr.String() + s.Token.String() } // ReturnStmt represents a return statement. type ReturnStmt struct { ReturnPos Pos Result Expr } func (s *ReturnStmt) stmtNode() {} // Pos returns the position of first character belonging to the node. func (s *ReturnStmt) Pos() Pos { return s.ReturnPos } // End returns the position of first character immediately after the node. func (s *ReturnStmt) End() Pos { if s.Result != nil { return s.Result.End() } return s.ReturnPos + 6 } func (s *ReturnStmt) String() string { if s.Result != nil { return "return " + s.Result.String() } return "return" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/require/0000755000175000017500000000000014607001502016616 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/require/require.go0000644000175000017500000002134514607001502020626 0ustar00maythammaythampackage require import ( "bytes" "fmt" "reflect" "runtime" "strings" "testing" "unicode" "unicode/utf8" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/token" ) // NoError asserts err is not an error. func NoError(t *testing.T, err error, msg ...interface{}) { if err != nil { failExpectedActual(t, "no error", err, msg...) } } // Error asserts err is an error. func Error(t *testing.T, err error, msg ...interface{}) { if err == nil { failExpectedActual(t, "error", err, msg...) } } // Nil asserts v is nil. func Nil(t *testing.T, v interface{}, msg ...interface{}) { if !isNil(v) { failExpectedActual(t, "nil", v, msg...) } } // True asserts v is true. func True(t *testing.T, v bool, msg ...interface{}) { if !v { failExpectedActual(t, "true", v, msg...) } } // False asserts vis false. func False(t *testing.T, v bool, msg ...interface{}) { if v { failExpectedActual(t, "false", v, msg...) } } // NotNil asserts v is not nil. func NotNil(t *testing.T, v interface{}, msg ...interface{}) { if isNil(v) { failExpectedActual(t, "not nil", v, msg...) } } // IsType asserts expected and actual are of the same type. func IsType( t *testing.T, expected, actual interface{}, msg ...interface{}, ) { if reflect.TypeOf(expected) != reflect.TypeOf(actual) { failExpectedActual(t, reflect.TypeOf(expected), reflect.TypeOf(actual), msg...) } } // Equal asserts expected and actual are equal. func Equal( t *testing.T, expected, actual interface{}, msg ...interface{}, ) { if isNil(expected) { Nil(t, actual, "expected nil, but got not nil") return } NotNil(t, actual, "expected not nil, but got nil") IsType(t, expected, actual, msg...) switch expected := expected.(type) { case int: if expected != actual.(int) { failExpectedActual(t, expected, actual, msg...) } case int64: if expected != actual.(int64) { failExpectedActual(t, expected, actual, msg...) } case float64: if expected != actual.(float64) { failExpectedActual(t, expected, actual, msg...) } case string: if expected != actual.(string) { failExpectedActual(t, expected, actual, msg...) } case []byte: if !bytes.Equal(expected, actual.([]byte)) { failExpectedActual(t, string(expected), string(actual.([]byte)), msg...) } case []string: if !equalStringSlice(expected, actual.([]string)) { failExpectedActual(t, expected, actual, msg...) } case []int: if !equalIntSlice(expected, actual.([]int)) { failExpectedActual(t, expected, actual, msg...) } case bool: if expected != actual.(bool) { failExpectedActual(t, expected, actual, msg...) } case rune: if expected != actual.(rune) { failExpectedActual(t, expected, actual, msg...) } case *tengo.Symbol: if !equalSymbol(expected, actual.(*tengo.Symbol)) { failExpectedActual(t, expected, actual, msg...) } case parser.Pos: if expected != actual.(parser.Pos) { failExpectedActual(t, expected, actual, msg...) } case token.Token: if expected != actual.(token.Token) { failExpectedActual(t, expected, actual, msg...) } case []tengo.Object: equalObjectSlice(t, expected, actual.([]tengo.Object), msg...) case *tengo.Int: Equal(t, expected.Value, actual.(*tengo.Int).Value, msg...) case *tengo.Float: Equal(t, expected.Value, actual.(*tengo.Float).Value, msg...) case *tengo.String: Equal(t, expected.Value, actual.(*tengo.String).Value, msg...) case *tengo.Char: Equal(t, expected.Value, actual.(*tengo.Char).Value, msg...) case *tengo.Bool: if expected != actual { failExpectedActual(t, expected, actual, msg...) } case *tengo.Array: equalObjectSlice(t, expected.Value, actual.(*tengo.Array).Value, msg...) case *tengo.ImmutableArray: equalObjectSlice(t, expected.Value, actual.(*tengo.ImmutableArray).Value, msg...) case *tengo.Bytes: if !bytes.Equal(expected.Value, actual.(*tengo.Bytes).Value) { failExpectedActual(t, string(expected.Value), string(actual.(*tengo.Bytes).Value), msg...) } case *tengo.Map: equalObjectMap(t, expected.Value, actual.(*tengo.Map).Value, msg...) case *tengo.ImmutableMap: equalObjectMap(t, expected.Value, actual.(*tengo.ImmutableMap).Value, msg...) case *tengo.CompiledFunction: equalCompiledFunction(t, expected, actual.(*tengo.CompiledFunction), msg...) case *tengo.Undefined: if expected != actual { failExpectedActual(t, expected, actual, msg...) } case *tengo.Error: Equal(t, expected.Value, actual.(*tengo.Error).Value, msg...) case tengo.Object: if !expected.Equals(actual.(tengo.Object)) { failExpectedActual(t, expected, actual, msg...) } case *parser.SourceFileSet: equalFileSet(t, expected, actual.(*parser.SourceFileSet), msg...) case *parser.SourceFile: Equal(t, expected.Name, actual.(*parser.SourceFile).Name, msg...) Equal(t, expected.Base, actual.(*parser.SourceFile).Base, msg...) Equal(t, expected.Size, actual.(*parser.SourceFile).Size, msg...) True(t, equalIntSlice(expected.Lines, actual.(*parser.SourceFile).Lines), msg...) case error: if expected != actual.(error) { failExpectedActual(t, expected, actual, msg...) } default: panic(fmt.Errorf("type not implemented: %T", expected)) } } // Fail marks the function as having failed but continues execution. func Fail(t *testing.T, msg ...interface{}) { t.Logf("\nError trace:\n\t%s\n%s", strings.Join(errorTrace(), "\n\t"), message(msg...)) t.Fail() } func failExpectedActual( t *testing.T, expected, actual interface{}, msg ...interface{}, ) { var addMsg string if len(msg) > 0 { addMsg = "\nMessage: " + message(msg...) } t.Logf("\nError trace:\n\t%s\nExpected: %v\nActual: %v%s", strings.Join(errorTrace(), "\n\t"), expected, actual, addMsg) t.FailNow() } func message(formatArgs ...interface{}) string { var format string var args []interface{} if len(formatArgs) > 0 { format = formatArgs[0].(string) } if len(formatArgs) > 1 { args = formatArgs[1:] } return fmt.Sprintf(format, args...) } func equalIntSlice(a, b []int) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if a[i] != b[i] { return false } } return true } func equalStringSlice(a, b []string) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if a[i] != b[i] { return false } } return true } func equalSymbol(a, b *tengo.Symbol) bool { return a.Name == b.Name && a.Index == b.Index && a.Scope == b.Scope } func equalObjectSlice( t *testing.T, expected, actual []tengo.Object, msg ...interface{}, ) { Equal(t, len(expected), len(actual), msg...) for i := 0; i < len(expected); i++ { Equal(t, expected[i], actual[i], msg...) } } func equalFileSet( t *testing.T, expected, actual *parser.SourceFileSet, msg ...interface{}, ) { Equal(t, len(expected.Files), len(actual.Files), msg...) for i, f := range expected.Files { Equal(t, f, actual.Files[i], msg...) } Equal(t, expected.Base, actual.Base) Equal(t, expected.LastFile, actual.LastFile) } func equalObjectMap( t *testing.T, expected, actual map[string]tengo.Object, msg ...interface{}, ) { Equal(t, len(expected), len(actual), msg...) for key, expectedVal := range expected { actualVal := actual[key] Equal(t, expectedVal, actualVal, msg...) } } func equalCompiledFunction( t *testing.T, expected, actual tengo.Object, msg ...interface{}, ) { expectedT := expected.(*tengo.CompiledFunction) actualT := actual.(*tengo.CompiledFunction) Equal(t, tengo.FormatInstructions(expectedT.Instructions, 0), tengo.FormatInstructions(actualT.Instructions, 0), msg...) } func isNil(v interface{}) bool { if v == nil { return true } value := reflect.ValueOf(v) kind := value.Kind() return kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() } func errorTrace() []string { var pc uintptr file := "" line := 0 var ok bool name := "" var callers []string for i := 0; ; i++ { pc, file, line, ok = runtime.Caller(i) if !ok { break } if file == "" { break } f := runtime.FuncForPC(pc) if f == nil { break } name = f.Name() if name == "testing.tRunner" { break } parts := strings.Split(file, "/") file = parts[len(parts)-1] if len(parts) > 1 { dir := parts[len(parts)-2] if dir != "require" || file == "mock_test.go" { callers = append(callers, fmt.Sprintf("%s:%d", file, line)) } } // Drop the package segments := strings.Split(name, ".") name = segments[len(segments)-1] if isTest(name, "Test") || isTest(name, "Benchmark") || isTest(name, "Example") { break } } return callers } func isTest(name, prefix string) bool { if !strings.HasPrefix(name, prefix) { return false } if len(name) == len(prefix) { // "Test" is ok return true } r, _ := utf8.DecodeRuneInString(name[len(prefix):]) return !unicode.IsLower(r) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/script.go0000644000175000017500000001673514607001502017011 0ustar00maythammaythampackage tengo import ( "context" "fmt" "path/filepath" "sync" "github.com/d5/tengo/v2/parser" ) // Script can simplify compilation and execution of embedded scripts. type Script struct { variables map[string]*Variable modules ModuleGetter input []byte maxAllocs int64 maxConstObjects int enableFileImport bool importDir string } // NewScript creates a Script instance with an input script. func NewScript(input []byte) *Script { return &Script{ variables: make(map[string]*Variable), input: input, maxAllocs: -1, maxConstObjects: -1, } } // Add adds a new variable or updates an existing variable to the script. func (s *Script) Add(name string, value interface{}) error { obj, err := FromInterface(value) if err != nil { return err } s.variables[name] = &Variable{ name: name, value: obj, } return nil } // Remove removes (undefines) an existing variable for the script. It returns // false if the variable name is not defined. func (s *Script) Remove(name string) bool { if _, ok := s.variables[name]; !ok { return false } delete(s.variables, name) return true } // SetImports sets import modules. func (s *Script) SetImports(modules ModuleGetter) { s.modules = modules } // SetImportDir sets the initial import directory for script files. func (s *Script) SetImportDir(dir string) error { dir, err := filepath.Abs(dir) if err != nil { return err } s.importDir = dir return nil } // SetMaxAllocs sets the maximum number of objects allocations during the run // time. Compiled script will return ErrObjectAllocLimit error if it // exceeds this limit. func (s *Script) SetMaxAllocs(n int64) { s.maxAllocs = n } // SetMaxConstObjects sets the maximum number of objects in the compiled // constants. func (s *Script) SetMaxConstObjects(n int) { s.maxConstObjects = n } // EnableFileImport enables or disables module loading from local files. Local // file modules are disabled by default. func (s *Script) EnableFileImport(enable bool) { s.enableFileImport = enable } // Compile compiles the script with all the defined variables, and, returns // Compiled object. func (s *Script) Compile() (*Compiled, error) { symbolTable, globals, err := s.prepCompile() if err != nil { return nil, err } fileSet := parser.NewFileSet() srcFile := fileSet.AddFile("(main)", -1, len(s.input)) p := parser.NewParser(srcFile, s.input, nil) file, err := p.ParseFile() if err != nil { return nil, err } c := NewCompiler(srcFile, symbolTable, nil, s.modules, nil) c.EnableFileImport(s.enableFileImport) c.SetImportDir(s.importDir) if err := c.Compile(file); err != nil { return nil, err } // reduce globals size globals = globals[:symbolTable.MaxSymbols()+1] // global symbol names to indexes globalIndexes := make(map[string]int, len(globals)) for _, name := range symbolTable.Names() { symbol, _, _ := symbolTable.Resolve(name, false) if symbol.Scope == ScopeGlobal { globalIndexes[name] = symbol.Index } } // remove duplicates from constants bytecode := c.Bytecode() bytecode.RemoveDuplicates() // check the constant objects limit if s.maxConstObjects >= 0 { cnt := bytecode.CountObjects() if cnt > s.maxConstObjects { return nil, fmt.Errorf("exceeding constant objects limit: %d", cnt) } } return &Compiled{ globalIndexes: globalIndexes, bytecode: bytecode, globals: globals, maxAllocs: s.maxAllocs, }, nil } // Run compiles and runs the scripts. Use returned compiled object to access // global variables. func (s *Script) Run() (compiled *Compiled, err error) { compiled, err = s.Compile() if err != nil { return } err = compiled.Run() return } // RunContext is like Run but includes a context. func (s *Script) RunContext( ctx context.Context, ) (compiled *Compiled, err error) { compiled, err = s.Compile() if err != nil { return } err = compiled.RunContext(ctx) return } func (s *Script) prepCompile() ( symbolTable *SymbolTable, globals []Object, err error, ) { var names []string for name := range s.variables { names = append(names, name) } symbolTable = NewSymbolTable() for idx, fn := range builtinFuncs { symbolTable.DefineBuiltin(idx, fn.Name) } globals = make([]Object, GlobalsSize) for idx, name := range names { symbol := symbolTable.Define(name) if symbol.Index != idx { panic(fmt.Errorf("wrong symbol index: %d != %d", idx, symbol.Index)) } globals[symbol.Index] = s.variables[name].value } return } // Compiled is a compiled instance of the user script. Use Script.Compile() to // create Compiled object. type Compiled struct { globalIndexes map[string]int // global symbol name to index bytecode *Bytecode globals []Object maxAllocs int64 lock sync.RWMutex } // Run executes the compiled script in the virtual machine. func (c *Compiled) Run() error { c.lock.Lock() defer c.lock.Unlock() v := NewVM(c.bytecode, c.globals, c.maxAllocs) return v.Run() } // RunContext is like Run but includes a context. func (c *Compiled) RunContext(ctx context.Context) (err error) { c.lock.Lock() defer c.lock.Unlock() v := NewVM(c.bytecode, c.globals, c.maxAllocs) ch := make(chan error, 1) go func() { defer func() { if r := recover(); r != nil { switch e := r.(type) { case string: ch <- fmt.Errorf(e) case error: ch <- e default: ch <- fmt.Errorf("unknown panic: %v", e) } } }() ch <- v.Run() }() select { case <-ctx.Done(): v.Abort() <-ch err = ctx.Err() case err = <-ch: } return } // Clone creates a new copy of Compiled. Cloned copies are safe for concurrent // use by multiple goroutines. func (c *Compiled) Clone() *Compiled { c.lock.RLock() defer c.lock.RUnlock() clone := &Compiled{ globalIndexes: c.globalIndexes, bytecode: c.bytecode, globals: make([]Object, len(c.globals)), maxAllocs: c.maxAllocs, } // copy global objects for idx, g := range c.globals { if g != nil { clone.globals[idx] = g.Copy() } } return clone } // IsDefined returns true if the variable name is defined (has value) before or // after the execution. func (c *Compiled) IsDefined(name string) bool { c.lock.RLock() defer c.lock.RUnlock() idx, ok := c.globalIndexes[name] if !ok { return false } v := c.globals[idx] if v == nil { return false } return v != UndefinedValue } // Get returns a variable identified by the name. func (c *Compiled) Get(name string) *Variable { c.lock.RLock() defer c.lock.RUnlock() value := UndefinedValue if idx, ok := c.globalIndexes[name]; ok { value = c.globals[idx] if value == nil { value = UndefinedValue } } return &Variable{ name: name, value: value, } } // GetAll returns all the variables that are defined by the compiled script. func (c *Compiled) GetAll() []*Variable { c.lock.RLock() defer c.lock.RUnlock() var vars []*Variable for name, idx := range c.globalIndexes { value := c.globals[idx] if value == nil { value = UndefinedValue } vars = append(vars, &Variable{ name: name, value: value, }) } return vars } // Set replaces the value of a global variable identified by the name. An error // will be returned if the name was not defined during compilation. func (c *Compiled) Set(name string, value interface{}) error { c.lock.Lock() defer c.lock.Unlock() obj, err := FromInterface(value) if err != nil { return err } idx, ok := c.globalIndexes[name] if !ok { return fmt.Errorf("'%s' is not defined", name) } c.globals[idx] = obj return nil } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/script_test.go0000644000175000017500000003547114607001502020046 0ustar00maythammaythampackage tengo_test import ( "context" "errors" "fmt" "math/rand" "strconv" "strings" "sync" "testing" "time" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" "github.com/d5/tengo/v2/stdlib" "github.com/d5/tengo/v2/token" ) func TestScript_Add(t *testing.T) { s := tengo.NewScript([]byte(`a := b; c := test(b); d := test(5)`)) require.NoError(t, s.Add("b", 5)) // b = 5 require.NoError(t, s.Add("b", "foo")) // b = "foo" (re-define before compilation) require.NoError(t, s.Add("test", func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) > 0 { switch arg := args[0].(type) { case *tengo.Int: return &tengo.Int{Value: arg.Value + 1}, nil } } return &tengo.Int{Value: 0}, nil })) c, err := s.Compile() require.NoError(t, err) require.NoError(t, c.Run()) require.Equal(t, "foo", c.Get("a").Value()) require.Equal(t, "foo", c.Get("b").Value()) require.Equal(t, int64(0), c.Get("c").Value()) require.Equal(t, int64(6), c.Get("d").Value()) } func TestScript_Remove(t *testing.T) { s := tengo.NewScript([]byte(`a := b`)) err := s.Add("b", 5) require.NoError(t, err) require.True(t, s.Remove("b")) // b is removed _, err = s.Compile() // should not compile because b is undefined require.Error(t, err) } func TestScript_Run(t *testing.T) { s := tengo.NewScript([]byte(`a := b`)) err := s.Add("b", 5) require.NoError(t, err) c, err := s.Run() require.NoError(t, err) require.NotNil(t, c) compiledGet(t, c, "a", int64(5)) } func TestScript_BuiltinModules(t *testing.T) { s := tengo.NewScript([]byte(`math := import("math"); a := math.abs(-19.84)`)) s.SetImports(stdlib.GetModuleMap("math")) c, err := s.Run() require.NoError(t, err) require.NotNil(t, c) compiledGet(t, c, "a", 19.84) c, err = s.Run() require.NoError(t, err) require.NotNil(t, c) compiledGet(t, c, "a", 19.84) s.SetImports(stdlib.GetModuleMap("os")) _, err = s.Run() require.Error(t, err) s.SetImports(nil) _, err = s.Run() require.Error(t, err) } func TestScript_SourceModules(t *testing.T) { s := tengo.NewScript([]byte(` enum := import("enum") a := enum.all([1,2,3], func(_, v) { return v > 0 }) `)) s.SetImports(stdlib.GetModuleMap("enum")) c, err := s.Run() require.NoError(t, err) require.NotNil(t, c) compiledGet(t, c, "a", true) s.SetImports(nil) _, err = s.Run() require.Error(t, err) } func TestScript_SetMaxConstObjects(t *testing.T) { // one constant '5' s := tengo.NewScript([]byte(`a := 5`)) s.SetMaxConstObjects(1) // limit = 1 _, err := s.Compile() require.NoError(t, err) s.SetMaxConstObjects(0) // limit = 0 _, err = s.Compile() require.Error(t, err) require.Equal(t, "exceeding constant objects limit: 1", err.Error()) // two constants '5' and '1' s = tengo.NewScript([]byte(`a := 5 + 1`)) s.SetMaxConstObjects(2) // limit = 2 _, err = s.Compile() require.NoError(t, err) s.SetMaxConstObjects(1) // limit = 1 _, err = s.Compile() require.Error(t, err) require.Equal(t, "exceeding constant objects limit: 2", err.Error()) // duplicates will be removed s = tengo.NewScript([]byte(`a := 5 + 5`)) s.SetMaxConstObjects(1) // limit = 1 _, err = s.Compile() require.NoError(t, err) s.SetMaxConstObjects(0) // limit = 0 _, err = s.Compile() require.Error(t, err) require.Equal(t, "exceeding constant objects limit: 1", err.Error()) // no limit set s = tengo.NewScript([]byte(`a := 1 + 2 + 3 + 4 + 5`)) _, err = s.Compile() require.NoError(t, err) } func TestScriptConcurrency(t *testing.T) { solve := func(a, b, c int) (d, e int) { a += 2 b += c a += b * 2 d = a + b + c e = 0 for i := 1; i <= d; i++ { e += i } e *= 2 return } code := []byte(` mod1 := import("mod1") a += 2 b += c a += b * 2 arr := [a, b, c] arrstr := string(arr) map := {a: a, b: b, c: c} d := a + b + c s := 0 for i:=1; i<=d; i++ { s += i } e := mod1.double(s) `) mod1 := map[string]tengo.Object{ "double": &tengo.UserFunction{ Value: func(args ...tengo.Object) ( ret tengo.Object, err error, ) { arg0, _ := tengo.ToInt64(args[0]) ret = &tengo.Int{Value: arg0 * 2} return }, }, } scr := tengo.NewScript(code) _ = scr.Add("a", 0) _ = scr.Add("b", 0) _ = scr.Add("c", 0) mods := tengo.NewModuleMap() mods.AddBuiltinModule("mod1", mod1) scr.SetImports(mods) compiled, err := scr.Compile() require.NoError(t, err) executeFn := func(compiled *tengo.Compiled, a, b, c int) (d, e int) { _ = compiled.Set("a", a) _ = compiled.Set("b", b) _ = compiled.Set("c", c) err := compiled.Run() require.NoError(t, err) d = compiled.Get("d").Int() e = compiled.Get("e").Int() return } concurrency := 500 var wg sync.WaitGroup wg.Add(concurrency) for i := 0; i < concurrency; i++ { go func(compiled *tengo.Compiled) { time.Sleep(time.Duration(rand.Int63n(50)) * time.Millisecond) defer wg.Done() a := rand.Intn(10) b := rand.Intn(10) c := rand.Intn(10) d, e := executeFn(compiled, a, b, c) expectedD, expectedE := solve(a, b, c) require.Equal(t, expectedD, d, "input: %d, %d, %d", a, b, c) require.Equal(t, expectedE, e, "input: %d, %d, %d", a, b, c) }(compiled.Clone()) } wg.Wait() } type Counter struct { tengo.ObjectImpl value int64 } func (o *Counter) TypeName() string { return "counter" } func (o *Counter) String() string { return fmt.Sprintf("Counter(%d)", o.value) } func (o *Counter) BinaryOp( op token.Token, rhs tengo.Object, ) (tengo.Object, error) { switch rhs := rhs.(type) { case *Counter: switch op { case token.Add: return &Counter{value: o.value + rhs.value}, nil case token.Sub: return &Counter{value: o.value - rhs.value}, nil } case *tengo.Int: switch op { case token.Add: return &Counter{value: o.value + rhs.Value}, nil case token.Sub: return &Counter{value: o.value - rhs.Value}, nil } } return nil, errors.New("invalid operator") } func (o *Counter) IsFalsy() bool { return o.value == 0 } func (o *Counter) Equals(t tengo.Object) bool { if tc, ok := t.(*Counter); ok { return o.value == tc.value } return false } func (o *Counter) Copy() tengo.Object { return &Counter{value: o.value} } func (o *Counter) Call(_ ...tengo.Object) (tengo.Object, error) { return &tengo.Int{Value: o.value}, nil } func (o *Counter) CanCall() bool { return true } func TestScript_CustomObjects(t *testing.T) { c := compile(t, `a := c1(); s := string(c1); c2 := c1; c2++`, M{ "c1": &Counter{value: 5}, }) compiledRun(t, c) compiledGet(t, c, "a", int64(5)) compiledGet(t, c, "s", "Counter(5)") compiledGetCounter(t, c, "c2", &Counter{value: 6}) c = compile(t, ` arr := [1, 2, 3, 4] for x in arr { c1 += x } out := c1() `, M{ "c1": &Counter{value: 5}, }) compiledRun(t, c) compiledGet(t, c, "out", int64(15)) } func compiledGetCounter( t *testing.T, c *tengo.Compiled, name string, expected *Counter, ) { v := c.Get(name) require.NotNil(t, v) actual := v.Value().(*Counter) require.NotNil(t, actual) require.Equal(t, expected.value, actual.value) } func TestScriptSourceModule(t *testing.T) { // script1 imports "mod1" scr := tengo.NewScript([]byte(`out := import("mod")`)) mods := tengo.NewModuleMap() mods.AddSourceModule("mod", []byte(`export 5`)) scr.SetImports(mods) c, err := scr.Run() require.NoError(t, err) require.Equal(t, int64(5), c.Get("out").Value()) // executing module function scr = tengo.NewScript([]byte(`fn := import("mod"); out := fn()`)) mods = tengo.NewModuleMap() mods.AddSourceModule("mod", []byte(`a := 3; export func() { return a + 5 }`)) scr.SetImports(mods) c, err = scr.Run() require.NoError(t, err) require.Equal(t, int64(8), c.Get("out").Value()) scr = tengo.NewScript([]byte(`out := import("mod")`)) mods = tengo.NewModuleMap() mods.AddSourceModule("mod", []byte(`text := import("text"); export text.title("foo")`)) mods.AddBuiltinModule("text", map[string]tengo.Object{ "title": &tengo.UserFunction{ Name: "title", Value: func(args ...tengo.Object) (tengo.Object, error) { s, _ := tengo.ToString(args[0]) return &tengo.String{Value: strings.Title(s)}, nil }}, }) scr.SetImports(mods) c, err = scr.Run() require.NoError(t, err) require.Equal(t, "Foo", c.Get("out").Value()) scr.SetImports(nil) _, err = scr.Run() require.Error(t, err) } func BenchmarkArrayIndex(b *testing.B) { bench(b.N, `a := [1, 2, 3, 4, 5, 6, 7, 8, 9]; for i := 0; i < 1000; i++ { a[0]; a[1]; a[2]; a[3]; a[4]; a[5]; a[6]; a[7]; a[7]; } `) } func BenchmarkArrayIndexCompare(b *testing.B) { bench(b.N, `a := [1, 2, 3, 4, 5, 6, 7, 8, 9]; for i := 0; i < 1000; i++ { 1; 2; 3; 4; 5; 6; 7; 8; 9; } `) } func bench(n int, input string) { s := tengo.NewScript([]byte(input)) c, err := s.Compile() if err != nil { panic(err) } for i := 0; i < n; i++ { if err := c.Run(); err != nil { panic(err) } } } type M map[string]interface{} func TestCompiled_Get(t *testing.T) { // simple script c := compile(t, `a := 5`, nil) compiledRun(t, c) compiledGet(t, c, "a", int64(5)) // user-defined variables compileError(t, `a := b`, nil) // compile error because "b" is not defined c = compile(t, `a := b`, M{"b": "foo"}) // now compile with b = "foo" defined compiledGet(t, c, "a", nil) // a = undefined; because it's before Compiled.Run() compiledRun(t, c) // Compiled.Run() compiledGet(t, c, "a", "foo") // a = "foo" } func TestCompiled_GetAll(t *testing.T) { c := compile(t, `a := 5`, nil) compiledRun(t, c) compiledGetAll(t, c, M{"a": int64(5)}) c = compile(t, `a := b`, M{"b": "foo"}) compiledRun(t, c) compiledGetAll(t, c, M{"a": "foo", "b": "foo"}) c = compile(t, `a := b; b = 5`, M{"b": "foo"}) compiledRun(t, c) compiledGetAll(t, c, M{"a": "foo", "b": int64(5)}) } func TestCompiled_IsDefined(t *testing.T) { c := compile(t, `a := 5`, nil) compiledIsDefined(t, c, "a", false) // a is not defined before Run() compiledRun(t, c) compiledIsDefined(t, c, "a", true) compiledIsDefined(t, c, "b", false) } func TestCompiled_Set(t *testing.T) { c := compile(t, `a := b`, M{"b": "foo"}) compiledRun(t, c) compiledGet(t, c, "a", "foo") // replace value of 'b' err := c.Set("b", "bar") require.NoError(t, err) compiledRun(t, c) compiledGet(t, c, "a", "bar") // try to replace undefined variable err = c.Set("c", 1984) require.Error(t, err) // 'c' is not defined // case #2 c = compile(t, ` a := func() { return func() { return b + 5 }() }()`, M{"b": 5}) compiledRun(t, c) compiledGet(t, c, "a", int64(10)) err = c.Set("b", 10) require.NoError(t, err) compiledRun(t, c) compiledGet(t, c, "a", int64(15)) } func TestCompiled_RunContext(t *testing.T) { // machine completes normally c := compile(t, `a := 5`, nil) err := c.RunContext(context.Background()) require.NoError(t, err) compiledGet(t, c, "a", int64(5)) // timeout c = compile(t, `for true {}`, nil) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() err = c.RunContext(ctx) require.Equal(t, context.DeadlineExceeded, err) } func TestCompiled_CustomObject(t *testing.T) { c := compile(t, `r := (t<130)`, M{"t": &customNumber{value: 123}}) compiledRun(t, c) compiledGet(t, c, "r", true) c = compile(t, `r := (t>13)`, M{"t": &customNumber{value: 123}}) compiledRun(t, c) compiledGet(t, c, "r", true) } // customNumber is a user defined object that can compare to tengo.Int // very shitty implementation, just to test that token.Less and token.Greater in BinaryOp works type customNumber struct { tengo.ObjectImpl value int64 } func (n *customNumber) TypeName() string { return "Number" } func (n *customNumber) String() string { return strconv.FormatInt(n.value, 10) } func (n *customNumber) BinaryOp(op token.Token, rhs tengo.Object) (tengo.Object, error) { tengoInt, ok := rhs.(*tengo.Int) if !ok { return nil, tengo.ErrInvalidOperator } return n.binaryOpInt(op, tengoInt) } func (n *customNumber) binaryOpInt(op token.Token, rhs *tengo.Int) (tengo.Object, error) { i := n.value switch op { case token.Less: if i < rhs.Value { return tengo.TrueValue, nil } return tengo.FalseValue, nil case token.Greater: if i > rhs.Value { return tengo.TrueValue, nil } return tengo.FalseValue, nil case token.LessEq: if i <= rhs.Value { return tengo.TrueValue, nil } return tengo.FalseValue, nil case token.GreaterEq: if i >= rhs.Value { return tengo.TrueValue, nil } return tengo.FalseValue, nil } return nil, tengo.ErrInvalidOperator } func TestScript_ImportError(t *testing.T) { m := ` exp := import("expression") r := exp(ctx) ` src := ` export func(ctx) { closure := func() { if ctx.actiontimes < 0 { // an error is thrown here because actiontimes is undefined return true } return false } return closure() }` s := tengo.NewScript([]byte(m)) mods := tengo.NewModuleMap() mods.AddSourceModule("expression", []byte(src)) s.SetImports(mods) err := s.Add("ctx", map[string]interface{}{ "ctx": 12, }) require.NoError(t, err) _, err = s.Run() require.True(t, strings.Contains(err.Error(), "expression:4:6")) } func compile(t *testing.T, input string, vars M) *tengo.Compiled { s := tengo.NewScript([]byte(input)) for vn, vv := range vars { err := s.Add(vn, vv) require.NoError(t, err) } c, err := s.Compile() require.NoError(t, err) require.NotNil(t, c) return c } func compileError(t *testing.T, input string, vars M) { s := tengo.NewScript([]byte(input)) for vn, vv := range vars { err := s.Add(vn, vv) require.NoError(t, err) } _, err := s.Compile() require.Error(t, err) } func compiledRun(t *testing.T, c *tengo.Compiled) { err := c.Run() require.NoError(t, err) } func compiledGet( t *testing.T, c *tengo.Compiled, name string, expected interface{}, ) { v := c.Get(name) require.NotNil(t, v) require.Equal(t, expected, v.Value()) } func compiledGetAll( t *testing.T, c *tengo.Compiled, expected M, ) { vars := c.GetAll() require.Equal(t, len(expected), len(vars)) for k, v := range expected { var found bool for _, e := range vars { if e.Name() == k { require.Equal(t, v, e.Value()) found = true } } require.True(t, found, "variable '%s' not found", k) } } func compiledIsDefined( t *testing.T, c *tengo.Compiled, name string, expected bool, ) { require.Equal(t, expected, c.IsDefined(name)) } func TestCompiled_Clone(t *testing.T) { script := tengo.NewScript([]byte(` count += 1 data["b"] = 2 `)) err := script.Add("data", map[string]interface{}{"a": 1}) require.NoError(t, err) err = script.Add("count", 1000) require.NoError(t, err) compiled, err := script.Compile() require.NoError(t, err) clone := compiled.Clone() err = clone.RunContext(context.Background()) require.NoError(t, err) require.Equal(t, 1000, compiled.Get("count").Int()) require.Equal(t, 1, len(compiled.Get("data").Map())) require.Equal(t, 1001, clone.Get("count").Int()) require.Equal(t, 2, len(clone.Get("data").Map())) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/0000755000175000017500000000000014607001502016423 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/base64.go0000644000175000017500000000156014607001502020040 0ustar00maythammaythampackage stdlib import ( "encoding/base64" "github.com/d5/tengo/v2" ) var base64Module = map[string]tengo.Object{ "encode": &tengo.UserFunction{ Value: FuncAYRS(base64.StdEncoding.EncodeToString), }, "decode": &tengo.UserFunction{ Value: FuncASRYE(base64.StdEncoding.DecodeString), }, "raw_encode": &tengo.UserFunction{ Value: FuncAYRS(base64.RawStdEncoding.EncodeToString), }, "raw_decode": &tengo.UserFunction{ Value: FuncASRYE(base64.RawStdEncoding.DecodeString), }, "url_encode": &tengo.UserFunction{ Value: FuncAYRS(base64.URLEncoding.EncodeToString), }, "url_decode": &tengo.UserFunction{ Value: FuncASRYE(base64.URLEncoding.DecodeString), }, "raw_url_encode": &tengo.UserFunction{ Value: FuncAYRS(base64.RawURLEncoding.EncodeToString), }, "raw_url_decode": &tengo.UserFunction{ Value: FuncASRYE(base64.RawURLEncoding.DecodeString), }, } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/base64_test.go0000644000175000017500000000161114607001502021074 0ustar00maythammaythampackage stdlib_test import "testing" var base64Bytes1 = []byte{ 0x06, 0xAC, 0x76, 0x1B, 0x1D, 0x6A, 0xFA, 0x9D, 0xB1, 0xA0, } const ( base64Std = "Bqx2Gx1q+p2xoA==" base64URL = "Bqx2Gx1q-p2xoA==" base64RawStd = "Bqx2Gx1q+p2xoA" base64RawURL = "Bqx2Gx1q-p2xoA" ) func TestBase64(t *testing.T) { module(t, `base64`).call("encode", base64Bytes1).expect(base64Std) module(t, `base64`).call("decode", base64Std).expect(base64Bytes1) module(t, `base64`).call("url_encode", base64Bytes1).expect(base64URL) module(t, `base64`).call("url_decode", base64URL).expect(base64Bytes1) module(t, `base64`).call("raw_encode", base64Bytes1).expect(base64RawStd) module(t, `base64`).call("raw_decode", base64RawStd).expect(base64Bytes1) module(t, `base64`).call("raw_url_encode", base64Bytes1). expect(base64RawURL) module(t, `base64`).call("raw_url_decode", base64RawURL). expect(base64Bytes1) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/builtin_modules.go0000644000175000017500000000057414607001502022156 0ustar00maythammaythampackage stdlib import ( "github.com/d5/tengo/v2" ) // BuiltinModules are builtin type standard library modules. var BuiltinModules = map[string]map[string]tengo.Object{ "math": mathModule, "os": osModule, "text": textModule, "times": timesModule, "rand": randModule, "fmt": fmtModule, "json": jsonModule, "base64": base64Module, "hex": hexModule, } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/errors.go0000644000175000017500000000031514607001502020265 0ustar00maythammaythampackage stdlib import ( "github.com/d5/tengo/v2" ) func wrapError(err error) tengo.Object { if err == nil { return tengo.TrueValue } return &tengo.Error{Value: &tengo.String{Value: err.Error()}} } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/fmt.go0000644000175000017500000000434214607001502017543 0ustar00maythammaythampackage stdlib import ( "fmt" "github.com/d5/tengo/v2" ) var fmtModule = map[string]tengo.Object{ "print": &tengo.UserFunction{Name: "print", Value: fmtPrint}, "printf": &tengo.UserFunction{Name: "printf", Value: fmtPrintf}, "println": &tengo.UserFunction{Name: "println", Value: fmtPrintln}, "sprintf": &tengo.UserFunction{Name: "sprintf", Value: fmtSprintf}, } func fmtPrint(args ...tengo.Object) (ret tengo.Object, err error) { printArgs, err := getPrintArgs(args...) if err != nil { return nil, err } _, _ = fmt.Print(printArgs...) return nil, nil } func fmtPrintf(args ...tengo.Object) (ret tengo.Object, err error) { numArgs := len(args) if numArgs == 0 { return nil, tengo.ErrWrongNumArguments } format, ok := args[0].(*tengo.String) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "format", Expected: "string", Found: args[0].TypeName(), } } if numArgs == 1 { fmt.Print(format) return nil, nil } s, err := tengo.Format(format.Value, args[1:]...) if err != nil { return nil, err } fmt.Print(s) return nil, nil } func fmtPrintln(args ...tengo.Object) (ret tengo.Object, err error) { printArgs, err := getPrintArgs(args...) if err != nil { return nil, err } printArgs = append(printArgs, "\n") _, _ = fmt.Print(printArgs...) return nil, nil } func fmtSprintf(args ...tengo.Object) (ret tengo.Object, err error) { numArgs := len(args) if numArgs == 0 { return nil, tengo.ErrWrongNumArguments } format, ok := args[0].(*tengo.String) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "format", Expected: "string", Found: args[0].TypeName(), } } if numArgs == 1 { // okay to return 'format' directly as String is immutable return format, nil } s, err := tengo.Format(format.Value, args[1:]...) if err != nil { return nil, err } return &tengo.String{Value: s}, nil } func getPrintArgs(args ...tengo.Object) ([]interface{}, error) { var printArgs []interface{} l := 0 for _, arg := range args { s, _ := tengo.ToString(arg) slen := len(s) // make sure length does not exceed the limit if l+slen > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } l += slen printArgs = append(printArgs, s) } return printArgs, nil } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/fmt_test.go0000644000175000017500000000131014607001502020572 0ustar00maythammaythampackage stdlib_test import "testing" func TestFmtSprintf(t *testing.T) { module(t, `fmt`).call("sprintf", "").expect("") module(t, `fmt`).call("sprintf", "foo").expect("foo") module(t, `fmt`).call("sprintf", `foo %d %v %s`, 1, 2, "bar"). expect("foo 1 2 bar") module(t, `fmt`).call("sprintf", "foo %v", ARR{1, "bar", true}). expect(`foo [1, "bar", true]`) module(t, `fmt`).call("sprintf", "foo %v %d", ARR{1, "bar", true}, 19). expect(`foo [1, "bar", true] 19`) module(t, `fmt`). call("sprintf", "foo %v", MAP{"a": IMAP{"b": IMAP{"c": ARR{1, 2, 3}}}}). expect(`foo {a: {b: {c: [1, 2, 3]}}}`) module(t, `fmt`).call("sprintf", "%v", IARR{1, IARR{2, IARR{3, 4}}}). expect(`[1, [2, [3, 4]]]`) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/func_typedefs.go0000644000175000017500000006705714607001502021627 0ustar00maythammaythampackage stdlib import ( "fmt" "github.com/d5/tengo/v2" ) // FuncAR transform a function of 'func()' signature into CallableFunc type. func FuncAR(fn func()) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } fn() return tengo.UndefinedValue, nil } } // FuncARI transform a function of 'func() int' signature into CallableFunc // type. func FuncARI(fn func() int) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } return &tengo.Int{Value: int64(fn())}, nil } } // FuncARI64 transform a function of 'func() int64' signature into CallableFunc // type. func FuncARI64(fn func() int64) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } return &tengo.Int{Value: fn()}, nil } } // FuncAI64RI64 transform a function of 'func(int64) int64' signature into // CallableFunc type. func FuncAI64RI64(fn func(int64) int64) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } return &tengo.Int{Value: fn(i1)}, nil } } // FuncAI64R transform a function of 'func(int64)' signature into CallableFunc // type. func FuncAI64R(fn func(int64)) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } fn(i1) return tengo.UndefinedValue, nil } } // FuncARB transform a function of 'func() bool' signature into CallableFunc // type. func FuncARB(fn func() bool) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } if fn() { return tengo.TrueValue, nil } return tengo.FalseValue, nil } } // FuncARE transform a function of 'func() error' signature into CallableFunc // type. func FuncARE(fn func() error) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } return wrapError(fn()), nil } } // FuncARS transform a function of 'func() string' signature into CallableFunc // type. func FuncARS(fn func() string) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } s := fn() if len(s) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: s}, nil } } // FuncARSE transform a function of 'func() (string, error)' signature into // CallableFunc type. func FuncARSE(fn func() (string, error)) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } res, err := fn() if err != nil { return wrapError(err), nil } if len(res) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: res}, nil } } // FuncARYE transform a function of 'func() ([]byte, error)' signature into // CallableFunc type. func FuncARYE(fn func() ([]byte, error)) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } res, err := fn() if err != nil { return wrapError(err), nil } if len(res) > tengo.MaxBytesLen { return nil, tengo.ErrBytesLimit } return &tengo.Bytes{Value: res}, nil } } // FuncARF transform a function of 'func() float64' signature into CallableFunc // type. func FuncARF(fn func() float64) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } return &tengo.Float{Value: fn()}, nil } } // FuncARSs transform a function of 'func() []string' signature into // CallableFunc type. func FuncARSs(fn func() []string) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } arr := &tengo.Array{} for _, elem := range fn() { if len(elem) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } arr.Value = append(arr.Value, &tengo.String{Value: elem}) } return arr, nil } } // FuncARIsE transform a function of 'func() ([]int, error)' signature into // CallableFunc type. func FuncARIsE(fn func() ([]int, error)) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } res, err := fn() if err != nil { return wrapError(err), nil } arr := &tengo.Array{} for _, v := range res { arr.Value = append(arr.Value, &tengo.Int{Value: int64(v)}) } return arr, nil } } // FuncAIRIs transform a function of 'func(int) []int' signature into // CallableFunc type. func FuncAIRIs(fn func(int) []int) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } res := fn(i1) arr := &tengo.Array{} for _, v := range res { arr.Value = append(arr.Value, &tengo.Int{Value: int64(v)}) } return arr, nil } } // FuncAFRF transform a function of 'func(float64) float64' signature into // CallableFunc type. func FuncAFRF(fn func(float64) float64) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } f1, ok := tengo.ToFloat64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "float(compatible)", Found: args[0].TypeName(), } } return &tengo.Float{Value: fn(f1)}, nil } } // FuncAIR transform a function of 'func(int)' signature into CallableFunc type. func FuncAIR(fn func(int)) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } fn(i1) return tengo.UndefinedValue, nil } } // FuncAIRF transform a function of 'func(int) float64' signature into // CallableFunc type. func FuncAIRF(fn func(int) float64) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } return &tengo.Float{Value: fn(i1)}, nil } } // FuncAFRI transform a function of 'func(float64) int' signature into // CallableFunc type. func FuncAFRI(fn func(float64) int) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } f1, ok := tengo.ToFloat64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "float(compatible)", Found: args[0].TypeName(), } } return &tengo.Int{Value: int64(fn(f1))}, nil } } // FuncAFFRF transform a function of 'func(float64, float64) float64' signature // into CallableFunc type. func FuncAFFRF(fn func(float64, float64) float64) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } f1, ok := tengo.ToFloat64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "float(compatible)", Found: args[0].TypeName(), } } f2, ok := tengo.ToFloat64(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "float(compatible)", Found: args[1].TypeName(), } } return &tengo.Float{Value: fn(f1, f2)}, nil } } // FuncAIFRF transform a function of 'func(int, float64) float64' signature // into CallableFunc type. func FuncAIFRF(fn func(int, float64) float64) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } f2, ok := tengo.ToFloat64(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "float(compatible)", Found: args[1].TypeName(), } } return &tengo.Float{Value: fn(i1, f2)}, nil } } // FuncAFIRF transform a function of 'func(float64, int) float64' signature // into CallableFunc type. func FuncAFIRF(fn func(float64, int) float64) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } f1, ok := tengo.ToFloat64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "float(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } return &tengo.Float{Value: fn(f1, i2)}, nil } } // FuncAFIRB transform a function of 'func(float64, int) bool' signature // into CallableFunc type. func FuncAFIRB(fn func(float64, int) bool) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } f1, ok := tengo.ToFloat64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "float(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } if fn(f1, i2) { return tengo.TrueValue, nil } return tengo.FalseValue, nil } } // FuncAFRB transform a function of 'func(float64) bool' signature // into CallableFunc type. func FuncAFRB(fn func(float64) bool) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } f1, ok := tengo.ToFloat64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "float(compatible)", Found: args[0].TypeName(), } } if fn(f1) { return tengo.TrueValue, nil } return tengo.FalseValue, nil } } // FuncASRS transform a function of 'func(string) string' signature into // CallableFunc type. User function will return 'true' if underlying native // function returns nil. func FuncASRS(fn func(string) string) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } s := fn(s1) if len(s) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: s}, nil } } // FuncASRSs transform a function of 'func(string) []string' signature into // CallableFunc type. func FuncASRSs(fn func(string) []string) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } res := fn(s1) arr := &tengo.Array{} for _, elem := range res { if len(elem) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } arr.Value = append(arr.Value, &tengo.String{Value: elem}) } return arr, nil } } // FuncASRSE transform a function of 'func(string) (string, error)' signature // into CallableFunc type. User function will return 'true' if underlying // native function returns nil. func FuncASRSE(fn func(string) (string, error)) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } res, err := fn(s1) if err != nil { return wrapError(err), nil } if len(res) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: res}, nil } } // FuncASRE transform a function of 'func(string) error' signature into // CallableFunc type. User function will return 'true' if underlying native // function returns nil. func FuncASRE(fn func(string) error) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } return wrapError(fn(s1)), nil } } // FuncASSRE transform a function of 'func(string, string) error' signature // into CallableFunc type. User function will return 'true' if underlying // native function returns nil. func FuncASSRE(fn func(string, string) error) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } s2, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } } return wrapError(fn(s1, s2)), nil } } // FuncASSRSs transform a function of 'func(string, string) []string' // signature into CallableFunc type. func FuncASSRSs(fn func(string, string) []string) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } s2, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[1].TypeName(), } } arr := &tengo.Array{} for _, res := range fn(s1, s2) { if len(res) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } arr.Value = append(arr.Value, &tengo.String{Value: res}) } return arr, nil } } // FuncASSIRSs transform a function of 'func(string, string, int) []string' // signature into CallableFunc type. func FuncASSIRSs(fn func(string, string, int) []string) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 3 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } s2, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } } i3, ok := tengo.ToInt(args[2]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } } arr := &tengo.Array{} for _, res := range fn(s1, s2, i3) { if len(res) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } arr.Value = append(arr.Value, &tengo.String{Value: res}) } return arr, nil } } // FuncASSRI transform a function of 'func(string, string) int' signature into // CallableFunc type. func FuncASSRI(fn func(string, string) int) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } s2, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[0].TypeName(), } } return &tengo.Int{Value: int64(fn(s1, s2))}, nil } } // FuncASSRS transform a function of 'func(string, string) string' signature // into CallableFunc type. func FuncASSRS(fn func(string, string) string) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } s2, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } } s := fn(s1, s2) if len(s) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: s}, nil } } // FuncASSRB transform a function of 'func(string, string) bool' signature // into CallableFunc type. func FuncASSRB(fn func(string, string) bool) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } s2, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } } if fn(s1, s2) { return tengo.TrueValue, nil } return tengo.FalseValue, nil } } // FuncASsSRS transform a function of 'func([]string, string) string' signature // into CallableFunc type. func FuncASsSRS(fn func([]string, string) string) tengo.CallableFunc { return func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } var ss1 []string switch arg0 := args[0].(type) { case *tengo.Array: for idx, a := range arg0.Value { as, ok := tengo.ToString(a) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: fmt.Sprintf("first[%d]", idx), Expected: "string(compatible)", Found: a.TypeName(), } } ss1 = append(ss1, as) } case *tengo.ImmutableArray: for idx, a := range arg0.Value { as, ok := tengo.ToString(a) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: fmt.Sprintf("first[%d]", idx), Expected: "string(compatible)", Found: a.TypeName(), } } ss1 = append(ss1, as) } default: return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "array", Found: args[0].TypeName(), } } s2, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } } s := fn(ss1, s2) if len(s) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: s}, nil } } // FuncASI64RE transform a function of 'func(string, int64) error' signature // into CallableFunc type. func FuncASI64RE(fn func(string, int64) error) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt64(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } return wrapError(fn(s1, i2)), nil } } // FuncAIIRE transform a function of 'func(int, int) error' signature // into CallableFunc type. func FuncAIIRE(fn func(int, int) error) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } return wrapError(fn(i1, i2)), nil } } // FuncASIRS transform a function of 'func(string, int) string' signature // into CallableFunc type. func FuncASIRS(fn func(string, int) string) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } s := fn(s1, i2) if len(s) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: s}, nil } } // FuncASIIRE transform a function of 'func(string, int, int) error' signature // into CallableFunc type. func FuncASIIRE(fn func(string, int, int) error) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 3 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } i3, ok := tengo.ToInt(args[2]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } } return wrapError(fn(s1, i2, i3)), nil } } // FuncAYRIE transform a function of 'func([]byte) (int, error)' signature // into CallableFunc type. func FuncAYRIE(fn func([]byte) (int, error)) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } y1, ok := tengo.ToByteSlice(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "bytes(compatible)", Found: args[0].TypeName(), } } res, err := fn(y1) if err != nil { return wrapError(err), nil } return &tengo.Int{Value: int64(res)}, nil } } // FuncAYRS transform a function of 'func([]byte) string' signature into // CallableFunc type. func FuncAYRS(fn func([]byte) string) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } y1, ok := tengo.ToByteSlice(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "bytes(compatible)", Found: args[0].TypeName(), } } res := fn(y1) return &tengo.String{Value: res}, nil } } // FuncASRIE transform a function of 'func(string) (int, error)' signature // into CallableFunc type. func FuncASRIE(fn func(string) (int, error)) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } res, err := fn(s1) if err != nil { return wrapError(err), nil } return &tengo.Int{Value: int64(res)}, nil } } // FuncASRYE transform a function of 'func(string) ([]byte, error)' signature // into CallableFunc type. func FuncASRYE(fn func(string) ([]byte, error)) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } res, err := fn(s1) if err != nil { return wrapError(err), nil } if len(res) > tengo.MaxBytesLen { return nil, tengo.ErrBytesLimit } return &tengo.Bytes{Value: res}, nil } } // FuncAIRSsE transform a function of 'func(int) ([]string, error)' signature // into CallableFunc type. func FuncAIRSsE(fn func(int) ([]string, error)) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } res, err := fn(i1) if err != nil { return wrapError(err), nil } arr := &tengo.Array{} for _, r := range res { if len(r) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } arr.Value = append(arr.Value, &tengo.String{Value: r}) } return arr, nil } } // FuncAIRS transform a function of 'func(int) string' signature into // CallableFunc type. func FuncAIRS(fn func(int) string) tengo.CallableFunc { return func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } s := fn(i1) if len(s) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: s}, nil } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/func_typedefs_test.go0000644000175000017500000004430314607001502022653 0ustar00maythammaythampackage stdlib_test import ( "errors" "strconv" "strings" "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" "github.com/d5/tengo/v2/stdlib" ) func TestFuncAIR(t *testing.T) { uf := stdlib.FuncAIR(func(int) {}) ret, err := funcCall(uf, &tengo.Int{Value: 10}) require.NoError(t, err) require.Equal(t, tengo.UndefinedValue, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAR(t *testing.T) { uf := stdlib.FuncAR(func() {}) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, tengo.UndefinedValue, ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARI(t *testing.T) { uf := stdlib.FuncARI(func() int { return 10 }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.Int{Value: 10}, ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARE(t *testing.T) { uf := stdlib.FuncARE(func() error { return nil }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) uf = stdlib.FuncARE(func() error { return errors.New("some error") }) ret, err = funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.Error{ Value: &tengo.String{Value: "some error"}, }, ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARIsE(t *testing.T) { uf := stdlib.FuncARIsE(func() ([]int, error) { return []int{1, 2, 3}, nil }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, array(&tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Int{Value: 3}), ret) uf = stdlib.FuncARIsE(func() ([]int, error) { return nil, errors.New("some error") }) ret, err = funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.Error{ Value: &tengo.String{Value: "some error"}, }, ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARS(t *testing.T) { uf := stdlib.FuncARS(func() string { return "foo" }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.String{Value: "foo"}, ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARSE(t *testing.T) { uf := stdlib.FuncARSE(func() (string, error) { return "foo", nil }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.String{Value: "foo"}, ret) uf = stdlib.FuncARSE(func() (string, error) { return "", errors.New("some error") }) ret, err = funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.Error{ Value: &tengo.String{Value: "some error"}, }, ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARSs(t *testing.T) { uf := stdlib.FuncARSs(func() []string { return []string{"foo", "bar"} }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, array(&tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}), ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASRE(t *testing.T) { uf := stdlib.FuncASRE(func(a string) error { return nil }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) uf = stdlib.FuncASRE(func(a string) error { return errors.New("some error") }) ret, err = funcCall(uf, &tengo.String{Value: "foo"}) require.NoError(t, err) require.Equal(t, &tengo.Error{ Value: &tengo.String{Value: "some error"}, }, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASRS(t *testing.T) { uf := stdlib.FuncASRS(func(a string) string { return a }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}) require.NoError(t, err) require.Equal(t, &tengo.String{Value: "foo"}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASRSs(t *testing.T) { uf := stdlib.FuncASRSs(func(a string) []string { return []string{a} }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}) require.NoError(t, err) require.Equal(t, array(&tengo.String{Value: "foo"}), ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASI64RE(t *testing.T) { uf := stdlib.FuncASI64RE(func(a string, b int64) error { return nil }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}, &tengo.Int{Value: 5}) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) uf = stdlib.FuncASI64RE(func(a string, b int64) error { return errors.New("some error") }) ret, err = funcCall(uf, &tengo.String{Value: "foo"}, &tengo.Int{Value: 5}) require.NoError(t, err) require.Equal(t, &tengo.Error{Value: &tengo.String{Value: "some error"}}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAIIRE(t *testing.T) { uf := stdlib.FuncAIIRE(func(a, b int) error { return nil }) ret, err := funcCall(uf, &tengo.Int{Value: 5}, &tengo.Int{Value: 7}) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) uf = stdlib.FuncAIIRE(func(a, b int) error { return errors.New("some error") }) ret, err = funcCall(uf, &tengo.Int{Value: 5}, &tengo.Int{Value: 7}) require.NoError(t, err) require.Equal(t, &tengo.Error{Value: &tengo.String{Value: "some error"}}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASIIRE(t *testing.T) { uf := stdlib.FuncASIIRE(func(a string, b, c int) error { return nil }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}, &tengo.Int{Value: 5}, &tengo.Int{Value: 7}) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) uf = stdlib.FuncASIIRE(func(a string, b, c int) error { return errors.New("some error") }) ret, err = funcCall(uf, &tengo.String{Value: "foo"}, &tengo.Int{Value: 5}, &tengo.Int{Value: 7}) require.NoError(t, err) require.Equal(t, &tengo.Error{Value: &tengo.String{Value: "some error"}}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASRSE(t *testing.T) { uf := stdlib.FuncASRSE(func(a string) (string, error) { return a, nil }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}) require.NoError(t, err) require.Equal(t, &tengo.String{Value: "foo"}, ret) uf = stdlib.FuncASRSE(func(a string) (string, error) { return a, errors.New("some error") }) ret, err = funcCall(uf, &tengo.String{Value: "foo"}) require.NoError(t, err) require.Equal(t, &tengo.Error{Value: &tengo.String{Value: "some error"}}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASSRE(t *testing.T) { uf := stdlib.FuncASSRE(func(a, b string) error { return nil }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) uf = stdlib.FuncASSRE(func(a, b string) error { return errors.New("some error") }) ret, err = funcCall(uf, &tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}) require.NoError(t, err) require.Equal(t, &tengo.Error{Value: &tengo.String{Value: "some error"}}, ret) _, err = funcCall(uf, &tengo.String{Value: "foo"}) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASsRS(t *testing.T) { uf := stdlib.FuncASsSRS(func(a []string, b string) string { return strings.Join(a, b) }) ret, err := funcCall(uf, array(&tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}), &tengo.String{Value: " "}) require.NoError(t, err) require.Equal(t, &tengo.String{Value: "foo bar"}, ret) _, err = funcCall(uf, &tengo.String{Value: "foo"}) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARF(t *testing.T) { uf := stdlib.FuncARF(func() float64 { return 10.0 }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.Float{Value: 10.0}, ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAFRF(t *testing.T) { uf := stdlib.FuncAFRF(func(a float64) float64 { return a }) ret, err := funcCall(uf, &tengo.Float{Value: 10.0}) require.NoError(t, err) require.Equal(t, &tengo.Float{Value: 10.0}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) _, err = funcCall(uf, tengo.TrueValue, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAIRF(t *testing.T) { uf := stdlib.FuncAIRF(func(a int) float64 { return float64(a) }) ret, err := funcCall(uf, &tengo.Int{Value: 10.0}) require.NoError(t, err) require.Equal(t, &tengo.Float{Value: 10.0}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) _, err = funcCall(uf, tengo.TrueValue, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAFRI(t *testing.T) { uf := stdlib.FuncAFRI(func(a float64) int { return int(a) }) ret, err := funcCall(uf, &tengo.Float{Value: 10.5}) require.NoError(t, err) require.Equal(t, &tengo.Int{Value: 10}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) _, err = funcCall(uf, tengo.TrueValue, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAFRB(t *testing.T) { uf := stdlib.FuncAFRB(func(a float64) bool { return a > 0.0 }) ret, err := funcCall(uf, &tengo.Float{Value: 0.1}) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) _, err = funcCall(uf, tengo.TrueValue, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAFFRF(t *testing.T) { uf := stdlib.FuncAFFRF(func(a, b float64) float64 { return a + b }) ret, err := funcCall(uf, &tengo.Float{Value: 10.0}, &tengo.Float{Value: 20.0}) require.NoError(t, err) require.Equal(t, &tengo.Float{Value: 30.0}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASIRS(t *testing.T) { uf := stdlib.FuncASIRS(func(a string, b int) string { return strings.Repeat(a, b) }) ret, err := funcCall(uf, &tengo.String{Value: "ab"}, &tengo.Int{Value: 2}) require.NoError(t, err) require.Equal(t, &tengo.String{Value: "abab"}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAIFRF(t *testing.T) { uf := stdlib.FuncAIFRF(func(a int, b float64) float64 { return float64(a) + b }) ret, err := funcCall(uf, &tengo.Int{Value: 10}, &tengo.Float{Value: 20.0}) require.NoError(t, err) require.Equal(t, &tengo.Float{Value: 30.0}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAFIRF(t *testing.T) { uf := stdlib.FuncAFIRF(func(a float64, b int) float64 { return a + float64(b) }) ret, err := funcCall(uf, &tengo.Float{Value: 10.0}, &tengo.Int{Value: 20}) require.NoError(t, err) require.Equal(t, &tengo.Float{Value: 30.0}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAFIRB(t *testing.T) { uf := stdlib.FuncAFIRB(func(a float64, b int) bool { return a < float64(b) }) ret, err := funcCall(uf, &tengo.Float{Value: 10.0}, &tengo.Int{Value: 20}) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAIRSsE(t *testing.T) { uf := stdlib.FuncAIRSsE(func(a int) ([]string, error) { return []string{"foo", "bar"}, nil }) ret, err := funcCall(uf, &tengo.Int{Value: 10}) require.NoError(t, err) require.Equal(t, array(&tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}), ret) uf = stdlib.FuncAIRSsE(func(a int) ([]string, error) { return nil, errors.New("some error") }) ret, err = funcCall(uf, &tengo.Int{Value: 10}) require.NoError(t, err) require.Equal(t, &tengo.Error{Value: &tengo.String{Value: "some error"}}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASSRSs(t *testing.T) { uf := stdlib.FuncASSRSs(func(a, b string) []string { return []string{a, b} }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}) require.NoError(t, err) require.Equal(t, array(&tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}), ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASSIRSs(t *testing.T) { uf := stdlib.FuncASSIRSs(func(a, b string, c int) []string { return []string{a, b, strconv.Itoa(c)} }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}, &tengo.Int{Value: 5}) require.NoError(t, err) require.Equal(t, array(&tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}, &tengo.String{Value: "5"}), ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARB(t *testing.T) { uf := stdlib.FuncARB(func() bool { return true }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARYE(t *testing.T) { uf := stdlib.FuncARYE(func() ([]byte, error) { return []byte("foo bar"), nil }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.Bytes{Value: []byte("foo bar")}, ret) uf = stdlib.FuncARYE(func() ([]byte, error) { return nil, errors.New("some error") }) ret, err = funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.Error{Value: &tengo.String{Value: "some error"}}, ret) _, err = funcCall(uf, tengo.TrueValue) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASRIE(t *testing.T) { uf := stdlib.FuncASRIE(func(a string) (int, error) { return 5, nil }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}) require.NoError(t, err) require.Equal(t, &tengo.Int{Value: 5}, ret) uf = stdlib.FuncASRIE(func(a string) (int, error) { return 0, errors.New("some error") }) ret, err = funcCall(uf, &tengo.String{Value: "foo"}) require.NoError(t, err) require.Equal(t, &tengo.Error{Value: &tengo.String{Value: "some error"}}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAYRIE(t *testing.T) { uf := stdlib.FuncAYRIE(func(a []byte) (int, error) { return 5, nil }) ret, err := funcCall(uf, &tengo.Bytes{Value: []byte("foo")}) require.NoError(t, err) require.Equal(t, &tengo.Int{Value: 5}, ret) uf = stdlib.FuncAYRIE(func(a []byte) (int, error) { return 0, errors.New("some error") }) ret, err = funcCall(uf, &tengo.Bytes{Value: []byte("foo")}) require.NoError(t, err) require.Equal(t, &tengo.Error{Value: &tengo.String{Value: "some error"}}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASSRI(t *testing.T) { uf := stdlib.FuncASSRI(func(a, b string) int { return len(a) + len(b) }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}) require.NoError(t, err) require.Equal(t, &tengo.Int{Value: 6}, ret) _, err = funcCall(uf, &tengo.String{Value: "foo"}) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASSRS(t *testing.T) { uf := stdlib.FuncASSRS(func(a, b string) string { return a + b }) ret, err := funcCall(uf, &tengo.String{Value: "foo"}, &tengo.String{Value: "bar"}) require.NoError(t, err) require.Equal(t, &tengo.String{Value: "foobar"}, ret) _, err = funcCall(uf, &tengo.String{Value: "foo"}) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASSRB(t *testing.T) { uf := stdlib.FuncASSRB(func(a, b string) bool { return len(a) > len(b) }) ret, err := funcCall(uf, &tengo.String{Value: "123"}, &tengo.String{Value: "12"}) require.NoError(t, err) require.Equal(t, tengo.TrueValue, ret) _, err = funcCall(uf, &tengo.String{Value: "foo"}) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAIRS(t *testing.T) { uf := stdlib.FuncAIRS(func(a int) string { return strconv.Itoa(a) }) ret, err := funcCall(uf, &tengo.Int{Value: 55}) require.NoError(t, err) require.Equal(t, &tengo.String{Value: "55"}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAIRIs(t *testing.T) { uf := stdlib.FuncAIRIs(func(a int) []int { return []int{a, a} }) ret, err := funcCall(uf, &tengo.Int{Value: 55}) require.NoError(t, err) require.Equal(t, array(&tengo.Int{Value: 55}, &tengo.Int{Value: 55}), ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAI64R(t *testing.T) { uf := stdlib.FuncAIR(func(a int) {}) ret, err := funcCall(uf, &tengo.Int{Value: 55}) require.NoError(t, err) require.Equal(t, tengo.UndefinedValue, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncARI64(t *testing.T) { uf := stdlib.FuncARI64(func() int64 { return 55 }) ret, err := funcCall(uf) require.NoError(t, err) require.Equal(t, &tengo.Int{Value: 55}, ret) _, err = funcCall(uf, &tengo.Int{Value: 55}) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncASsSRS(t *testing.T) { uf := stdlib.FuncASsSRS(func(a []string, b string) string { return strings.Join(a, b) }) ret, err := funcCall(uf, array(&tengo.String{Value: "abc"}, &tengo.String{Value: "def"}), &tengo.String{Value: "-"}) require.NoError(t, err) require.Equal(t, &tengo.String{Value: "abc-def"}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func TestFuncAI64RI64(t *testing.T) { uf := stdlib.FuncAI64RI64(func(a int64) int64 { return a * 2 }) ret, err := funcCall(uf, &tengo.Int{Value: 55}) require.NoError(t, err) require.Equal(t, &tengo.Int{Value: 110}, ret) _, err = funcCall(uf) require.Equal(t, tengo.ErrWrongNumArguments, err) } func funcCall( fn tengo.CallableFunc, args ...tengo.Object, ) (tengo.Object, error) { userFunc := &tengo.UserFunction{Value: fn} return userFunc.Call(args...) } func array(elements ...tengo.Object) *tengo.Array { return &tengo.Array{Value: elements} } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/gensrcmods.go0000644000175000017500000000213314607001502021115 0ustar00maythammaytham// +build ignore package main import ( "bytes" "io/ioutil" "log" "regexp" "strconv" ) var tengoModFileRE = regexp.MustCompile(`^srcmod_(\w+).tengo$`) func main() { modules := make(map[string]string) // enumerate all Tengo module files files, err := ioutil.ReadDir(".") if err != nil { log.Fatal(err) } for _, file := range files { m := tengoModFileRE.FindStringSubmatch(file.Name()) if m != nil { modName := m[1] src, err := ioutil.ReadFile(file.Name()) if err != nil { log.Fatalf("file '%s' read error: %s", file.Name(), err.Error()) } modules[modName] = string(src) } } var out bytes.Buffer out.WriteString(`// Code generated using gensrcmods.go; DO NOT EDIT. package stdlib // SourceModules are source type standard library modules. var SourceModules = map[string]string{` + "\n") for modName, modSrc := range modules { out.WriteString("\t\"" + modName + "\": " + strconv.Quote(modSrc) + ",\n") } out.WriteString("}\n") const target = "source_modules.go" if err := ioutil.WriteFile(target, out.Bytes(), 0644); err != nil { log.Fatal(err) } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/hex.go0000644000175000017500000000037314607001502017541 0ustar00maythammaythampackage stdlib import ( "encoding/hex" "github.com/d5/tengo/v2" ) var hexModule = map[string]tengo.Object{ "encode": &tengo.UserFunction{Value: FuncAYRS(hex.EncodeToString)}, "decode": &tengo.UserFunction{Value: FuncASRYE(hex.DecodeString)}, } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/hex_test.go0000644000175000017500000000046514607001502020602 0ustar00maythammaythampackage stdlib_test import "testing" var hexBytes1 = []byte{ 0x06, 0xAC, 0x76, 0x1B, 0x1D, 0x6A, 0xFA, 0x9D, 0xB1, 0xA0, } const hex1 = "06ac761b1d6afa9db1a0" func TestHex(t *testing.T) { module(t, `hex`).call("encode", hexBytes1).expect(hex1) module(t, `hex`).call("decode", hex1).expect(hexBytes1) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/json/0000755000175000017500000000000014607001502017374 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/json/decode.go0000644000175000017500000001634714607001502021161 0ustar00maythammaytham// A modified version of Go's JSON implementation. // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package json import ( "strconv" "unicode" "unicode/utf16" "unicode/utf8" "github.com/d5/tengo/v2" ) // Decode parses the JSON-encoded data and returns the result object. func Decode(data []byte) (tengo.Object, error) { var d decodeState err := checkValid(data, &d.scan) if err != nil { return nil, err } d.init(data) d.scan.reset() d.scanWhile(scanSkipSpace) return d.value() } // decodeState represents the state while decoding a JSON value. type decodeState struct { data []byte off int // next read offset in data opcode int // last read result scan scanner } // readIndex returns the position of the last byte read. func (d *decodeState) readIndex() int { return d.off - 1 } const phasePanicMsg = "JSON decoder out of sync - data changing underfoot?" func (d *decodeState) init(data []byte) *decodeState { d.data = data d.off = 0 return d } // scanNext processes the byte at d.data[d.off]. func (d *decodeState) scanNext() { if d.off < len(d.data) { d.opcode = d.scan.step(&d.scan, d.data[d.off]) d.off++ } else { d.opcode = d.scan.eof() d.off = len(d.data) + 1 // mark processed EOF with len+1 } } // scanWhile processes bytes in d.data[d.off:] until it // receives a scan code not equal to op. func (d *decodeState) scanWhile(op int) (isFloat bool) { s, data, i := &d.scan, d.data, d.off for i < len(data) { if data[i] == '.' || data[i] == 'e' || data[i] == 'E' { isFloat = true } newOp := s.step(s, data[i]) i++ if newOp != op { d.opcode = newOp d.off = i return } } d.off = len(data) + 1 // mark processed EOF with len+1 d.opcode = d.scan.eof() return } func (d *decodeState) value() (tengo.Object, error) { switch d.opcode { default: panic(phasePanicMsg) case scanBeginArray: o, err := d.array() if err != nil { return nil, err } d.scanNext() return o, nil case scanBeginObject: o, err := d.object() if err != nil { return nil, err } d.scanNext() return o, nil case scanBeginLiteral: return d.literal() } } func (d *decodeState) array() (tengo.Object, error) { var arr []tengo.Object for { // Look ahead for ] - can only happen on first iteration. d.scanWhile(scanSkipSpace) if d.opcode == scanEndArray { break } o, err := d.value() if err != nil { return nil, err } arr = append(arr, o) // Next token must be , or ]. if d.opcode == scanSkipSpace { d.scanWhile(scanSkipSpace) } if d.opcode == scanEndArray { break } if d.opcode != scanArrayValue { panic(phasePanicMsg) } } return &tengo.Array{Value: arr}, nil } func (d *decodeState) object() (tengo.Object, error) { m := make(map[string]tengo.Object) for { // Read opening " of string key or closing }. d.scanWhile(scanSkipSpace) if d.opcode == scanEndObject { // closing } - can only happen on first iteration. break } if d.opcode != scanBeginLiteral { panic(phasePanicMsg) } // Read string key. start := d.readIndex() d.scanWhile(scanContinue) item := d.data[start:d.readIndex()] key, ok := unquote(item) if !ok { panic(phasePanicMsg) } // Read : before value. if d.opcode == scanSkipSpace { d.scanWhile(scanSkipSpace) } if d.opcode != scanObjectKey { panic(phasePanicMsg) } d.scanWhile(scanSkipSpace) // Read value. o, err := d.value() if err != nil { return nil, err } m[key] = o // Next token must be , or }. if d.opcode == scanSkipSpace { d.scanWhile(scanSkipSpace) } if d.opcode == scanEndObject { break } if d.opcode != scanObjectValue { panic(phasePanicMsg) } } return &tengo.Map{Value: m}, nil } func (d *decodeState) literal() (tengo.Object, error) { // All bytes inside literal return scanContinue op code. start := d.readIndex() isFloat := d.scanWhile(scanContinue) item := d.data[start:d.readIndex()] switch c := item[0]; c { case 'n': // null return tengo.UndefinedValue, nil case 't', 'f': // true, false if c == 't' { return tengo.TrueValue, nil } return tengo.FalseValue, nil case '"': // string s, ok := unquote(item) if !ok { panic(phasePanicMsg) } return &tengo.String{Value: s}, nil default: // number if c != '-' && (c < '0' || c > '9') { panic(phasePanicMsg) } if isFloat { n, _ := strconv.ParseFloat(string(item), 10) return &tengo.Float{Value: n}, nil } n, _ := strconv.ParseInt(string(item), 10, 64) return &tengo.Int{Value: n}, nil } } // getu4 decodes \uXXXX from the beginning of s, returning the hex value, // or it returns -1. func getu4(s []byte) rune { if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { return -1 } var r rune for _, c := range s[2:6] { switch { case '0' <= c && c <= '9': c = c - '0' case 'a' <= c && c <= 'f': c = c - 'a' + 10 case 'A' <= c && c <= 'F': c = c - 'A' + 10 default: return -1 } r = r*16 + rune(c) } return r } // unquote converts a quoted JSON string literal s into an actual string t. // The rules are different than for Go, so cannot use strconv.Unquote. func unquote(s []byte) (t string, ok bool) { s, ok = unquoteBytes(s) t = string(s) return } func unquoteBytes(s []byte) (t []byte, ok bool) { if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { return } s = s[1 : len(s)-1] // Check for unusual characters. If there are none, then no unquoting is // needed, so return a slice of the original bytes. r := 0 for r < len(s) { c := s[r] if c == '\\' || c == '"' || c < ' ' { break } if c < utf8.RuneSelf { r++ continue } rr, size := utf8.DecodeRune(s[r:]) if rr == utf8.RuneError && size == 1 { break } r += size } if r == len(s) { return s, true } b := make([]byte, len(s)+2*utf8.UTFMax) w := copy(b, s[0:r]) for r < len(s) { // Out of room? Can only happen if s is full of // malformed UTF-8 and we're replacing each // byte with RuneError. if w >= len(b)-2*utf8.UTFMax { nb := make([]byte, (len(b)+utf8.UTFMax)*2) copy(nb, b[0:w]) b = nb } switch c := s[r]; { case c == '\\': r++ if r >= len(s) { return } switch s[r] { default: return case '"', '\\', '/', '\'': b[w] = s[r] r++ w++ case 'b': b[w] = '\b' r++ w++ case 'f': b[w] = '\f' r++ w++ case 'n': b[w] = '\n' r++ w++ case 'r': b[w] = '\r' r++ w++ case 't': b[w] = '\t' r++ w++ case 'u': r-- rr := getu4(s[r:]) if rr < 0 { return } r += 6 if utf16.IsSurrogate(rr) { rr1 := getu4(s[r:]) dec := utf16.DecodeRune(rr, rr1) if dec != unicode.ReplacementChar { // A valid pair; consume. r += 6 w += utf8.EncodeRune(b[w:], dec) break } // Invalid surrogate; fall back to replacement rune. rr = unicode.ReplacementChar } w += utf8.EncodeRune(b[w:], rr) } // Quote, control characters are invalid. case c == '"', c < ' ': return // ASCII case c < utf8.RuneSelf: b[w] = c r++ w++ // Coerce to well-formed UTF-8. default: rr, size := utf8.DecodeRune(s[r:]) r += size w += utf8.EncodeRune(b[w:], rr) } } return b[0:w], true } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/json/encode.go0000644000175000017500000001567414607001502021175 0ustar00maythammaytham// A modified version of Go's JSON implementation. // Copyright 2010, 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package json import ( "bytes" "encoding/base64" "errors" "math" "strconv" "unicode/utf8" "github.com/d5/tengo/v2" ) // safeSet holds the value true if the ASCII character with the given array // position can be represented inside a JSON string without any further // escaping. // // All values are true except for the ASCII control characters (0-31), the // double quote ("), and the backslash character ("\"). var safeSet = [utf8.RuneSelf]bool{ ' ': true, '!': true, '"': false, '#': true, '$': true, '%': true, '&': true, '\'': true, '(': true, ')': true, '*': true, '+': true, ',': true, '-': true, '.': true, '/': true, '0': true, '1': true, '2': true, '3': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true, ':': true, ';': true, '<': true, '=': true, '>': true, '?': true, '@': true, 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true, 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true, 'Y': true, 'Z': true, '[': true, '\\': false, ']': true, '^': true, '_': true, '`': true, 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true, 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true, 'y': true, 'z': true, '{': true, '|': true, '}': true, '~': true, '\u007f': true, } var hex = "0123456789abcdef" // Encode returns the JSON encoding of the object. func Encode(o tengo.Object) ([]byte, error) { var b []byte switch o := o.(type) { case *tengo.Array: b = append(b, '[') len1 := len(o.Value) - 1 for idx, elem := range o.Value { eb, err := Encode(elem) if err != nil { return nil, err } b = append(b, eb...) if idx < len1 { b = append(b, ',') } } b = append(b, ']') case *tengo.ImmutableArray: b = append(b, '[') len1 := len(o.Value) - 1 for idx, elem := range o.Value { eb, err := Encode(elem) if err != nil { return nil, err } b = append(b, eb...) if idx < len1 { b = append(b, ',') } } b = append(b, ']') case *tengo.Map: b = append(b, '{') len1 := len(o.Value) - 1 idx := 0 for key, value := range o.Value { b = encodeString(b, key) b = append(b, ':') eb, err := Encode(value) if err != nil { return nil, err } b = append(b, eb...) if idx < len1 { b = append(b, ',') } idx++ } b = append(b, '}') case *tengo.ImmutableMap: b = append(b, '{') len1 := len(o.Value) - 1 idx := 0 for key, value := range o.Value { b = encodeString(b, key) b = append(b, ':') eb, err := Encode(value) if err != nil { return nil, err } b = append(b, eb...) if idx < len1 { b = append(b, ',') } idx++ } b = append(b, '}') case *tengo.Bool: if o.IsFalsy() { b = strconv.AppendBool(b, false) } else { b = strconv.AppendBool(b, true) } case *tengo.Bytes: b = append(b, '"') encodedLen := base64.StdEncoding.EncodedLen(len(o.Value)) dst := make([]byte, encodedLen) base64.StdEncoding.Encode(dst, o.Value) b = append(b, dst...) b = append(b, '"') case *tengo.Char: b = strconv.AppendInt(b, int64(o.Value), 10) case *tengo.Float: var y []byte f := o.Value if math.IsInf(f, 0) || math.IsNaN(f) { return nil, errors.New("unsupported float value") } // Convert as if by ES6 number to string conversion. // This matches most other JSON generators. abs := math.Abs(f) fmt := byte('f') if abs != 0 { if abs < 1e-6 || abs >= 1e21 { fmt = 'e' } } y = strconv.AppendFloat(y, f, fmt, -1, 64) if fmt == 'e' { // clean up e-09 to e-9 n := len(y) if n >= 4 && y[n-4] == 'e' && y[n-3] == '-' && y[n-2] == '0' { y[n-2] = y[n-1] y = y[:n-1] } } b = append(b, y...) case *tengo.Int: b = strconv.AppendInt(b, o.Value, 10) case *tengo.String: // string encoding bug is fixed with newly introduced function // encodeString(). See: https://github.com/d5/tengo/issues/268 b = encodeString(b, o.Value) case *tengo.Time: y, err := o.Value.MarshalJSON() if err != nil { return nil, err } b = append(b, y...) case *tengo.Undefined: b = append(b, "null"...) default: // unknown type: ignore } return b, nil } // encodeString encodes given string as JSON string according to // https://www.json.org/img/string.png // Implementation is inspired by https://github.com/json-iterator/go // See encodeStringSlowPath() for more information. func encodeString(b []byte, val string) []byte { valLen := len(val) buf := bytes.NewBuffer(b) buf.WriteByte('"') // write string, the fast path, without utf8 and escape support i := 0 for ; i < valLen; i++ { c := val[i] if c > 31 && c != '"' && c != '\\' { buf.WriteByte(c) } else { break } } if i == valLen { buf.WriteByte('"') return buf.Bytes() } encodeStringSlowPath(buf, i, val, valLen) buf.WriteByte('"') return buf.Bytes() } // encodeStringSlowPath is ported from Go 1.14.2 encoding/json package. // U+2028 U+2029 JSONP security holes can be fixed with addition call to // json.html_escape() thus it is removed from the implementation below. // Note: Invalid runes are not checked as they are checked in original // implementation. func encodeStringSlowPath(buf *bytes.Buffer, i int, val string, valLen int) { start := i for i < valLen { if b := val[i]; b < utf8.RuneSelf { if safeSet[b] { i++ continue } if start < i { buf.WriteString(val[start:i]) } buf.WriteByte('\\') switch b { case '\\', '"': buf.WriteByte(b) case '\n': buf.WriteByte('n') case '\r': buf.WriteByte('r') case '\t': buf.WriteByte('t') default: // This encodes bytes < 0x20 except for \t, \n and \r. // If escapeHTML is set, it also escapes <, >, and & // because they can lead to security holes when // user-controlled strings are rendered into JSON // and served to some browsers. buf.WriteString(`u00`) buf.WriteByte(hex[b>>4]) buf.WriteByte(hex[b&0xF]) } i++ start = i continue } i++ continue } if start < valLen { buf.WriteString(val[start:]) } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/json/json_test.go0000644000175000017500000000551714607001502021743 0ustar00maythammaythampackage json_test import ( gojson "encoding/json" "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" "github.com/d5/tengo/v2/stdlib/json" ) type ARR = []interface{} type MAP = map[string]interface{} func TestJSON(t *testing.T) { testJSONEncodeDecode(t, nil) testJSONEncodeDecode(t, 0) testJSONEncodeDecode(t, 1) testJSONEncodeDecode(t, -1) testJSONEncodeDecode(t, 1984) testJSONEncodeDecode(t, -1984) testJSONEncodeDecode(t, 0.0) testJSONEncodeDecode(t, 1.0) testJSONEncodeDecode(t, -1.0) testJSONEncodeDecode(t, 19.84) testJSONEncodeDecode(t, -19.84) testJSONEncodeDecode(t, "") testJSONEncodeDecode(t, "foo") testJSONEncodeDecode(t, "foo bar") testJSONEncodeDecode(t, "foo \"bar\"") // See: https://github.com/d5/tengo/issues/268 testJSONEncodeDecode(t, "1\u001C04") testJSONEncodeDecode(t, "çığöşü") testJSONEncodeDecode(t, "ç1\u001C04IĞÖŞÜ") testJSONEncodeDecode(t, "错误测试") testJSONEncodeDecode(t, true) testJSONEncodeDecode(t, false) testJSONEncodeDecode(t, ARR{}) testJSONEncodeDecode(t, ARR{0}) testJSONEncodeDecode(t, ARR{false}) testJSONEncodeDecode(t, ARR{1, 2, 3, "four", false}) testJSONEncodeDecode(t, ARR{1, 2, 3, "four", false, MAP{"a": 0, "b": "bee", "bool": true}}) testJSONEncodeDecode(t, MAP{}) testJSONEncodeDecode(t, MAP{"a": 0}) testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee"}) testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee", "bool": true}) testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee", "arr": ARR{1, 2, 3, "four"}}) testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee", "arr": ARR{1, 2, 3, MAP{"a": false, "b": 109.4}}}) testJSONEncodeDecode(t, MAP{"id1": 7075984636689534001, "id2": 7075984636689534002}) testJSONEncodeDecode(t, ARR{1e3, 1E7}) } func TestDecode(t *testing.T) { testDecodeError(t, `{`) testDecodeError(t, `}`) testDecodeError(t, `{}a`) testDecodeError(t, `{{}`) testDecodeError(t, `{}}`) testDecodeError(t, `[`) testDecodeError(t, `]`) testDecodeError(t, `[]a`) testDecodeError(t, `[[]`) testDecodeError(t, `[]]`) testDecodeError(t, `"`) testDecodeError(t, `"abc`) testDecodeError(t, `abc"`) testDecodeError(t, `.123`) testDecodeError(t, `123.`) testDecodeError(t, `1.2.3`) testDecodeError(t, `'a'`) testDecodeError(t, `true, false`) testDecodeError(t, `{"a:"b"}`) testDecodeError(t, `{a":"b"}`) testDecodeError(t, `{"a":"b":"c"}`) } func testDecodeError(t *testing.T, input string) { _, err := json.Decode([]byte(input)) require.Error(t, err) } func testJSONEncodeDecode(t *testing.T, v interface{}) { o, err := tengo.FromInterface(v) require.NoError(t, err) b, err := json.Encode(o) require.NoError(t, err) a, err := json.Decode(b) require.NoError(t, err, string(b)) vj, err := gojson.Marshal(v) require.NoError(t, err) aj, err := gojson.Marshal(tengo.ToInterface(a)) require.NoError(t, err) require.Equal(t, vj, aj) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/json/scanner.go0000644000175000017500000003510314607001502021356 0ustar00maythammaytham// A modified version of Go's JSON implementation. // Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package json import "strconv" func checkValid(data []byte, scan *scanner) error { scan.reset() for _, c := range data { scan.bytes++ if scan.step(scan, c) == scanError { return scan.err } } if scan.eof() == scanError { return scan.err } return nil } // A SyntaxError is a description of a JSON syntax error. type SyntaxError struct { msg string // description of error Offset int64 // error occurred after reading Offset bytes } func (e *SyntaxError) Error() string { return e.msg } // A scanner is a JSON scanning state machine. // Callers call scan.reset() and then pass bytes in one at a time // by calling scan.step(&scan, c) for each byte. // The return value, referred to as an opcode, tells the // caller about significant parsing events like beginning // and ending literals, objects, and arrays, so that the // caller can follow along if it wishes. // The return value scanEnd indicates that a single top-level // JSON value has been completed, *before* the byte that // just got passed in. (The indication must be delayed in order // to recognize the end of numbers: is 123 a whole value or // the beginning of 12345e+6?). type scanner struct { // The step is a func to be called to execute the next transition. // Also tried using an integer constant and a single func // with a switch, but using the func directly was 10% faster // on a 64-bit Mac Mini, and it's nicer to read. step func(*scanner, byte) int // Reached end of top-level value. endTop bool // Stack of what we're in the middle of - array values, object keys, object values. parseState []int // Error that happened, if any. err error // total bytes consumed, updated by decoder.Decode bytes int64 } // These values are returned by the state transition functions // assigned to scanner.state and the method scanner.eof. // They give details about the current state of the scan that // callers might be interested to know about. // It is okay to ignore the return value of any particular // call to scanner.state: if one call returns scanError, // every subsequent call will return scanError too. const ( // Continue. scanContinue = iota // uninteresting byte scanBeginLiteral // end implied by next result != scanContinue scanBeginObject // begin object scanObjectKey // just finished object key (string) scanObjectValue // just finished non-last object value scanEndObject // end object (implies scanObjectValue if possible) scanBeginArray // begin array scanArrayValue // just finished array value scanEndArray // end array (implies scanArrayValue if possible) scanSkipSpace // space byte; can skip; known to be last "continue" result // Stop. scanEnd // top-level value ended *before* this byte; known to be first "stop" result scanError // hit an error, scanner.err. ) // These values are stored in the parseState stack. // They give the current state of a composite value // being scanned. If the parser is inside a nested value // the parseState describes the nested state, outermost at entry 0. const ( parseObjectKey = iota // parsing object key (before colon) parseObjectValue // parsing object value (after colon) parseArrayValue // parsing array value ) // reset prepares the scanner for use. // It must be called before calling s.step. func (s *scanner) reset() { s.step = stateBeginValue s.parseState = s.parseState[0:0] s.err = nil s.endTop = false } // eof tells the scanner that the end of input has been reached. // It returns a scan status just as s.step does. func (s *scanner) eof() int { if s.err != nil { return scanError } if s.endTop { return scanEnd } s.step(s, ' ') if s.endTop { return scanEnd } if s.err == nil { s.err = &SyntaxError{"unexpected end of JSON input", s.bytes} } return scanError } // pushParseState pushes a new parse state p onto the parse stack. func (s *scanner) pushParseState(p int) { s.parseState = append(s.parseState, p) } // popParseState pops a parse state (already obtained) off the stack // and updates s.step accordingly. func (s *scanner) popParseState() { n := len(s.parseState) - 1 s.parseState = s.parseState[0:n] if n == 0 { s.step = stateEndTop s.endTop = true } else { s.step = stateEndValue } } func isSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\r' || c == '\n' } // stateBeginValueOrEmpty is the state after reading `[`. func stateBeginValueOrEmpty(s *scanner, c byte) int { if c <= ' ' && isSpace(c) { return scanSkipSpace } if c == ']' { return stateEndValue(s, c) } return stateBeginValue(s, c) } // stateBeginValue is the state at the beginning of the input. func stateBeginValue(s *scanner, c byte) int { if c <= ' ' && isSpace(c) { return scanSkipSpace } switch c { case '{': s.step = stateBeginStringOrEmpty s.pushParseState(parseObjectKey) return scanBeginObject case '[': s.step = stateBeginValueOrEmpty s.pushParseState(parseArrayValue) return scanBeginArray case '"': s.step = stateInString return scanBeginLiteral case '-': s.step = stateNeg return scanBeginLiteral case '0': // beginning of 0.123 s.step = state0 return scanBeginLiteral case 't': // beginning of true s.step = stateT return scanBeginLiteral case 'f': // beginning of false s.step = stateF return scanBeginLiteral case 'n': // beginning of null s.step = stateN return scanBeginLiteral } if '1' <= c && c <= '9' { // beginning of 1234.5 s.step = state1 return scanBeginLiteral } return s.error(c, "looking for beginning of value") } // stateBeginStringOrEmpty is the state after reading `{`. func stateBeginStringOrEmpty(s *scanner, c byte) int { if c <= ' ' && isSpace(c) { return scanSkipSpace } if c == '}' { n := len(s.parseState) s.parseState[n-1] = parseObjectValue return stateEndValue(s, c) } return stateBeginString(s, c) } // stateBeginString is the state after reading `{"key": value,`. func stateBeginString(s *scanner, c byte) int { if c <= ' ' && isSpace(c) { return scanSkipSpace } if c == '"' { s.step = stateInString return scanBeginLiteral } return s.error(c, "looking for beginning of object key string") } // stateEndValue is the state after completing a value, // such as after reading `{}` or `true` or `["x"`. func stateEndValue(s *scanner, c byte) int { n := len(s.parseState) if n == 0 { // Completed top-level before the current byte. s.step = stateEndTop s.endTop = true return stateEndTop(s, c) } if c <= ' ' && isSpace(c) { s.step = stateEndValue return scanSkipSpace } ps := s.parseState[n-1] switch ps { case parseObjectKey: if c == ':' { s.parseState[n-1] = parseObjectValue s.step = stateBeginValue return scanObjectKey } return s.error(c, "after object key") case parseObjectValue: if c == ',' { s.parseState[n-1] = parseObjectKey s.step = stateBeginString return scanObjectValue } if c == '}' { s.popParseState() return scanEndObject } return s.error(c, "after object key:value pair") case parseArrayValue: if c == ',' { s.step = stateBeginValue return scanArrayValue } if c == ']' { s.popParseState() return scanEndArray } return s.error(c, "after array element") } return s.error(c, "") } // stateEndTop is the state after finishing the top-level value, // such as after reading `{}` or `[1,2,3]`. // Only space characters should be seen now. func stateEndTop(s *scanner, c byte) int { if !isSpace(c) { // Complain about non-space byte on next call. s.error(c, "after top-level value") } return scanEnd } // stateInString is the state after reading `"`. func stateInString(s *scanner, c byte) int { if c == '"' { s.step = stateEndValue return scanContinue } if c == '\\' { s.step = stateInStringEsc return scanContinue } if c < 0x20 { return s.error(c, "in string literal") } return scanContinue } // stateInStringEsc is the state after reading `"\` during a quoted string. func stateInStringEsc(s *scanner, c byte) int { switch c { case 'b', 'f', 'n', 'r', 't', '\\', '/', '"': s.step = stateInString return scanContinue case 'u': s.step = stateInStringEscU return scanContinue } return s.error(c, "in string escape code") } // stateInStringEscU is the state after reading `"\u` during a quoted string. func stateInStringEscU(s *scanner, c byte) int { if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' { s.step = stateInStringEscU1 return scanContinue } // numbers return s.error(c, "in \\u hexadecimal character escape") } // stateInStringEscU1 is the state after reading `"\u1` during a quoted string. func stateInStringEscU1(s *scanner, c byte) int { if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' { s.step = stateInStringEscU12 return scanContinue } // numbers return s.error(c, "in \\u hexadecimal character escape") } // stateInStringEscU12 is the state after reading `"\u12` during a quoted string. func stateInStringEscU12(s *scanner, c byte) int { if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' { s.step = stateInStringEscU123 return scanContinue } // numbers return s.error(c, "in \\u hexadecimal character escape") } // stateInStringEscU123 is the state after reading `"\u123` during a quoted string. func stateInStringEscU123(s *scanner, c byte) int { if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' { s.step = stateInString return scanContinue } // numbers return s.error(c, "in \\u hexadecimal character escape") } // stateNeg is the state after reading `-` during a number. func stateNeg(s *scanner, c byte) int { if c == '0' { s.step = state0 return scanContinue } if '1' <= c && c <= '9' { s.step = state1 return scanContinue } return s.error(c, "in numeric literal") } // state1 is the state after reading a non-zero integer during a number, // such as after reading `1` or `100` but not `0`. func state1(s *scanner, c byte) int { if '0' <= c && c <= '9' { s.step = state1 return scanContinue } return state0(s, c) } // state0 is the state after reading `0` during a number. func state0(s *scanner, c byte) int { if c == '.' { s.step = stateDot return scanContinue } if c == 'e' || c == 'E' { s.step = stateE return scanContinue } return stateEndValue(s, c) } // stateDot is the state after reading the integer and decimal point in a number, // such as after reading `1.`. func stateDot(s *scanner, c byte) int { if '0' <= c && c <= '9' { s.step = stateDot0 return scanContinue } return s.error(c, "after decimal point in numeric literal") } // stateDot0 is the state after reading the integer, decimal point, and subsequent // digits of a number, such as after reading `3.14`. func stateDot0(s *scanner, c byte) int { if '0' <= c && c <= '9' { return scanContinue } if c == 'e' || c == 'E' { s.step = stateE return scanContinue } return stateEndValue(s, c) } // stateE is the state after reading the mantissa and e in a number, // such as after reading `314e` or `0.314e`. func stateE(s *scanner, c byte) int { if c == '+' || c == '-' { s.step = stateESign return scanContinue } return stateESign(s, c) } // stateESign is the state after reading the mantissa, e, and sign in a number, // such as after reading `314e-` or `0.314e+`. func stateESign(s *scanner, c byte) int { if '0' <= c && c <= '9' { s.step = stateE0 return scanContinue } return s.error(c, "in exponent of numeric literal") } // stateE0 is the state after reading the mantissa, e, optional sign, // and at least one digit of the exponent in a number, // such as after reading `314e-2` or `0.314e+1` or `3.14e0`. func stateE0(s *scanner, c byte) int { if '0' <= c && c <= '9' { return scanContinue } return stateEndValue(s, c) } // stateT is the state after reading `t`. func stateT(s *scanner, c byte) int { if c == 'r' { s.step = stateTr return scanContinue } return s.error(c, "in literal true (expecting 'r')") } // stateTr is the state after reading `tr`. func stateTr(s *scanner, c byte) int { if c == 'u' { s.step = stateTru return scanContinue } return s.error(c, "in literal true (expecting 'u')") } // stateTru is the state after reading `tru`. func stateTru(s *scanner, c byte) int { if c == 'e' { s.step = stateEndValue return scanContinue } return s.error(c, "in literal true (expecting 'e')") } // stateF is the state after reading `f`. func stateF(s *scanner, c byte) int { if c == 'a' { s.step = stateFa return scanContinue } return s.error(c, "in literal false (expecting 'a')") } // stateFa is the state after reading `fa`. func stateFa(s *scanner, c byte) int { if c == 'l' { s.step = stateFal return scanContinue } return s.error(c, "in literal false (expecting 'l')") } // stateFal is the state after reading `fal`. func stateFal(s *scanner, c byte) int { if c == 's' { s.step = stateFals return scanContinue } return s.error(c, "in literal false (expecting 's')") } // stateFals is the state after reading `fals`. func stateFals(s *scanner, c byte) int { if c == 'e' { s.step = stateEndValue return scanContinue } return s.error(c, "in literal false (expecting 'e')") } // stateN is the state after reading `n`. func stateN(s *scanner, c byte) int { if c == 'u' { s.step = stateNu return scanContinue } return s.error(c, "in literal null (expecting 'u')") } // stateNu is the state after reading `nu`. func stateNu(s *scanner, c byte) int { if c == 'l' { s.step = stateNul return scanContinue } return s.error(c, "in literal null (expecting 'l')") } // stateNul is the state after reading `nul`. func stateNul(s *scanner, c byte) int { if c == 'l' { s.step = stateEndValue return scanContinue } return s.error(c, "in literal null (expecting 'l')") } // stateError is the state after reaching a syntax error, // such as after reading `[1}` or `5.1.2`. func stateError(_ *scanner, _ byte) int { return scanError } // error records an error and switches to the error state. func (s *scanner) error(c byte, context string) int { s.step = stateError s.err = &SyntaxError{ msg: "invalid character " + quoteChar(c) + " " + context, Offset: s.bytes, } return scanError } // quoteChar formats c as a quoted character literal func quoteChar(c byte) string { // special cases - different from quoted strings if c == '\'' { return `'\''` } if c == '"' { return `'"'` } // use quoted string with different quotation marks s := strconv.Quote(string(c)) return "'" + s[1:len(s)-1] + "'" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/json.go0000644000175000017500000000623214607001502017726 0ustar00maythammaythampackage stdlib import ( "bytes" gojson "encoding/json" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/stdlib/json" ) var jsonModule = map[string]tengo.Object{ "decode": &tengo.UserFunction{ Name: "decode", Value: jsonDecode, }, "encode": &tengo.UserFunction{ Name: "encode", Value: jsonEncode, }, "indent": &tengo.UserFunction{ Name: "encode", Value: jsonIndent, }, "html_escape": &tengo.UserFunction{ Name: "html_escape", Value: jsonHTMLEscape, }, } func jsonDecode(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } switch o := args[0].(type) { case *tengo.Bytes: v, err := json.Decode(o.Value) if err != nil { return &tengo.Error{ Value: &tengo.String{Value: err.Error()}, }, nil } return v, nil case *tengo.String: v, err := json.Decode([]byte(o.Value)) if err != nil { return &tengo.Error{ Value: &tengo.String{Value: err.Error()}, }, nil } return v, nil default: return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "bytes/string", Found: args[0].TypeName(), } } } func jsonEncode(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } b, err := json.Encode(args[0]) if err != nil { return &tengo.Error{Value: &tengo.String{Value: err.Error()}}, nil } return &tengo.Bytes{Value: b}, nil } func jsonIndent(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 3 { return nil, tengo.ErrWrongNumArguments } prefix, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "prefix", Expected: "string(compatible)", Found: args[1].TypeName(), } } indent, ok := tengo.ToString(args[2]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "indent", Expected: "string(compatible)", Found: args[2].TypeName(), } } switch o := args[0].(type) { case *tengo.Bytes: var dst bytes.Buffer err := gojson.Indent(&dst, o.Value, prefix, indent) if err != nil { return &tengo.Error{ Value: &tengo.String{Value: err.Error()}, }, nil } return &tengo.Bytes{Value: dst.Bytes()}, nil case *tengo.String: var dst bytes.Buffer err := gojson.Indent(&dst, []byte(o.Value), prefix, indent) if err != nil { return &tengo.Error{ Value: &tengo.String{Value: err.Error()}, }, nil } return &tengo.Bytes{Value: dst.Bytes()}, nil default: return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "bytes/string", Found: args[0].TypeName(), } } } func jsonHTMLEscape(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } switch o := args[0].(type) { case *tengo.Bytes: var dst bytes.Buffer gojson.HTMLEscape(&dst, o.Value) return &tengo.Bytes{Value: dst.Bytes()}, nil case *tengo.String: var dst bytes.Buffer gojson.HTMLEscape(&dst, []byte(o.Value)) return &tengo.Bytes{Value: dst.Bytes()}, nil default: return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "bytes/string", Found: args[0].TypeName(), } } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/json_test.go0000644000175000017500000000560114607001502020764 0ustar00maythammaythampackage stdlib_test import "testing" func TestJSON(t *testing.T) { module(t, "json").call("encode", 5). expect([]byte("5")) module(t, "json").call("encode", "foobar"). expect([]byte(`"foobar"`)) module(t, "json").call("encode", MAP{"foo": 5}). expect([]byte("{\"foo\":5}")) module(t, "json").call("encode", IMAP{"foo": 5}). expect([]byte("{\"foo\":5}")) module(t, "json").call("encode", ARR{1, 2, 3}). expect([]byte("[1,2,3]")) module(t, "json").call("encode", IARR{1, 2, 3}). expect([]byte("[1,2,3]")) module(t, "json").call("encode", MAP{"foo": "bar"}). expect([]byte("{\"foo\":\"bar\"}")) module(t, "json").call("encode", MAP{"foo": 1.8}). expect([]byte("{\"foo\":1.8}")) module(t, "json").call("encode", MAP{"foo": true}). expect([]byte("{\"foo\":true}")) module(t, "json").call("encode", MAP{"foo": '8'}). expect([]byte("{\"foo\":56}")) module(t, "json").call("encode", MAP{"foo": []byte("foo")}). expect([]byte("{\"foo\":\"Zm9v\"}")) // json encoding returns []byte as base64 encoded string module(t, "json"). call("encode", MAP{"foo": ARR{"bar", 1, 1.8, '8', true}}). expect([]byte("{\"foo\":[\"bar\",1,1.8,56,true]}")) module(t, "json"). call("encode", MAP{"foo": IARR{"bar", 1, 1.8, '8', true}}). expect([]byte("{\"foo\":[\"bar\",1,1.8,56,true]}")) module(t, "json"). call("encode", MAP{"foo": ARR{ARR{"bar", 1}, ARR{"bar", 1}}}). expect([]byte("{\"foo\":[[\"bar\",1],[\"bar\",1]]}")) module(t, "json"). call("encode", MAP{"foo": MAP{"string": "bar"}}). expect([]byte("{\"foo\":{\"string\":\"bar\"}}")) module(t, "json"). call("encode", MAP{"foo": IMAP{"string": "bar"}}). expect([]byte("{\"foo\":{\"string\":\"bar\"}}")) module(t, "json"). call("encode", MAP{"foo": MAP{"map1": MAP{"string": "bar"}}}). expect([]byte("{\"foo\":{\"map1\":{\"string\":\"bar\"}}}")) module(t, "json"). call("encode", ARR{ARR{"bar", 1}, ARR{"bar", 1}}). expect([]byte("[[\"bar\",1],[\"bar\",1]]")) module(t, "json").call("decode", `5`). expect(5) module(t, "json").call("decode", `"foo"`). expect("foo") module(t, "json").call("decode", `[1,2,3,"bar"]`). expect(ARR{1, 2, 3, "bar"}) module(t, "json").call("decode", `{"foo":5}`). expect(MAP{"foo": 5}) module(t, "json").call("decode", `{"foo":2.5}`). expect(MAP{"foo": 2.5}) module(t, "json").call("decode", `{"foo":true}`). expect(MAP{"foo": true}) module(t, "json").call("decode", `{"foo":"bar"}`). expect(MAP{"foo": "bar"}) module(t, "json").call("decode", `{"foo":[1,2,3,"bar"]}`). expect(MAP{"foo": ARR{1, 2, 3, "bar"}}) module(t, "json"). call("indent", []byte("{\"foo\":[\"bar\",1,1.8,56,true]}"), "", " "). expect([]byte(`{ "foo": [ "bar", 1, 1.8, 56, true ] }`)) module(t, "json"). call("html_escape", []byte( `{"M":"foo &`+"\xe2\x80\xa8 \xe2\x80\xa9"+`"}`)). expect([]byte( `{"M":"\u003chtml\u003efoo \u0026\u2028 \u2029\u003c/html\u003e"}`)) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/math.go0000644000175000017500000001353214607001502017707 0ustar00maythammaythampackage stdlib import ( "math" "github.com/d5/tengo/v2" ) var mathModule = map[string]tengo.Object{ "e": &tengo.Float{Value: math.E}, "pi": &tengo.Float{Value: math.Pi}, "phi": &tengo.Float{Value: math.Phi}, "sqrt2": &tengo.Float{Value: math.Sqrt2}, "sqrtE": &tengo.Float{Value: math.SqrtE}, "sqrtPi": &tengo.Float{Value: math.SqrtPi}, "sqrtPhi": &tengo.Float{Value: math.SqrtPhi}, "ln2": &tengo.Float{Value: math.Ln2}, "log2E": &tengo.Float{Value: math.Log2E}, "ln10": &tengo.Float{Value: math.Ln10}, "log10E": &tengo.Float{Value: math.Log10E}, "maxFloat32": &tengo.Float{Value: math.MaxFloat32}, "smallestNonzeroFloat32": &tengo.Float{Value: math.SmallestNonzeroFloat32}, "maxFloat64": &tengo.Float{Value: math.MaxFloat64}, "smallestNonzeroFloat64": &tengo.Float{Value: math.SmallestNonzeroFloat64}, "maxInt": &tengo.Int{Value: math.MaxInt}, "minInt": &tengo.Int{Value: math.MinInt}, "maxInt8": &tengo.Int{Value: math.MaxInt8}, "minInt8": &tengo.Int{Value: math.MinInt8}, "maxInt16": &tengo.Int{Value: math.MaxInt16}, "minInt16": &tengo.Int{Value: math.MinInt16}, "maxInt32": &tengo.Int{Value: math.MaxInt32}, "minInt32": &tengo.Int{Value: math.MinInt32}, "maxInt64": &tengo.Int{Value: math.MaxInt64}, "minInt64": &tengo.Int{Value: math.MinInt64}, "abs": &tengo.UserFunction{ Name: "abs", Value: FuncAFRF(math.Abs), }, "acos": &tengo.UserFunction{ Name: "acos", Value: FuncAFRF(math.Acos), }, "acosh": &tengo.UserFunction{ Name: "acosh", Value: FuncAFRF(math.Acosh), }, "asin": &tengo.UserFunction{ Name: "asin", Value: FuncAFRF(math.Asin), }, "asinh": &tengo.UserFunction{ Name: "asinh", Value: FuncAFRF(math.Asinh), }, "atan": &tengo.UserFunction{ Name: "atan", Value: FuncAFRF(math.Atan), }, "atan2": &tengo.UserFunction{ Name: "atan2", Value: FuncAFFRF(math.Atan2), }, "atanh": &tengo.UserFunction{ Name: "atanh", Value: FuncAFRF(math.Atanh), }, "cbrt": &tengo.UserFunction{ Name: "cbrt", Value: FuncAFRF(math.Cbrt), }, "ceil": &tengo.UserFunction{ Name: "ceil", Value: FuncAFRF(math.Ceil), }, "copysign": &tengo.UserFunction{ Name: "copysign", Value: FuncAFFRF(math.Copysign), }, "cos": &tengo.UserFunction{ Name: "cos", Value: FuncAFRF(math.Cos), }, "cosh": &tengo.UserFunction{ Name: "cosh", Value: FuncAFRF(math.Cosh), }, "dim": &tengo.UserFunction{ Name: "dim", Value: FuncAFFRF(math.Dim), }, "erf": &tengo.UserFunction{ Name: "erf", Value: FuncAFRF(math.Erf), }, "erfc": &tengo.UserFunction{ Name: "erfc", Value: FuncAFRF(math.Erfc), }, "exp": &tengo.UserFunction{ Name: "exp", Value: FuncAFRF(math.Exp), }, "exp2": &tengo.UserFunction{ Name: "exp2", Value: FuncAFRF(math.Exp2), }, "expm1": &tengo.UserFunction{ Name: "expm1", Value: FuncAFRF(math.Expm1), }, "floor": &tengo.UserFunction{ Name: "floor", Value: FuncAFRF(math.Floor), }, "gamma": &tengo.UserFunction{ Name: "gamma", Value: FuncAFRF(math.Gamma), }, "hypot": &tengo.UserFunction{ Name: "hypot", Value: FuncAFFRF(math.Hypot), }, "ilogb": &tengo.UserFunction{ Name: "ilogb", Value: FuncAFRI(math.Ilogb), }, "inf": &tengo.UserFunction{ Name: "inf", Value: FuncAIRF(math.Inf), }, "is_inf": &tengo.UserFunction{ Name: "is_inf", Value: FuncAFIRB(math.IsInf), }, "is_nan": &tengo.UserFunction{ Name: "is_nan", Value: FuncAFRB(math.IsNaN), }, "j0": &tengo.UserFunction{ Name: "j0", Value: FuncAFRF(math.J0), }, "j1": &tengo.UserFunction{ Name: "j1", Value: FuncAFRF(math.J1), }, "jn": &tengo.UserFunction{ Name: "jn", Value: FuncAIFRF(math.Jn), }, "ldexp": &tengo.UserFunction{ Name: "ldexp", Value: FuncAFIRF(math.Ldexp), }, "log": &tengo.UserFunction{ Name: "log", Value: FuncAFRF(math.Log), }, "log10": &tengo.UserFunction{ Name: "log10", Value: FuncAFRF(math.Log10), }, "log1p": &tengo.UserFunction{ Name: "log1p", Value: FuncAFRF(math.Log1p), }, "log2": &tengo.UserFunction{ Name: "log2", Value: FuncAFRF(math.Log2), }, "logb": &tengo.UserFunction{ Name: "logb", Value: FuncAFRF(math.Logb), }, "max": &tengo.UserFunction{ Name: "max", Value: FuncAFFRF(math.Max), }, "min": &tengo.UserFunction{ Name: "min", Value: FuncAFFRF(math.Min), }, "mod": &tengo.UserFunction{ Name: "mod", Value: FuncAFFRF(math.Mod), }, "nan": &tengo.UserFunction{ Name: "nan", Value: FuncARF(math.NaN), }, "nextafter": &tengo.UserFunction{ Name: "nextafter", Value: FuncAFFRF(math.Nextafter), }, "pow": &tengo.UserFunction{ Name: "pow", Value: FuncAFFRF(math.Pow), }, "pow10": &tengo.UserFunction{ Name: "pow10", Value: FuncAIRF(math.Pow10), }, "remainder": &tengo.UserFunction{ Name: "remainder", Value: FuncAFFRF(math.Remainder), }, "signbit": &tengo.UserFunction{ Name: "signbit", Value: FuncAFRB(math.Signbit), }, "sin": &tengo.UserFunction{ Name: "sin", Value: FuncAFRF(math.Sin), }, "sinh": &tengo.UserFunction{ Name: "sinh", Value: FuncAFRF(math.Sinh), }, "sqrt": &tengo.UserFunction{ Name: "sqrt", Value: FuncAFRF(math.Sqrt), }, "tan": &tengo.UserFunction{ Name: "tan", Value: FuncAFRF(math.Tan), }, "tanh": &tengo.UserFunction{ Name: "tanh", Value: FuncAFRF(math.Tanh), }, "trunc": &tengo.UserFunction{ Name: "trunc", Value: FuncAFRF(math.Trunc), }, "y0": &tengo.UserFunction{ Name: "y0", Value: FuncAFRF(math.Y0), }, "y1": &tengo.UserFunction{ Name: "y1", Value: FuncAFRF(math.Y1), }, "yn": &tengo.UserFunction{ Name: "yn", Value: FuncAIFRF(math.Yn), }, } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/os.go0000644000175000017500000003613714607001502017405 0ustar00maythammaythampackage stdlib import ( "fmt" "io" "io/ioutil" "os" "os/exec" "runtime" "github.com/d5/tengo/v2" ) var osModule = map[string]tengo.Object{ "platform": &tengo.String{Value: runtime.GOOS}, "arch": &tengo.String{Value: runtime.GOARCH}, "o_rdonly": &tengo.Int{Value: int64(os.O_RDONLY)}, "o_wronly": &tengo.Int{Value: int64(os.O_WRONLY)}, "o_rdwr": &tengo.Int{Value: int64(os.O_RDWR)}, "o_append": &tengo.Int{Value: int64(os.O_APPEND)}, "o_create": &tengo.Int{Value: int64(os.O_CREATE)}, "o_excl": &tengo.Int{Value: int64(os.O_EXCL)}, "o_sync": &tengo.Int{Value: int64(os.O_SYNC)}, "o_trunc": &tengo.Int{Value: int64(os.O_TRUNC)}, "mode_dir": &tengo.Int{Value: int64(os.ModeDir)}, "mode_append": &tengo.Int{Value: int64(os.ModeAppend)}, "mode_exclusive": &tengo.Int{Value: int64(os.ModeExclusive)}, "mode_temporary": &tengo.Int{Value: int64(os.ModeTemporary)}, "mode_symlink": &tengo.Int{Value: int64(os.ModeSymlink)}, "mode_device": &tengo.Int{Value: int64(os.ModeDevice)}, "mode_named_pipe": &tengo.Int{Value: int64(os.ModeNamedPipe)}, "mode_socket": &tengo.Int{Value: int64(os.ModeSocket)}, "mode_setuid": &tengo.Int{Value: int64(os.ModeSetuid)}, "mode_setgui": &tengo.Int{Value: int64(os.ModeSetgid)}, "mode_char_device": &tengo.Int{Value: int64(os.ModeCharDevice)}, "mode_sticky": &tengo.Int{Value: int64(os.ModeSticky)}, "mode_type": &tengo.Int{Value: int64(os.ModeType)}, "mode_perm": &tengo.Int{Value: int64(os.ModePerm)}, "path_separator": &tengo.Char{Value: os.PathSeparator}, "path_list_separator": &tengo.Char{Value: os.PathListSeparator}, "dev_null": &tengo.String{Value: os.DevNull}, "seek_set": &tengo.Int{Value: int64(io.SeekStart)}, "seek_cur": &tengo.Int{Value: int64(io.SeekCurrent)}, "seek_end": &tengo.Int{Value: int64(io.SeekEnd)}, "args": &tengo.UserFunction{ Name: "args", Value: osArgs, }, // args() => array(string) "chdir": &tengo.UserFunction{ Name: "chdir", Value: FuncASRE(os.Chdir), }, // chdir(dir string) => error "chmod": osFuncASFmRE("chmod", os.Chmod), // chmod(name string, mode int) => error "chown": &tengo.UserFunction{ Name: "chown", Value: FuncASIIRE(os.Chown), }, // chown(name string, uid int, gid int) => error "clearenv": &tengo.UserFunction{ Name: "clearenv", Value: FuncAR(os.Clearenv), }, // clearenv() "environ": &tengo.UserFunction{ Name: "environ", Value: FuncARSs(os.Environ), }, // environ() => array(string) "exit": &tengo.UserFunction{ Name: "exit", Value: FuncAIR(os.Exit), }, // exit(code int) "expand_env": &tengo.UserFunction{ Name: "expand_env", Value: osExpandEnv, }, // expand_env(s string) => string "getegid": &tengo.UserFunction{ Name: "getegid", Value: FuncARI(os.Getegid), }, // getegid() => int "getenv": &tengo.UserFunction{ Name: "getenv", Value: FuncASRS(os.Getenv), }, // getenv(s string) => string "geteuid": &tengo.UserFunction{ Name: "geteuid", Value: FuncARI(os.Geteuid), }, // geteuid() => int "getgid": &tengo.UserFunction{ Name: "getgid", Value: FuncARI(os.Getgid), }, // getgid() => int "getgroups": &tengo.UserFunction{ Name: "getgroups", Value: FuncARIsE(os.Getgroups), }, // getgroups() => array(string)/error "getpagesize": &tengo.UserFunction{ Name: "getpagesize", Value: FuncARI(os.Getpagesize), }, // getpagesize() => int "getpid": &tengo.UserFunction{ Name: "getpid", Value: FuncARI(os.Getpid), }, // getpid() => int "getppid": &tengo.UserFunction{ Name: "getppid", Value: FuncARI(os.Getppid), }, // getppid() => int "getuid": &tengo.UserFunction{ Name: "getuid", Value: FuncARI(os.Getuid), }, // getuid() => int "getwd": &tengo.UserFunction{ Name: "getwd", Value: FuncARSE(os.Getwd), }, // getwd() => string/error "hostname": &tengo.UserFunction{ Name: "hostname", Value: FuncARSE(os.Hostname), }, // hostname() => string/error "lchown": &tengo.UserFunction{ Name: "lchown", Value: FuncASIIRE(os.Lchown), }, // lchown(name string, uid int, gid int) => error "link": &tengo.UserFunction{ Name: "link", Value: FuncASSRE(os.Link), }, // link(oldname string, newname string) => error "lookup_env": &tengo.UserFunction{ Name: "lookup_env", Value: osLookupEnv, }, // lookup_env(key string) => string/false "mkdir": osFuncASFmRE("mkdir", os.Mkdir), // mkdir(name string, perm int) => error "mkdir_all": osFuncASFmRE("mkdir_all", os.MkdirAll), // mkdir_all(name string, perm int) => error "readlink": &tengo.UserFunction{ Name: "readlink", Value: FuncASRSE(os.Readlink), }, // readlink(name string) => string/error "remove": &tengo.UserFunction{ Name: "remove", Value: FuncASRE(os.Remove), }, // remove(name string) => error "remove_all": &tengo.UserFunction{ Name: "remove_all", Value: FuncASRE(os.RemoveAll), }, // remove_all(name string) => error "rename": &tengo.UserFunction{ Name: "rename", Value: FuncASSRE(os.Rename), }, // rename(oldpath string, newpath string) => error "setenv": &tengo.UserFunction{ Name: "setenv", Value: FuncASSRE(os.Setenv), }, // setenv(key string, value string) => error "symlink": &tengo.UserFunction{ Name: "symlink", Value: FuncASSRE(os.Symlink), }, // symlink(oldname string newname string) => error "temp_dir": &tengo.UserFunction{ Name: "temp_dir", Value: FuncARS(os.TempDir), }, // temp_dir() => string "truncate": &tengo.UserFunction{ Name: "truncate", Value: FuncASI64RE(os.Truncate), }, // truncate(name string, size int) => error "unsetenv": &tengo.UserFunction{ Name: "unsetenv", Value: FuncASRE(os.Unsetenv), }, // unsetenv(key string) => error "create": &tengo.UserFunction{ Name: "create", Value: osCreate, }, // create(name string) => imap(file)/error "open": &tengo.UserFunction{ Name: "open", Value: osOpen, }, // open(name string) => imap(file)/error "open_file": &tengo.UserFunction{ Name: "open_file", Value: osOpenFile, }, // open_file(name string, flag int, perm int) => imap(file)/error "find_process": &tengo.UserFunction{ Name: "find_process", Value: osFindProcess, }, // find_process(pid int) => imap(process)/error "start_process": &tengo.UserFunction{ Name: "start_process", Value: osStartProcess, }, // start_process(name string, argv array(string), dir string, env array(string)) => imap(process)/error "exec_look_path": &tengo.UserFunction{ Name: "exec_look_path", Value: FuncASRSE(exec.LookPath), }, // exec_look_path(file) => string/error "exec": &tengo.UserFunction{ Name: "exec", Value: osExec, }, // exec(name, args...) => command "stat": &tengo.UserFunction{ Name: "stat", Value: osStat, }, // stat(name) => imap(fileinfo)/error "read_file": &tengo.UserFunction{ Name: "read_file", Value: osReadFile, }, // readfile(name) => array(byte)/error } func osReadFile(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } fname, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } bytes, err := ioutil.ReadFile(fname) if err != nil { return wrapError(err), nil } if len(bytes) > tengo.MaxBytesLen { return nil, tengo.ErrBytesLimit } return &tengo.Bytes{Value: bytes}, nil } func osStat(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } fname, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } stat, err := os.Stat(fname) if err != nil { return wrapError(err), nil } fstat := &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "name": &tengo.String{Value: stat.Name()}, "mtime": &tengo.Time{Value: stat.ModTime()}, "size": &tengo.Int{Value: stat.Size()}, "mode": &tengo.Int{Value: int64(stat.Mode())}, }, } if stat.IsDir() { fstat.Value["directory"] = tengo.TrueValue } else { fstat.Value["directory"] = tengo.FalseValue } return fstat, nil } func osCreate(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } res, err := os.Create(s1) if err != nil { return wrapError(err), nil } return makeOSFile(res), nil } func osOpen(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } res, err := os.Open(s1) if err != nil { return wrapError(err), nil } return makeOSFile(res), nil } func osOpenFile(args ...tengo.Object) (tengo.Object, error) { if len(args) != 3 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } i3, ok := tengo.ToInt(args[2]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } } res, err := os.OpenFile(s1, i2, os.FileMode(i3)) if err != nil { return wrapError(err), nil } return makeOSFile(res), nil } func osArgs(args ...tengo.Object) (tengo.Object, error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } arr := &tengo.Array{} for _, osArg := range os.Args { if len(osArg) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } arr.Value = append(arr.Value, &tengo.String{Value: osArg}) } return arr, nil } func osFuncASFmRE( name string, fn func(string, os.FileMode) error, ) *tengo.UserFunction { return &tengo.UserFunction{ Name: name, Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt64(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } return wrapError(fn(s1, os.FileMode(i2))), nil }, } } func osLookupEnv(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } res, ok := os.LookupEnv(s1) if !ok { return tengo.FalseValue, nil } if len(res) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: res}, nil } func osExpandEnv(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } var vlen int var failed bool s := os.Expand(s1, func(k string) string { if failed { return "" } v := os.Getenv(k) // this does not count the other texts that are not being replaced // but the code checks the final length at the end vlen += len(v) if vlen > tengo.MaxStringLen { failed = true return "" } return v }) if failed || len(s) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: s}, nil } func osExec(args ...tengo.Object) (tengo.Object, error) { if len(args) == 0 { return nil, tengo.ErrWrongNumArguments } name, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } var execArgs []string for idx, arg := range args[1:] { execArg, ok := tengo.ToString(arg) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: fmt.Sprintf("args[%d]", idx), Expected: "string(compatible)", Found: args[1+idx].TypeName(), } } execArgs = append(execArgs, execArg) } return makeOSExecCommand(exec.Command(name, execArgs...)), nil } func osFindProcess(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } proc, err := os.FindProcess(i1) if err != nil { return wrapError(err), nil } return makeOSProcess(proc), nil } func osStartProcess(args ...tengo.Object) (tengo.Object, error) { if len(args) != 4 { return nil, tengo.ErrWrongNumArguments } name, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } var argv []string var err error switch arg1 := args[1].(type) { case *tengo.Array: argv, err = stringArray(arg1.Value, "second") if err != nil { return nil, err } case *tengo.ImmutableArray: argv, err = stringArray(arg1.Value, "second") if err != nil { return nil, err } default: return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "array", Found: arg1.TypeName(), } } dir, ok := tengo.ToString(args[2]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "third", Expected: "string(compatible)", Found: args[2].TypeName(), } } var env []string switch arg3 := args[3].(type) { case *tengo.Array: env, err = stringArray(arg3.Value, "fourth") if err != nil { return nil, err } case *tengo.ImmutableArray: env, err = stringArray(arg3.Value, "fourth") if err != nil { return nil, err } default: return nil, tengo.ErrInvalidArgumentType{ Name: "fourth", Expected: "array", Found: arg3.TypeName(), } } proc, err := os.StartProcess(name, argv, &os.ProcAttr{ Dir: dir, Env: env, }) if err != nil { return wrapError(err), nil } return makeOSProcess(proc), nil } func stringArray(arr []tengo.Object, argName string) ([]string, error) { var sarr []string for idx, elem := range arr { str, ok := elem.(*tengo.String) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: fmt.Sprintf("%s[%d]", argName, idx), Expected: "string", Found: elem.TypeName(), } } sarr = append(sarr, str.Value) } return sarr, nil } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/os_exec.go0000644000175000017500000000556214607001502020407 0ustar00maythammaythampackage stdlib import ( "os/exec" "github.com/d5/tengo/v2" ) func makeOSExecCommand(cmd *exec.Cmd) *tengo.ImmutableMap { return &tengo.ImmutableMap{ Value: map[string]tengo.Object{ // combined_output() => bytes/error "combined_output": &tengo.UserFunction{ Name: "combined_output", Value: FuncARYE(cmd.CombinedOutput), }, // output() => bytes/error "output": &tengo.UserFunction{ Name: "output", Value: FuncARYE(cmd.Output), }, // // run() => error "run": &tengo.UserFunction{ Name: "run", Value: FuncARE(cmd.Run), }, // // start() => error "start": &tengo.UserFunction{ Name: "start", Value: FuncARE(cmd.Start), }, // // wait() => error "wait": &tengo.UserFunction{ Name: "wait", Value: FuncARE(cmd.Wait), }, // // set_path(path string) "set_path": &tengo.UserFunction{ Name: "set_path", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } cmd.Path = s1 return tengo.UndefinedValue, nil }, }, // set_dir(dir string) "set_dir": &tengo.UserFunction{ Name: "set_dir", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } cmd.Dir = s1 return tengo.UndefinedValue, nil }, }, // set_env(env array(string)) "set_env": &tengo.UserFunction{ Name: "set_env", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } var env []string var err error switch arg0 := args[0].(type) { case *tengo.Array: env, err = stringArray(arg0.Value, "first") if err != nil { return nil, err } case *tengo.ImmutableArray: env, err = stringArray(arg0.Value, "first") if err != nil { return nil, err } default: return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "array", Found: arg0.TypeName(), } } cmd.Env = env return tengo.UndefinedValue, nil }, }, // process() => imap(process) "process": &tengo.UserFunction{ Name: "process", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } return makeOSProcess(cmd.Process), nil }, }, }, } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/os_file.go0000644000175000017500000000563414607001502020402 0ustar00maythammaythampackage stdlib import ( "os" "github.com/d5/tengo/v2" ) func makeOSFile(file *os.File) *tengo.ImmutableMap { return &tengo.ImmutableMap{ Value: map[string]tengo.Object{ // chdir() => true/error "chdir": &tengo.UserFunction{ Name: "chdir", Value: FuncARE(file.Chdir), }, // // chown(uid int, gid int) => true/error "chown": &tengo.UserFunction{ Name: "chown", Value: FuncAIIRE(file.Chown), }, // // close() => error "close": &tengo.UserFunction{ Name: "close", Value: FuncARE(file.Close), }, // // name() => string "name": &tengo.UserFunction{ Name: "name", Value: FuncARS(file.Name), }, // // readdirnames(n int) => array(string)/error "readdirnames": &tengo.UserFunction{ Name: "readdirnames", Value: FuncAIRSsE(file.Readdirnames), }, // // sync() => error "sync": &tengo.UserFunction{ Name: "sync", Value: FuncARE(file.Sync), }, // // write(bytes) => int/error "write": &tengo.UserFunction{ Name: "write", Value: FuncAYRIE(file.Write), }, // // write(string) => int/error "write_string": &tengo.UserFunction{ Name: "write_string", Value: FuncASRIE(file.WriteString), }, // // read(bytes) => int/error "read": &tengo.UserFunction{ Name: "read", Value: FuncAYRIE(file.Read), }, // // chmod(mode int) => error "chmod": &tengo.UserFunction{ Name: "chmod", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } return wrapError(file.Chmod(os.FileMode(i1))), nil }, }, // seek(offset int, whence int) => int/error "seek": &tengo.UserFunction{ Name: "seek", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } res, err := file.Seek(i1, i2) if err != nil { return wrapError(err), nil } return &tengo.Int{Value: res}, nil }, }, // stat() => imap(fileinfo)/error "stat": &tengo.UserFunction{ Name: "stat", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } return osStat(&tengo.String{Value: file.Name()}) }, }, }, } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/os_process.go0000644000175000017500000000327414607001502021137 0ustar00maythammaythampackage stdlib import ( "os" "syscall" "github.com/d5/tengo/v2" ) func makeOSProcessState(state *os.ProcessState) *tengo.ImmutableMap { return &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "exited": &tengo.UserFunction{ Name: "exited", Value: FuncARB(state.Exited), }, "pid": &tengo.UserFunction{ Name: "pid", Value: FuncARI(state.Pid), }, "string": &tengo.UserFunction{ Name: "string", Value: FuncARS(state.String), }, "success": &tengo.UserFunction{ Name: "success", Value: FuncARB(state.Success), }, }, } } func makeOSProcess(proc *os.Process) *tengo.ImmutableMap { return &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "kill": &tengo.UserFunction{ Name: "kill", Value: FuncARE(proc.Kill), }, "release": &tengo.UserFunction{ Name: "release", Value: FuncARE(proc.Release), }, "signal": &tengo.UserFunction{ Name: "signal", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } return wrapError(proc.Signal(syscall.Signal(i1))), nil }, }, "wait": &tengo.UserFunction{ Name: "wait", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } state, err := proc.Wait() if err != nil { return wrapError(err), nil } return makeOSProcessState(state), nil }, }, }, } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/os_test.go0000644000175000017500000000563414607001502020442 0ustar00maythammaythampackage stdlib_test import ( "io/ioutil" "os" "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" ) func TestReadFile(t *testing.T) { content := []byte("the quick brown fox jumps over the lazy dog") tf, err := ioutil.TempFile("", "test") require.NoError(t, err) defer func() { _ = os.Remove(tf.Name()) }() _, err = tf.Write(content) require.NoError(t, err) _ = tf.Close() module(t, "os").call("read_file", tf.Name()). expect(&tengo.Bytes{Value: content}) } func TestReadFileArgs(t *testing.T) { module(t, "os").call("read_file").expectError() } func TestFileStatArgs(t *testing.T) { module(t, "os").call("stat").expectError() } func TestFileStatFile(t *testing.T) { content := []byte("the quick brown fox jumps over the lazy dog") tf, err := ioutil.TempFile("", "test") require.NoError(t, err) defer func() { _ = os.Remove(tf.Name()) }() _, err = tf.Write(content) require.NoError(t, err) _ = tf.Close() stat, err := os.Stat(tf.Name()) if err != nil { t.Logf("could not get tmp file stat: %s", err) return } module(t, "os").call("stat", tf.Name()).expect(&tengo.ImmutableMap{ Value: map[string]tengo.Object{ "name": &tengo.String{Value: stat.Name()}, "mtime": &tengo.Time{Value: stat.ModTime()}, "size": &tengo.Int{Value: stat.Size()}, "mode": &tengo.Int{Value: int64(stat.Mode())}, "directory": tengo.FalseValue, }, }) } func TestFileStatDir(t *testing.T) { td, err := ioutil.TempDir("", "test") require.NoError(t, err) defer func() { _ = os.RemoveAll(td) }() stat, err := os.Stat(td) require.NoError(t, err) module(t, "os").call("stat", td).expect(&tengo.ImmutableMap{ Value: map[string]tengo.Object{ "name": &tengo.String{Value: stat.Name()}, "mtime": &tengo.Time{Value: stat.ModTime()}, "size": &tengo.Int{Value: stat.Size()}, "mode": &tengo.Int{Value: int64(stat.Mode())}, "directory": tengo.TrueValue, }, }) } func TestOSExpandEnv(t *testing.T) { curMaxStringLen := tengo.MaxStringLen defer func() { tengo.MaxStringLen = curMaxStringLen }() tengo.MaxStringLen = 12 _ = os.Setenv("TENGO", "FOO BAR") module(t, "os").call("expand_env", "$TENGO").expect("FOO BAR") _ = os.Setenv("TENGO", "FOO") module(t, "os").call("expand_env", "$TENGO $TENGO").expect("FOO FOO") _ = os.Setenv("TENGO", "123456789012") module(t, "os").call("expand_env", "$TENGO").expect("123456789012") _ = os.Setenv("TENGO", "1234567890123") module(t, "os").call("expand_env", "$TENGO").expectError() _ = os.Setenv("TENGO", "123456") module(t, "os").call("expand_env", "$TENGO$TENGO").expect("123456123456") _ = os.Setenv("TENGO", "123456") module(t, "os").call("expand_env", "${TENGO}${TENGO}"). expect("123456123456") _ = os.Setenv("TENGO", "123456") module(t, "os").call("expand_env", "$TENGO $TENGO").expectError() _ = os.Setenv("TENGO", "123456") module(t, "os").call("expand_env", "${TENGO} ${TENGO}").expectError() } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/rand.go0000644000175000017500000000575114607001502017706 0ustar00maythammaythampackage stdlib import ( "math/rand" "github.com/d5/tengo/v2" ) var randModule = map[string]tengo.Object{ "int": &tengo.UserFunction{ Name: "int", Value: FuncARI64(rand.Int63), }, "float": &tengo.UserFunction{ Name: "float", Value: FuncARF(rand.Float64), }, "intn": &tengo.UserFunction{ Name: "intn", Value: FuncAI64RI64(rand.Int63n), }, "exp_float": &tengo.UserFunction{ Name: "exp_float", Value: FuncARF(rand.ExpFloat64), }, "norm_float": &tengo.UserFunction{ Name: "norm_float", Value: FuncARF(rand.NormFloat64), }, "perm": &tengo.UserFunction{ Name: "perm", Value: FuncAIRIs(rand.Perm), }, "seed": &tengo.UserFunction{ Name: "seed", Value: FuncAI64R(rand.Seed), }, "read": &tengo.UserFunction{ Name: "read", Value: func(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } y1, ok := args[0].(*tengo.Bytes) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "bytes", Found: args[0].TypeName(), } } res, err := rand.Read(y1.Value) if err != nil { ret = wrapError(err) return } return &tengo.Int{Value: int64(res)}, nil }, }, "rand": &tengo.UserFunction{ Name: "rand", Value: func(args ...tengo.Object) (tengo.Object, error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } i1, ok := tengo.ToInt64(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } } src := rand.NewSource(i1) return randRand(rand.New(src)), nil }, }, } func randRand(r *rand.Rand) *tengo.ImmutableMap { return &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "int": &tengo.UserFunction{ Name: "int", Value: FuncARI64(r.Int63), }, "float": &tengo.UserFunction{ Name: "float", Value: FuncARF(r.Float64), }, "intn": &tengo.UserFunction{ Name: "intn", Value: FuncAI64RI64(r.Int63n), }, "exp_float": &tengo.UserFunction{ Name: "exp_float", Value: FuncARF(r.ExpFloat64), }, "norm_float": &tengo.UserFunction{ Name: "norm_float", Value: FuncARF(r.NormFloat64), }, "perm": &tengo.UserFunction{ Name: "perm", Value: FuncAIRIs(r.Perm), }, "seed": &tengo.UserFunction{ Name: "seed", Value: FuncAI64R(r.Seed), }, "read": &tengo.UserFunction{ Name: "read", Value: func(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } y1, ok := args[0].(*tengo.Bytes) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "bytes", Found: args[0].TypeName(), } } res, err := r.Read(y1.Value) if err != nil { ret = wrapError(err) return } return &tengo.Int{Value: int64(res)}, nil }, }, }, } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/rand_test.go0000644000175000017500000000256514607001502020745 0ustar00maythammaythampackage stdlib_test import ( "math/rand" "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" ) func TestRand(t *testing.T) { var seed int64 = 1234 r := rand.New(rand.NewSource(seed)) module(t, "rand").call("seed", seed).expect(tengo.UndefinedValue) module(t, "rand").call("int").expect(r.Int63()) module(t, "rand").call("float").expect(r.Float64()) module(t, "rand").call("intn", 111).expect(r.Int63n(111)) module(t, "rand").call("exp_float").expect(r.ExpFloat64()) module(t, "rand").call("norm_float").expect(r.NormFloat64()) module(t, "rand").call("perm", 10).expect(r.Perm(10)) buf1 := make([]byte, 10) buf2 := &tengo.Bytes{Value: make([]byte, 10)} n, _ := r.Read(buf1) module(t, "rand").call("read", buf2).expect(n) require.Equal(t, buf1, buf2.Value) seed = 9191 r = rand.New(rand.NewSource(seed)) randObj := module(t, "rand").call("rand", seed) randObj.call("seed", seed).expect(tengo.UndefinedValue) randObj.call("int").expect(r.Int63()) randObj.call("float").expect(r.Float64()) randObj.call("intn", 111).expect(r.Int63n(111)) randObj.call("exp_float").expect(r.ExpFloat64()) randObj.call("norm_float").expect(r.NormFloat64()) randObj.call("perm", 10).expect(r.Perm(10)) buf1 = make([]byte, 12) buf2 = &tengo.Bytes{Value: make([]byte, 12)} n, _ = r.Read(buf1) randObj.call("read", buf2).expect(n) require.Equal(t, buf1, buf2.Value) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/source_modules.go0000644000175000017500000001024614607001502022005 0ustar00maythammaytham// Code generated using gensrcmods.go; DO NOT EDIT. package stdlib // SourceModules are source type standard library modules. var SourceModules = map[string]string{ "enum": "is_enumerable := func(x) {\n return is_array(x) || is_map(x) || is_immutable_array(x) || is_immutable_map(x)\n}\n\nis_array_like := func(x) {\n return is_array(x) || is_immutable_array(x)\n}\n\nexport {\n // all returns true if the given function `fn` evaluates to a truthy value on\n // all of the items in `x`. It returns undefined if `x` is not enumerable.\n all: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if !fn(k, v) { return false }\n }\n\n return true\n },\n // any returns true if the given function `fn` evaluates to a truthy value on\n // any of the items in `x`. It returns undefined if `x` is not enumerable.\n any: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if fn(k, v) { return true }\n }\n\n return false\n },\n // chunk returns an array of elements split into groups the length of size.\n // If `x` can't be split evenly, the final chunk will be the remaining elements.\n // It returns undefined if `x` is not array.\n chunk: func(x, size) {\n if !is_array_like(x) || !size { return undefined }\n\n numElements := len(x)\n if !numElements { return [] }\n\n res := []\n idx := 0\n for idx < numElements {\n res = append(res, x[idx:idx+size])\n idx += size\n }\n\n return res\n },\n // at returns an element at the given index (if `x` is array) or\n // key (if `x` is map). It returns undefined if `x` is not enumerable.\n at: func(x, key) {\n if !is_enumerable(x) { return undefined }\n\n if is_array_like(x) {\n if !is_int(key) { return undefined }\n } else {\n if !is_string(key) { return undefined }\n }\n\n return x[key]\n },\n // each iterates over elements of `x` and invokes `fn` for each element. `fn` is\n // invoked with two arguments: `key` and `value`. `key` is an int index\n // if `x` is array. `key` is a string key if `x` is map. It does not iterate\n // and returns undefined if `x` is not enumerable.\n each: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n fn(k, v)\n }\n },\n // filter iterates over elements of `x`, returning an array of all elements `fn`\n // returns truthy for. `fn` is invoked with two arguments: `key` and `value`.\n // `key` is an int index if `x` is array. It returns undefined if `x` is not array.\n filter: func(x, fn) {\n if !is_array_like(x) { return undefined }\n\n dst := []\n for k, v in x {\n if fn(k, v) { dst = append(dst, v) }\n }\n\n return dst\n },\n // find iterates over elements of `x`, returning value of the first element `fn`\n // returns truthy for. `fn` is invoked with two arguments: `key` and `value`.\n // `key` is an int index if `x` is array. `key` is a string key if `x` is map.\n // It returns undefined if `x` is not enumerable.\n find: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if fn(k, v) { return v }\n }\n },\n // find_key iterates over elements of `x`, returning key or index of the first\n // element `fn` returns truthy for. `fn` is invoked with two arguments: `key`\n // and `value`. `key` is an int index if `x` is array. `key` is a string key if\n // `x` is map. It returns undefined if `x` is not enumerable.\n find_key: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n for k, v in x {\n if fn(k, v) { return k }\n }\n },\n // map creates an array of values by running each element in `x` through `fn`.\n // `fn` is invoked with two arguments: `key` and `value`. `key` is an int index\n // if `x` is array. `key` is a string key if `x` is map. It returns undefined\n // if `x` is not enumerable.\n map: func(x, fn) {\n if !is_enumerable(x) { return undefined }\n\n dst := []\n for k, v in x {\n dst = append(dst, fn(k, v))\n }\n\n return dst\n },\n // key returns the first argument.\n key: func(k, _) { return k },\n // value returns the second argument.\n value: func(_, v) { return v }\n}\n", } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/srcmod_enum.tengo0000644000175000017500000000756114607001502022005 0ustar00maythammaythamis_enumerable := func(x) { return is_array(x) || is_map(x) || is_immutable_array(x) || is_immutable_map(x) } is_array_like := func(x) { return is_array(x) || is_immutable_array(x) } export { // all returns true if the given function `fn` evaluates to a truthy value on // all of the items in `x`. It returns undefined if `x` is not enumerable. all: func(x, fn) { if !is_enumerable(x) { return undefined } for k, v in x { if !fn(k, v) { return false } } return true }, // any returns true if the given function `fn` evaluates to a truthy value on // any of the items in `x`. It returns undefined if `x` is not enumerable. any: func(x, fn) { if !is_enumerable(x) { return undefined } for k, v in x { if fn(k, v) { return true } } return false }, // chunk returns an array of elements split into groups the length of size. // If `x` can't be split evenly, the final chunk will be the remaining elements. // It returns undefined if `x` is not array. chunk: func(x, size) { if !is_array_like(x) || !size { return undefined } numElements := len(x) if !numElements { return [] } res := [] idx := 0 for idx < numElements { res = append(res, x[idx:idx+size]) idx += size } return res }, // at returns an element at the given index (if `x` is array) or // key (if `x` is map). It returns undefined if `x` is not enumerable. at: func(x, key) { if !is_enumerable(x) { return undefined } if is_array_like(x) { if !is_int(key) { return undefined } } else { if !is_string(key) { return undefined } } return x[key] }, // each iterates over elements of `x` and invokes `fn` for each element. `fn` is // invoked with two arguments: `key` and `value`. `key` is an int index // if `x` is array. `key` is a string key if `x` is map. It does not iterate // and returns undefined if `x` is not enumerable. each: func(x, fn) { if !is_enumerable(x) { return undefined } for k, v in x { fn(k, v) } }, // filter iterates over elements of `x`, returning an array of all elements `fn` // returns truthy for. `fn` is invoked with two arguments: `key` and `value`. // `key` is an int index if `x` is array. It returns undefined if `x` is not array. filter: func(x, fn) { if !is_array_like(x) { return undefined } dst := [] for k, v in x { if fn(k, v) { dst = append(dst, v) } } return dst }, // find iterates over elements of `x`, returning value of the first element `fn` // returns truthy for. `fn` is invoked with two arguments: `key` and `value`. // `key` is an int index if `x` is array. `key` is a string key if `x` is map. // It returns undefined if `x` is not enumerable. find: func(x, fn) { if !is_enumerable(x) { return undefined } for k, v in x { if fn(k, v) { return v } } }, // find_key iterates over elements of `x`, returning key or index of the first // element `fn` returns truthy for. `fn` is invoked with two arguments: `key` // and `value`. `key` is an int index if `x` is array. `key` is a string key if // `x` is map. It returns undefined if `x` is not enumerable. find_key: func(x, fn) { if !is_enumerable(x) { return undefined } for k, v in x { if fn(k, v) { return k } } }, // map creates an array of values by running each element in `x` through `fn`. // `fn` is invoked with two arguments: `key` and `value`. `key` is an int index // if `x` is array. `key` is a string key if `x` is map. It returns undefined // if `x` is not enumerable. map: func(x, fn) { if !is_enumerable(x) { return undefined } dst := [] for k, v in x { dst = append(dst, fn(k, v)) } return dst }, // key returns the first argument. key: func(k, _) { return k }, // value returns the second argument. value: func(_, v) { return v } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/stdlib.go0000644000175000017500000000141214607001502020231 0ustar00maythammaythampackage stdlib //go:generate go run gensrcmods.go import ( "github.com/d5/tengo/v2" ) // AllModuleNames returns a list of all default module names. func AllModuleNames() []string { var names []string for name := range BuiltinModules { names = append(names, name) } for name := range SourceModules { names = append(names, name) } return names } // GetModuleMap returns the module map that includes all modules // for the given module names. func GetModuleMap(names ...string) *tengo.ModuleMap { modules := tengo.NewModuleMap() for _, name := range names { if mod := BuiltinModules[name]; mod != nil { modules.AddBuiltinModule(name, mod) } if mod := SourceModules[name]; mod != "" { modules.AddSourceModule(name, []byte(mod)) } } return modules } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/stdlib_test.go0000644000175000017500000001173414607001502021300 0ustar00maythammaythampackage stdlib_test import ( "fmt" "testing" "time" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" "github.com/d5/tengo/v2/stdlib" ) type ARR = []interface{} type MAP = map[string]interface{} type IARR []interface{} type IMAP map[string]interface{} func TestAllModuleNames(t *testing.T) { names := stdlib.AllModuleNames() require.Equal(t, len(stdlib.BuiltinModules)+len(stdlib.SourceModules), len(names)) } func TestModulesRun(t *testing.T) { // os.File expect(t, ` os := import("os") out := "" write_file := func(filename, data) { file := os.create(filename) if !file { return file } if res := file.write(bytes(data)); is_error(res) { return res } return file.close() } read_file := func(filename) { file := os.open(filename) if !file { return file } data := bytes(100) cnt := file.read(data) if is_error(cnt) { return cnt } file.close() return data[:cnt] } if write_file("./temp", "foobar") { out = string(read_file("./temp")) } os.remove("./temp") `, "foobar") // exec.command expect(t, ` out := "" os := import("os") cmd := os.exec("echo", "foo", "bar") if !is_error(cmd) { out = cmd.output() } `, []byte("foo bar\n")) } func TestGetModules(t *testing.T) { mods := stdlib.GetModuleMap() require.Equal(t, 0, mods.Len()) mods = stdlib.GetModuleMap("os") require.Equal(t, 1, mods.Len()) require.NotNil(t, mods.Get("os")) mods = stdlib.GetModuleMap("os", "rand") require.Equal(t, 2, mods.Len()) require.NotNil(t, mods.Get("os")) require.NotNil(t, mods.Get("rand")) mods = stdlib.GetModuleMap("text", "text") require.Equal(t, 1, mods.Len()) require.NotNil(t, mods.Get("text")) mods = stdlib.GetModuleMap("nonexisting", "text") require.Equal(t, 1, mods.Len()) require.NotNil(t, mods.Get("text")) } type callres struct { t *testing.T o interface{} e error } func (c callres) call(funcName string, args ...interface{}) callres { if c.e != nil { return c } var oargs []tengo.Object for _, v := range args { oargs = append(oargs, object(v)) } switch o := c.o.(type) { case *tengo.BuiltinModule: m, ok := o.Attrs[funcName] if !ok { return callres{t: c.t, e: fmt.Errorf( "function not found: %s", funcName)} } f, ok := m.(*tengo.UserFunction) if !ok { return callres{t: c.t, e: fmt.Errorf( "non-callable: %s", funcName)} } res, err := f.Value(oargs...) return callres{t: c.t, o: res, e: err} case *tengo.UserFunction: res, err := o.Value(oargs...) return callres{t: c.t, o: res, e: err} case *tengo.ImmutableMap: m, ok := o.Value[funcName] if !ok { return callres{t: c.t, e: fmt.Errorf("function not found: %s", funcName)} } f, ok := m.(*tengo.UserFunction) if !ok { return callres{t: c.t, e: fmt.Errorf("non-callable: %s", funcName)} } res, err := f.Value(oargs...) return callres{t: c.t, o: res, e: err} default: panic(fmt.Errorf("unexpected object: %v (%T)", o, o)) } } func (c callres) expect(expected interface{}, msgAndArgs ...interface{}) { require.NoError(c.t, c.e, msgAndArgs...) require.Equal(c.t, object(expected), c.o, msgAndArgs...) } func (c callres) expectError() { require.Error(c.t, c.e) } func module(t *testing.T, moduleName string) callres { mod := stdlib.GetModuleMap(moduleName).GetBuiltinModule(moduleName) if mod == nil { return callres{t: t, e: fmt.Errorf("module not found: %s", moduleName)} } return callres{t: t, o: mod} } func object(v interface{}) tengo.Object { switch v := v.(type) { case tengo.Object: return v case string: return &tengo.String{Value: v} case int64: return &tengo.Int{Value: v} case int: // for convenience return &tengo.Int{Value: int64(v)} case bool: if v { return tengo.TrueValue } return tengo.FalseValue case rune: return &tengo.Char{Value: v} case byte: // for convenience return &tengo.Char{Value: rune(v)} case float64: return &tengo.Float{Value: v} case []byte: return &tengo.Bytes{Value: v} case MAP: objs := make(map[string]tengo.Object) for k, v := range v { objs[k] = object(v) } return &tengo.Map{Value: objs} case ARR: var objs []tengo.Object for _, e := range v { objs = append(objs, object(e)) } return &tengo.Array{Value: objs} case IMAP: objs := make(map[string]tengo.Object) for k, v := range v { objs[k] = object(v) } return &tengo.ImmutableMap{Value: objs} case IARR: var objs []tengo.Object for _, e := range v { objs = append(objs, object(e)) } return &tengo.ImmutableArray{Value: objs} case time.Time: return &tengo.Time{Value: v} case []int: var objs []tengo.Object for _, e := range v { objs = append(objs, &tengo.Int{Value: int64(e)}) } return &tengo.Array{Value: objs} } panic(fmt.Errorf("unknown type: %T", v)) } func expect(t *testing.T, input string, expected interface{}) { s := tengo.NewScript([]byte(input)) s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) c, err := s.Run() require.NoError(t, err) require.NotNil(t, c) v := c.Get("out") require.NotNil(t, v) require.Equal(t, expected, v.Value()) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/text.go0000644000175000017500000005362014607001502017744 0ustar00maythammaythampackage stdlib import ( "fmt" "regexp" "strconv" "strings" "unicode/utf8" "github.com/d5/tengo/v2" ) var textModule = map[string]tengo.Object{ "re_match": &tengo.UserFunction{ Name: "re_match", Value: textREMatch, }, // re_match(pattern, text) => bool/error "re_find": &tengo.UserFunction{ Name: "re_find", Value: textREFind, }, // re_find(pattern, text, count) => [[{text:,begin:,end:}]]/undefined "re_replace": &tengo.UserFunction{ Name: "re_replace", Value: textREReplace, }, // re_replace(pattern, text, repl) => string/error "re_split": &tengo.UserFunction{ Name: "re_split", Value: textRESplit, }, // re_split(pattern, text, count) => [string]/error "re_compile": &tengo.UserFunction{ Name: "re_compile", Value: textRECompile, }, // re_compile(pattern) => Regexp/error "compare": &tengo.UserFunction{ Name: "compare", Value: FuncASSRI(strings.Compare), }, // compare(a, b) => int "contains": &tengo.UserFunction{ Name: "contains", Value: FuncASSRB(strings.Contains), }, // contains(s, substr) => bool "contains_any": &tengo.UserFunction{ Name: "contains_any", Value: FuncASSRB(strings.ContainsAny), }, // contains_any(s, chars) => bool "count": &tengo.UserFunction{ Name: "count", Value: FuncASSRI(strings.Count), }, // count(s, substr) => int "equal_fold": &tengo.UserFunction{ Name: "equal_fold", Value: FuncASSRB(strings.EqualFold), }, // "equal_fold(s, t) => bool "fields": &tengo.UserFunction{ Name: "fields", Value: FuncASRSs(strings.Fields), }, // fields(s) => [string] "has_prefix": &tengo.UserFunction{ Name: "has_prefix", Value: FuncASSRB(strings.HasPrefix), }, // has_prefix(s, prefix) => bool "has_suffix": &tengo.UserFunction{ Name: "has_suffix", Value: FuncASSRB(strings.HasSuffix), }, // has_suffix(s, suffix) => bool "index": &tengo.UserFunction{ Name: "index", Value: FuncASSRI(strings.Index), }, // index(s, substr) => int "index_any": &tengo.UserFunction{ Name: "index_any", Value: FuncASSRI(strings.IndexAny), }, // index_any(s, chars) => int "join": &tengo.UserFunction{ Name: "join", Value: textJoin, }, // join(arr, sep) => string "last_index": &tengo.UserFunction{ Name: "last_index", Value: FuncASSRI(strings.LastIndex), }, // last_index(s, substr) => int "last_index_any": &tengo.UserFunction{ Name: "last_index_any", Value: FuncASSRI(strings.LastIndexAny), }, // last_index_any(s, chars) => int "repeat": &tengo.UserFunction{ Name: "repeat", Value: textRepeat, }, // repeat(s, count) => string "replace": &tengo.UserFunction{ Name: "replace", Value: textReplace, }, // replace(s, old, new, n) => string "substr": &tengo.UserFunction{ Name: "substr", Value: textSubstring, }, // substr(s, lower, upper) => string "split": &tengo.UserFunction{ Name: "split", Value: FuncASSRSs(strings.Split), }, // split(s, sep) => [string] "split_after": &tengo.UserFunction{ Name: "split_after", Value: FuncASSRSs(strings.SplitAfter), }, // split_after(s, sep) => [string] "split_after_n": &tengo.UserFunction{ Name: "split_after_n", Value: FuncASSIRSs(strings.SplitAfterN), }, // split_after_n(s, sep, n) => [string] "split_n": &tengo.UserFunction{ Name: "split_n", Value: FuncASSIRSs(strings.SplitN), }, // split_n(s, sep, n) => [string] "title": &tengo.UserFunction{ Name: "title", Value: FuncASRS(strings.Title), }, // title(s) => string "to_lower": &tengo.UserFunction{ Name: "to_lower", Value: FuncASRS(strings.ToLower), }, // to_lower(s) => string "to_title": &tengo.UserFunction{ Name: "to_title", Value: FuncASRS(strings.ToTitle), }, // to_title(s) => string "to_upper": &tengo.UserFunction{ Name: "to_upper", Value: FuncASRS(strings.ToUpper), }, // to_upper(s) => string "pad_left": &tengo.UserFunction{ Name: "pad_left", Value: textPadLeft, }, // pad_left(s, pad_len, pad_with) => string "pad_right": &tengo.UserFunction{ Name: "pad_right", Value: textPadRight, }, // pad_right(s, pad_len, pad_with) => string "trim": &tengo.UserFunction{ Name: "trim", Value: FuncASSRS(strings.Trim), }, // trim(s, cutset) => string "trim_left": &tengo.UserFunction{ Name: "trim_left", Value: FuncASSRS(strings.TrimLeft), }, // trim_left(s, cutset) => string "trim_prefix": &tengo.UserFunction{ Name: "trim_prefix", Value: FuncASSRS(strings.TrimPrefix), }, // trim_prefix(s, prefix) => string "trim_right": &tengo.UserFunction{ Name: "trim_right", Value: FuncASSRS(strings.TrimRight), }, // trim_right(s, cutset) => string "trim_space": &tengo.UserFunction{ Name: "trim_space", Value: FuncASRS(strings.TrimSpace), }, // trim_space(s) => string "trim_suffix": &tengo.UserFunction{ Name: "trim_suffix", Value: FuncASSRS(strings.TrimSuffix), }, // trim_suffix(s, suffix) => string "atoi": &tengo.UserFunction{ Name: "atoi", Value: FuncASRIE(strconv.Atoi), }, // atoi(str) => int/error "format_bool": &tengo.UserFunction{ Name: "format_bool", Value: textFormatBool, }, // format_bool(b) => string "format_float": &tengo.UserFunction{ Name: "format_float", Value: textFormatFloat, }, // format_float(f, fmt, prec, bits) => string "format_int": &tengo.UserFunction{ Name: "format_int", Value: textFormatInt, }, // format_int(i, base) => string "itoa": &tengo.UserFunction{ Name: "itoa", Value: FuncAIRS(strconv.Itoa), }, // itoa(i) => string "parse_bool": &tengo.UserFunction{ Name: "parse_bool", Value: textParseBool, }, // parse_bool(str) => bool/error "parse_float": &tengo.UserFunction{ Name: "parse_float", Value: textParseFloat, }, // parse_float(str, bits) => float/error "parse_int": &tengo.UserFunction{ Name: "parse_int", Value: textParseInt, }, // parse_int(str, base, bits) => int/error "quote": &tengo.UserFunction{ Name: "quote", Value: FuncASRS(strconv.Quote), }, // quote(str) => string "unquote": &tengo.UserFunction{ Name: "unquote", Value: FuncASRSE(strconv.Unquote), }, // unquote(str) => string/error } func textREMatch(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } matched, err := regexp.MatchString(s1, s2) if err != nil { ret = wrapError(err) return } if matched { ret = tengo.TrueValue } else { ret = tengo.FalseValue } return } func textREFind(args ...tengo.Object) (ret tengo.Object, err error) { numArgs := len(args) if numArgs != 2 && numArgs != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } re, err := regexp.Compile(s1) if err != nil { ret = wrapError(err) return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } if numArgs < 3 { m := re.FindStringSubmatchIndex(s2) if m == nil { ret = tengo.UndefinedValue return } arr := &tengo.Array{} for i := 0; i < len(m); i += 2 { arr.Value = append(arr.Value, &tengo.ImmutableMap{Value: map[string]tengo.Object{ "text": &tengo.String{Value: s2[m[i]:m[i+1]]}, "begin": &tengo.Int{Value: int64(m[i])}, "end": &tengo.Int{Value: int64(m[i+1])}, }}) } ret = &tengo.Array{Value: []tengo.Object{arr}} return } i3, ok := tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } m := re.FindAllStringSubmatchIndex(s2, i3) if m == nil { ret = tengo.UndefinedValue return } arr := &tengo.Array{} for _, m := range m { subMatch := &tengo.Array{} for i := 0; i < len(m); i += 2 { subMatch.Value = append(subMatch.Value, &tengo.ImmutableMap{Value: map[string]tengo.Object{ "text": &tengo.String{Value: s2[m[i]:m[i+1]]}, "begin": &tengo.Int{Value: int64(m[i])}, "end": &tengo.Int{Value: int64(m[i+1])}, }}) } arr.Value = append(arr.Value, subMatch) } ret = arr return } func textREReplace(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } s3, ok := tengo.ToString(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "string(compatible)", Found: args[2].TypeName(), } return } re, err := regexp.Compile(s1) if err != nil { ret = wrapError(err) } else { s, ok := doTextRegexpReplace(re, s2, s3) if !ok { return nil, tengo.ErrStringLimit } ret = &tengo.String{Value: s} } return } func textRESplit(args ...tengo.Object) (ret tengo.Object, err error) { numArgs := len(args) if numArgs != 2 && numArgs != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } var i3 = -1 if numArgs > 2 { i3, ok = tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } } re, err := regexp.Compile(s1) if err != nil { ret = wrapError(err) return } arr := &tengo.Array{} for _, s := range re.Split(s2, i3) { arr.Value = append(arr.Value, &tengo.String{Value: s}) } ret = arr return } func textRECompile(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } re, err := regexp.Compile(s1) if err != nil { ret = wrapError(err) } else { ret = makeTextRegexp(re) } return } func textReplace(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 4 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } s3, ok := tengo.ToString(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "string(compatible)", Found: args[2].TypeName(), } return } i4, ok := tengo.ToInt(args[3]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "fourth", Expected: "int(compatible)", Found: args[3].TypeName(), } return } s, ok := doTextReplace(s1, s2, s3, i4) if !ok { err = tengo.ErrStringLimit return } ret = &tengo.String{Value: s} return } func textSubstring(args ...tengo.Object) (ret tengo.Object, err error) { argslen := len(args) if argslen != 2 && argslen != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } strlen := len(s1) i3 := strlen if argslen == 3 { i3, ok = tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } } if i2 > i3 { err = tengo.ErrInvalidIndexType return } if i2 < 0 { i2 = 0 } else if i2 > strlen { i2 = strlen } if i3 < 0 { i3 = 0 } else if i3 > strlen { i3 = strlen } ret = &tengo.String{Value: s1[i2:i3]} return } func textPadLeft(args ...tengo.Object) (ret tengo.Object, err error) { argslen := len(args) if argslen != 2 && argslen != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } if i2 > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } sLen := len(s1) if sLen >= i2 { ret = &tengo.String{Value: s1} return } s3 := " " if argslen == 3 { s3, ok = tengo.ToString(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "string(compatible)", Found: args[2].TypeName(), } return } } padStrLen := len(s3) if padStrLen == 0 { ret = &tengo.String{Value: s1} return } padCount := ((i2 - padStrLen) / padStrLen) + 1 retStr := strings.Repeat(s3, padCount) + s1 ret = &tengo.String{Value: retStr[len(retStr)-i2:]} return } func textPadRight(args ...tengo.Object) (ret tengo.Object, err error) { argslen := len(args) if argslen != 2 && argslen != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } if i2 > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } sLen := len(s1) if sLen >= i2 { ret = &tengo.String{Value: s1} return } s3 := " " if argslen == 3 { s3, ok = tengo.ToString(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "string(compatible)", Found: args[2].TypeName(), } return } } padStrLen := len(s3) if padStrLen == 0 { ret = &tengo.String{Value: s1} return } padCount := ((i2 - padStrLen) / padStrLen) + 1 retStr := s1 + strings.Repeat(s3, padCount) ret = &tengo.String{Value: retStr[:i2]} return } func textRepeat(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } i2, ok := tengo.ToInt(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } } if len(s1)*i2 > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: strings.Repeat(s1, i2)}, nil } func textJoin(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { return nil, tengo.ErrWrongNumArguments } var slen int var ss1 []string switch arg0 := args[0].(type) { case *tengo.Array: for idx, a := range arg0.Value { as, ok := tengo.ToString(a) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: fmt.Sprintf("first[%d]", idx), Expected: "string(compatible)", Found: a.TypeName(), } } slen += len(as) ss1 = append(ss1, as) } case *tengo.ImmutableArray: for idx, a := range arg0.Value { as, ok := tengo.ToString(a) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: fmt.Sprintf("first[%d]", idx), Expected: "string(compatible)", Found: a.TypeName(), } } slen += len(as) ss1 = append(ss1, as) } default: return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "array", Found: args[0].TypeName(), } } s2, ok := tengo.ToString(args[1]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } } // make sure output length does not exceed the limit if slen+len(s2)*(len(ss1)-1) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } return &tengo.String{Value: strings.Join(ss1, s2)}, nil } func textFormatBool(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } b1, ok := args[0].(*tengo.Bool) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "bool", Found: args[0].TypeName(), } return } if b1 == tengo.TrueValue { ret = &tengo.String{Value: "true"} } else { ret = &tengo.String{Value: "false"} } return } func textFormatFloat(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 4 { err = tengo.ErrWrongNumArguments return } f1, ok := args[0].(*tengo.Float) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "float", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } i3, ok := tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } i4, ok := tengo.ToInt(args[3]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "fourth", Expected: "int(compatible)", Found: args[3].TypeName(), } return } ret = &tengo.String{Value: strconv.FormatFloat(f1.Value, s2[0], i3, i4)} return } func textFormatInt(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } i1, ok := args[0].(*tengo.Int) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } ret = &tengo.String{Value: strconv.FormatInt(i1.Value, i2)} return } func textParseBool(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } s1, ok := args[0].(*tengo.String) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string", Found: args[0].TypeName(), } return } parsed, err := strconv.ParseBool(s1.Value) if err != nil { ret = wrapError(err) return } if parsed { ret = tengo.TrueValue } else { ret = tengo.FalseValue } return } func textParseFloat(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } s1, ok := args[0].(*tengo.String) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } parsed, err := strconv.ParseFloat(s1.Value, i2) if err != nil { ret = wrapError(err) return } ret = &tengo.Float{Value: parsed} return } func textParseInt(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 3 { err = tengo.ErrWrongNumArguments return } s1, ok := args[0].(*tengo.String) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } i3, ok := tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } parsed, err := strconv.ParseInt(s1.Value, i2, i3) if err != nil { ret = wrapError(err) return } ret = &tengo.Int{Value: parsed} return } // Modified implementation of strings.Replace // to limit the maximum length of output string. func doTextReplace(s, old, new string, n int) (string, bool) { if old == new || n == 0 { return s, true // avoid allocation } // Compute number of replacements. if m := strings.Count(s, old); m == 0 { return s, true // avoid allocation } else if n < 0 || m < n { n = m } // Apply replacements to buffer. t := make([]byte, len(s)+n*(len(new)-len(old))) w := 0 start := 0 for i := 0; i < n; i++ { j := start if len(old) == 0 { if i > 0 { _, wid := utf8.DecodeRuneInString(s[start:]) j += wid } } else { j += strings.Index(s[start:], old) } ssj := s[start:j] if w+len(ssj)+len(new) > tengo.MaxStringLen { return "", false } w += copy(t[w:], ssj) w += copy(t[w:], new) start = j + len(old) } ss := s[start:] if w+len(ss) > tengo.MaxStringLen { return "", false } w += copy(t[w:], ss) return string(t[0:w]), true } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/text_regexp.go0000644000175000017500000001244614607001502021317 0ustar00maythammaythampackage stdlib import ( "regexp" "github.com/d5/tengo/v2" ) func makeTextRegexp(re *regexp.Regexp) *tengo.ImmutableMap { return &tengo.ImmutableMap{ Value: map[string]tengo.Object{ // match(text) => bool "match": &tengo.UserFunction{ Value: func(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } if re.MatchString(s1) { ret = tengo.TrueValue } else { ret = tengo.FalseValue } return }, }, // find(text) => array(array({text:,begin:,end:}))/undefined // find(text, maxCount) => array(array({text:,begin:,end:}))/undefined "find": &tengo.UserFunction{ Value: func(args ...tengo.Object) ( ret tengo.Object, err error, ) { numArgs := len(args) if numArgs != 1 && numArgs != 2 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } if numArgs == 1 { m := re.FindStringSubmatchIndex(s1) if m == nil { ret = tengo.UndefinedValue return } arr := &tengo.Array{} for i := 0; i < len(m); i += 2 { arr.Value = append(arr.Value, &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "text": &tengo.String{ Value: s1[m[i]:m[i+1]], }, "begin": &tengo.Int{ Value: int64(m[i]), }, "end": &tengo.Int{ Value: int64(m[i+1]), }, }}) } ret = &tengo.Array{Value: []tengo.Object{arr}} return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } m := re.FindAllStringSubmatchIndex(s1, i2) if m == nil { ret = tengo.UndefinedValue return } arr := &tengo.Array{} for _, m := range m { subMatch := &tengo.Array{} for i := 0; i < len(m); i += 2 { subMatch.Value = append(subMatch.Value, &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "text": &tengo.String{ Value: s1[m[i]:m[i+1]], }, "begin": &tengo.Int{ Value: int64(m[i]), }, "end": &tengo.Int{ Value: int64(m[i+1]), }, }}) } arr.Value = append(arr.Value, subMatch) } ret = arr return }, }, // replace(src, repl) => string "replace": &tengo.UserFunction{ Value: func(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } s, ok := doTextRegexpReplace(re, s1, s2) if !ok { return nil, tengo.ErrStringLimit } ret = &tengo.String{Value: s} return }, }, // split(text) => array(string) // split(text, maxCount) => array(string) "split": &tengo.UserFunction{ Value: func(args ...tengo.Object) ( ret tengo.Object, err error, ) { numArgs := len(args) if numArgs != 1 && numArgs != 2 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } var i2 = -1 if numArgs > 1 { i2, ok = tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } } arr := &tengo.Array{} for _, s := range re.Split(s1, i2) { arr.Value = append(arr.Value, &tengo.String{Value: s}) } ret = arr return }, }, }, } } // Size-limit checking implementation of regexp.ReplaceAllString. func doTextRegexpReplace(re *regexp.Regexp, src, repl string) (string, bool) { idx := 0 out := "" for _, m := range re.FindAllStringSubmatchIndex(src, -1) { var exp []byte exp = re.ExpandString(exp, repl, src, m) if len(out)+m[0]-idx+len(exp) > tengo.MaxStringLen { return "", false } out += src[idx:m[0]] + string(exp) idx = m[1] } if idx < len(src) { if len(out)+len(src)-idx > tengo.MaxStringLen { return "", false } out += src[idx:] } return out, true } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/text_test.go0000644000175000017500000002400714607001502021000 0ustar00maythammaythampackage stdlib_test import ( "regexp" "testing" "github.com/d5/tengo/v2" ) func TestTextRE(t *testing.T) { // re_match(pattern, text) for _, d := range []struct { pattern string text string }{ {"abc", ""}, {"abc", "abc"}, {"a", "abc"}, {"b", "abc"}, {"^a", "abc"}, {"^b", "abc"}, } { expected := regexp.MustCompile(d.pattern).MatchString(d.text) module(t, "text").call("re_match", d.pattern, d.text). expect(expected, "pattern: %q, src: %q", d.pattern, d.text) module(t, "text").call("re_compile", d.pattern).call("match", d.text). expect(expected, "patter: %q, src: %q", d.pattern, d.text) } // re_find(pattern, text) for _, d := range []struct { pattern string text string expected interface{} }{ {"a(b)", "", tengo.UndefinedValue}, {"a(b)", "ab", ARR{ ARR{ IMAP{"text": "ab", "begin": 0, "end": 2}, IMAP{"text": "b", "begin": 1, "end": 2}, }, }}, {"a(bc)d", "abcdefgabcd", ARR{ ARR{ IMAP{"text": "abcd", "begin": 0, "end": 4}, IMAP{"text": "bc", "begin": 1, "end": 3}, }, }}, {"(a)b(c)d", "abcdefgabcd", ARR{ ARR{ IMAP{"text": "abcd", "begin": 0, "end": 4}, IMAP{"text": "a", "begin": 0, "end": 1}, IMAP{"text": "c", "begin": 2, "end": 3}, }, }}, } { module(t, "text").call("re_find", d.pattern, d.text). expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) module(t, "text").call("re_compile", d.pattern).call("find", d.text). expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) } // re_find(pattern, text, count)) for _, d := range []struct { pattern string text string count int expected interface{} }{ {"a(b)", "", -1, tengo.UndefinedValue}, {"a(b)", "ab", -1, ARR{ ARR{ IMAP{"text": "ab", "begin": 0, "end": 2}, IMAP{"text": "b", "begin": 1, "end": 2}, }, }}, {"a(bc)d", "abcdefgabcd", -1, ARR{ ARR{ IMAP{"text": "abcd", "begin": 0, "end": 4}, IMAP{"text": "bc", "begin": 1, "end": 3}, }, ARR{ IMAP{"text": "abcd", "begin": 7, "end": 11}, IMAP{"text": "bc", "begin": 8, "end": 10}, }, }}, {"(a)b(c)d", "abcdefgabcd", -1, ARR{ ARR{ IMAP{"text": "abcd", "begin": 0, "end": 4}, IMAP{"text": "a", "begin": 0, "end": 1}, IMAP{"text": "c", "begin": 2, "end": 3}, }, ARR{ IMAP{"text": "abcd", "begin": 7, "end": 11}, IMAP{"text": "a", "begin": 7, "end": 8}, IMAP{"text": "c", "begin": 9, "end": 10}, }, }}, {"(a)b(c)d", "abcdefgabcd", 0, tengo.UndefinedValue}, {"(a)b(c)d", "abcdefgabcd", 1, ARR{ ARR{ IMAP{"text": "abcd", "begin": 0, "end": 4}, IMAP{"text": "a", "begin": 0, "end": 1}, IMAP{"text": "c", "begin": 2, "end": 3}, }, }}, } { module(t, "text").call("re_find", d.pattern, d.text, d.count). expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) module(t, "text").call("re_compile", d.pattern). call("find", d.text, d.count). expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) } // re_replace(pattern, text, repl) for _, d := range []struct { pattern string text string repl string }{ {"a", "", "b"}, {"a", "a", "b"}, {"a", "acac", "b"}, {"b", "acac", "x"}, {"a", "acac", "123"}, {"ac", "acac", "99"}, {"ac$", "acac", "foo"}, {"a(b)", "ababab", "$1"}, {"a(b)(c)", "abcabcabc", "$2$1"}, {"(a(b)c)", "abcabcabc", "$1$2"}, {"(일(2)삼)", "일2삼12삼일23", "$1$2"}, {"((일)(2)3)", "일23\n일이3\n일23", "$1$2$3"}, {"(a(b)c)", "abc\nabc\nabc", "$1$2"}, } { expected := regexp.MustCompile(d.pattern). ReplaceAllString(d.text, d.repl) module(t, "text").call("re_replace", d.pattern, d.text, d.repl). expect(expected, "pattern: %q, text: %q, repl: %q", d.pattern, d.text, d.repl) module(t, "text").call("re_compile", d.pattern). call("replace", d.text, d.repl). expect(expected, "pattern: %q, text: %q, repl: %q", d.pattern, d.text, d.repl) } // re_split(pattern, text) for _, d := range []struct { pattern string text string }{ {"a", ""}, {"a", "abcabc"}, {"ab", "abcabc"}, {"^a", "abcabc"}, } { var expected []interface{} for _, ex := range regexp.MustCompile(d.pattern).Split(d.text, -1) { expected = append(expected, ex) } module(t, "text").call("re_split", d.pattern, d.text). expect(expected, "pattern: %q, text: %q", d.pattern, d.text) module(t, "text").call("re_compile", d.pattern).call("split", d.text). expect(expected, "pattern: %q, text: %q", d.pattern, d.text) } // re_split(pattern, text, count)) for _, d := range []struct { pattern string text string count int }{ {"a", "", -1}, {"a", "abcabc", -1}, {"ab", "abcabc", -1}, {"^a", "abcabc", -1}, {"a", "abcabc", 0}, {"a", "abcabc", 1}, {"a", "abcabc", 2}, {"a", "abcabc", 3}, {"b", "abcabc", 1}, {"b", "abcabc", 2}, {"b", "abcabc", 3}, } { var expected []interface{} for _, ex := range regexp.MustCompile(d.pattern).Split(d.text, d.count) { expected = append(expected, ex) } module(t, "text").call("re_split", d.pattern, d.text, d.count). expect(expected, "pattern: %q, text: %q", d.pattern, d.text) module(t, "text").call("re_compile", d.pattern). call("split", d.text, d.count). expect(expected, "pattern: %q, text: %q", d.pattern, d.text) } } func TestText(t *testing.T) { module(t, "text").call("compare", "", "").expect(0) module(t, "text").call("compare", "", "a").expect(-1) module(t, "text").call("compare", "a", "").expect(1) module(t, "text").call("compare", "a", "a").expect(0) module(t, "text").call("compare", "a", "b").expect(-1) module(t, "text").call("compare", "b", "a").expect(1) module(t, "text").call("compare", "abcde", "abcde").expect(0) module(t, "text").call("compare", "abcde", "abcdf").expect(-1) module(t, "text").call("compare", "abcdf", "abcde").expect(1) module(t, "text").call("contains", "", "").expect(true) module(t, "text").call("contains", "", "a").expect(false) module(t, "text").call("contains", "a", "").expect(true) module(t, "text").call("contains", "a", "a").expect(true) module(t, "text").call("contains", "abcde", "a").expect(true) module(t, "text").call("contains", "abcde", "abcde").expect(true) module(t, "text").call("contains", "abc", "abcde").expect(false) module(t, "text").call("contains", "ab cd", "bc").expect(false) module(t, "text").call("replace", "", "", "", -1).expect("") module(t, "text").call("replace", "abcd", "a", "x", -1).expect("xbcd") module(t, "text").call("replace", "aaaa", "a", "x", -1).expect("xxxx") module(t, "text").call("replace", "aaaa", "a", "x", 0).expect("aaaa") module(t, "text").call("replace", "aaaa", "a", "x", 2).expect("xxaa") module(t, "text").call("replace", "abcd", "bc", "x", -1).expect("axd") module(t, "text").call("format_bool", true).expect("true") module(t, "text").call("format_bool", false).expect("false") module(t, "text").call("format_float", -19.84, 'f', -1, 64).expect("-19.84") module(t, "text").call("format_int", -1984, 10).expect("-1984") module(t, "text").call("format_int", 1984, 8).expect("3700") module(t, "text").call("parse_bool", "true").expect(true) module(t, "text").call("parse_bool", "0").expect(false) module(t, "text").call("parse_float", "-19.84", 64).expect(-19.84) module(t, "text").call("parse_int", "-1984", 10, 64).expect(-1984) } func TestReplaceLimit(t *testing.T) { curMaxStringLen := tengo.MaxStringLen defer func() { tengo.MaxStringLen = curMaxStringLen }() tengo.MaxStringLen = 12 module(t, "text").call("replace", "123456789012", "1", "x", -1). expect("x234567890x2") module(t, "text").call("replace", "123456789012", "12", "x", -1). expect("x34567890x") module(t, "text").call("replace", "123456789012", "1", "xy", -1). expectError() module(t, "text").call("replace", "123456789012", "0", "xy", -1). expectError() module(t, "text").call("replace", "123456789012", "012", "xyz", -1). expect("123456789xyz") module(t, "text").call("replace", "123456789012", "012", "xyzz", -1). expectError() module(t, "text").call("re_replace", "1", "123456789012", "x"). expect("x234567890x2") module(t, "text").call("re_replace", "12", "123456789012", "x"). expect("x34567890x") module(t, "text").call("re_replace", "1", "123456789012", "xy"). expectError() module(t, "text").call("re_replace", "1(2)", "123456789012", "x$1"). expect("x234567890x2") module(t, "text").call("re_replace", "(1)(2)", "123456789012", "$2$1"). expect("213456789021") module(t, "text").call("re_replace", "(1)(2)", "123456789012", "${2}${1}x"). expectError() } func TestTextRepeat(t *testing.T) { curMaxStringLen := tengo.MaxStringLen defer func() { tengo.MaxStringLen = curMaxStringLen }() tengo.MaxStringLen = 12 module(t, "text").call("repeat", "1234", "3"). expect("123412341234") module(t, "text").call("repeat", "1234", "4"). expectError() module(t, "text").call("repeat", "1", "12"). expect("111111111111") module(t, "text").call("repeat", "1", "13"). expectError() } func TestSubstr(t *testing.T) { module(t, "text").call("substr", "", 0, 0).expect("") module(t, "text").call("substr", "abcdef", 0, 3).expect("abc") module(t, "text").call("substr", "abcdef", 0, 6).expect("abcdef") module(t, "text").call("substr", "abcdef", 0, 10).expect("abcdef") module(t, "text").call("substr", "abcdef", -10, 10).expect("abcdef") module(t, "text").call("substr", "abcdef", 0).expect("abcdef") module(t, "text").call("substr", "abcdef", 3).expect("def") module(t, "text").call("substr", "", 10, 0).expectError() module(t, "text").call("substr", "", "10", 0).expectError() module(t, "text").call("substr", "", 10, "0").expectError() module(t, "text").call("substr", "", "10", "0").expectError() module(t, "text").call("substr", 0, 0, 1).expect("0") module(t, "text").call("substr", 123, 0, 1).expect("1") module(t, "text").call("substr", 123.456, 4, 7).expect("456") } func TestPadLeft(t *testing.T) { module(t, "text").call("pad_left", "ab", 7, 0).expect("00000ab") module(t, "text").call("pad_right", "ab", 7, 0).expect("ab00000") module(t, "text").call("pad_left", "ab", 7, "+-").expect("-+-+-ab") module(t, "text").call("pad_right", "ab", 7, "+-").expect("ab+-+-+") } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/times.go0000644000175000017500000005725314607001502020107 0ustar00maythammaythampackage stdlib import ( "time" "github.com/d5/tengo/v2" ) var timesModule = map[string]tengo.Object{ "format_ansic": &tengo.String{Value: time.ANSIC}, "format_unix_date": &tengo.String{Value: time.UnixDate}, "format_ruby_date": &tengo.String{Value: time.RubyDate}, "format_rfc822": &tengo.String{Value: time.RFC822}, "format_rfc822z": &tengo.String{Value: time.RFC822Z}, "format_rfc850": &tengo.String{Value: time.RFC850}, "format_rfc1123": &tengo.String{Value: time.RFC1123}, "format_rfc1123z": &tengo.String{Value: time.RFC1123Z}, "format_rfc3339": &tengo.String{Value: time.RFC3339}, "format_rfc3339_nano": &tengo.String{Value: time.RFC3339Nano}, "format_kitchen": &tengo.String{Value: time.Kitchen}, "format_stamp": &tengo.String{Value: time.Stamp}, "format_stamp_milli": &tengo.String{Value: time.StampMilli}, "format_stamp_micro": &tengo.String{Value: time.StampMicro}, "format_stamp_nano": &tengo.String{Value: time.StampNano}, "nanosecond": &tengo.Int{Value: int64(time.Nanosecond)}, "microsecond": &tengo.Int{Value: int64(time.Microsecond)}, "millisecond": &tengo.Int{Value: int64(time.Millisecond)}, "second": &tengo.Int{Value: int64(time.Second)}, "minute": &tengo.Int{Value: int64(time.Minute)}, "hour": &tengo.Int{Value: int64(time.Hour)}, "january": &tengo.Int{Value: int64(time.January)}, "february": &tengo.Int{Value: int64(time.February)}, "march": &tengo.Int{Value: int64(time.March)}, "april": &tengo.Int{Value: int64(time.April)}, "may": &tengo.Int{Value: int64(time.May)}, "june": &tengo.Int{Value: int64(time.June)}, "july": &tengo.Int{Value: int64(time.July)}, "august": &tengo.Int{Value: int64(time.August)}, "september": &tengo.Int{Value: int64(time.September)}, "october": &tengo.Int{Value: int64(time.October)}, "november": &tengo.Int{Value: int64(time.November)}, "december": &tengo.Int{Value: int64(time.December)}, "sleep": &tengo.UserFunction{ Name: "sleep", Value: timesSleep, }, // sleep(int) "parse_duration": &tengo.UserFunction{ Name: "parse_duration", Value: timesParseDuration, }, // parse_duration(str) => int "since": &tengo.UserFunction{ Name: "since", Value: timesSince, }, // since(time) => int "until": &tengo.UserFunction{ Name: "until", Value: timesUntil, }, // until(time) => int "duration_hours": &tengo.UserFunction{ Name: "duration_hours", Value: timesDurationHours, }, // duration_hours(int) => float "duration_minutes": &tengo.UserFunction{ Name: "duration_minutes", Value: timesDurationMinutes, }, // duration_minutes(int) => float "duration_nanoseconds": &tengo.UserFunction{ Name: "duration_nanoseconds", Value: timesDurationNanoseconds, }, // duration_nanoseconds(int) => int "duration_seconds": &tengo.UserFunction{ Name: "duration_seconds", Value: timesDurationSeconds, }, // duration_seconds(int) => float "duration_string": &tengo.UserFunction{ Name: "duration_string", Value: timesDurationString, }, // duration_string(int) => string "month_string": &tengo.UserFunction{ Name: "month_string", Value: timesMonthString, }, // month_string(int) => string "date": &tengo.UserFunction{ Name: "date", Value: timesDate, }, // date(year, month, day, hour, min, sec, nsec) => time "now": &tengo.UserFunction{ Name: "now", Value: timesNow, }, // now() => time "parse": &tengo.UserFunction{ Name: "parse", Value: timesParse, }, // parse(format, str) => time "unix": &tengo.UserFunction{ Name: "unix", Value: timesUnix, }, // unix(sec, nsec) => time "add": &tengo.UserFunction{ Name: "add", Value: timesAdd, }, // add(time, int) => time "add_date": &tengo.UserFunction{ Name: "add_date", Value: timesAddDate, }, // add_date(time, years, months, days) => time "sub": &tengo.UserFunction{ Name: "sub", Value: timesSub, }, // sub(t time, u time) => int "after": &tengo.UserFunction{ Name: "after", Value: timesAfter, }, // after(t time, u time) => bool "before": &tengo.UserFunction{ Name: "before", Value: timesBefore, }, // before(t time, u time) => bool "time_year": &tengo.UserFunction{ Name: "time_year", Value: timesTimeYear, }, // time_year(time) => int "time_month": &tengo.UserFunction{ Name: "time_month", Value: timesTimeMonth, }, // time_month(time) => int "time_day": &tengo.UserFunction{ Name: "time_day", Value: timesTimeDay, }, // time_day(time) => int "time_weekday": &tengo.UserFunction{ Name: "time_weekday", Value: timesTimeWeekday, }, // time_weekday(time) => int "time_hour": &tengo.UserFunction{ Name: "time_hour", Value: timesTimeHour, }, // time_hour(time) => int "time_minute": &tengo.UserFunction{ Name: "time_minute", Value: timesTimeMinute, }, // time_minute(time) => int "time_second": &tengo.UserFunction{ Name: "time_second", Value: timesTimeSecond, }, // time_second(time) => int "time_nanosecond": &tengo.UserFunction{ Name: "time_nanosecond", Value: timesTimeNanosecond, }, // time_nanosecond(time) => int "time_unix": &tengo.UserFunction{ Name: "time_unix", Value: timesTimeUnix, }, // time_unix(time) => int "time_unix_nano": &tengo.UserFunction{ Name: "time_unix_nano", Value: timesTimeUnixNano, }, // time_unix_nano(time) => int "time_format": &tengo.UserFunction{ Name: "time_format", Value: timesTimeFormat, }, // time_format(time, format) => string "time_location": &tengo.UserFunction{ Name: "time_location", Value: timesTimeLocation, }, // time_location(time) => string "time_string": &tengo.UserFunction{ Name: "time_string", Value: timesTimeString, }, // time_string(time) => string "is_zero": &tengo.UserFunction{ Name: "is_zero", Value: timesIsZero, }, // is_zero(time) => bool "to_local": &tengo.UserFunction{ Name: "to_local", Value: timesToLocal, }, // to_local(time) => time "to_utc": &tengo.UserFunction{ Name: "to_utc", Value: timesToUTC, }, // to_utc(time) => time "in_location": &tengo.UserFunction{ Name: "in_location", Value: timesInLocation, }, // in_location(time, location) => time } func timesSleep(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } i1, ok := tengo.ToInt64(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } return } time.Sleep(time.Duration(i1)) ret = tengo.UndefinedValue return } func timesParseDuration(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } dur, err := time.ParseDuration(s1) if err != nil { ret = wrapError(err) return } ret = &tengo.Int{Value: int64(dur)} return } func timesSince(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(time.Since(t1))} return } func timesUntil(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(time.Until(t1))} return } func timesDurationHours(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } i1, ok := tengo.ToInt64(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Float{Value: time.Duration(i1).Hours()} return } func timesDurationMinutes(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } i1, ok := tengo.ToInt64(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Float{Value: time.Duration(i1).Minutes()} return } func timesDurationNanoseconds(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } i1, ok := tengo.ToInt64(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: time.Duration(i1).Nanoseconds()} return } func timesDurationSeconds(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } i1, ok := tengo.ToInt64(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Float{Value: time.Duration(i1).Seconds()} return } func timesDurationString(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } i1, ok := tengo.ToInt64(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.String{Value: time.Duration(i1).String()} return } func timesMonthString(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } i1, ok := tengo.ToInt64(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.String{Value: time.Month(i1).String()} return } func timesDate(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) < 7 || len(args) > 8 { err = tengo.ErrWrongNumArguments return } i1, ok := tengo.ToInt(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } i3, ok := tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } i4, ok := tengo.ToInt(args[3]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "fourth", Expected: "int(compatible)", Found: args[3].TypeName(), } return } i5, ok := tengo.ToInt(args[4]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "fifth", Expected: "int(compatible)", Found: args[4].TypeName(), } return } i6, ok := tengo.ToInt(args[5]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "sixth", Expected: "int(compatible)", Found: args[5].TypeName(), } return } i7, ok := tengo.ToInt(args[6]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "seventh", Expected: "int(compatible)", Found: args[6].TypeName(), } return } var loc *time.Location if len(args) == 8 { i8, ok := tengo.ToString(args[7]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "eighth", Expected: "string(compatible)", Found: args[7].TypeName(), } return } loc, err = time.LoadLocation(i8) if err != nil { ret = wrapError(err) return } } else { loc = time.Now().Location() } ret = &tengo.Time{ Value: time.Date(i1, time.Month(i2), i3, i4, i5, i6, i7, loc), } return } func timesNow(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 0 { err = tengo.ErrWrongNumArguments return } ret = &tengo.Time{Value: time.Now()} return } func timesParse(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } s1, ok := tengo.ToString(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } parsed, err := time.Parse(s1, s2) if err != nil { ret = wrapError(err) return } ret = &tengo.Time{Value: parsed} return } func timesUnix(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } i1, ok := tengo.ToInt64(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "int(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt64(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } ret = &tengo.Time{Value: time.Unix(i1, i2)} return } func timesAdd(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt64(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } ret = &tengo.Time{Value: t1.Add(time.Duration(i2))} return } func timesSub(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } t2, ok := tengo.ToTime(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "time(compatible)", Found: args[1].TypeName(), } return } ret = &tengo.Int{Value: int64(t1.Sub(t2))} return } func timesAddDate(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 4 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } i2, ok := tengo.ToInt(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "int(compatible)", Found: args[1].TypeName(), } return } i3, ok := tengo.ToInt(args[2]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "third", Expected: "int(compatible)", Found: args[2].TypeName(), } return } i4, ok := tengo.ToInt(args[3]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "fourth", Expected: "int(compatible)", Found: args[3].TypeName(), } return } ret = &tengo.Time{Value: t1.AddDate(i2, i3, i4)} return } func timesAfter(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } t2, ok := tengo.ToTime(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "time(compatible)", Found: args[1].TypeName(), } return } if t1.After(t2) { ret = tengo.TrueValue } else { ret = tengo.FalseValue } return } func timesBefore(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } t2, ok := tengo.ToTime(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "time(compatible)", Found: args[0].TypeName(), } return } if t1.Before(t2) { ret = tengo.TrueValue } else { ret = tengo.FalseValue } return } func timesTimeYear(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(t1.Year())} return } func timesTimeMonth(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(t1.Month())} return } func timesTimeDay(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(t1.Day())} return } func timesTimeWeekday(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(t1.Weekday())} return } func timesTimeHour(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(t1.Hour())} return } func timesTimeMinute(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(t1.Minute())} return } func timesTimeSecond(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(t1.Second())} return } func timesTimeNanosecond(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: int64(t1.Nanosecond())} return } func timesTimeUnix(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: t1.Unix()} return } func timesTimeUnixNano(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Int{Value: t1.UnixNano()} return } func timesTimeFormat(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } s := t1.Format(s2) if len(s) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } ret = &tengo.String{Value: s} return } func timesIsZero(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } if t1.IsZero() { ret = tengo.TrueValue } else { ret = tengo.FalseValue } return } func timesToLocal(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Time{Value: t1.Local()} return } func timesToUTC(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.Time{Value: t1.UTC()} return } func timesTimeLocation(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.String{Value: t1.Location().String()} return } func timesInLocation(args ...tengo.Object) ( ret tengo.Object, err error, ) { if len(args) != 2 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } s2, ok := tengo.ToString(args[1]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "second", Expected: "string(compatible)", Found: args[1].TypeName(), } return } location, err := time.LoadLocation(s2) if err != nil { ret = wrapError(err) return } ret = &tengo.Time{Value: t1.In(location)} return } func timesTimeString(args ...tengo.Object) (ret tengo.Object, err error) { if len(args) != 1 { err = tengo.ErrWrongNumArguments return } t1, ok := tengo.ToTime(args[0]) if !ok { err = tengo.ErrInvalidArgumentType{ Name: "first", Expected: "time(compatible)", Found: args[0].TypeName(), } return } ret = &tengo.String{Value: t1.String()} return } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/stdlib/times_test.go0000644000175000017500000000726314607001502021142 0ustar00maythammaythampackage stdlib_test import ( "testing" "time" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" ) func TestTimes(t *testing.T) { time1 := time.Date(1982, 9, 28, 19, 21, 44, 999, time.Now().Location()) time2 := time.Now() location, _ := time.LoadLocation("Pacific/Auckland") time3 := time.Date(1982, 9, 28, 19, 21, 44, 999, location) module(t, "times").call("sleep", 1).expect(tengo.UndefinedValue) require.True(t, module(t, "times"). call("since", time.Now().Add(-time.Hour)). o.(*tengo.Int).Value > 3600000000000) require.True(t, module(t, "times"). call("until", time.Now().Add(time.Hour)). o.(*tengo.Int).Value < 3600000000000) module(t, "times").call("parse_duration", "1ns").expect(1) module(t, "times").call("parse_duration", "1ms").expect(1000000) module(t, "times").call("parse_duration", "1h").expect(3600000000000) module(t, "times").call("duration_hours", 1800000000000).expect(0.5) module(t, "times").call("duration_minutes", 1800000000000).expect(30.0) module(t, "times").call("duration_nanoseconds", 100).expect(100) module(t, "times").call("duration_seconds", 1000000).expect(0.001) module(t, "times").call("duration_string", 1800000000000).expect("30m0s") module(t, "times").call("month_string", 1).expect("January") module(t, "times").call("month_string", 12).expect("December") module(t, "times").call("date", 1982, 9, 28, 19, 21, 44, 999). expect(time1) module(t, "times").call("date", 1982, 9, 28, 19, 21, 44, 999, "Pacific/Auckland"). expect(time3) nowD := time.Until(module(t, "times").call("now"). o.(*tengo.Time).Value).Nanoseconds() require.True(t, 0 > nowD && nowD > -100000000) // within 100ms parsed, _ := time.Parse(time.RFC3339, "1982-09-28T19:21:44+07:00") module(t, "times"). call("parse", time.RFC3339, "1982-09-28T19:21:44+07:00"). expect(parsed) module(t, "times"). call("unix", 1234325, 94493). expect(time.Unix(1234325, 94493)) module(t, "times").call("add", time2, 3600000000000). expect(time2.Add(time.Duration(3600000000000))) module(t, "times").call("sub", time2, time2.Add(-time.Hour)). expect(3600000000000) module(t, "times").call("add_date", time2, 1, 2, 3). expect(time2.AddDate(1, 2, 3)) module(t, "times").call("after", time2, time2.Add(time.Hour)). expect(false) module(t, "times").call("after", time2, time2.Add(-time.Hour)). expect(true) module(t, "times").call("before", time2, time2.Add(time.Hour)). expect(true) module(t, "times").call("before", time2, time2.Add(-time.Hour)). expect(false) module(t, "times").call("time_year", time1).expect(time1.Year()) module(t, "times").call("time_month", time1).expect(int(time1.Month())) module(t, "times").call("time_day", time1).expect(time1.Day()) module(t, "times").call("time_hour", time1).expect(time1.Hour()) module(t, "times").call("time_minute", time1).expect(time1.Minute()) module(t, "times").call("time_second", time1).expect(time1.Second()) module(t, "times").call("time_nanosecond", time1). expect(time1.Nanosecond()) module(t, "times").call("time_unix", time1).expect(time1.Unix()) module(t, "times").call("time_unix_nano", time1).expect(time1.UnixNano()) module(t, "times").call("time_format", time1, time.RFC3339). expect(time1.Format(time.RFC3339)) module(t, "times").call("is_zero", time1).expect(false) module(t, "times").call("is_zero", time.Time{}).expect(true) module(t, "times").call("to_local", time1).expect(time1.Local()) module(t, "times").call("to_utc", time1).expect(time1.UTC()) module(t, "times").call("time_location", time1). expect(time1.Location().String()) module(t, "times").call("time_string", time1).expect(time1.String()) module(t, "times").call("in_location", time1, location.String()).expect(time1.In(location)) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/symbol_table.go0000644000175000017500000001031714607001502020147 0ustar00maythammaythampackage tengo // SymbolScope represents a symbol scope. type SymbolScope string // List of symbol scopes const ( ScopeGlobal SymbolScope = "GLOBAL" ScopeLocal SymbolScope = "LOCAL" ScopeBuiltin SymbolScope = "BUILTIN" ScopeFree SymbolScope = "FREE" ) // Symbol represents a symbol in the symbol table. type Symbol struct { Name string Scope SymbolScope Index int LocalAssigned bool // if the local symbol is assigned at least once } // SymbolTable represents a symbol table. type SymbolTable struct { parent *SymbolTable block bool store map[string]*Symbol numDefinition int maxDefinition int freeSymbols []*Symbol builtinSymbols []*Symbol } // NewSymbolTable creates a SymbolTable. func NewSymbolTable() *SymbolTable { return &SymbolTable{ store: make(map[string]*Symbol), } } // Define adds a new symbol in the current scope. func (t *SymbolTable) Define(name string) *Symbol { symbol := &Symbol{Name: name, Index: t.nextIndex()} t.numDefinition++ if t.Parent(true) == nil { symbol.Scope = ScopeGlobal // if symbol is defined in a block of global scope, symbol index must // be tracked at the root-level table instead. if p := t.parent; p != nil { for p.parent != nil { p = p.parent } t.numDefinition-- p.numDefinition++ } } else { symbol.Scope = ScopeLocal } t.store[name] = symbol t.updateMaxDefs(symbol.Index + 1) return symbol } // DefineBuiltin adds a symbol for builtin function. func (t *SymbolTable) DefineBuiltin(index int, name string) *Symbol { if t.parent != nil { return t.parent.DefineBuiltin(index, name) } symbol := &Symbol{ Name: name, Index: index, Scope: ScopeBuiltin, } t.store[name] = symbol t.builtinSymbols = append(t.builtinSymbols, symbol) return symbol } // Resolve resolves a symbol with a given name. func (t *SymbolTable) Resolve( name string, recur bool, ) (*Symbol, int, bool) { symbol, ok := t.store[name] if ok { // symbol can be used if if symbol.Scope != ScopeLocal || // it's not of local scope, OR, symbol.LocalAssigned || // it's assigned at least once, OR, recur { // it's defined in higher level return symbol, 0, true } } if t.parent == nil { return nil, 0, false } symbol, depth, ok := t.parent.Resolve(name, true) if !ok { return nil, 0, false } depth++ // if symbol is defined in parent table and if it's not global/builtin // then it's free variable. if !t.block && depth > 0 && symbol.Scope != ScopeGlobal && symbol.Scope != ScopeBuiltin { return t.defineFree(symbol), depth, true } return symbol, depth, true } // Fork creates a new symbol table for a new scope. func (t *SymbolTable) Fork(block bool) *SymbolTable { return &SymbolTable{ store: make(map[string]*Symbol), parent: t, block: block, } } // Parent returns the outer scope of the current symbol table. func (t *SymbolTable) Parent(skipBlock bool) *SymbolTable { if skipBlock && t.block { return t.parent.Parent(skipBlock) } return t.parent } // MaxSymbols returns the total number of symbols defined in the scope. func (t *SymbolTable) MaxSymbols() int { return t.maxDefinition } // FreeSymbols returns free symbols for the scope. func (t *SymbolTable) FreeSymbols() []*Symbol { return t.freeSymbols } // BuiltinSymbols returns builtin symbols for the scope. func (t *SymbolTable) BuiltinSymbols() []*Symbol { if t.parent != nil { return t.parent.BuiltinSymbols() } return t.builtinSymbols } // Names returns the name of all the symbols. func (t *SymbolTable) Names() []string { var names []string for name := range t.store { names = append(names, name) } return names } func (t *SymbolTable) nextIndex() int { if t.block { return t.parent.nextIndex() + t.numDefinition } return t.numDefinition } func (t *SymbolTable) updateMaxDefs(numDefs int) { if numDefs > t.maxDefinition { t.maxDefinition = numDefs } if t.block { t.parent.updateMaxDefs(numDefs) } } func (t *SymbolTable) defineFree(original *Symbol) *Symbol { // TODO: should we check duplicates? t.freeSymbols = append(t.freeSymbols, original) symbol := &Symbol{ Name: original.Name, Index: len(t.freeSymbols) - 1, Scope: ScopeFree, } t.store[original.Name] = symbol return symbol } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/symbol_table_test.go0000644000175000017500000000653414607001502021214 0ustar00maythammaythampackage tengo_test import ( "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" ) func TestSymbolTable(t *testing.T) { /* GLOBAL [0] a [1] b LOCAL 1 [0] d LOCAL 2 [0] e [1] f LOCAL 2 BLOCK 1 [2] g [3] h LOCAL 2 BLOCK 2 [2] i [3] j [4] k LOCAL 1 BLOCK 1 [1] l [2] m [3] n [4] o [5] p LOCAL 3 [0] q [1] r */ global := symbolTable() require.Equal(t, globalSymbol("a", 0), global.Define("a")) require.Equal(t, globalSymbol("b", 1), global.Define("b")) local1 := global.Fork(false) require.Equal(t, localSymbol("d", 0), local1.Define("d")) local1Block1 := local1.Fork(true) require.Equal(t, localSymbol("l", 1), local1Block1.Define("l")) require.Equal(t, localSymbol("m", 2), local1Block1.Define("m")) require.Equal(t, localSymbol("n", 3), local1Block1.Define("n")) require.Equal(t, localSymbol("o", 4), local1Block1.Define("o")) require.Equal(t, localSymbol("p", 5), local1Block1.Define("p")) local2 := local1.Fork(false) require.Equal(t, localSymbol("e", 0), local2.Define("e")) require.Equal(t, localSymbol("f", 1), local2.Define("f")) local2Block1 := local2.Fork(true) require.Equal(t, localSymbol("g", 2), local2Block1.Define("g")) require.Equal(t, localSymbol("h", 3), local2Block1.Define("h")) local2Block2 := local2.Fork(true) require.Equal(t, localSymbol("i", 2), local2Block2.Define("i")) require.Equal(t, localSymbol("j", 3), local2Block2.Define("j")) require.Equal(t, localSymbol("k", 4), local2Block2.Define("k")) local3 := local1Block1.Fork(false) require.Equal(t, localSymbol("q", 0), local3.Define("q")) require.Equal(t, localSymbol("r", 1), local3.Define("r")) require.Equal(t, 2, global.MaxSymbols()) require.Equal(t, 6, local1.MaxSymbols()) require.Equal(t, 6, local1Block1.MaxSymbols()) require.Equal(t, 5, local2.MaxSymbols()) require.Equal(t, 4, local2Block1.MaxSymbols()) require.Equal(t, 5, local2Block2.MaxSymbols()) require.Equal(t, 2, local3.MaxSymbols()) resolveExpect(t, global, "a", globalSymbol("a", 0), 0) resolveExpect(t, local1, "d", localSymbol("d", 0), 0) resolveExpect(t, local1, "a", globalSymbol("a", 0), 1) resolveExpect(t, local3, "a", globalSymbol("a", 0), 3) resolveExpect(t, local3, "d", freeSymbol("d", 0), 2) resolveExpect(t, local3, "r", localSymbol("r", 1), 0) resolveExpect(t, local2Block2, "k", localSymbol("k", 4), 0) resolveExpect(t, local2Block2, "e", localSymbol("e", 0), 1) resolveExpect(t, local2Block2, "b", globalSymbol("b", 1), 3) } func symbol( name string, scope tengo.SymbolScope, index int, ) *tengo.Symbol { return &tengo.Symbol{ Name: name, Scope: scope, Index: index, } } func globalSymbol(name string, index int) *tengo.Symbol { return symbol(name, tengo.ScopeGlobal, index) } func localSymbol(name string, index int) *tengo.Symbol { return symbol(name, tengo.ScopeLocal, index) } func freeSymbol(name string, index int) *tengo.Symbol { return symbol(name, tengo.ScopeFree, index) } func symbolTable() *tengo.SymbolTable { return tengo.NewSymbolTable() } func resolveExpect( t *testing.T, symbolTable *tengo.SymbolTable, name string, expectedSymbol *tengo.Symbol, expectedDepth int, ) { actualSymbol, actualDepth, ok := symbolTable.Resolve(name, true) require.True(t, ok) require.Equal(t, expectedSymbol, actualSymbol) require.Equal(t, expectedDepth, actualDepth) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/tengo.go0000644000175000017500000001430114607001502016604 0ustar00maythammaythampackage tengo import ( "errors" "fmt" "strconv" "time" ) var ( // MaxStringLen is the maximum byte-length for string value. Note this // limit applies to all compiler/VM instances in the process. MaxStringLen = 2147483647 // MaxBytesLen is the maximum length for bytes value. Note this limit // applies to all compiler/VM instances in the process. MaxBytesLen = 2147483647 ) const ( // GlobalsSize is the maximum number of global variables for a VM. GlobalsSize = 1024 // StackSize is the maximum stack size for a VM. StackSize = 2048 // MaxFrames is the maximum number of function frames for a VM. MaxFrames = 1024 // SourceFileExtDefault is the default extension for source files. SourceFileExtDefault = ".tengo" ) // CallableFunc is a function signature for the callable functions. type CallableFunc = func(args ...Object) (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. func CountObjects(o Object) (c int) { c = 1 switch o := o.(type) { case *Array: for _, v := range o.Value { c += CountObjects(v) } case *ImmutableArray: for _, v := range o.Value { c += CountObjects(v) } case *Map: for _, v := range o.Value { c += CountObjects(v) } case *ImmutableMap: for _, v := range o.Value { c += CountObjects(v) } case *Error: c += CountObjects(o.Value) } return } // ToString will try to convert object o to string value. func ToString(o Object) (v string, ok bool) { if o == UndefinedValue { return } ok = true if str, isStr := o.(*String); isStr { v = str.Value } else { v = o.String() } return } // ToInt will try to convert object o to int value. func ToInt(o Object) (v int, ok bool) { switch o := o.(type) { case *Int: v = int(o.Value) ok = true case *Float: v = int(o.Value) ok = true case *Char: v = int(o.Value) ok = true case *Bool: if o == TrueValue { v = 1 } ok = true case *String: c, err := strconv.ParseInt(o.Value, 10, 64) if err == nil { v = int(c) ok = true } } return } // ToInt64 will try to convert object o to int64 value. func ToInt64(o Object) (v int64, ok bool) { switch o := o.(type) { case *Int: v = o.Value ok = true case *Float: v = int64(o.Value) ok = true case *Char: v = int64(o.Value) ok = true case *Bool: if o == TrueValue { v = 1 } ok = true case *String: c, err := strconv.ParseInt(o.Value, 10, 64) if err == nil { v = c ok = true } } return } // ToFloat64 will try to convert object o to float64 value. func ToFloat64(o Object) (v float64, ok bool) { switch o := o.(type) { case *Int: v = float64(o.Value) ok = true case *Float: v = o.Value ok = true case *String: c, err := strconv.ParseFloat(o.Value, 64) if err == nil { v = c ok = true } } return } // ToBool will try to convert object o to bool value. func ToBool(o Object) (v bool, ok bool) { ok = true v = !o.IsFalsy() return } // ToRune will try to convert object o to rune value. func ToRune(o Object) (v rune, ok bool) { switch o := o.(type) { case *Int: v = rune(o.Value) ok = true case *Char: v = o.Value ok = true } return } // ToByteSlice will try to convert object o to []byte value. func ToByteSlice(o Object) (v []byte, ok bool) { switch o := o.(type) { case *Bytes: v = o.Value ok = true case *String: v = []byte(o.Value) ok = true } return } // ToTime will try to convert object o to time.Time value. func ToTime(o Object) (v time.Time, ok bool) { switch o := o.(type) { case *Time: v = o.Value ok = true case *Int: v = time.Unix(o.Value, 0) ok = true } return } // ToInterface attempts to convert an object o to an interface{} value func ToInterface(o Object) (res interface{}) { switch o := o.(type) { case *Int: res = o.Value case *String: res = o.Value case *Float: res = o.Value case *Bool: res = o == TrueValue case *Char: res = o.Value case *Bytes: res = o.Value case *Array: res = make([]interface{}, len(o.Value)) for i, val := range o.Value { res.([]interface{})[i] = ToInterface(val) } case *ImmutableArray: res = make([]interface{}, len(o.Value)) for i, val := range o.Value { res.([]interface{})[i] = ToInterface(val) } case *Map: res = make(map[string]interface{}) for key, v := range o.Value { res.(map[string]interface{})[key] = ToInterface(v) } case *ImmutableMap: res = make(map[string]interface{}) for key, v := range o.Value { res.(map[string]interface{})[key] = ToInterface(v) } case *Time: res = o.Value case *Error: res = errors.New(o.String()) case *Undefined: res = nil case Object: return o } return } // FromInterface will attempt to convert an interface{} v to a Tengo Object func FromInterface(v interface{}) (Object, error) { switch v := v.(type) { case nil: return UndefinedValue, nil case string: if len(v) > MaxStringLen { return nil, ErrStringLimit } return &String{Value: v}, nil case int64: return &Int{Value: v}, nil case int: return &Int{Value: int64(v)}, nil case bool: if v { return TrueValue, nil } return FalseValue, nil case rune: return &Char{Value: v}, nil case byte: return &Char{Value: rune(v)}, nil case float64: return &Float{Value: v}, nil case []byte: if len(v) > MaxBytesLen { return nil, ErrBytesLimit } return &Bytes{Value: v}, nil case error: return &Error{Value: &String{Value: v.Error()}}, nil case map[string]Object: return &Map{Value: v}, nil case map[string]interface{}: kv := make(map[string]Object) for vk, vv := range v { vo, err := FromInterface(vv) if err != nil { return nil, err } kv[vk] = vo } return &Map{Value: kv}, nil case []Object: return &Array{Value: v}, nil case []interface{}: arr := make([]Object, len(v)) for i, e := range v { vo, err := FromInterface(e) if err != nil { return nil, err } arr[i] = vo } return &Array{Value: arr}, nil case time.Time: return &Time{Value: v}, nil case Object: return v, nil case CallableFunc: return &UserFunction{Value: v}, nil } return nil, fmt.Errorf("cannot convert to object: %T", v) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/tengo_test.go0000644000175000017500000000735314607001502017654 0ustar00maythammaythampackage tengo_test import ( "strings" "testing" "time" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/require" ) func TestInstructions_String(t *testing.T) { assertInstructionString(t, [][]byte{ tengo.MakeInstruction(parser.OpConstant, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 65535), }, `0000 CONST 1 0003 CONST 2 0006 CONST 65535`) assertInstructionString(t, [][]byte{ tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 65535), }, `0000 BINARYOP 11 0002 CONST 2 0005 CONST 65535`) assertInstructionString(t, [][]byte{ tengo.MakeInstruction(parser.OpBinaryOp, 11), tengo.MakeInstruction(parser.OpGetLocal, 1), tengo.MakeInstruction(parser.OpConstant, 2), tengo.MakeInstruction(parser.OpConstant, 65535), }, `0000 BINARYOP 11 0002 GETL 1 0004 CONST 2 0007 CONST 65535`) } func TestMakeInstruction(t *testing.T) { makeInstruction(t, []byte{parser.OpConstant, 0, 0}, parser.OpConstant, 0) makeInstruction(t, []byte{parser.OpConstant, 0, 1}, parser.OpConstant, 1) makeInstruction(t, []byte{parser.OpConstant, 255, 254}, parser.OpConstant, 65534) makeInstruction(t, []byte{parser.OpPop}, parser.OpPop) makeInstruction(t, []byte{parser.OpTrue}, parser.OpTrue) makeInstruction(t, []byte{parser.OpFalse}, parser.OpFalse) } func TestNumObjects(t *testing.T) { testCountObjects(t, &tengo.Array{}, 1) testCountObjects(t, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 3}, &tengo.Int{Value: 4}, &tengo.Int{Value: 5}, }}, }}, 7) testCountObjects(t, tengo.TrueValue, 1) testCountObjects(t, tengo.FalseValue, 1) testCountObjects(t, &tengo.BuiltinFunction{}, 1) testCountObjects(t, &tengo.Bytes{Value: []byte("foobar")}, 1) testCountObjects(t, &tengo.Char{Value: '가'}, 1) testCountObjects(t, &tengo.CompiledFunction{}, 1) testCountObjects(t, &tengo.Error{Value: &tengo.Int{Value: 5}}, 2) testCountObjects(t, &tengo.Float{Value: 19.84}, 1) testCountObjects(t, &tengo.ImmutableArray{Value: []tengo.Object{ &tengo.Int{Value: 1}, &tengo.Int{Value: 2}, &tengo.ImmutableArray{Value: []tengo.Object{ &tengo.Int{Value: 3}, &tengo.Int{Value: 4}, &tengo.Int{Value: 5}, }}, }}, 7) testCountObjects(t, &tengo.ImmutableMap{ Value: map[string]tengo.Object{ "k1": &tengo.Int{Value: 1}, "k2": &tengo.Int{Value: 2}, "k3": &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 3}, &tengo.Int{Value: 4}, &tengo.Int{Value: 5}, }}, }}, 7) testCountObjects(t, &tengo.Int{Value: 1984}, 1) testCountObjects(t, &tengo.Map{Value: map[string]tengo.Object{ "k1": &tengo.Int{Value: 1}, "k2": &tengo.Int{Value: 2}, "k3": &tengo.Array{Value: []tengo.Object{ &tengo.Int{Value: 3}, &tengo.Int{Value: 4}, &tengo.Int{Value: 5}, }}, }}, 7) testCountObjects(t, &tengo.String{Value: "foo bar"}, 1) testCountObjects(t, &tengo.Time{Value: time.Now()}, 1) testCountObjects(t, tengo.UndefinedValue, 1) } func testCountObjects(t *testing.T, o tengo.Object, expected int) { require.Equal(t, expected, tengo.CountObjects(o)) } func assertInstructionString( t *testing.T, instructions [][]byte, expected string, ) { concatted := make([]byte, 0) for _, e := range instructions { concatted = append(concatted, e...) } require.Equal(t, expected, strings.Join( tengo.FormatInstructions(concatted, 0), "\n")) } func makeInstruction( t *testing.T, expected []byte, opcode parser.Opcode, operands ...int, ) { inst := tengo.MakeInstruction(opcode, operands...) require.Equal(t, expected, inst) } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/0000755000175000017500000000000014607001502016753 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/0000755000175000017500000000000014607001502017522 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/one.tengo0000644000175000017500000000014414607001502021340 0ustar00maythammaytham export { fn: func(a) { two := import("two/two") return two.fn(a, "one") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/test.tengo0000755000175000017500000000065414607001502021547 0ustar00maythammaytham#!/usr/bin/env tengo os := import("os") one := import("one") fmt := import("fmt") text := import("text") expected := ["test", "one", "two", "three", "four", "five"] expected = text.join(expected, " ") if v := one.fn("test"); v != expected { fmt.printf("relative import test error:\n\texpected: %v\n\tgot : %v\n", expected, v) os.exit(1) } args := text.join(os.args(), " ") fmt.println("ok\t", args) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/three.tengo0000644000175000017500000000020114607001502021660 0ustar00maythammaythamexport { fn: func(a, b, c) { four := import("./two/four/four.tengo") return four.fn(a, b, c, "three") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/two/0000755000175000017500000000000014607001502020333 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/two/five/0000755000175000017500000000000014607001502021264 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/two/five/five.tengo0000644000175000017500000000021714607001502023253 0ustar00maythammaythamexport { fn: func(...args) { text := import("text") args = append(args, "five") return text.join(args, " ") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/two/four/0000755000175000017500000000000014607001502021306 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/two/four/four.tengo0000644000175000017500000000017514607001502023322 0ustar00maythammaythamexport { fn: func(a, b, c, d) { five := import("../five/five") return five.fn(a, b, c, d, "four") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/cli/two/two.tengo0000644000175000017500000000015614607001502022204 0ustar00maythammaythamexport { fn: func(a, b) { three := import("../three") return three.fn(a, b, "two") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/0000755000175000017500000000000014607001502020343 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/dos/0000755000175000017500000000000014607001502021130 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/dos/cinco/0000755000175000017500000000000014607001502022223 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/dos/cinco/cinco.mshk0000644000175000017500000000022114607001502024175 0ustar00maythammaythamexport { fn: func(...args) { text := import("text") args = append(args, "cinco") return text.join(args, " ") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/dos/dos.mshk0000644000175000017500000000015414607001502022601 0ustar00maythammaythamexport { fn: func(a, b) { tres := import("../tres") return tres.fn(a, b, "dos") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/dos/quatro/0000755000175000017500000000000014607001502022443 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/dos/quatro/quatro.mshk0000644000175000017500000000020414607001502024636 0ustar00maythammaythamexport { fn: func(a, b, c, d) { cinco := import("../cinco/cinco") return cinco.fn(a, b, c, d, "quatro") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/test.mshk0000644000175000017500000000136614607001502022214 0ustar00maythammaytham#!/usr/bin/env tengo // This is a test of custom extension for issue #286 and PR #350. // Which allows the tengo library to use custom extension names for the // source files. // // This test should pass if the interpreter's tengo.Compiler.SetImportExt() // was set as `c.SetImportExt(".tengo", ".mshk")`. os := import("os") uno := import("uno") // it will search uno.tengo and uno.mshk fmt := import("fmt") text := import("text") expected := ["test", "uno", "dos", "tres", "quatro", "cinco"] expected = text.join(expected, " ") if v := uno.fn("test"); v != expected { fmt.printf("relative import test error:\n\texpected: %v\n\tgot : %v\n", expected, v) os.exit(1) } args := text.join(os.args(), " ") fmt.println("ok\t", args) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/tres.tengo0000644000175000017500000000020714607001502022355 0ustar00maythammaythamexport { fn: func(a, b, c) { quatro := import("./dos/quatro/quatro.mshk") return quatro.fn(a, b, c, "tres") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/testdata/issue286/uno.mshk0000644000175000017500000000014314607001502022026 0ustar00maythammaythamexport { fn: func(a) { dos := import("dos/dos") return dos.fn(a, "uno") } }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/token/0000755000175000017500000000000014607001502016262 5ustar00maythammaytham././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/token/token.go0000644000175000017500000001015414607001502017732 0ustar00maythammaythampackage token import "strconv" var keywords map[string]Token // Token represents a token. type Token int // List of tokens const ( Illegal Token = iota EOF Comment _literalBeg Ident Int Float Char String _literalEnd _operatorBeg Add // + Sub // - Mul // * Quo // / Rem // % And // & Or // | Xor // ^ Shl // << Shr // >> AndNot // &^ AddAssign // += SubAssign // -= MulAssign // *= QuoAssign // /= RemAssign // %= AndAssign // &= OrAssign // |= XorAssign // ^= ShlAssign // <<= ShrAssign // >>= AndNotAssign // &^= LAnd // && LOr // || Inc // ++ Dec // -- Equal // == Less // < Greater // > Assign // = Not // ! NotEqual // != LessEq // <= GreaterEq // >= Define // := Ellipsis // ... LParen // ( LBrack // [ LBrace // { Comma // , Period // . RParen // ) RBrack // ] RBrace // } Semicolon // ; Colon // : Question // ? _operatorEnd _keywordBeg Break Continue Else For Func Error Immutable If Return Export True False In Undefined Import _keywordEnd ) var tokens = [...]string{ Illegal: "ILLEGAL", EOF: "EOF", Comment: "COMMENT", Ident: "IDENT", Int: "INT", Float: "FLOAT", Char: "CHAR", String: "STRING", Add: "+", Sub: "-", Mul: "*", Quo: "/", Rem: "%", And: "&", Or: "|", Xor: "^", Shl: "<<", Shr: ">>", AndNot: "&^", AddAssign: "+=", SubAssign: "-=", MulAssign: "*=", QuoAssign: "/=", RemAssign: "%=", AndAssign: "&=", OrAssign: "|=", XorAssign: "^=", ShlAssign: "<<=", ShrAssign: ">>=", AndNotAssign: "&^=", LAnd: "&&", LOr: "||", Inc: "++", Dec: "--", Equal: "==", Less: "<", Greater: ">", Assign: "=", Not: "!", NotEqual: "!=", LessEq: "<=", GreaterEq: ">=", Define: ":=", Ellipsis: "...", LParen: "(", LBrack: "[", LBrace: "{", Comma: ",", Period: ".", RParen: ")", RBrack: "]", RBrace: "}", Semicolon: ";", Colon: ":", Question: "?", Break: "break", Continue: "continue", Else: "else", For: "for", Func: "func", Error: "error", Immutable: "immutable", If: "if", Return: "return", Export: "export", True: "true", False: "false", In: "in", Undefined: "undefined", Import: "import", } func (tok Token) String() string { s := "" if 0 <= tok && tok < Token(len(tokens)) { s = tokens[tok] } if s == "" { s = "token(" + strconv.Itoa(int(tok)) + ")" } return s } // LowestPrec represents lowest operator precedence. const LowestPrec = 0 // Precedence returns the precedence for the operator token. func (tok Token) Precedence() int { switch tok { case LOr: return 1 case LAnd: return 2 case Equal, NotEqual, Less, LessEq, Greater, GreaterEq: return 3 case Add, Sub, Or, Xor: return 4 case Mul, Quo, Rem, Shl, Shr, And, AndNot: return 5 } return LowestPrec } // IsLiteral returns true if the token is a literal. func (tok Token) IsLiteral() bool { return _literalBeg < tok && tok < _literalEnd } // IsOperator returns true if the token is an operator. func (tok Token) IsOperator() bool { return _operatorBeg < tok && tok < _operatorEnd } // IsKeyword returns true if the token is a keyword. func (tok Token) IsKeyword() bool { return _keywordBeg < tok && tok < _keywordEnd } // Lookup returns corresponding keyword if ident is a keyword. func Lookup(ident string) Token { if tok, isKeyword := keywords[ident]; isKeyword { return tok } return Ident } func init() { keywords = make(map[string]Token) for i := _keywordBeg + 1; i < _keywordEnd; i++ { keywords[tokens[i]] = i } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1713111873.8840413 golang-github-d5-tengo/variable.go0000644000175000017500000000633114607001502017261 0ustar00maythammaythampackage tengo import ( "errors" ) // Variable is a user-defined variable for the script. type Variable struct { name string value Object } // NewVariable creates a Variable. func NewVariable(name string, value interface{}) (*Variable, error) { obj, err := FromInterface(value) if err != nil { return nil, err } return &Variable{ name: name, value: obj, }, nil } // Name returns the name of the variable. func (v *Variable) Name() string { return v.name } // Value returns an empty interface of the variable value. func (v *Variable) Value() interface{} { return ToInterface(v.value) } // ValueType returns the name of the value type. func (v *Variable) ValueType() string { return v.value.TypeName() } // Int returns int value of the variable value. // It returns 0 if the value is not convertible to int. func (v *Variable) Int() int { c, _ := ToInt(v.value) return c } // Int64 returns int64 value of the variable value. It returns 0 if the value // is not convertible to int64. func (v *Variable) Int64() int64 { c, _ := ToInt64(v.value) return c } // Float returns float64 value of the variable value. It returns 0.0 if the // value is not convertible to float64. func (v *Variable) Float() float64 { c, _ := ToFloat64(v.value) return c } // Char returns rune value of the variable value. It returns 0 if the value is // not convertible to rune. func (v *Variable) Char() rune { c, _ := ToRune(v.value) return c } // Bool returns bool value of the variable value. It returns 0 if the value is // not convertible to bool. func (v *Variable) Bool() bool { c, _ := ToBool(v.value) return c } // Array returns []interface value of the variable value. It returns 0 if the // value is not convertible to []interface. func (v *Variable) Array() []interface{} { switch val := v.value.(type) { case *Array: var arr []interface{} for _, e := range val.Value { arr = append(arr, ToInterface(e)) } return arr } return nil } // Map returns map[string]interface{} value of the variable value. It returns // 0 if the value is not convertible to map[string]interface{}. func (v *Variable) Map() map[string]interface{} { switch val := v.value.(type) { case *Map: kv := make(map[string]interface{}) for mk, mv := range val.Value { kv[mk] = ToInterface(mv) } return kv } return nil } // String returns string value of the variable value. It returns 0 if the value // is not convertible to string. func (v *Variable) String() string { c, _ := ToString(v.value) return c } // Bytes returns a byte slice of the variable value. It returns nil if the // value is not convertible to byte slice. func (v *Variable) Bytes() []byte { c, _ := ToByteSlice(v.value) return c } // Error returns an error if the underlying value is error object. If not, // this returns nil. func (v *Variable) Error() error { err, ok := v.value.(*Error) if ok { return errors.New(err.String()) } return nil } // Object returns an underlying Object of the variable value. Note that // returned Object is a copy of an actual Object used in the script. func (v *Variable) Object() Object { return v.value } // IsUndefined returns true if the underlying value is undefined. func (v *Variable) IsUndefined() bool { return v.value == UndefinedValue } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1713111873.888041 golang-github-d5-tengo/variable_test.go0000644000175000017500000000367014607001502020323 0ustar00maythammaythampackage tengo_test import ( "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/require" ) type VariableTest struct { Name string Value interface{} ValueType string IntValue int Int64Value int64 FloatValue float64 CharValue rune BoolValue bool StringValue string Object tengo.Object IsUndefined bool } func TestVariable(t *testing.T) { vars := []VariableTest{ { Name: "a", Value: int64(1), ValueType: "int", IntValue: 1, Int64Value: 1, FloatValue: 1.0, CharValue: rune(1), BoolValue: true, StringValue: "1", Object: &tengo.Int{Value: 1}, }, { Name: "b", Value: "52.11", ValueType: "string", FloatValue: 52.11, StringValue: "52.11", BoolValue: true, Object: &tengo.String{Value: "52.11"}, }, { Name: "c", Value: true, ValueType: "bool", IntValue: 1, Int64Value: 1, FloatValue: 0, BoolValue: true, StringValue: "true", Object: tengo.TrueValue, }, { Name: "d", Value: nil, ValueType: "undefined", Object: tengo.UndefinedValue, IsUndefined: true, }, } for _, tc := range vars { v, err := tengo.NewVariable(tc.Name, tc.Value) require.NoError(t, err) require.Equal(t, tc.Value, v.Value(), "Name: %s", tc.Name) require.Equal(t, tc.ValueType, v.ValueType(), "Name: %s", tc.Name) require.Equal(t, tc.IntValue, v.Int(), "Name: %s", tc.Name) require.Equal(t, tc.Int64Value, v.Int64(), "Name: %s", tc.Name) require.Equal(t, tc.FloatValue, v.Float(), "Name: %s", tc.Name) require.Equal(t, tc.CharValue, v.Char(), "Name: %s", tc.Name) require.Equal(t, tc.BoolValue, v.Bool(), "Name: %s", tc.Name) require.Equal(t, tc.StringValue, v.String(), "Name: %s", tc.Name) require.Equal(t, tc.Object, v.Object(), "Name: %s", tc.Name) require.Equal(t, tc.IsUndefined, v.IsUndefined(), "Name: %s", tc.Name) } } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1713111873.888041 golang-github-d5-tengo/vm.go0000644000175000017500000005233014607001502016116 0ustar00maythammaythampackage tengo import ( "fmt" "sync/atomic" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/token" ) // frame represents a function call frame. type frame struct { fn *CompiledFunction freeVars []*ObjectPtr ip int basePointer int } // VM is a virtual machine that executes the bytecode compiled by Compiler. type VM struct { constants []Object stack [StackSize]Object sp int globals []Object fileSet *parser.SourceFileSet frames [MaxFrames]frame framesIndex int curFrame *frame curInsts []byte ip int aborting int64 maxAllocs int64 allocs int64 err error } // NewVM creates a VM. func NewVM( bytecode *Bytecode, globals []Object, maxAllocs int64, ) *VM { if globals == nil { globals = make([]Object, GlobalsSize) } v := &VM{ constants: bytecode.Constants, sp: 0, globals: globals, fileSet: bytecode.FileSet, framesIndex: 1, ip: -1, maxAllocs: maxAllocs, } v.frames[0].fn = bytecode.MainFunction v.frames[0].ip = -1 v.curFrame = &v.frames[0] v.curInsts = v.curFrame.fn.Instructions return v } // Abort aborts the execution. func (v *VM) Abort() { atomic.StoreInt64(&v.aborting, 1) } // Run starts the execution. func (v *VM) Run() (err error) { // reset VM states v.sp = 0 v.curFrame = &(v.frames[0]) v.curInsts = v.curFrame.fn.Instructions v.framesIndex = 1 v.ip = -1 v.allocs = v.maxAllocs + 1 v.run() atomic.StoreInt64(&v.aborting, 0) err = v.err if err != nil { filePos := v.fileSet.Position( v.curFrame.fn.SourcePos(v.ip - 1)) err = fmt.Errorf("Runtime Error: %w\n\tat %s", err, filePos) for v.framesIndex > 1 { v.framesIndex-- v.curFrame = &v.frames[v.framesIndex-1] filePos = v.fileSet.Position( v.curFrame.fn.SourcePos(v.curFrame.ip - 1)) err = fmt.Errorf("%w\n\tat %s", err, filePos) } return err } return nil } func (v *VM) run() { for atomic.LoadInt64(&v.aborting) == 0 { v.ip++ switch v.curInsts[v.ip] { case parser.OpConstant: v.ip += 2 cidx := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 v.stack[v.sp] = v.constants[cidx] v.sp++ case parser.OpNull: v.stack[v.sp] = UndefinedValue v.sp++ case parser.OpBinaryOp: v.ip++ right := v.stack[v.sp-1] left := v.stack[v.sp-2] tok := token.Token(v.curInsts[v.ip]) res, e := left.BinaryOp(tok, right) if e != nil { v.sp -= 2 if e == ErrInvalidOperator { v.err = fmt.Errorf("invalid operation: %s %s %s", left.TypeName(), tok.String(), right.TypeName()) return } v.err = e return } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp-2] = res v.sp-- case parser.OpEqual: right := v.stack[v.sp-1] left := v.stack[v.sp-2] v.sp -= 2 if left.Equals(right) { v.stack[v.sp] = TrueValue } else { v.stack[v.sp] = FalseValue } v.sp++ case parser.OpNotEqual: right := v.stack[v.sp-1] left := v.stack[v.sp-2] v.sp -= 2 if left.Equals(right) { v.stack[v.sp] = FalseValue } else { v.stack[v.sp] = TrueValue } v.sp++ case parser.OpPop: v.sp-- case parser.OpTrue: v.stack[v.sp] = TrueValue v.sp++ case parser.OpFalse: v.stack[v.sp] = FalseValue v.sp++ case parser.OpLNot: operand := v.stack[v.sp-1] v.sp-- if operand.IsFalsy() { v.stack[v.sp] = TrueValue } else { v.stack[v.sp] = FalseValue } v.sp++ case parser.OpBComplement: operand := v.stack[v.sp-1] v.sp-- switch x := operand.(type) { case *Int: var res Object = &Int{Value: ^x.Value} v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = res v.sp++ default: v.err = fmt.Errorf("invalid operation: ^%s", operand.TypeName()) return } case parser.OpMinus: operand := v.stack[v.sp-1] v.sp-- switch x := operand.(type) { case *Int: var res Object = &Int{Value: -x.Value} v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = res v.sp++ case *Float: var res Object = &Float{Value: -x.Value} v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = res v.sp++ default: v.err = fmt.Errorf("invalid operation: -%s", operand.TypeName()) return } case parser.OpJumpFalsy: v.ip += 4 v.sp-- if v.stack[v.sp].IsFalsy() { pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 | int(v.curInsts[v.ip-2])<<16 | int(v.curInsts[v.ip-3])<<24 v.ip = pos - 1 } case parser.OpAndJump: v.ip += 4 if v.stack[v.sp-1].IsFalsy() { pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 | int(v.curInsts[v.ip-2])<<16 | int(v.curInsts[v.ip-3])<<24 v.ip = pos - 1 } else { v.sp-- } case parser.OpOrJump: v.ip += 4 if v.stack[v.sp-1].IsFalsy() { v.sp-- } else { pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 | int(v.curInsts[v.ip-2])<<16 | int(v.curInsts[v.ip-3])<<24 v.ip = pos - 1 } case parser.OpJump: pos := int(v.curInsts[v.ip+4]) | int(v.curInsts[v.ip+3])<<8 | int(v.curInsts[v.ip+2])<<16 | int(v.curInsts[v.ip+1])<<24 v.ip = pos - 1 case parser.OpSetGlobal: v.ip += 2 v.sp-- globalIndex := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 v.globals[globalIndex] = v.stack[v.sp] case parser.OpSetSelGlobal: v.ip += 3 globalIndex := int(v.curInsts[v.ip-1]) | int(v.curInsts[v.ip-2])<<8 numSelectors := int(v.curInsts[v.ip]) // selectors and RHS value selectors := make([]Object, numSelectors) for i := 0; i < numSelectors; i++ { selectors[i] = v.stack[v.sp-numSelectors+i] } val := v.stack[v.sp-numSelectors-1] v.sp -= numSelectors + 1 e := indexAssign(v.globals[globalIndex], val, selectors) if e != nil { v.err = e return } case parser.OpGetGlobal: v.ip += 2 globalIndex := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 val := v.globals[globalIndex] v.stack[v.sp] = val v.sp++ case parser.OpArray: v.ip += 2 numElements := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 var elements []Object for i := v.sp - numElements; i < v.sp; i++ { elements = append(elements, v.stack[i]) } v.sp -= numElements var arr Object = &Array{Value: elements} v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = arr v.sp++ case parser.OpMap: v.ip += 2 numElements := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 kv := make(map[string]Object, numElements) for i := v.sp - numElements; i < v.sp; i += 2 { key := v.stack[i] value := v.stack[i+1] kv[key.(*String).Value] = value } v.sp -= numElements var m Object = &Map{Value: kv} v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = m v.sp++ case parser.OpError: value := v.stack[v.sp-1] var e Object = &Error{ Value: value, } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp-1] = e case parser.OpImmutable: value := v.stack[v.sp-1] switch value := value.(type) { case *Array: var immutableArray Object = &ImmutableArray{ Value: value.Value, } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp-1] = immutableArray case *Map: var immutableMap Object = &ImmutableMap{ Value: value.Value, } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp-1] = immutableMap } case parser.OpIndex: index := v.stack[v.sp-1] left := v.stack[v.sp-2] v.sp -= 2 val, err := left.IndexGet(index) if err != nil { if err == ErrNotIndexable { v.err = fmt.Errorf("not indexable: %s", index.TypeName()) return } if err == ErrInvalidIndexType { v.err = fmt.Errorf("invalid index type: %s", index.TypeName()) return } v.err = err return } if val == nil { val = UndefinedValue } v.stack[v.sp] = val v.sp++ case parser.OpSliceIndex: high := v.stack[v.sp-1] low := v.stack[v.sp-2] left := v.stack[v.sp-3] v.sp -= 3 var lowIdx int64 if low != UndefinedValue { if lowInt, ok := low.(*Int); ok { lowIdx = lowInt.Value } else { v.err = fmt.Errorf("invalid slice index type: %s", low.TypeName()) return } } switch left := left.(type) { case *Array: numElements := int64(len(left.Value)) var highIdx int64 if high == UndefinedValue { highIdx = numElements } else if highInt, ok := high.(*Int); ok { highIdx = highInt.Value } else { v.err = fmt.Errorf("invalid slice index type: %s", high.TypeName()) return } if lowIdx > highIdx { v.err = fmt.Errorf("invalid slice index: %d > %d", lowIdx, highIdx) return } if lowIdx < 0 { lowIdx = 0 } else if lowIdx > numElements { lowIdx = numElements } if highIdx < 0 { highIdx = 0 } else if highIdx > numElements { highIdx = numElements } var val Object = &Array{ Value: left.Value[lowIdx:highIdx], } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = val v.sp++ case *ImmutableArray: numElements := int64(len(left.Value)) var highIdx int64 if high == UndefinedValue { highIdx = numElements } else if highInt, ok := high.(*Int); ok { highIdx = highInt.Value } else { v.err = fmt.Errorf("invalid slice index type: %s", high.TypeName()) return } if lowIdx > highIdx { v.err = fmt.Errorf("invalid slice index: %d > %d", lowIdx, highIdx) return } if lowIdx < 0 { lowIdx = 0 } else if lowIdx > numElements { lowIdx = numElements } if highIdx < 0 { highIdx = 0 } else if highIdx > numElements { highIdx = numElements } var val Object = &Array{ Value: left.Value[lowIdx:highIdx], } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = val v.sp++ case *String: numElements := int64(len(left.Value)) var highIdx int64 if high == UndefinedValue { highIdx = numElements } else if highInt, ok := high.(*Int); ok { highIdx = highInt.Value } else { v.err = fmt.Errorf("invalid slice index type: %s", high.TypeName()) return } if lowIdx > highIdx { v.err = fmt.Errorf("invalid slice index: %d > %d", lowIdx, highIdx) return } if lowIdx < 0 { lowIdx = 0 } else if lowIdx > numElements { lowIdx = numElements } if highIdx < 0 { highIdx = 0 } else if highIdx > numElements { highIdx = numElements } var val Object = &String{ Value: left.Value[lowIdx:highIdx], } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = val v.sp++ case *Bytes: numElements := int64(len(left.Value)) var highIdx int64 if high == UndefinedValue { highIdx = numElements } else if highInt, ok := high.(*Int); ok { highIdx = highInt.Value } else { v.err = fmt.Errorf("invalid slice index type: %s", high.TypeName()) return } if lowIdx > highIdx { v.err = fmt.Errorf("invalid slice index: %d > %d", lowIdx, highIdx) return } if lowIdx < 0 { lowIdx = 0 } else if lowIdx > numElements { lowIdx = numElements } if highIdx < 0 { highIdx = 0 } else if highIdx > numElements { highIdx = numElements } var val Object = &Bytes{ Value: left.Value[lowIdx:highIdx], } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = val v.sp++ default: v.err = fmt.Errorf("not indexable: %s", left.TypeName()) return } case parser.OpCall: numArgs := int(v.curInsts[v.ip+1]) spread := int(v.curInsts[v.ip+2]) v.ip += 2 value := v.stack[v.sp-1-numArgs] if !value.CanCall() { v.err = fmt.Errorf("not callable: %s", value.TypeName()) return } if spread == 1 { v.sp-- switch arr := v.stack[v.sp].(type) { case *Array: for _, item := range arr.Value { v.stack[v.sp] = item v.sp++ } numArgs += len(arr.Value) - 1 case *ImmutableArray: for _, item := range arr.Value { v.stack[v.sp] = item v.sp++ } numArgs += len(arr.Value) - 1 default: v.err = fmt.Errorf("not an array: %s", arr.TypeName()) return } } if callee, ok := value.(*CompiledFunction); ok { if callee.VarArgs { // if the closure is variadic, // 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] } v.stack[spStart] = &Array{Value: args} v.sp = spStart + 1 } } if numArgs != callee.NumParameters { if callee.VarArgs { v.err = fmt.Errorf( "wrong number of arguments: want>=%d, got=%d", callee.NumParameters-1, numArgs) } else { v.err = fmt.Errorf( "wrong number of arguments: want=%d, got=%d", callee.NumParameters, numArgs) } return } // test if it's tail-call if callee == v.curFrame.fn { // recursion nextOp := v.curInsts[v.ip+1] 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] } v.sp -= numArgs + 1 v.ip = -1 // reset IP to beginning of the frame continue } } if v.framesIndex >= MaxFrames { v.err = ErrStackOverflow return } // update call frame v.curFrame.ip = v.ip // store current ip before call v.curFrame = &(v.frames[v.framesIndex]) v.curFrame.fn = callee v.curFrame.freeVars = callee.Free v.curFrame.basePointer = v.sp - numArgs v.curInsts = callee.Instructions v.ip = -1 v.framesIndex++ v.sp = v.sp - numArgs + 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 // runtime error if e != nil { if e == ErrWrongNumArguments { v.err = fmt.Errorf( "wrong number of arguments 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': "+ "expected %s, found %s", e.Name, value.TypeName(), e.Expected, e.Found) return } v.err = e return } // nil return -> undefined if ret == nil { ret = UndefinedValue } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = ret v.sp++ } case parser.OpReturn: v.ip++ var retVal Object if int(v.curInsts[v.ip]) == 1 { retVal = v.stack[v.sp-1] } else { retVal = UndefinedValue } //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 = v.frames[v.framesIndex].basePointer // skip stack overflow check because (newSP) <= (oldSP) v.stack[v.sp-1] = retVal //v.sp++ case parser.OpDefineLocal: v.ip++ localIndex := int(v.curInsts[v.ip]) sp := v.curFrame.basePointer + localIndex // local variables can be mutated by other actions // so always store the copy of popped value val := v.stack[v.sp-1] v.sp-- v.stack[sp] = val case parser.OpSetLocal: localIndex := int(v.curInsts[v.ip+1]) v.ip++ sp := v.curFrame.basePointer + localIndex // update pointee of v.stack[sp] instead of replacing the pointer // itself. this is needed because there can be free variables // referencing the same local variables. val := v.stack[v.sp-1] v.sp-- if obj, ok := v.stack[sp].(*ObjectPtr); ok { *obj.Value = val val = obj } v.stack[sp] = val // also use a copy of popped value case parser.OpSetSelLocal: localIndex := int(v.curInsts[v.ip+1]) numSelectors := int(v.curInsts[v.ip+2]) v.ip += 2 // selectors and RHS value selectors := make([]Object, numSelectors) for i := 0; i < numSelectors; i++ { selectors[i] = v.stack[v.sp-numSelectors+i] } val := v.stack[v.sp-numSelectors-1] v.sp -= numSelectors + 1 dst := v.stack[v.curFrame.basePointer+localIndex] if obj, ok := dst.(*ObjectPtr); ok { dst = *obj.Value } if e := indexAssign(dst, val, selectors); e != nil { v.err = e return } case parser.OpGetLocal: v.ip++ localIndex := int(v.curInsts[v.ip]) val := v.stack[v.curFrame.basePointer+localIndex] if obj, ok := val.(*ObjectPtr); ok { val = *obj.Value } v.stack[v.sp] = val v.sp++ case parser.OpGetBuiltin: v.ip++ builtinIndex := int(v.curInsts[v.ip]) v.stack[v.sp] = builtinFuncs[builtinIndex] v.sp++ case parser.OpClosure: v.ip += 3 constIndex := int(v.curInsts[v.ip-1]) | int(v.curInsts[v.ip-2])<<8 numFree := int(v.curInsts[v.ip]) fn, ok := v.constants[constIndex].(*CompiledFunction) if !ok { v.err = fmt.Errorf("not function: %s", fn.TypeName()) return } free := make([]*ObjectPtr, numFree) for i := 0; i < numFree; i++ { switch freeVar := (v.stack[v.sp-numFree+i]).(type) { case *ObjectPtr: free[i] = freeVar default: free[i] = &ObjectPtr{ Value: &v.stack[v.sp-numFree+i], } } } v.sp -= numFree cl := &CompiledFunction{ Instructions: fn.Instructions, NumLocals: fn.NumLocals, NumParameters: fn.NumParameters, VarArgs: fn.VarArgs, SourceMap: fn.SourceMap, Free: free, } v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = cl v.sp++ case parser.OpGetFreePtr: v.ip++ freeIndex := int(v.curInsts[v.ip]) val := v.curFrame.freeVars[freeIndex] v.stack[v.sp] = val v.sp++ case parser.OpGetFree: v.ip++ freeIndex := int(v.curInsts[v.ip]) val := *v.curFrame.freeVars[freeIndex].Value v.stack[v.sp] = val v.sp++ case parser.OpSetFree: v.ip++ freeIndex := int(v.curInsts[v.ip]) *v.curFrame.freeVars[freeIndex].Value = v.stack[v.sp-1] v.sp-- case parser.OpGetLocalPtr: v.ip++ localIndex := int(v.curInsts[v.ip]) sp := v.curFrame.basePointer + localIndex val := v.stack[sp] var freeVar *ObjectPtr if obj, ok := val.(*ObjectPtr); ok { freeVar = obj } else { freeVar = &ObjectPtr{Value: &val} v.stack[sp] = freeVar } v.stack[v.sp] = freeVar v.sp++ case parser.OpSetSelFree: v.ip += 2 freeIndex := int(v.curInsts[v.ip-1]) numSelectors := int(v.curInsts[v.ip]) // selectors and RHS value selectors := make([]Object, numSelectors) for i := 0; i < numSelectors; i++ { selectors[i] = v.stack[v.sp-numSelectors+i] } val := v.stack[v.sp-numSelectors-1] v.sp -= numSelectors + 1 e := indexAssign(*v.curFrame.freeVars[freeIndex].Value, val, selectors) if e != nil { v.err = e return } case parser.OpIteratorInit: var iterator Object dst := v.stack[v.sp-1] v.sp-- if !dst.CanIterate() { v.err = fmt.Errorf("not iterable: %s", dst.TypeName()) return } iterator = dst.Iterate() v.allocs-- if v.allocs == 0 { v.err = ErrObjectAllocLimit return } v.stack[v.sp] = iterator v.sp++ case parser.OpIteratorNext: iterator := v.stack[v.sp-1] v.sp-- hasMore := iterator.(Iterator).Next() if hasMore { v.stack[v.sp] = TrueValue } else { v.stack[v.sp] = FalseValue } v.sp++ case parser.OpIteratorKey: iterator := v.stack[v.sp-1] v.sp-- val := iterator.(Iterator).Key() v.stack[v.sp] = val v.sp++ case parser.OpIteratorValue: iterator := v.stack[v.sp-1] v.sp-- val := iterator.(Iterator).Value() v.stack[v.sp] = val v.sp++ case parser.OpSuspend: return default: v.err = fmt.Errorf("unknown opcode: %d", v.curInsts[v.ip]) return } } } // IsStackEmpty tests if the stack is empty or not. func (v *VM) IsStackEmpty() bool { return v.sp == 0 } func indexAssign(dst, src Object, selectors []Object) error { numSel := len(selectors) for sidx := numSel - 1; sidx > 0; sidx-- { next, err := dst.IndexGet(selectors[sidx]) if err != nil { if err == ErrNotIndexable { return fmt.Errorf("not indexable: %s", dst.TypeName()) } if err == ErrInvalidIndexType { return fmt.Errorf("invalid index type: %s", selectors[sidx].TypeName()) } return err } dst = next } if err := dst.IndexSet(selectors[0], src); err != nil { if err == ErrNotIndexAssignable { return fmt.Errorf("not index-assignable: %s", dst.TypeName()) } if err == ErrInvalidIndexValueType { return fmt.Errorf("invaid index value type: %s", src.TypeName()) } return err } return nil } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1713111873.888041 golang-github-d5-tengo/vm_test.go0000644000175000017500000033074314607001502017164 0ustar00maythammaythampackage tengo_test import ( "errors" "fmt" "math" "math/rand" "reflect" _runtime "runtime" "strings" "testing" "github.com/d5/tengo/v2" "github.com/d5/tengo/v2/parser" "github.com/d5/tengo/v2/require" "github.com/d5/tengo/v2/stdlib" "github.com/d5/tengo/v2/token" ) const testOut = "out" type IARR []interface{} type IMAP map[string]interface{} type MAP = map[string]interface{} type ARR = []interface{} type testopts struct { modules *tengo.ModuleMap symbols map[string]tengo.Object maxAllocs int64 skip2ndPass bool } func Opts() *testopts { return &testopts{ modules: tengo.NewModuleMap(), symbols: make(map[string]tengo.Object), maxAllocs: -1, skip2ndPass: false, } } func (o *testopts) copy() *testopts { c := &testopts{ modules: o.modules.Copy(), symbols: make(map[string]tengo.Object), maxAllocs: o.maxAllocs, skip2ndPass: o.skip2ndPass, } for k, v := range o.symbols { c.symbols[k] = v } return c } func (o *testopts) Stdlib() *testopts { o.modules.AddMap(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) return o } func (o *testopts) Module(name string, mod interface{}) *testopts { c := o.copy() switch mod := mod.(type) { case tengo.Importable: c.modules.Add(name, mod) case string: c.modules.AddSourceModule(name, []byte(mod)) case []byte: c.modules.AddSourceModule(name, mod) default: panic(fmt.Errorf("invalid module type: %T", mod)) } return c } func (o *testopts) Symbol(name string, value tengo.Object) *testopts { c := o.copy() c.symbols[name] = value return c } func (o *testopts) MaxAllocs(limit int64) *testopts { c := o.copy() c.maxAllocs = limit return c } func (o *testopts) Skip2ndPass() *testopts { c := o.copy() c.skip2ndPass = true return c } type customError struct { err error str string } func (c *customError) Error() string { return c.str } func (c *customError) Unwrap() error { return c.err } func TestArray(t *testing.T) { expectRun(t, `out = [1, 2 * 2, 3 + 3]`, nil, ARR{1, 4, 6}) // array copy-by-reference expectRun(t, `a1 := [1, 2, 3]; a2 := a1; a1[0] = 5; out = a2`, nil, ARR{5, 2, 3}) expectRun(t, `func () { a1 := [1, 2, 3]; a2 := a1; a1[0] = 5; out = a2 }()`, nil, ARR{5, 2, 3}) // array index set expectError(t, `a1 := [1, 2, 3]; a1[3] = 5`, nil, "index out of bounds") // index operator arr := ARR{1, 2, 3, 4, 5, 6} arrStr := `[1, 2, 3, 4, 5, 6]` arrLen := 6 for idx := 0; idx < arrLen; idx++ { expectRun(t, fmt.Sprintf("out = %s[%d]", arrStr, idx), nil, arr[idx]) expectRun(t, fmt.Sprintf("out = %s[0 + %d]", arrStr, idx), nil, arr[idx]) expectRun(t, fmt.Sprintf("out = %s[1 + %d - 1]", arrStr, idx), nil, arr[idx]) expectRun(t, fmt.Sprintf("idx := %d; out = %s[idx]", idx, arrStr), nil, arr[idx]) } expectRun(t, fmt.Sprintf("%s[%d]", arrStr, -1), nil, tengo.UndefinedValue) expectRun(t, fmt.Sprintf("%s[%d]", arrStr, arrLen), nil, tengo.UndefinedValue) // slice operator for low := 0; low < arrLen; low++ { expectRun(t, fmt.Sprintf("out = %s[%d:%d]", arrStr, low, low), nil, ARR{}) for high := low; high <= arrLen; high++ { expectRun(t, fmt.Sprintf("out = %s[%d:%d]", arrStr, low, high), nil, arr[low:high]) expectRun(t, fmt.Sprintf("out = %s[0 + %d : 0 + %d]", arrStr, low, high), nil, arr[low:high]) expectRun(t, fmt.Sprintf("out = %s[1 + %d - 1 : 1 + %d - 1]", arrStr, low, high), nil, arr[low:high]) expectRun(t, fmt.Sprintf("out = %s[:%d]", arrStr, high), nil, arr[:high]) expectRun(t, fmt.Sprintf("out = %s[%d:]", arrStr, low), nil, arr[low:]) } } expectRun(t, fmt.Sprintf("out = %s[:]", arrStr), nil, arr) expectRun(t, fmt.Sprintf("out = %s[%d:]", arrStr, -1), nil, arr) expectRun(t, fmt.Sprintf("out = %s[:%d]", arrStr, arrLen+1), nil, arr) expectRun(t, fmt.Sprintf("out = %s[%d:%d]", arrStr, 2, 2), nil, ARR{}) expectError(t, fmt.Sprintf("%s[:%d]", arrStr, -1), nil, "invalid slice index") expectError(t, fmt.Sprintf("%s[%d:]", arrStr, arrLen+1), nil, "invalid slice index") expectError(t, fmt.Sprintf("%s[%d:%d]", arrStr, 0, -1), nil, "invalid slice index") expectError(t, fmt.Sprintf("%s[%d:%d]", arrStr, 2, 1), nil, "invalid slice index") } func TestAssignment(t *testing.T) { expectRun(t, `a := 1; a = 2; out = a`, nil, 2) expectRun(t, `a := 1; a = 2; out = a`, nil, 2) expectRun(t, `a := 1; a = a + 4; out = a`, nil, 5) expectRun(t, `a := 1; f1 := func() { a = 2; return a }; out = f1()`, nil, 2) expectRun(t, `a := 1; f1 := func() { a := 3; a = 2; return a }; out = f1()`, nil, 2) expectRun(t, `a := 1; out = a`, nil, 1) expectRun(t, `a := 1; a = 2; out = a`, nil, 2) expectRun(t, `a := 1; func() { a = 2 }(); out = a`, nil, 2) expectRun(t, `a := 1; func() { a := 2 }(); out = a`, nil, 1) // "a := 2" defines a new local variable 'a' expectRun(t, `a := 1; func() { b := 2; out = b }()`, nil, 2) expectRun(t, ` out = func() { a := 2 func() { a = 3 // captured from outer scope }() return a }() `, nil, 3) expectRun(t, ` func() { a := 5 out = func() { a := 4 return a }() }()`, nil, 4) expectError(t, `a := 1; a := 2`, nil, "redeclared") // redeclared in the same scope expectError(t, `func() { a := 1; a := 2 }()`, nil, "redeclared") // redeclared in the same scope expectRun(t, `a := 1; a += 2; out = a`, nil, 3) expectRun(t, `a := 1; a += 4 - 2;; out = a`, nil, 3) expectRun(t, `a := 3; a -= 1;; out = a`, nil, 2) expectRun(t, `a := 3; a -= 5 - 4;; out = a`, nil, 2) expectRun(t, `a := 2; a *= 4;; out = a`, nil, 8) expectRun(t, `a := 2; a *= 1 + 3;; out = a`, nil, 8) expectRun(t, `a := 10; a /= 2;; out = a`, nil, 5) expectRun(t, `a := 10; a /= 5 - 3;; out = a`, nil, 5) // compound assignment operator does not define new variable expectError(t, `a += 4`, nil, "unresolved reference") expectError(t, `a -= 4`, nil, "unresolved reference") expectError(t, `a *= 4`, nil, "unresolved reference") expectError(t, `a /= 4`, nil, "unresolved reference") expectRun(t, ` f1 := func() { f2 := func() { a := 1 a += 2 // it's a statement, not an expression return a }; return f2(); }; out = f1();`, nil, 3) expectRun(t, `f1 := func() { f2 := func() { a := 1; a += 4 - 2; return a }; return f2(); }; out = f1()`, nil, 3) expectRun(t, `f1 := func() { f2 := func() { a := 3; a -= 1; return a }; return f2(); }; out = f1()`, nil, 2) expectRun(t, `f1 := func() { f2 := func() { a := 3; a -= 5 - 4; return a }; return f2(); }; out = f1()`, nil, 2) expectRun(t, `f1 := func() { f2 := func() { a := 2; a *= 4; return a }; return f2(); }; out = f1()`, nil, 8) expectRun(t, `f1 := func() { f2 := func() { a := 2; a *= 1 + 3; return a }; return f2(); }; out = f1()`, nil, 8) expectRun(t, `f1 := func() { f2 := func() { a := 10; a /= 2; return a }; return f2(); }; out = f1()`, nil, 5) expectRun(t, `f1 := func() { f2 := func() { a := 10; a /= 5 - 3; return a }; return f2(); }; out = f1()`, nil, 5) expectRun(t, `a := 1; f1 := func() { f2 := func() { a += 2; return a }; return f2(); }; out = f1()`, nil, 3) expectRun(t, ` f1 := func(a) { return func(b) { c := a c += b * 2 return c } } out = f1(3)(4) `, nil, 11) expectRun(t, ` out = func() { a := 1 func() { a = 2 func() { a = 3 func() { a := 4 // declared new }() }() }() return a }() `, nil, 3) // write on free variables expectRun(t, ` f1 := func() { a := 5 return func() { a += 3 return a }() } out = f1() `, nil, 8) expectRun(t, ` out = func() { f1 := func() { a := 5 add1 := func() { a += 1 } add2 := func() { a += 2 } a += 3 return func() { a += 4; add1(); add2(); a += 5; return a } } return f1() }()() `, nil, 20) expectRun(t, ` it := func(seq, fn) { fn(seq[0]) fn(seq[1]) fn(seq[2]) } foo := func(a) { b := 0 it([1, 2, 3], func(x) { b = x + a }) return b } out = foo(2) `, nil, 5) expectRun(t, ` it := func(seq, fn) { fn(seq[0]) fn(seq[1]) fn(seq[2]) } foo := func(a) { b := 0 it([1, 2, 3], func(x) { b += x + a }) return b } out = foo(2) `, nil, 12) expectRun(t, ` out = func() { a := 1 func() { a = 2 }() return a }() `, nil, 2) expectRun(t, ` f := func() { a := 1 return { b: func() { a += 3 }, c: func() { a += 2 }, d: func() { return a } } } m := f() m.b() m.c() out = m.d() `, nil, 6) expectRun(t, ` each := func(s, x) { for i:=0; i> 2`, nil, 4) expectRun(t, `out = 1; out &= 1`, nil, 1) expectRun(t, `out = 1; out |= 0`, nil, 1) expectRun(t, `out = 1; out ^= 0`, nil, 1) expectRun(t, `out = 1; out &^= 0`, nil, 1) expectRun(t, `out = 1; out <<= 2`, nil, 4) expectRun(t, `out = 16; out >>= 2`, nil, 4) expectRun(t, `out = ^0`, nil, ^0) expectRun(t, `out = ^1`, nil, ^1) expectRun(t, `out = ^55`, nil, ^55) expectRun(t, `out = ^-55`, nil, ^-55) } func TestBoolean(t *testing.T) { expectRun(t, `out = true`, nil, true) expectRun(t, `out = false`, nil, false) expectRun(t, `out = 1 < 2`, nil, true) expectRun(t, `out = 1 > 2`, nil, false) expectRun(t, `out = 1 < 1`, nil, false) expectRun(t, `out = 1 > 2`, nil, false) expectRun(t, `out = 1 == 1`, nil, true) expectRun(t, `out = 1 != 1`, nil, false) expectRun(t, `out = 1 == 2`, nil, false) expectRun(t, `out = 1 != 2`, nil, true) expectRun(t, `out = 1 <= 2`, nil, true) expectRun(t, `out = 1 >= 2`, nil, false) expectRun(t, `out = 1 <= 1`, nil, true) expectRun(t, `out = 1 >= 2`, nil, false) expectRun(t, `out = true == true`, nil, true) expectRun(t, `out = false == false`, nil, true) expectRun(t, `out = true == false`, nil, false) expectRun(t, `out = true != false`, nil, true) expectRun(t, `out = false != true`, nil, true) expectRun(t, `out = (1 < 2) == true`, nil, true) expectRun(t, `out = (1 < 2) == false`, nil, false) expectRun(t, `out = (1 > 2) == true`, nil, false) expectRun(t, `out = (1 > 2) == false`, nil, true) expectError(t, `5 + true`, nil, "invalid operation") expectError(t, `5 + true; 5`, nil, "invalid operation") expectError(t, `-true`, nil, "invalid operation") expectError(t, `true + false`, nil, "invalid operation") expectError(t, `5; true + false; 5`, nil, "invalid operation") expectError(t, `if (10 > 1) { true + false; }`, nil, "invalid operation") expectError(t, ` func() { if (10 > 1) { if (10 > 1) { return true + false; } return 1; } }() `, nil, "invalid operation") expectError(t, `if (true + false) { 10 }`, nil, "invalid operation") expectError(t, `10 + (true + false)`, nil, "invalid operation") expectError(t, `(true + false) + 20`, nil, "invalid operation") expectError(t, `!(true + false)`, nil, "invalid operation") } func TestUndefined(t *testing.T) { expectRun(t, `out = undefined`, nil, tengo.UndefinedValue) expectRun(t, `out = undefined.a`, nil, tengo.UndefinedValue) expectRun(t, `out = undefined[1]`, nil, tengo.UndefinedValue) expectRun(t, `out = undefined.a.b`, nil, tengo.UndefinedValue) expectRun(t, `out = undefined[1][2]`, nil, tengo.UndefinedValue) expectRun(t, `out = undefined ? 1 : 2`, nil, 2) expectRun(t, `out = undefined == undefined`, nil, true) expectRun(t, `out = undefined == 1`, nil, false) expectRun(t, `out = 1 == undefined`, nil, false) expectRun(t, `out = undefined == float([])`, nil, true) expectRun(t, `out = float([]) == undefined`, nil, true) } func TestBuiltinFunction(t *testing.T) { expectRun(t, `out = len("")`, nil, 0) expectRun(t, `out = len("four")`, nil, 4) expectRun(t, `out = len("hello world")`, nil, 11) expectRun(t, `out = len([])`, nil, 0) expectRun(t, `out = len([1, 2, 3])`, nil, 3) expectRun(t, `out = len({})`, nil, 0) expectRun(t, `out = len({a:1, b:2})`, nil, 2) expectRun(t, `out = len(immutable([]))`, nil, 0) expectRun(t, `out = len(immutable([1, 2, 3]))`, nil, 3) expectRun(t, `out = len(immutable({}))`, nil, 0) expectRun(t, `out = len(immutable({a:1, b:2}))`, nil, 2) expectError(t, `len(1)`, nil, "invalid type for argument") expectError(t, `len("one", "two")`, nil, "wrong number of arguments") expectRun(t, `out = copy(1)`, nil, 1) expectError(t, `copy(1, 2)`, nil, "wrong number of arguments") expectRun(t, `out = append([1, 2, 3], 4)`, nil, ARR{1, 2, 3, 4}) expectRun(t, `out = append([1, 2, 3], 4, 5, 6)`, nil, ARR{1, 2, 3, 4, 5, 6}) expectRun(t, `out = append([1, 2, 3], "foo", false)`, nil, ARR{1, 2, 3, "foo", false}) expectRun(t, `out = int(1)`, nil, 1) expectRun(t, `out = int(1.8)`, nil, 1) expectRun(t, `out = int("-522")`, nil, -522) expectRun(t, `out = int(true)`, nil, 1) expectRun(t, `out = int(false)`, nil, 0) expectRun(t, `out = int('8')`, nil, 56) expectRun(t, `out = int([1])`, nil, tengo.UndefinedValue) expectRun(t, `out = int({a: 1})`, nil, tengo.UndefinedValue) expectRun(t, `out = int(undefined)`, nil, tengo.UndefinedValue) expectRun(t, `out = int("-522", 1)`, nil, -522) expectRun(t, `out = int(undefined, 1)`, nil, 1) expectRun(t, `out = int(undefined, 1.8)`, nil, 1.8) expectRun(t, `out = int(undefined, string(1))`, nil, "1") expectRun(t, `out = int(undefined, undefined)`, nil, tengo.UndefinedValue) expectRun(t, `out = string(1)`, nil, "1") expectRun(t, `out = string(1.8)`, nil, "1.8") expectRun(t, `out = string("-522")`, nil, "-522") expectRun(t, `out = string(true)`, nil, "true") expectRun(t, `out = string(false)`, nil, "false") expectRun(t, `out = string('8')`, nil, "8") expectRun(t, `out = string([1,8.1,true,3])`, nil, "[1, 8.1, true, 3]") expectRun(t, `out = string({b: "foo"})`, nil, `{b: "foo"}`) expectRun(t, `out = string(undefined)`, nil, tengo.UndefinedValue) // not "undefined" expectRun(t, `out = string(1, "-522")`, nil, "1") expectRun(t, `out = string(undefined, "-522")`, nil, "-522") // not "undefined" expectRun(t, `out = float(1)`, nil, 1.0) expectRun(t, `out = float(1.8)`, nil, 1.8) expectRun(t, `out = float("-52.2")`, nil, -52.2) expectRun(t, `out = float(true)`, nil, tengo.UndefinedValue) expectRun(t, `out = float(false)`, nil, tengo.UndefinedValue) expectRun(t, `out = float('8')`, nil, tengo.UndefinedValue) expectRun(t, `out = float([1,8.1,true,3])`, nil, tengo.UndefinedValue) expectRun(t, `out = float({a: 1, b: "foo"})`, nil, tengo.UndefinedValue) expectRun(t, `out = float(undefined)`, nil, tengo.UndefinedValue) expectRun(t, `out = float("-52.2", 1.8)`, nil, -52.2) expectRun(t, `out = float(undefined, 1)`, nil, 1) expectRun(t, `out = float(undefined, 1.8)`, nil, 1.8) expectRun(t, `out = float(undefined, "-52.2")`, nil, "-52.2") expectRun(t, `out = float(undefined, char(56))`, nil, '8') expectRun(t, `out = float(undefined, undefined)`, nil, tengo.UndefinedValue) expectRun(t, `out = char(56)`, nil, '8') expectRun(t, `out = char(1.8)`, nil, tengo.UndefinedValue) expectRun(t, `out = char("-52.2")`, nil, tengo.UndefinedValue) expectRun(t, `out = char(true)`, nil, tengo.UndefinedValue) expectRun(t, `out = char(false)`, nil, tengo.UndefinedValue) expectRun(t, `out = char('8')`, nil, '8') expectRun(t, `out = char([1,8.1,true,3])`, nil, tengo.UndefinedValue) expectRun(t, `out = char({a: 1, b: "foo"})`, nil, tengo.UndefinedValue) expectRun(t, `out = char(undefined)`, nil, tengo.UndefinedValue) expectRun(t, `out = char(56, 'a')`, nil, '8') expectRun(t, `out = char(undefined, '8')`, nil, '8') expectRun(t, `out = char(undefined, 56)`, nil, 56) expectRun(t, `out = char(undefined, "-52.2")`, nil, "-52.2") expectRun(t, `out = char(undefined, undefined)`, nil, tengo.UndefinedValue) expectRun(t, `out = bool(1)`, nil, true) // non-zero integer: true expectRun(t, `out = bool(0)`, nil, false) // zero: true expectRun(t, `out = bool(1.8)`, nil, true) // all floats (except for NaN): true expectRun(t, `out = bool(0.0)`, nil, true) // all floats (except for NaN): true expectRun(t, `out = bool("false")`, nil, true) // non-empty string: true expectRun(t, `out = bool("")`, nil, false) // empty string: false expectRun(t, `out = bool(true)`, nil, true) // true: true expectRun(t, `out = bool(false)`, nil, false) // false: false expectRun(t, `out = bool('8')`, nil, true) // non-zero chars: true expectRun(t, `out = bool(char(0))`, nil, false) // zero char: false expectRun(t, `out = bool([1])`, nil, true) // non-empty arrays: true expectRun(t, `out = bool([])`, nil, false) // empty array: false expectRun(t, `out = bool({a: 1})`, nil, true) // non-empty maps: true expectRun(t, `out = bool({})`, nil, false) // empty maps: false expectRun(t, `out = bool(undefined)`, nil, false) // undefined: false expectRun(t, `out = bytes(1)`, nil, []byte{0}) expectRun(t, `out = bytes(1.8)`, nil, tengo.UndefinedValue) expectRun(t, `out = bytes("-522")`, nil, []byte{'-', '5', '2', '2'}) expectRun(t, `out = bytes(true)`, nil, tengo.UndefinedValue) expectRun(t, `out = bytes(false)`, nil, tengo.UndefinedValue) expectRun(t, `out = bytes('8')`, nil, tengo.UndefinedValue) expectRun(t, `out = bytes([1])`, nil, tengo.UndefinedValue) expectRun(t, `out = bytes({a: 1})`, nil, tengo.UndefinedValue) expectRun(t, `out = bytes(undefined)`, nil, tengo.UndefinedValue) expectRun(t, `out = bytes("-522", ['8'])`, nil, []byte{'-', '5', '2', '2'}) expectRun(t, `out = bytes(undefined, "-522")`, nil, "-522") expectRun(t, `out = bytes(undefined, 1)`, nil, 1) expectRun(t, `out = bytes(undefined, 1.8)`, nil, 1.8) expectRun(t, `out = bytes(undefined, int("-522"))`, nil, -522) expectRun(t, `out = bytes(undefined, undefined)`, nil, tengo.UndefinedValue) expectRun(t, `out = is_error(error(1))`, nil, true) expectRun(t, `out = is_error(1)`, nil, false) expectRun(t, `out = is_undefined(undefined)`, nil, true) expectRun(t, `out = is_undefined(error(1))`, nil, false) // type_name expectRun(t, `out = type_name(1)`, nil, "int") expectRun(t, `out = type_name(1.1)`, nil, "float") expectRun(t, `out = type_name("a")`, nil, "string") expectRun(t, `out = type_name([1,2,3])`, nil, "array") expectRun(t, `out = type_name({k:1})`, nil, "map") expectRun(t, `out = type_name('a')`, nil, "char") expectRun(t, `out = type_name(true)`, nil, "bool") expectRun(t, `out = type_name(false)`, nil, "bool") expectRun(t, `out = type_name(bytes( 1))`, nil, "bytes") expectRun(t, `out = type_name(undefined)`, nil, "undefined") expectRun(t, `out = type_name(error("err"))`, nil, "error") expectRun(t, `out = type_name(func() {})`, nil, "compiled-function") expectRun(t, `a := func(x) { return func() { return x } }; out = type_name(a(5))`, nil, "compiled-function") // closure // is_function expectRun(t, `out = is_function(1)`, nil, false) expectRun(t, `out = is_function(func() {})`, nil, true) expectRun(t, `out = is_function(func(x) { return x })`, nil, true) expectRun(t, `out = is_function(len)`, nil, false) // builtin function expectRun(t, `a := func(x) { return func() { return x } }; out = is_function(a)`, nil, true) // function expectRun(t, `a := func(x) { return func() { return x } }; out = is_function(a(5))`, nil, true) // closure expectRun(t, `out = is_function(x)`, Opts().Symbol("x", &StringArray{ Value: []string{"foo", "bar"}, }).Skip2ndPass(), false) // user object // is_callable expectRun(t, `out = is_callable(1)`, nil, false) expectRun(t, `out = is_callable(func() {})`, nil, true) expectRun(t, `out = is_callable(func(x) { return x })`, nil, true) expectRun(t, `out = is_callable(len)`, nil, true) // builtin function expectRun(t, `a := func(x) { return func() { return x } }; out = is_callable(a)`, nil, true) // function expectRun(t, `a := func(x) { return func() { return x } }; out = is_callable(a(5))`, nil, true) // closure expectRun(t, `out = is_callable(x)`, Opts().Symbol("x", &StringArray{ Value: []string{"foo", "bar"}, }).Skip2ndPass(), true) // user object expectRun(t, `out = format("")`, nil, "") expectRun(t, `out = format("foo")`, nil, "foo") expectRun(t, `out = format("foo %d %v %s", 1, 2, "bar")`, nil, "foo 1 2 bar") expectRun(t, `out = format("foo %v", [1, "bar", true])`, nil, `foo [1, "bar", true]`) expectRun(t, `out = format("foo %v %d", [1, "bar", true], 19)`, nil, `foo [1, "bar", true] 19`) expectRun(t, `out = format("foo %v", {"a": {"b": {"c": [1, 2, 3]}}})`, nil, `foo {a: {b: {c: [1, 2, 3]}}}`) expectRun(t, `out = format("%v", [1, [2, [3, 4]]])`, nil, `[1, [2, [3, 4]]]`) tengo.MaxStringLen = 9 expectError(t, `format("%s", "1234567890")`, nil, "exceeding string size limit") tengo.MaxStringLen = 2147483647 // delete expectError(t, `delete()`, nil, tengo.ErrWrongNumArguments.Error()) expectError(t, `delete(1)`, nil, tengo.ErrWrongNumArguments.Error()) expectError(t, `delete(1, 2, 3)`, nil, tengo.ErrWrongNumArguments.Error()) expectError(t, `delete({}, "", 3)`, nil, tengo.ErrWrongNumArguments.Error()) expectError(t, `delete(1, 1)`, nil, `invalid type for argument 'first'`) expectError(t, `delete(1.0, 1)`, nil, `invalid type for argument 'first'`) expectError(t, `delete("str", 1)`, nil, `invalid type for argument 'first'`) expectError(t, `delete(bytes("str"), 1)`, nil, `invalid type for argument 'first'`) expectError(t, `delete(error("err"), 1)`, nil, `invalid type for argument 'first'`) expectError(t, `delete(true, 1)`, nil, `invalid type for argument 'first'`) expectError(t, `delete(char('c'), 1)`, nil, `invalid type for argument 'first'`) expectError(t, `delete(undefined, 1)`, nil, `invalid type for argument 'first'`) expectError(t, `delete(time(1257894000), 1)`, nil, `invalid type for argument 'first'`) expectError(t, `delete(immutable({}), "key")`, nil, `invalid type for argument 'first'`) expectError(t, `delete(immutable([]), "")`, nil, `invalid type for argument 'first'`) expectError(t, `delete([], "")`, nil, `invalid type for argument 'first'`) expectError(t, `delete({}, 1)`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, 1.0)`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, undefined)`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, [])`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, {})`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, error("err"))`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, bytes("str"))`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, char(35))`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, time(1257894000))`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, immutable({}))`, nil, `invalid type for argument 'second'`) expectError(t, `delete({}, immutable([]))`, nil, `invalid type for argument 'second'`) expectRun(t, `out = delete({}, "")`, nil, tengo.UndefinedValue) expectRun(t, `out = {key1: 1}; delete(out, "key1")`, nil, MAP{}) expectRun(t, `out = {key1: 1, key2: "2"}; delete(out, "key1")`, nil, MAP{"key2": "2"}) expectRun(t, `out = [1, "2", {a: "b", c: 10}]; delete(out[2], "c")`, nil, ARR{1, "2", MAP{"a": "b"}}) // splice expectError(t, `splice()`, nil, tengo.ErrWrongNumArguments.Error()) expectError(t, `splice(1)`, nil, `invalid type for argument 'first'`) expectError(t, `splice(1.0)`, nil, `invalid type for argument 'first'`) expectError(t, `splice("str")`, nil, `invalid type for argument 'first'`) expectError(t, `splice(bytes("str"))`, nil, `invalid type for argument 'first'`) expectError(t, `splice(error("err"))`, nil, `invalid type for argument 'first'`) expectError(t, `splice(true)`, nil, `invalid type for argument 'first'`) expectError(t, `splice(char('c'))`, nil, `invalid type for argument 'first'`) expectError(t, `splice(undefined)`, nil, `invalid type for argument 'first'`) expectError(t, `splice(time(1257894000))`, nil, `invalid type for argument 'first'`) expectError(t, `splice(immutable({}))`, nil, `invalid type for argument 'first'`) expectError(t, `splice(immutable([]))`, nil, `invalid type for argument 'first'`) expectError(t, `splice({})`, nil, `invalid type for argument 'first'`) expectError(t, `splice([], 1.0)`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], "str")`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], bytes("str"))`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], error("error"))`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], false)`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], char('d'))`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], undefined)`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], time(0))`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], [])`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], {})`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], immutable([]))`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], immutable({}))`, nil, `invalid type for argument 'second'`) expectError(t, `splice([], 0, 1.0)`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, "string")`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, bytes("string"))`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, error("string"))`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, true)`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, char('f'))`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, undefined)`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, time(0))`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, [])`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, {})`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, immutable([]))`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 0, immutable({}))`, nil, `invalid type for argument 'third'`) expectError(t, `splice([], 1)`, nil, tengo.ErrIndexOutOfBounds.Error()) expectError(t, `splice([1, 2, 3], 0, -1)`, nil, tengo.ErrIndexOutOfBounds.Error()) expectError(t, `splice([1, 2, 3], 99, 0, "a", "b")`, nil, tengo.ErrIndexOutOfBounds.Error()) expectRun(t, `out = []; splice(out)`, nil, ARR{}) expectRun(t, `out = ["a"]; splice(out, 1)`, nil, ARR{"a"}) expectRun(t, `out = ["a"]; out = splice(out, 1)`, nil, ARR{}) expectRun(t, `out = [1, 2, 3]; splice(out, 0, 1)`, nil, ARR{2, 3}) expectRun(t, `out = [1, 2, 3]; out = splice(out, 0, 1)`, nil, ARR{1}) expectRun(t, `out = [1, 2, 3]; splice(out, 0, 0, "a", "b")`, nil, ARR{"a", "b", 1, 2, 3}) expectRun(t, `out = [1, 2, 3]; out = splice(out, 0, 0, "a", "b")`, nil, ARR{}) expectRun(t, `out = [1, 2, 3]; splice(out, 1, 0, "a", "b")`, nil, ARR{1, "a", "b", 2, 3}) expectRun(t, `out = [1, 2, 3]; out = splice(out, 1, 0, "a", "b")`, nil, ARR{}) expectRun(t, `out = [1, 2, 3]; splice(out, 1, 0, "a", "b")`, nil, ARR{1, "a", "b", 2, 3}) expectRun(t, `out = [1, 2, 3]; splice(out, 2, 0, "a", "b")`, nil, ARR{1, 2, "a", "b", 3}) expectRun(t, `out = [1, 2, 3]; splice(out, 3, 0, "a", "b")`, nil, ARR{1, 2, 3, "a", "b"}) expectRun(t, `array := [1, 2, 3]; deleted := splice(array, 1, 1, "a", "b"); out = [deleted, array]`, nil, ARR{ARR{2}, ARR{1, "a", "b", 3}}) expectRun(t, `array := [1, 2, 3]; deleted := splice(array, 1); out = [deleted, array]`, nil, ARR{ARR{2, 3}, ARR{1}}) expectRun(t, `out = []; splice(out, 0, 0, "a", "b")`, nil, ARR{"a", "b"}) expectRun(t, `out = []; splice(out, 0, 1, "a", "b")`, nil, ARR{"a", "b"}) expectRun(t, `out = []; out = splice(out, 0, 0, "a", "b")`, nil, ARR{}) expectRun(t, `out = splice(splice([1, 2, 3], 0, 3), 1, 3)`, nil, ARR{2, 3}) // splice doc examples expectRun(t, `v := [1, 2, 3]; deleted := splice(v, 0); out = [deleted, v]`, nil, ARR{ARR{1, 2, 3}, ARR{}}) expectRun(t, `v := [1, 2, 3]; deleted := splice(v, 1); out = [deleted, v]`, nil, ARR{ARR{2, 3}, ARR{1}}) expectRun(t, `v := [1, 2, 3]; deleted := splice(v, 0, 1); out = [deleted, v]`, nil, ARR{ARR{1}, ARR{2, 3}}) expectRun(t, `v := ["a", "b", "c"]; deleted := splice(v, 1, 2); out = [deleted, v]`, nil, ARR{ARR{"b", "c"}, ARR{"a"}}) expectRun(t, `v := ["a", "b", "c"]; deleted := splice(v, 2, 1, "d"); out = [deleted, v]`, nil, ARR{ARR{"c"}, ARR{"a", "b", "d"}}) expectRun(t, `v := ["a", "b", "c"]; deleted := splice(v, 0, 0, "d", "e"); out = [deleted, v]`, nil, ARR{ARR{}, ARR{"d", "e", "a", "b", "c"}}) expectRun(t, `v := ["a", "b", "c"]; deleted := splice(v, 1, 1, "d", "e"); out = [deleted, v]`, nil, ARR{ARR{"b"}, ARR{"a", "d", "e", "c"}}) } func TestBytesN(t *testing.T) { curMaxBytesLen := tengo.MaxBytesLen defer func() { tengo.MaxBytesLen = curMaxBytesLen }() tengo.MaxBytesLen = 10 expectRun(t, `out = bytes(0)`, nil, make([]byte, 0)) expectRun(t, `out = bytes(10)`, nil, make([]byte, 10)) expectError(t, `bytes(11)`, nil, "bytes size limit") tengo.MaxBytesLen = 1000 expectRun(t, `out = bytes(1000)`, nil, make([]byte, 1000)) expectError(t, `bytes(1001)`, nil, "bytes size limit") } func TestBytes(t *testing.T) { expectRun(t, `out = bytes("Hello World!")`, nil, []byte("Hello World!")) expectRun(t, `out = bytes("Hello") + bytes(" ") + bytes("World!")`, nil, []byte("Hello World!")) // bytes[] -> int expectRun(t, `out = bytes("abcde")[0]`, nil, 97) expectRun(t, `out = bytes("abcde")[1]`, nil, 98) expectRun(t, `out = bytes("abcde")[4]`, nil, 101) expectRun(t, `out = bytes("abcde")[10]`, nil, tengo.UndefinedValue) } 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) expectError(t, `a := 1 b := func(a, c) { c(a) } c := func(a) { 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") } func TestChar(t *testing.T) { expectRun(t, `out = 'a'`, nil, 'a') expectRun(t, `out = '九'`, nil, rune(20061)) expectRun(t, `out = 'Æ'`, nil, rune(198)) expectRun(t, `out = '0' + '9'`, nil, rune(105)) expectRun(t, `out = '0' + 9`, nil, '9') expectRun(t, `out = '9' - 4`, nil, '5') expectRun(t, `out = '0' == '0'`, nil, true) expectRun(t, `out = '0' != '0'`, nil, false) expectRun(t, `out = '2' < '4'`, nil, true) expectRun(t, `out = '2' > '4'`, nil, false) expectRun(t, `out = '2' <= '4'`, nil, true) expectRun(t, `out = '2' >= '4'`, nil, false) expectRun(t, `out = '4' < '4'`, nil, false) expectRun(t, `out = '4' > '4'`, nil, false) expectRun(t, `out = '4' <= '4'`, nil, true) expectRun(t, `out = '4' >= '4'`, nil, true) } func TestCondExpr(t *testing.T) { expectRun(t, `out = true ? 5 : 10`, nil, 5) expectRun(t, `out = false ? 5 : 10`, nil, 10) expectRun(t, `out = (1 == 1) ? 2 + 3 : 12 - 2`, nil, 5) expectRun(t, `out = (1 != 1) ? 2 + 3 : 12 - 2`, nil, 10) expectRun(t, `out = (1 == 1) ? true ? 10 - 8 : 1 + 3 : 12 - 2`, nil, 2) expectRun(t, `out = (1 == 1) ? false ? 10 - 8 : 1 + 3 : 12 - 2`, nil, 4) expectRun(t, ` out = 0 f1 := func() { out += 10 } f2 := func() { out = -out } true ? f1() : f2() `, nil, 10) expectRun(t, ` out = 5 f1 := func() { out += 10 } f2 := func() { out = -out } false ? f1() : f2() `, nil, -5) expectRun(t, ` f1 := func(a) { return a + 2 } f2 := func(a) { return a - 2 } f3 := func(a) { return a + 10 } f4 := func(a) { return -a } f := func(c) { return c == 0 ? f1(c) : f2(c) ? f3(c) : f4(c) } out = [f(0), f(1), f(2)] `, nil, ARR{2, 11, -2}) expectRun(t, `f := func(a) { return -a }; out = f(true ? 5 : 3)`, nil, -5) expectRun(t, `out = [false?5:10, true?1:2]`, nil, ARR{10, 1}) expectRun(t, ` out = 1 > 2 ? 1 + 2 + 3 : 10 - 5`, nil, 5) } func TestEquality(t *testing.T) { testEquality(t, `1`, `1`, true) testEquality(t, `1`, `2`, false) testEquality(t, `1.0`, `1.0`, true) testEquality(t, `1.0`, `1.1`, false) testEquality(t, `true`, `true`, true) testEquality(t, `true`, `false`, false) testEquality(t, `"foo"`, `"foo"`, true) testEquality(t, `"foo"`, `"bar"`, false) testEquality(t, `'f'`, `'f'`, true) testEquality(t, `'f'`, `'b'`, false) testEquality(t, `[]`, `[]`, true) testEquality(t, `[1]`, `[1]`, true) testEquality(t, `[1]`, `[1, 2]`, false) testEquality(t, `["foo", "bar"]`, `["foo", "bar"]`, true) testEquality(t, `["foo", "bar"]`, `["bar", "foo"]`, false) testEquality(t, `{}`, `{}`, true) testEquality(t, `{a: 1, b: 2}`, `{b: 2, a: 1}`, true) testEquality(t, `{a: 1, b: 2}`, `{b: 2}`, false) testEquality(t, `{a: 1, b: {}}`, `{b: {}, a: 1}`, true) testEquality(t, `1`, `"foo"`, false) testEquality(t, `1`, `true`, false) testEquality(t, `[1]`, `["1"]`, false) testEquality(t, `[1, [2]]`, `[1, ["2"]]`, false) testEquality(t, `{a: 1}`, `{a: "1"}`, false) testEquality(t, `{a: 1, b: {c: 2}}`, `{a: 1, b: {c: "2"}}`, false) } func testEquality(t *testing.T, lhs, rhs string, expected bool) { // 1. equality is commutative // 2. equality and inequality must be always opposite expectRun(t, fmt.Sprintf("out = %s == %s", lhs, rhs), nil, expected) expectRun(t, fmt.Sprintf("out = %s == %s", rhs, lhs), nil, expected) expectRun(t, fmt.Sprintf("out = %s != %s", lhs, rhs), nil, !expected) expectRun(t, fmt.Sprintf("out = %s != %s", rhs, lhs), nil, !expected) } func TestVMErrorInfo(t *testing.T) { expectError(t, `a := 5 a + "boo"`, nil, "Runtime Error: invalid operation: int + string\n\tat test:2:1") expectError(t, `a := 5 b := a(5)`, nil, "Runtime Error: not callable: int\n\tat test:2:6") expectError(t, `a := 5 b := {} b.x.y = 10`, nil, "Runtime Error: not index-assignable: undefined\n\tat test:3:1") expectError(t, ` a := func() { b := 5 b += "foo" } a()`, nil, "Runtime Error: invalid operation: int + string\n\tat test:4:2") expectError(t, `a := 5 a + import("mod1")`, Opts().Module( "mod1", `export "foo"`, ), ": invalid operation: int + string\n\tat test:2:1") expectError(t, `a := import("mod1")()`, Opts().Module( "mod1", ` export func() { b := 5 return b + "foo" }`), "Runtime Error: invalid operation: int + string\n\tat mod1:4:9") expectError(t, `a := import("mod1")()`, Opts().Module( "mod1", `export import("mod2")()`). Module( "mod2", ` export func() { b := 5 return b + "foo" }`), "Runtime Error: invalid operation: int + string\n\tat mod2:4:9") expectError(t, `a := [1, 2, 3]; b := a[:"invalid"];`, nil, "Runtime Error: invalid slice index type: string") expectError(t, `a := immutable([4, 5, 6]); b := a[:false];`, nil, "Runtime Error: invalid slice index type: bool") expectError(t, `a := "hello"; b := a[:1.23];`, nil, "Runtime Error: invalid slice index type: float") expectError(t, `a := bytes("world"); b := a[:time(1)];`, nil, "Runtime Error: invalid slice index type: time") } func TestVMErrorUnwrap(t *testing.T) { userErr := errors.New("user runtime error") userFunc := func(err error) *tengo.UserFunction { return &tengo.UserFunction{Name: "user_func", Value: func(args ...tengo.Object) (tengo.Object, error) { return nil, err }} } userModule := func(err error) *tengo.BuiltinModule { return &tengo.BuiltinModule{ Attrs: map[string]tengo.Object{ "afunction": &tengo.UserFunction{ Name: "afunction", Value: func(a ...tengo.Object) (tengo.Object, error) { return nil, err }, }, }, } } expectError(t, `user_func()`, Opts().Symbol("user_func", userFunc(userErr)), "Runtime Error: "+userErr.Error(), ) expectErrorIs(t, `user_func()`, Opts().Symbol("user_func", userFunc(userErr)), userErr, ) wrapUserErr := &customError{err: userErr, str: "custom error"} expectErrorIs(t, `user_func()`, Opts().Symbol("user_func", userFunc(wrapUserErr)), wrapUserErr, ) expectErrorIs(t, `user_func()`, Opts().Symbol("user_func", userFunc(wrapUserErr)), userErr, ) var asErr1 *customError expectErrorAs(t, `user_func()`, Opts().Symbol("user_func", userFunc(wrapUserErr)), &asErr1, ) require.True(t, asErr1.Error() == wrapUserErr.Error(), "expected error as:%v, got:%v", wrapUserErr, asErr1) expectError(t, `import("mod1").afunction()`, Opts().Module("mod1", userModule(userErr)), "Runtime Error: "+userErr.Error(), ) expectErrorIs(t, `import("mod1").afunction()`, Opts().Module("mod1", userModule(userErr)), userErr, ) expectError(t, `import("mod1").afunction()`, Opts().Module("mod1", userModule(wrapUserErr)), "Runtime Error: "+wrapUserErr.Error(), ) expectErrorIs(t, `import("mod1").afunction()`, Opts().Module("mod1", userModule(wrapUserErr)), wrapUserErr, ) expectErrorIs(t, `import("mod1").afunction()`, Opts().Module("mod1", userModule(wrapUserErr)), userErr, ) var asErr2 *customError expectErrorAs(t, `import("mod1").afunction()`, Opts().Module("mod1", userModule(wrapUserErr)), &asErr2, ) require.True(t, asErr2.Error() == wrapUserErr.Error(), "expected error as:%v, got:%v", wrapUserErr, asErr2) } func TestError(t *testing.T) { expectRun(t, `out = error(1)`, nil, errorObject(1)) expectRun(t, `out = error(1).value`, nil, 1) expectRun(t, `out = error("some error")`, nil, errorObject("some error")) expectRun(t, `out = error("some" + " error")`, nil, errorObject("some error")) expectRun(t, `out = func() { return error(5) }()`, nil, errorObject(5)) expectRun(t, `out = error(error("foo"))`, nil, errorObject(errorObject("foo"))) expectRun(t, `out = error("some error")`, nil, errorObject("some error")) expectRun(t, `out = error("some error").value`, nil, "some error") expectRun(t, `out = error("some error")["value"]`, nil, "some error") expectError(t, `error("error").err`, nil, "invalid index on error") expectError(t, `error("error").value_`, nil, "invalid index on error") expectError(t, `error([1,2,3])[1]`, nil, "invalid index on error") } func TestFloat(t *testing.T) { expectRun(t, `out = 0.0`, nil, 0.0) expectRun(t, `out = -10.3`, nil, -10.3) expectRun(t, `out = 3.2 + 2.0 * -4.0`, nil, -4.8) expectRun(t, `out = 4 + 2.3`, nil, 6.3) expectRun(t, `out = 2.3 + 4`, nil, 6.3) expectRun(t, `out = +5.0`, nil, 5.0) expectRun(t, `out = -5.0 + +5.0`, nil, 0.0) } func TestForIn(t *testing.T) { // array expectRun(t, `out = 0; for x in [1, 2, 3] { out += x }`, nil, 6) // value expectRun(t, `out = 0; for i, x in [1, 2, 3] { out += i + x }`, nil, 9) // index, value expectRun(t, `out = 0; func() { for i, x in [1, 2, 3] { out += i + x } }()`, nil, 9) // index, value expectRun(t, `out = 0; for i, _ in [1, 2, 3] { out += i }`, nil, 3) // index, _ expectRun(t, `out = 0; func() { for i, _ in [1, 2, 3] { out += i } }()`, nil, 3) // index, _ // map expectRun(t, `out = 0; for v in {a:2,b:3,c:4} { out += v }`, nil, 9) // value expectRun(t, `out = ""; for k, v in {a:2,b:3,c:4} { out = k; if v==3 { break } }`, nil, "b") // key, value expectRun(t, `out = ""; for k, _ in {a:2} { out += k }`, nil, "a") // key, _ expectRun(t, `out = 0; for _, v in {a:2,b:3,c:4} { out += v }`, nil, 9) // _, value expectRun(t, `out = ""; func() { for k, v in {a:2,b:3,c:4} { out = k; if v==3 { break } } }()`, nil, "b") // key, value // string expectRun(t, `out = ""; for c in "abcde" { out += c }`, nil, "abcde") expectRun(t, `out = ""; for i, c in "abcde" { if i == 2 { continue }; out += c }`, nil, "abde") } func TestFor(t *testing.T) { expectRun(t, ` out = 0 for { out++ if out == 5 { break } }`, nil, 5) expectRun(t, ` out = 0 for { out++ if out == 5 { break } }`, nil, 5) expectRun(t, ` out = 0 a := 0 for { a++ if a == 3 { continue } if a == 5 { break } out += a }`, nil, 7) // 1 + 2 + 4 expectRun(t, ` out = 0 a := 0 for { a++ if a == 3 { continue } out += a if a == 5 { break } }`, nil, 12) // 1 + 2 + 4 + 5 expectRun(t, ` out = 0 for true { out++ if out == 5 { break } }`, nil, 5) expectRun(t, ` a := 0 for true { a++ if a == 5 { break } } out = a`, nil, 5) expectRun(t, ` out = 0 a := 0 for true { a++ if a == 3 { continue } if a == 5 { break } out += a }`, nil, 7) // 1 + 2 + 4 expectRun(t, ` out = 0 a := 0 for true { a++ if a == 3 { continue } out += a if a == 5 { break } }`, nil, 12) // 1 + 2 + 4 + 5 expectRun(t, ` out = 0 func() { for true { out++ if out == 5 { return } } }()`, nil, 5) expectRun(t, ` out = 0 for a:=1; a<=10; a++ { out += a }`, nil, 55) expectRun(t, ` out = 0 for a:=1; a<=3; a++ { for b:=3; b<=6; b++ { out += b } }`, nil, 54) expectRun(t, ` out = 0 func() { for { out++ if out == 5 { break } } }()`, nil, 5) expectRun(t, ` out = 0 func() { for true { out++ if out == 5 { break } } }()`, nil, 5) expectRun(t, ` out = func() { a := 0 for { a++ if a == 5 { break } } return a }()`, nil, 5) expectRun(t, ` out = func() { a := 0 for true { a++ if a== 5 { break } } return a }()`, nil, 5) expectRun(t, ` out = func() { a := 0 func() { for { a++ if a == 5 { break } } }() return a }()`, nil, 5) expectRun(t, ` out = func() { a := 0 func() { for true { a++ if a == 5 { break } } }() return a }()`, nil, 5) expectRun(t, ` out = func() { sum := 0 for a:=1; a<=10; a++ { sum += a } return sum }()`, nil, 55) expectRun(t, ` out = func() { sum := 0 for a:=1; a<=4; a++ { for b:=3; b<=5; b++ { sum += b } } return sum }()`, nil, 48) // (3+4+5) * 4 expectRun(t, ` a := 1 for ; a<=10; a++ { if a == 5 { break } } out = a`, nil, 5) expectRun(t, ` out = 0 for a:=1; a<=10; a++ { if a == 3 { continue } out += a if a == 5 { break } }`, nil, 12) // 1 + 2 + 4 + 5 expectRun(t, ` out = 0 for a:=1; a<=10; { if a == 3 { a++ continue } out += a if a == 5 { break } a++ }`, nil, 12) // 1 + 2 + 4 + 5 } func TestFunction(t *testing.T) { // function with no "return" statement returns "invalid" value. expectRun(t, `f1 := func() {}; out = f1();`, nil, tengo.UndefinedValue) expectRun(t, `f1 := func() {}; f2 := func() { return f1(); }; f1(); out = f2();`, nil, tengo.UndefinedValue) expectRun(t, `f := func(x) { x; }; out = f(5);`, nil, tengo.UndefinedValue) expectRun(t, `f := func(...x) { return x; }; out = f(1,2,3);`, nil, ARR{1, 2, 3}) expectRun(t, `f := func(a, b, ...x) { return [a, b, x]; }; out = f(8,9,1,2,3);`, nil, ARR{8, 9, ARR{1, 2, 3}}) expectRun(t, `f := func(v) { x := 2; return func(a, ...b){ return [a, b, v+x]}; }; out = f(5)("a", "b");`, nil, ARR{"a", ARR{"b"}, 7}) expectRun(t, `f := func(...x) { return x; }; out = f();`, nil, &tengo.Array{Value: []tengo.Object{}}) expectRun(t, `f := func(a, b, ...x) { return [a, b, x]; }; out = f(8, 9);`, nil, ARR{8, 9, ARR{}}) expectRun(t, `f := func(v) { x := 2; return func(a, ...b){ return [a, b, v+x]}; }; out = f(5)("a");`, nil, ARR{"a", ARR{}, 7}) expectError(t, `f := func(a, b, ...x) { return [a, b, x]; }; f();`, nil, "Runtime Error: wrong number of arguments: want>=2, got=0\n\tat test:1:46") expectError(t, `f := func(a, b, ...x) { return [a, b, x]; }; f(1);`, nil, "Runtime Error: wrong number of arguments: want>=2, got=1\n\tat test:1:46") expectRun(t, `f := func(x) { return x; }; out = f(5);`, nil, 5) expectRun(t, `f := func(x) { return x * 2; }; out = f(5);`, nil, 10) expectRun(t, `f := func(x, y) { return x + y; }; out = f(5, 5);`, nil, 10) expectRun(t, `f := func(x, y) { return x + y; }; out = f(5 + 5, f(5, 5));`, nil, 20) expectRun(t, `out = func(x) { return x; }(5)`, nil, 5) expectRun(t, `x := 10; f := func(x) { return x; }; f(5); out = x;`, nil, 10) expectRun(t, ` f2 := func(a) { f1 := func(a) { return a * 2; }; return f1(a) * 3; }; out = f2(10); `, nil, 60) expectRun(t, ` f1 := func(f) { a := [undefined] a[0] = func() { return f(a) } return a[0]() } out = f1(func(a) { return 2 }) `, nil, 2) // closures expectRun(t, ` newAdder := func(x) { return func(y) { return x + y }; }; add2 := newAdder(2); out = add2(5); `, nil, 7) expectRun(t, ` m := {a: 1} for k,v in m { func(){ out = k }() } `, nil, "a") expectRun(t, ` m := {a: 1} for k,v in m { func(){ out = v }() } `, nil, 1) // function as a argument expectRun(t, ` add := func(a, b) { return a + b }; sub := func(a, b) { return a - b }; applyFunc := func(a, b, f) { return f(a, b) }; out = applyFunc(applyFunc(2, 2, add), 3, sub); `, nil, 1) expectRun(t, `f1 := func() { return 5 + 10; }; out = f1();`, nil, 15) expectRun(t, `f1 := func() { return 1 }; f2 := func() { return 2 }; out = f1() + f2()`, nil, 3) expectRun(t, `f1 := func() { return 1 }; f2 := func() { return f1() + 2 }; f3 := func() { return f2() + 3 }; out = f3()`, nil, 6) expectRun(t, `f1 := func() { return 99; 100 }; out = f1();`, nil, 99) expectRun(t, `f1 := func() { return 99; return 100 }; out = f1();`, nil, 99) expectRun(t, `f1 := func() { return 33; }; f2 := func() { return f1 }; out = f2()();`, nil, 33) expectRun(t, `one := func() { one = 1; return one }; out = one()`, nil, 1) expectRun(t, `three := func() { one := 1; two := 2; return one + two }; out = three()`, nil, 3) expectRun(t, `three := func() { one := 1; two := 2; return one + two }; seven := func() { three := 3; four := 4; return three + four }; out = three() + seven()`, nil, 10) expectRun(t, ` foo1 := func() { foo := 50 return foo } foo2 := func() { foo := 100 return foo } out = foo1() + foo2()`, nil, 150) expectRun(t, ` g := 50; minusOne := func() { n := 1; return g - n; }; minusTwo := func() { n := 2; return g - n; }; out = minusOne() + minusTwo() `, nil, 97) expectRun(t, ` f1 := func() { f2 := func() { return 1; } return f2 }; out = f1()() `, nil, 1) expectRun(t, ` f1 := func(a) { return a; }; out = f1(4)`, nil, 4) expectRun(t, ` f1 := func(a, b) { return a + b; }; out = f1(1, 2)`, nil, 3) expectRun(t, ` sum := func(a, b) { c := a + b; return c; }; out = sum(1, 2);`, nil, 3) expectRun(t, ` sum := func(a, b) { c := a + b; return c; }; out = sum(1, 2) + sum(3, 4);`, nil, 10) expectRun(t, ` sum := func(a, b) { c := a + b return c }; outer := func() { return sum(1, 2) + sum(3, 4) }; out = outer();`, nil, 10) expectRun(t, ` g := 10; sum := func(a, b) { c := a + b; return c + g; } outer := func() { return sum(1, 2) + sum(3, 4) + g; } out = outer() + g `, nil, 50) expectError(t, `func() { return 1; }(1)`, nil, "wrong number of arguments") expectError(t, `func(a) { return a; }()`, nil, "wrong number of arguments") expectError(t, `func(a, b) { return a + b; }(1)`, nil, "wrong number of arguments") expectRun(t, ` f1 := func(a) { return func() { return a; }; }; f2 := f1(99); out = f2() `, nil, 99) expectRun(t, ` f1 := func(a, b) { return func(c) { return a + b + c }; }; f2 := f1(1, 2); out = f2(8); `, nil, 11) expectRun(t, ` f1 := func(a, b) { c := a + b; return func(d) { return c + d }; }; f2 := f1(1, 2); out = f2(8); `, nil, 11) expectRun(t, ` f1 := func(a, b) { c := a + b; return func(d) { e := d + c; return func(f) { return e + f }; } }; f2 := f1(1, 2); f3 := f2(3); out = f3(8); `, nil, 14) expectRun(t, ` a := 1; f1 := func(b) { return func(c) { return func(d) { return a + b + c + d } }; }; f2 := f1(2); f3 := f2(3); out = f3(8); `, nil, 14) expectRun(t, ` f1 := func(a, b) { one := func() { return a; }; two := func() { return b; }; return func() { return one() + two(); } }; f2 := f1(9, 90); out = f2(); `, nil, 99) // global function recursion expectRun(t, ` fib := func(x) { if x == 0 { return 0 } else if x == 1 { return 1 } else { return fib(x-1) + fib(x-2) } } out = fib(15)`, nil, 610) // local function recursion expectRun(t, ` out = func() { sum := func(x) { return x == 0 ? 0 : x + sum(x-1) } return sum(5) }()`, nil, 15) expectError(t, `return 5`, nil, "return not allowed outside function") // closure and block scopes expectRun(t, ` func() { a := 10 func() { b := 5 if true { out = a + 5 } }() }()`, nil, 15) expectRun(t, ` func() { a := 10 b := func() { return 5 } func() { if b() { out = a + b() } }() }()`, nil, 15) expectRun(t, ` func() { a := 10 func() { b := func() { return 5 } func() { if true { out = a + b() } }() }() }()`, nil, 15) // function skipping return expectRun(t, `out = func() {}()`, nil, tengo.UndefinedValue) expectRun(t, `out = func(v) { if v { return true } }(1)`, nil, true) expectRun(t, `out = func(v) { if v { return true } }(0)`, nil, tengo.UndefinedValue) expectRun(t, `out = func(v) { if v { } else { return true } }(1)`, nil, tengo.UndefinedValue) expectRun(t, `out = func(v) { if v { return } }(1)`, nil, tengo.UndefinedValue) expectRun(t, `out = func(v) { if v { return } }(0)`, nil, tengo.UndefinedValue) expectRun(t, `out = func(v) { if v { } else { return } }(1)`, nil, tengo.UndefinedValue) expectRun(t, `out = func(v) { for ;;v++ { if v == 3 { return true } } }(1)`, nil, true) expectRun(t, `out = func(v) { for ;;v++ { if v == 3 { break } } }(1)`, nil, tengo.UndefinedValue) // 'f' in RHS at line 4 must reference global variable 'f' // See https://github.com/d5/tengo/issues/314 expectRun(t, ` f := func() { return 2 } out = (func() { f := f() return f })() `, nil, 2) } func TestBlocksInGlobalScope(t *testing.T) { expectRun(t, ` f := undefined if true { a := 1 f = func() { a = 2 } } b := 3 f() out = b`, nil, 3) expectRun(t, ` func() { f := undefined if true { a := 10 f = func() { a = 20 } } b := 5 f() out = b }() `, nil, 5) expectRun(t, ` f := undefined if true { a := 1 b := 2 f = func() { a = 3 b = 4 } } c := 5 d := 6 f() out = c + d`, nil, 11) expectRun(t, ` fn := undefined if true { a := 1 b := 2 if true { c := 3 d := 4 fn = func() { a = 5 b = 6 c = 7 d = 8 } } } e := 9 f := 10 fn() out = e + f`, nil, 19) expectRun(t, ` out = 0 func() { for x in [1, 2, 3] { out += x } }()`, nil, 6) expectRun(t, ` out = 0 for x in [1, 2, 3] { out += x }`, nil, 6) } 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) expectRun(t, `if (1) { out = 10 }`, nil, 10) expectRun(t, `if (0) { out = 10 } else { out = 20 }`, nil, 20) expectRun(t, `if (1 < 2) { out = 10 }`, nil, 10) expectRun(t, `if (1 > 2) { out = 10 }`, nil, tengo.UndefinedValue) expectRun(t, `if (1 < 2) { out = 10 } else { out = 20 }`, nil, 10) expectRun(t, `if (1 > 2) { out = 10 } else { out = 20 }`, nil, 20) expectRun(t, `if (1 < 2) { out = 10 } else if (1 > 2) { out = 20 } else { out = 30 }`, nil, 10) expectRun(t, `if (1 > 2) { out = 10 } else if (1 < 2) { out = 20 } else { out = 30 }`, nil, 20) expectRun(t, `if (1 > 2) { out = 10 } else if (1 == 2) { out = 20 } else { out = 30 }`, nil, 30) expectRun(t, `if (1 > 2) { out = 10 } else if (1 == 2) { out = 20 } else if (1 < 2) { out = 30 } else { out = 40 }`, nil, 30) expectRun(t, `if (1 > 2) { out = 10 } else if (1 < 2) { out = 20; out = 21; out = 22 } else { out = 30 }`, nil, 22) expectRun(t, `if (1 > 2) { out = 10 } else if (1 == 2) { out = 20 } else { out = 30; out = 31; out = 32}`, nil, 32) expectRun(t, `if (1 > 2) { out = 10 } else if (1 < 2) { if (1 == 2) { out = 21 } else { out = 22 } } else { out = 30 }`, nil, 22) expectRun(t, `if (1 > 2) { out = 10 } else if (1 < 2) { if (1 == 2) { out = 21 } else if (2 == 3) { out = 22 } else { out = 23 } } else { out = 30 }`, nil, 23) expectRun(t, `if (1 > 2) { out = 10 } else if (1 == 2) { if (1 == 2) { out = 21 } else if (2 == 3) { out = 22 } else { out = 23 } } else { out = 30 }`, nil, 30) expectRun(t, `if (1 > 2) { out = 10 } else if (1 == 2) { out = 20 } else { if (1 == 2) { out = 31 } else if (2 == 3) { out = 32 } else { out = 33 } }`, nil, 33) expectRun(t, `if a:=0; a<1 { out = 10 }`, nil, 10) expectRun(t, `a:=0; if a++; a==1 { out = 10 }`, nil, 10) expectRun(t, ` func() { a := 1 if a++; a > 1 { out = a } }() `, nil, 2) expectRun(t, ` func() { a := 1 if a++; a == 1 { out = 10 } else { out = 20 } }() `, nil, 20) expectRun(t, ` func() { a := 1 func() { if a++; a > 1 { a++ } }() out = a }() `, nil, 3) // expression statement in init (should not leave objects on stack) expectRun(t, `a := 1; if a; a { out = a }`, nil, 1) expectRun(t, `a := 1; if a + 4; a { out = a }`, nil, 1) // dead code elimination expectRun(t, ` out = func() { if false { return 1 } a := undefined a = 2 if !a { b := func() { return is_callable(a) ? a(8) : a }() if is_error(b) { return b } else if !is_undefined(b) { return immutable(b) } } a = 3 if a { b := func() { return is_callable(a) ? a(9) : a }() if is_error(b) { return b } else if !is_undefined(b) { return immutable(b) } } return a }() `, nil, 3) } func TestImmutable(t *testing.T) { // primitive types are already immutable values // immutable expression has no effects. expectRun(t, `a := immutable(1); out = a`, nil, 1) expectRun(t, `a := 5; b := immutable(a); out = b`, nil, 5) expectRun(t, `a := immutable(1); a = 5; out = a`, nil, 5) // array expectError(t, `a := immutable([1, 2, 3]); a[1] = 5`, nil, "not index-assignable") expectError(t, `a := immutable(["foo", [1,2,3]]); a[1] = "bar"`, nil, "not index-assignable") expectRun(t, `a := immutable(["foo", [1,2,3]]); a[1][1] = "bar"; out = a`, nil, IARR{"foo", ARR{1, "bar", 3}}) expectError(t, `a := immutable(["foo", immutable([1,2,3])]); a[1][1] = "bar"`, nil, "not index-assignable") expectError(t, `a := ["foo", immutable([1,2,3])]; a[1][1] = "bar"`, nil, "not index-assignable") expectRun(t, `a := immutable([1,2,3]); b := copy(a); b[1] = 5; out = b`, nil, ARR{1, 5, 3}) expectRun(t, `a := immutable([1,2,3]); b := copy(a); b[1] = 5; out = a`, nil, IARR{1, 2, 3}) expectRun(t, `out = immutable([1,2,3]) == [1,2,3]`, nil, true) expectRun(t, `out = immutable([1,2,3]) == immutable([1,2,3])`, nil, true) expectRun(t, `out = [1,2,3] == immutable([1,2,3])`, nil, true) expectRun(t, `out = immutable([1,2,3]) == [1,2]`, nil, false) expectRun(t, `out = immutable([1,2,3]) == immutable([1,2])`, nil, false) expectRun(t, `out = [1,2,3] == immutable([1,2])`, nil, false) expectRun(t, `out = immutable([1, 2, 3, 4])[1]`, nil, 2) expectRun(t, `out = immutable([1, 2, 3, 4])[1:3]`, nil, ARR{2, 3}) expectRun(t, `a := immutable([1,2,3]); a = 5; out = a`, nil, 5) expectRun(t, `a := immutable([1, 2, 3]); out = a[5]`, nil, tengo.UndefinedValue) // map expectError(t, `a := immutable({b: 1, c: 2}); a.b = 5`, nil, "not index-assignable") expectError(t, `a := immutable({b: 1, c: 2}); a["b"] = "bar"`, nil, "not index-assignable") expectRun(t, `a := immutable({b: 1, c: [1,2,3]}); a.c[1] = "bar"; out = a`, nil, IMAP{"b": 1, "c": ARR{1, "bar", 3}}) expectError(t, `a := immutable({b: 1, c: immutable([1,2,3])}); a.c[1] = "bar"`, nil, "not index-assignable") expectError(t, `a := {b: 1, c: immutable([1,2,3])}; a.c[1] = "bar"`, nil, "not index-assignable") expectRun(t, `out = immutable({a:1,b:2}) == {a:1,b:2}`, nil, true) expectRun(t, `out = immutable({a:1,b:2}) == immutable({a:1,b:2})`, nil, true) expectRun(t, `out = {a:1,b:2} == immutable({a:1,b:2})`, nil, true) expectRun(t, `out = immutable({a:1,b:2}) == {a:1,b:3}`, nil, false) expectRun(t, `out = immutable({a:1,b:2}) == immutable({a:1,b:3})`, nil, false) expectRun(t, `out = {a:1,b:2} == immutable({a:1,b:3})`, nil, false) expectRun(t, `out = immutable({a:1,b:2}).b`, nil, 2) expectRun(t, `out = immutable({a:1,b:2})["b"]`, nil, 2) expectRun(t, `a := immutable({a:1,b:2}); a = 5; out = 5`, nil, 5) expectRun(t, `a := immutable({a:1,b:2}); out = a.c`, nil, tengo.UndefinedValue) expectRun(t, `a := immutable({b: 5, c: "foo"}); out = a.b`, nil, 5) expectError(t, `a := immutable({b: 5, c: "foo"}); a.b = 10`, nil, "not index-assignable") } func TestIncDec(t *testing.T) { expectRun(t, `out = 0; out++`, nil, 1) expectRun(t, `out = 0; out--`, nil, -1) expectRun(t, `a := 0; a++; out = a`, nil, 1) expectRun(t, `a := 0; a++; a--; out = a`, nil, 0) // this seems strange but it works because 'a += b' is // translated into 'a = a + b' and string type takes other types for + operator. expectRun(t, `a := "foo"; a++; out = a`, nil, "foo1") expectError(t, `a := "foo"; a--`, nil, "invalid operation") expectError(t, `a++`, nil, "unresolved reference") // not declared expectError(t, `a--`, nil, "unresolved reference") // not declared expectError(t, `4++`, nil, "unresolved reference") } type StringDict struct { tengo.ObjectImpl Value map[string]string } func (o *StringDict) String() string { return "" } func (o *StringDict) TypeName() string { return "string-dict" } func (o *StringDict) IndexGet(index tengo.Object) (tengo.Object, error) { strIdx, ok := index.(*tengo.String) if !ok { return nil, tengo.ErrInvalidIndexType } for k, v := range o.Value { if strings.EqualFold(strIdx.Value, k) { return &tengo.String{Value: v}, nil } } return tengo.UndefinedValue, nil } func (o *StringDict) IndexSet(index, value tengo.Object) error { strIdx, ok := index.(*tengo.String) if !ok { return tengo.ErrInvalidIndexType } strVal, ok := tengo.ToString(value) if !ok { return tengo.ErrInvalidIndexValueType } o.Value[strings.ToLower(strIdx.Value)] = strVal return nil } type StringCircle struct { tengo.ObjectImpl Value []string } func (o *StringCircle) TypeName() string { return "string-circle" } func (o *StringCircle) String() string { return "" } func (o *StringCircle) IndexGet(index tengo.Object) (tengo.Object, error) { intIdx, ok := index.(*tengo.Int) if !ok { return nil, tengo.ErrInvalidIndexType } r := int(intIdx.Value) % len(o.Value) if r < 0 { r = len(o.Value) + r } return &tengo.String{Value: o.Value[r]}, nil } func (o *StringCircle) IndexSet(index, value tengo.Object) error { intIdx, ok := index.(*tengo.Int) if !ok { return tengo.ErrInvalidIndexType } r := int(intIdx.Value) % len(o.Value) if r < 0 { r = len(o.Value) + r } strVal, ok := tengo.ToString(value) if !ok { return tengo.ErrInvalidIndexValueType } o.Value[r] = strVal return nil } type StringArray struct { tengo.ObjectImpl Value []string } func (o *StringArray) String() string { return strings.Join(o.Value, ", ") } func (o *StringArray) BinaryOp( op token.Token, rhs tengo.Object, ) (tengo.Object, error) { if rhs, ok := rhs.(*StringArray); ok { switch op { case token.Add: if len(rhs.Value) == 0 { return o, nil } return &StringArray{Value: append(o.Value, rhs.Value...)}, nil } } return nil, tengo.ErrInvalidOperator } func (o *StringArray) IsFalsy() bool { return len(o.Value) == 0 } func (o *StringArray) Equals(x tengo.Object) bool { if x, ok := x.(*StringArray); ok { if len(o.Value) != len(x.Value) { return false } for i, v := range o.Value { if v != x.Value[i] { return false } } return true } return false } func (o *StringArray) Copy() tengo.Object { return &StringArray{ Value: append([]string{}, o.Value...), } } func (o *StringArray) TypeName() string { return "string-array" } func (o *StringArray) IndexGet(index tengo.Object) (tengo.Object, error) { intIdx, ok := index.(*tengo.Int) if ok { if intIdx.Value >= 0 && intIdx.Value < int64(len(o.Value)) { return &tengo.String{Value: o.Value[intIdx.Value]}, nil } return nil, tengo.ErrIndexOutOfBounds } strIdx, ok := index.(*tengo.String) if ok { for vidx, str := range o.Value { if strIdx.Value == str { return &tengo.Int{Value: int64(vidx)}, nil } } return tengo.UndefinedValue, nil } return nil, tengo.ErrInvalidIndexType } func (o *StringArray) IndexSet(index, value tengo.Object) error { strVal, ok := tengo.ToString(value) if !ok { return tengo.ErrInvalidIndexValueType } intIdx, ok := index.(*tengo.Int) if ok { if intIdx.Value >= 0 && intIdx.Value < int64(len(o.Value)) { o.Value[intIdx.Value] = strVal return nil } return tengo.ErrIndexOutOfBounds } return tengo.ErrInvalidIndexType } func (o *StringArray) Call( args ...tengo.Object, ) (ret tengo.Object, err error) { if len(args) != 1 { return nil, tengo.ErrWrongNumArguments } s1, ok := tengo.ToString(args[0]) if !ok { return nil, tengo.ErrInvalidArgumentType{ Name: "first", Expected: "string(compatible)", Found: args[0].TypeName(), } } for i, v := range o.Value { if v == s1 { return &tengo.Int{Value: int64(i)}, nil } } return tengo.UndefinedValue, nil } func (o *StringArray) CanCall() bool { return true } func TestIndexable(t *testing.T) { dict := func() *StringDict { return &StringDict{Value: map[string]string{"a": "foo", "b": "bar"}} } expectRun(t, `out = dict["a"]`, Opts().Symbol("dict", dict()).Skip2ndPass(), "foo") expectRun(t, `out = dict["B"]`, Opts().Symbol("dict", dict()).Skip2ndPass(), "bar") expectRun(t, `out = dict["x"]`, Opts().Symbol("dict", dict()).Skip2ndPass(), tengo.UndefinedValue) expectError(t, `dict[0]`, Opts().Symbol("dict", dict()).Skip2ndPass(), "invalid index type") strCir := func() *StringCircle { return &StringCircle{Value: []string{"one", "two", "three"}} } expectRun(t, `out = cir[0]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "one") expectRun(t, `out = cir[1]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "two") expectRun(t, `out = cir[-1]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "three") expectRun(t, `out = cir[-2]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "two") expectRun(t, `out = cir[3]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "one") expectError(t, `cir["a"]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "invalid index type") strArr := func() *StringArray { return &StringArray{Value: []string{"one", "two", "three"}} } expectRun(t, `out = arr["one"]`, Opts().Symbol("arr", strArr()).Skip2ndPass(), 0) expectRun(t, `out = arr["three"]`, Opts().Symbol("arr", strArr()).Skip2ndPass(), 2) expectRun(t, `out = arr["four"]`, Opts().Symbol("arr", strArr()).Skip2ndPass(), tengo.UndefinedValue) expectRun(t, `out = arr[0]`, Opts().Symbol("arr", strArr()).Skip2ndPass(), "one") expectRun(t, `out = arr[1]`, Opts().Symbol("arr", strArr()).Skip2ndPass(), "two") expectError(t, `arr[-1]`, Opts().Symbol("arr", strArr()).Skip2ndPass(), "index out of bounds") } func TestIndexAssignable(t *testing.T) { dict := func() *StringDict { return &StringDict{Value: map[string]string{"a": "foo", "b": "bar"}} } expectRun(t, `dict["a"] = "1984"; out = dict["a"]`, Opts().Symbol("dict", dict()).Skip2ndPass(), "1984") expectRun(t, `dict["c"] = "1984"; out = dict["c"]`, Opts().Symbol("dict", dict()).Skip2ndPass(), "1984") expectRun(t, `dict["c"] = 1984; out = dict["C"]`, Opts().Symbol("dict", dict()).Skip2ndPass(), "1984") expectError(t, `dict[0] = "1984"`, Opts().Symbol("dict", dict()).Skip2ndPass(), "invalid index type") strCir := func() *StringCircle { return &StringCircle{Value: []string{"one", "two", "three"}} } expectRun(t, `cir[0] = "ONE"; out = cir[0]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "ONE") expectRun(t, `cir[1] = "TWO"; out = cir[1]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "TWO") expectRun(t, `cir[-1] = "THREE"; out = cir[2]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "THREE") expectRun(t, `cir[0] = "ONE"; out = cir[3]`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "ONE") expectError(t, `cir["a"] = "ONE"`, Opts().Symbol("cir", strCir()).Skip2ndPass(), "invalid index type") strArr := func() *StringArray { return &StringArray{Value: []string{"one", "two", "three"}} } expectRun(t, `arr[0] = "ONE"; out = arr[0]`, Opts().Symbol("arr", strArr()).Skip2ndPass(), "ONE") expectRun(t, `arr[1] = "TWO"; out = arr[1]`, Opts().Symbol("arr", strArr()).Skip2ndPass(), "TWO") expectError(t, `arr["one"] = "ONE"`, Opts().Symbol("arr", strArr()).Skip2ndPass(), "invalid index type") } func TestInteger(t *testing.T) { expectRun(t, `out = 5`, nil, 5) expectRun(t, `out = 10`, nil, 10) expectRun(t, `out = -5`, nil, -5) expectRun(t, `out = -10`, nil, -10) expectRun(t, `out = 5 + 5 + 5 + 5 - 10`, nil, 10) expectRun(t, `out = 2 * 2 * 2 * 2 * 2`, nil, 32) expectRun(t, `out = -50 + 100 + -50`, nil, 0) expectRun(t, `out = 5 * 2 + 10`, nil, 20) expectRun(t, `out = 5 + 2 * 10`, nil, 25) expectRun(t, `out = 20 + 2 * -10`, nil, 0) expectRun(t, `out = 50 / 2 * 2 + 10`, nil, 60) expectRun(t, `out = 2 * (5 + 10)`, nil, 30) expectRun(t, `out = 3 * 3 * 3 + 10`, nil, 37) expectRun(t, `out = 3 * (3 * 3) + 10`, nil, 37) expectRun(t, `out = (5 + 10 * 2 + 15 /3) * 2 + -10`, nil, 50) expectRun(t, `out = 5 % 3`, nil, 2) expectRun(t, `out = 5 % 3 + 4`, nil, 6) expectRun(t, `out = +5`, nil, 5) expectRun(t, `out = +5 + -5`, nil, 0) expectRun(t, `out = 9 + '0'`, nil, '9') expectRun(t, `out = '9' - 5`, nil, '4') } type StringArrayIterator struct { tengo.ObjectImpl strArr *StringArray idx int } func (i *StringArrayIterator) TypeName() string { return "string-array-iterator" } func (i *StringArrayIterator) String() string { return "" } func (i *StringArrayIterator) Next() bool { i.idx++ return i.idx <= len(i.strArr.Value) } func (i *StringArrayIterator) Key() tengo.Object { return &tengo.Int{Value: int64(i.idx - 1)} } func (i *StringArrayIterator) Value() tengo.Object { return &tengo.String{Value: i.strArr.Value[i.idx-1]} } func (o *StringArray) Iterate() tengo.Iterator { return &StringArrayIterator{ strArr: o, } } func (o *StringArray) CanIterate() bool { return true } func TestIterable(t *testing.T) { strArr := func() *StringArray { return &StringArray{Value: []string{"one", "two", "three"}} } expectRun(t, `for i, s in arr { out += i }`, Opts().Symbol("arr", strArr()).Skip2ndPass(), 3) expectRun(t, `for i, s in arr { out += s }`, Opts().Symbol("arr", strArr()).Skip2ndPass(), "onetwothree") expectRun(t, `for i, s in arr { out += s + i }`, Opts().Symbol("arr", strArr()).Skip2ndPass(), "one0two1three2") } func TestLogical(t *testing.T) { expectRun(t, `out = true && true`, nil, true) expectRun(t, `out = true && false`, nil, false) expectRun(t, `out = false && true`, nil, false) expectRun(t, `out = false && false`, nil, false) expectRun(t, `out = !true && true`, nil, false) expectRun(t, `out = !true && false`, nil, false) expectRun(t, `out = !false && true`, nil, true) expectRun(t, `out = !false && false`, nil, false) expectRun(t, `out = true || true`, nil, true) expectRun(t, `out = true || false`, nil, true) expectRun(t, `out = false || true`, nil, true) expectRun(t, `out = false || false`, nil, false) expectRun(t, `out = !true || true`, nil, true) expectRun(t, `out = !true || false`, nil, false) expectRun(t, `out = !false || true`, nil, true) expectRun(t, `out = !false || false`, nil, true) expectRun(t, `out = 1 && 2`, nil, 2) expectRun(t, `out = 1 || 2`, nil, 1) expectRun(t, `out = 1 && 0`, nil, 0) expectRun(t, `out = 1 || 0`, nil, 1) expectRun(t, `out = 1 && (0 || 2)`, nil, 2) expectRun(t, `out = 0 || (0 || 2)`, nil, 2) expectRun(t, `out = 0 || (0 && 2)`, nil, 0) expectRun(t, `out = 0 || (2 && 0)`, nil, 0) expectRun(t, `t:=func() {out = 3; return true}; f:=func() {out = 7; return false}; t() && f()`, nil, 7) expectRun(t, `t:=func() {out = 3; return true}; f:=func() {out = 7; return false}; f() && t()`, nil, 7) expectRun(t, `t:=func() {out = 3; return true}; f:=func() {out = 7; return false}; f() || t()`, nil, 3) expectRun(t, `t:=func() {out = 3; return true}; f:=func() {out = 7; return false}; t() || f()`, nil, 3) expectRun(t, `t:=func() {out = 3; return true}; f:=func() {out = 7; return false}; !t() && f()`, nil, 3) expectRun(t, `t:=func() {out = 3; return true}; f:=func() {out = 7; return false}; !f() && t()`, nil, 3) expectRun(t, `t:=func() {out = 3; return true}; f:=func() {out = 7; return false}; !f() || t()`, nil, 7) expectRun(t, `t:=func() {out = 3; return true}; f:=func() {out = 7; return false}; !t() || f()`, nil, 7) } func TestMap(t *testing.T) { expectRun(t, ` out = { one: 10 - 9, two: 1 + 1, three: 6 / 2 }`, nil, MAP{ "one": 1, "two": 2, "three": 3, }) expectRun(t, ` out = { "one": 10 - 9, "two": 1 + 1, "three": 6 / 2 }`, nil, MAP{ "one": 1, "two": 2, "three": 3, }) expectRun(t, `out = {foo: 5}["foo"]`, nil, 5) expectRun(t, `out = {foo: 5}["bar"]`, nil, tengo.UndefinedValue) expectRun(t, `key := "foo"; out = {foo: 5}[key]`, nil, 5) expectRun(t, `out = {}["foo"]`, nil, tengo.UndefinedValue) expectRun(t, ` m := { foo: func(x) { return x * 2 } } out = m["foo"](2) + m["foo"](3) `, nil, 10) // map assignment is copy-by-reference expectRun(t, `m1 := {k1: 1, k2: "foo"}; m2 := m1; m1.k1 = 5; out = m2.k1`, nil, 5) expectRun(t, `m1 := {k1: 1, k2: "foo"}; m2 := m1; m2.k1 = 3; out = m1.k1`, nil, 3) expectRun(t, `func() { m1 := {k1: 1, k2: "foo"}; m2 := m1; m1.k1 = 5; out = m2.k1 }()`, nil, 5) expectRun(t, `func() { m1 := {k1: 1, k2: "foo"}; m2 := m1; m2.k1 = 3; out = m1.k1 }()`, nil, 3) } func TestBuiltin(t *testing.T) { m := Opts().Module("math", &tengo.BuiltinModule{ Attrs: map[string]tengo.Object{ "abs": &tengo.UserFunction{ Name: "abs", Value: func(a ...tengo.Object) (tengo.Object, error) { v, _ := tengo.ToFloat64(a[0]) return &tengo.Float{Value: math.Abs(v)}, nil }, }, }, }) // builtin expectRun(t, `math := import("math"); out = math.abs(1)`, m, 1.0) expectRun(t, `math := import("math"); out = math.abs(-1)`, m, 1.0) expectRun(t, `math := import("math"); out = math.abs(1.0)`, m, 1.0) expectRun(t, `math := import("math"); out = math.abs(-1.0)`, m, 1.0) } func TestUserModules(t *testing.T) { // export none expectRun(t, `out = import("mod1")`, Opts().Module("mod1", `fn := func() { return 5.0 }; a := 2`), tengo.UndefinedValue) // export values expectRun(t, `out = import("mod1")`, Opts().Module("mod1", `export 5`), 5) expectRun(t, `out = import("mod1")`, Opts().Module("mod1", `export "foo"`), "foo") // export compound types expectRun(t, `out = import("mod1")`, Opts().Module("mod1", `export [1, 2, 3]`), IARR{1, 2, 3}) expectRun(t, `out = import("mod1")`, Opts().Module("mod1", `export {a: 1, b: 2}`), IMAP{"a": 1, "b": 2}) // export value is immutable expectError(t, `m1 := import("mod1"); m1.a = 5`, Opts().Module("mod1", `export {a: 1, b: 2}`), "not index-assignable") expectError(t, `m1 := import("mod1"); m1[1] = 5`, Opts().Module("mod1", `export [1, 2, 3]`), "not index-assignable") // code after export statement will not be executed expectRun(t, `out = import("mod1")`, Opts().Module("mod1", `a := 10; export a; a = 20`), 10) expectRun(t, `out = import("mod1")`, Opts().Module("mod1", `a := 10; export a; a = 20; export a`), 10) // export function expectRun(t, `out = import("mod1")()`, Opts().Module("mod1", `export func() { return 5.0 }`), 5.0) // export function that reads module-global variable expectRun(t, `out = import("mod1")()`, Opts().Module("mod1", `a := 1.5; export func() { return a + 5.0 }`), 6.5) // export function that read local variable expectRun(t, `out = import("mod1")()`, Opts().Module("mod1", `export func() { a := 1.5; return a + 5.0 }`), 6.5) // export function that read free variables expectRun(t, `out = import("mod1")()`, Opts().Module("mod1", `export func() { a := 1.5; return func() { return a + 5.0 }() }`), 6.5) // recursive function in module expectRun(t, `out = import("mod1")`, Opts().Module( "mod1", ` a := func(x) { return x == 0 ? 0 : x + a(x-1) } export a(5) `), 15) expectRun(t, `out = import("mod1")`, Opts().Module( "mod1", ` export func() { a := func(x) { return x == 0 ? 0 : x + a(x-1) } return a(5) }() `), 15) // (main) -> mod1 -> mod2 expectRun(t, `out = import("mod1")()`, Opts().Module("mod1", `export import("mod2")`). Module("mod2", `export func() { return 5.0 }`), 5.0) // (main) -> mod1 -> mod2 // -> mod2 expectRun(t, `import("mod1"); out = import("mod2")()`, Opts().Module("mod1", `export import("mod2")`). Module("mod2", `export func() { return 5.0 }`), 5.0) // (main) -> mod1 -> mod2 -> mod3 // -> mod2 -> mod3 expectRun(t, `import("mod1"); out = import("mod2")()`, Opts().Module("mod1", `export import("mod2")`). Module("mod2", `export import("mod3")`). Module("mod3", `export func() { return 5.0 }`), 5.0) // cyclic imports // (main) -> mod1 -> mod2 -> mod1 expectError(t, `import("mod1")`, Opts().Module("mod1", `import("mod2")`). Module("mod2", `import("mod1")`), "Compile Error: cyclic module import: mod1\n\tat mod2:1:1") // (main) -> mod1 -> mod2 -> mod3 -> mod1 expectError(t, `import("mod1")`, Opts().Module("mod1", `import("mod2")`). Module("mod2", `import("mod3")`). Module("mod3", `import("mod1")`), "Compile Error: cyclic module import: mod1\n\tat mod3:1:1") // (main) -> mod1 -> mod2 -> mod3 -> mod2 expectError(t, `import("mod1")`, Opts().Module("mod1", `import("mod2")`). Module("mod2", `import("mod3")`). Module("mod3", `import("mod2")`), "Compile Error: cyclic module import: mod2\n\tat mod3:1:1") // unknown modules expectError(t, `import("mod0")`, Opts().Module("mod1", `a := 5`), "module 'mod0' not found") expectError(t, `import("mod1")`, Opts().Module("mod1", `import("mod2")`), "module 'mod2' not found") // module is immutable but its variables is not necessarily immutable. expectRun(t, `m1 := import("mod1"); m1.a.b = 5; out = m1.a.b`, Opts().Module("mod1", `export {a: {b: 3}}`), 5) // make sure module has same builtin functions expectRun(t, `out = import("mod1")`, Opts().Module("mod1", `export func() { return type_name(0) }()`), "int") // 'export' statement is ignored outside module expectRun(t, `a := 5; export func() { a = 10 }(); out = a`, Opts().Skip2ndPass(), 5) // 'export' must be in the top-level expectError(t, `import("mod1")`, Opts().Module("mod1", `func() { export 5 }()`), "Compile Error: export not allowed inside function\n\tat mod1:1:10") expectError(t, `import("mod1")`, Opts().Module("mod1", `func() { func() { export 5 }() }()`), "Compile Error: export not allowed inside function\n\tat mod1:1:19") // module cannot access outer scope expectError(t, `a := 5; import("mod1")`, Opts().Module("mod1", `export a`), "Compile Error: unresolved reference 'a'\n\tat mod1:1:8") // runtime error within modules expectError(t, ` a := 1; b := import("mod1"); b(a)`, Opts().Module("mod1", ` export func(a) { a() } `), "Runtime Error: not callable: int\n\tat mod1:3:4\n\tat test:4:1") // module skipping export expectRun(t, `out = import("mod0")`, Opts().Module("mod0", ``), tengo.UndefinedValue) expectRun(t, `out = import("mod0")`, Opts().Module("mod0", `if 1 { export true }`), true) expectRun(t, `out = import("mod0")`, Opts().Module("mod0", `if 0 { export true }`), tengo.UndefinedValue) expectRun(t, `out = import("mod0")`, Opts().Module("mod0", `if 1 { } else { export true }`), tengo.UndefinedValue) expectRun(t, `out = import("mod0")`, Opts().Module("mod0", `for v:=0;;v++ { if v == 3 { export true } }`), true) expectRun(t, `out = import("mod0")`, Opts().Module("mod0", `for v:=0;;v++ { if v == 3 { break } }`), tengo.UndefinedValue) // duplicate compiled functions // NOTE: module "mod" has a function with some local variable, and it's // imported twice by the main script. That causes the same CompiledFunction // put in constants twice and the Bytecode optimization (removing duplicate // constants) should still work correctly. expectRun(t, ` m1 := import("mod") m2 := import("mod") out = m1.x `, Opts().Module("mod", ` f1 := func(a, b) { c := a + b + 1 return a + b + 1 } export { x: 1 } `), 1) } func TestModuleBlockScopes(t *testing.T) { m := Opts().Module("rand", &tengo.BuiltinModule{ Attrs: map[string]tengo.Object{ "intn": &tengo.UserFunction{ Name: "abs", Value: func(a ...tengo.Object) (tengo.Object, error) { v, _ := tengo.ToInt64(a[0]) return &tengo.Int{Value: rand.Int63n(v)}, nil }, }, }, }) // block scopes in module expectRun(t, `out = import("mod1")()`, m.Module( "mod1", ` rand := import("rand") foo := func() { return 1 } export func() { rand.intn(3) return foo() }`), 1) expectRun(t, `out = import("mod1")()`, m.Module( "mod1", ` rand := import("rand") foo := func() { return 1 } export func() { rand.intn(3) if foo() {} return 10 } `), 10) expectRun(t, `out = import("mod1")()`, m.Module( "mod1", ` rand := import("rand") foo := func() { return 1 } export func() { rand.intn(3) if true { foo() } return 10 } `), 10) } func TestBangOperator(t *testing.T) { expectRun(t, `out = !true`, nil, false) expectRun(t, `out = !false`, nil, true) expectRun(t, `out = !0`, nil, true) expectRun(t, `out = !5`, nil, false) expectRun(t, `out = !!true`, nil, true) expectRun(t, `out = !!false`, nil, false) expectRun(t, `out = !!5`, nil, true) } func TestObjectsLimit(t *testing.T) { testAllocsLimit(t, `5`, 0) testAllocsLimit(t, `5 + 5`, 1) testAllocsLimit(t, `a := [1, 2, 3]`, 1) testAllocsLimit(t, `a := 1; b := 2; c := 3; d := [a, b, c]`, 1) testAllocsLimit(t, `a := {foo: 1, bar: 2}`, 1) testAllocsLimit(t, `a := 1; b := 2; c := {foo: a, bar: b}`, 1) testAllocsLimit(t, ` f := func() { return 5 + 5 } a := f() + 5 `, 2) testAllocsLimit(t, ` f := func() { return 5 + 5 } a := f() `, 1) testAllocsLimit(t, ` a := [] f := func() { a = append(a, 5) } f() f() f() `, 4) } func testAllocsLimit(t *testing.T, src string, limit int64) { expectRun(t, src, Opts().Skip2ndPass(), tengo.UndefinedValue) // no limit expectRun(t, src, Opts().MaxAllocs(limit).Skip2ndPass(), tengo.UndefinedValue) expectRun(t, src, Opts().MaxAllocs(limit+1).Skip2ndPass(), tengo.UndefinedValue) if limit > 1 { expectError(t, src, Opts().MaxAllocs(limit-1).Skip2ndPass(), "allocation limit exceeded") } if limit > 2 { expectError(t, src, Opts().MaxAllocs(limit-2).Skip2ndPass(), "allocation limit exceeded") } } func TestReturn(t *testing.T) { expectRun(t, `out = func() { return 10; }()`, nil, 10) expectRun(t, `out = func() { return 10; return 9; }()`, nil, 10) expectRun(t, `out = func() { return 2 * 5; return 9 }()`, nil, 10) expectRun(t, `out = func() { 9; return 2 * 5; return 9 }()`, nil, 10) expectRun(t, ` out = func() { if (10 > 1) { if (10 > 1) { return 10; } return 1; } }()`, nil, 10) expectRun(t, `f1 := func() { return 2 * 5; }; out = f1()`, nil, 10) } func TestVMScopes(t *testing.T) { // shadowed global variable expectRun(t, ` c := 5 if a := 3; a { c := 6 } else { c := 7 } out = c `, nil, 5) // shadowed local variable expectRun(t, ` func() { c := 5 if a := 3; a { c := 6 } else { c := 7 } out = c }() `, nil, 5) // 'b' is declared in 2 separate blocks expectRun(t, ` c := 5 if a := 3; a { b := 8 c = b } else { b := 9 c = b } out = c `, nil, 8) // shadowing inside for statement expectRun(t, ` a := 4 b := 5 for i:=0;i<3;i++ { b := 6 for j:=0;j<2;j++ { b := 7 a = i*j } } out = a`, nil, 2) // shadowing variable declared in init statement expectRun(t, ` if a := 5; a { a := 6 out = a }`, nil, 6) expectRun(t, ` a := 4 if a := 5; a { a := 6 out = a }`, nil, 6) expectRun(t, ` a := 4 if a := 0; a { a := 6 out = a } else { a := 7 out = a }`, nil, 7) expectRun(t, ` a := 4 if a := 0; a { out = a } else { out = a }`, nil, 0) // shadowing function level expectRun(t, ` a := 5 func() { a := 6 a = 7 }() out = a `, nil, 5) expectRun(t, ` a := 5 func() { if a := 7; true { a = 8 } }() out = a `, nil, 5) } func TestSelector(t *testing.T) { expectRun(t, `a := {k1: 5, k2: "foo"}; out = a.k1`, nil, 5) expectRun(t, `a := {k1: 5, k2: "foo"}; out = a.k2`, nil, "foo") expectRun(t, `a := {k1: 5, k2: "foo"}; out = a.k3`, nil, tengo.UndefinedValue) expectRun(t, ` a := { b: { c: 4, a: false }, c: "foo bar" } out = a.b.c`, nil, 4) expectRun(t, ` a := { b: { c: 4, a: false }, c: "foo bar" } b := a.x.c`, nil, tengo.UndefinedValue) expectRun(t, ` a := { b: { c: 4, a: false }, c: "foo bar" } b := a.x.y`, nil, tengo.UndefinedValue) expectRun(t, `a := {b: 1, c: "foo"}; a.b = 2; out = a.b`, nil, 2) expectRun(t, `a := {b: 1, c: "foo"}; a.c = 2; out = a.c`, nil, 2) // type not checked on sub-field expectRun(t, `a := {b: {c: 1}}; a.b.c = 2; out = a.b.c`, nil, 2) expectRun(t, `a := {b: 1}; a.c = 2; out = a`, nil, MAP{"b": 1, "c": 2}) expectRun(t, `a := {b: {c: 1}}; a.b.d = 2; out = a`, nil, MAP{"b": MAP{"c": 1, "d": 2}}) expectRun(t, `func() { a := {b: 1, c: "foo"}; a.b = 2; out = a.b }()`, nil, 2) expectRun(t, `func() { a := {b: 1, c: "foo"}; a.c = 2; out = a.c }()`, nil, 2) // type not checked on sub-field expectRun(t, `func() { a := {b: {c: 1}}; a.b.c = 2; out = a.b.c }()`, nil, 2) expectRun(t, `func() { a := {b: 1}; a.c = 2; out = a }()`, nil, MAP{"b": 1, "c": 2}) expectRun(t, `func() { a := {b: {c: 1}}; a.b.d = 2; out = a }()`, nil, MAP{"b": MAP{"c": 1, "d": 2}}) expectRun(t, `func() { a := {b: 1, c: "foo"}; func() { a.b = 2 }(); out = a.b }()`, nil, 2) expectRun(t, `func() { a := {b: 1, c: "foo"}; func() { a.c = 2 }(); out = a.c }()`, nil, 2) // type not checked on sub-field expectRun(t, `func() { a := {b: {c: 1}}; func() { a.b.c = 2 }(); out = a.b.c }()`, nil, 2) expectRun(t, `func() { a := {b: 1}; func() { a.c = 2 }(); out = a }()`, nil, MAP{"b": 1, "c": 2}) expectRun(t, `func() { a := {b: {c: 1}}; func() { a.b.d = 2 }(); out = a }()`, nil, MAP{"b": MAP{"c": 1, "d": 2}}) expectRun(t, ` a := { b: [1, 2, 3], c: { d: 8, e: "foo", f: [9, 8] } } out = [a.b[2], a.c.d, a.c.e, a.c.f[1]] `, nil, ARR{3, 8, "foo", 8}) expectRun(t, ` func() { a := [1, 2, 3] b := 9 a[1] = b b = 7 // make sure a[1] has a COPY of value of 'b' out = a[1] }() `, nil, 9) expectError(t, `a := {b: {c: 1}}; a.d.c = 2`, nil, "not index-assignable") expectError(t, `a := [1, 2, 3]; a.b = 2`, nil, "invalid index type") expectError(t, `a := "foo"; a.b = 2`, nil, "not index-assignable") expectError(t, `func() { a := {b: {c: 1}}; a.d.c = 2 }()`, nil, "not index-assignable") expectError(t, `func() { a := [1, 2, 3]; a.b = 2 }()`, nil, "invalid index type") expectError(t, `func() { a := "foo"; a.b = 2 }()`, nil, "not index-assignable") } func TestSourceModules(t *testing.T) { testEnumModule(t, `out = enum.key(0, 20)`, 0) testEnumModule(t, `out = enum.key(10, 20)`, 10) testEnumModule(t, `out = enum.value(0, 0)`, 0) testEnumModule(t, `out = enum.value(10, 20)`, 20) testEnumModule(t, `out = enum.all([], enum.value)`, true) testEnumModule(t, `out = enum.all([1], enum.value)`, true) testEnumModule(t, `out = enum.all([true, 1], enum.value)`, true) testEnumModule(t, `out = enum.all([true, 0], enum.value)`, false) testEnumModule(t, `out = enum.all([true, 0, 1], enum.value)`, false) testEnumModule(t, `out = enum.all(immutable([true, 0, 1]), enum.value)`, false) // immutable-array testEnumModule(t, `out = enum.all({}, enum.value)`, true) testEnumModule(t, `out = enum.all({a:1}, enum.value)`, true) testEnumModule(t, `out = enum.all({a:true, b:1}, enum.value)`, true) testEnumModule(t, `out = enum.all(immutable({a:true, b:1}), enum.value)`, true) // immutable-map testEnumModule(t, `out = enum.all({a:true, b:0}, enum.value)`, false) testEnumModule(t, `out = enum.all({a:true, b:0, c:1}, enum.value)`, false) testEnumModule(t, `out = enum.all(0, enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.all("123", enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.any([], enum.value)`, false) testEnumModule(t, `out = enum.any([1], enum.value)`, true) testEnumModule(t, `out = enum.any([true, 1], enum.value)`, true) testEnumModule(t, `out = enum.any([true, 0], enum.value)`, true) testEnumModule(t, `out = enum.any([true, 0, 1], enum.value)`, true) testEnumModule(t, `out = enum.any(immutable([true, 0, 1]), enum.value)`, true) // immutable-array testEnumModule(t, `out = enum.any([false], enum.value)`, false) testEnumModule(t, `out = enum.any([false, 0], enum.value)`, false) testEnumModule(t, `out = enum.any({}, enum.value)`, false) testEnumModule(t, `out = enum.any({a:1}, enum.value)`, true) testEnumModule(t, `out = enum.any({a:true, b:1}, enum.value)`, true) testEnumModule(t, `out = enum.any({a:true, b:0}, enum.value)`, true) testEnumModule(t, `out = enum.any({a:true, b:0, c:1}, enum.value)`, true) testEnumModule(t, `out = enum.any(immutable({a:true, b:0, c:1}), enum.value)`, true) // immutable-map testEnumModule(t, `out = enum.any({a:false}, enum.value)`, false) testEnumModule(t, `out = enum.any({a:false, b:0}, enum.value)`, false) testEnumModule(t, `out = enum.any(0, enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.any("123", enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.chunk([], 1)`, ARR{}) testEnumModule(t, `out = enum.chunk([1], 1)`, ARR{ARR{1}}) testEnumModule(t, `out = enum.chunk([1,2,3], 1)`, ARR{ARR{1}, ARR{2}, ARR{3}}) testEnumModule(t, `out = enum.chunk([1,2,3], 2)`, ARR{ARR{1, 2}, ARR{3}}) testEnumModule(t, `out = enum.chunk([1,2,3], 3)`, ARR{ARR{1, 2, 3}}) testEnumModule(t, `out = enum.chunk([1,2,3], 4)`, ARR{ARR{1, 2, 3}}) testEnumModule(t, `out = enum.chunk([1,2,3,4], 3)`, ARR{ARR{1, 2, 3}, ARR{4}}) testEnumModule(t, `out = enum.chunk([], 0)`, tengo.UndefinedValue) // size=0: undefined testEnumModule(t, `out = enum.chunk([1], 0)`, tengo.UndefinedValue) // size=0: undefined testEnumModule(t, `out = enum.chunk([1,2,3], 0)`, tengo.UndefinedValue) // size=0: undefined testEnumModule(t, `out = enum.chunk({a:1,b:2,c:3}, 1)`, tengo.UndefinedValue) // map: undefined testEnumModule(t, `out = enum.chunk(0, 1)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.chunk("123", 1)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.at([], 0)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at([], 1)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at([], -1)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at(["one"], 0)`, "one") testEnumModule(t, `out = enum.at(["one"], 1)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at(["one"], -1)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at(["one","two","three"], 0)`, "one") testEnumModule(t, `out = enum.at(["one","two","three"], 1)`, "two") testEnumModule(t, `out = enum.at(["one","two","three"], 2)`, "three") testEnumModule(t, `out = enum.at(["one","two","three"], -1)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at(["one","two","three"], 3)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at(["one","two","three"], "1")`, tengo.UndefinedValue) // non-int index: undefined testEnumModule(t, `out = enum.at({}, "a")`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at({a:"one"}, "a")`, "one") testEnumModule(t, `out = enum.at({a:"one"}, "b")`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at({a:"one",b:"two",c:"three"}, "a")`, "one") testEnumModule(t, `out = enum.at({a:"one",b:"two",c:"three"}, "b")`, "two") testEnumModule(t, `out = enum.at({a:"one",b:"two",c:"three"}, "c")`, "three") testEnumModule(t, `out = enum.at({a:"one",b:"two",c:"three"}, "d")`, tengo.UndefinedValue) testEnumModule(t, `out = enum.at({a:"one",b:"two",c:"three"}, 'a')`, tengo.UndefinedValue) // non-string index: undefined testEnumModule(t, `out = enum.at(0, 1)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.at("abc", 1)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out=0; enum.each([],func(k,v){out+=v})`, 0) testEnumModule(t, `out=0; enum.each([1,2,3],func(k,v){out+=v})`, 6) testEnumModule(t, `out=0; enum.each([1,2,3],func(k,v){out+=k})`, 3) testEnumModule(t, `out=0; enum.each({a:1,b:2,c:3},func(k,v){out+=v})`, 6) testEnumModule(t, `out=""; enum.each({a:1,b:2,c:3},func(k,v){out+=k}); out=len(out)`, 3) testEnumModule(t, `out=0; enum.each(5,func(k,v){out+=v})`, 0) // non-enumerable: no iteration testEnumModule(t, `out=0; enum.each("123",func(k,v){out+=v})`, 0) // non-enumerable: no iteration testEnumModule(t, `out = enum.filter([], enum.value)`, ARR{}) testEnumModule(t, `out = enum.filter([false,1,2], enum.value)`, ARR{1, 2}) testEnumModule(t, `out = enum.filter([false,1,0,2], enum.value)`, ARR{1, 2}) testEnumModule(t, `out = enum.filter({}, enum.value)`, tengo.UndefinedValue) // non-array: undefined testEnumModule(t, `out = enum.filter(0, enum.value)`, tengo.UndefinedValue) // non-array: undefined testEnumModule(t, `out = enum.filter("123", enum.value)`, tengo.UndefinedValue) // non-array: undefined testEnumModule(t, `out = enum.find([], enum.value)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.find([0], enum.value)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.find([1], enum.value)`, 1) testEnumModule(t, `out = enum.find([false,0,undefined,1], enum.value)`, 1) testEnumModule(t, `out = enum.find([1,2,3], enum.value)`, 1) testEnumModule(t, `out = enum.find({}, enum.value)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.find({a:0}, enum.value)`, tengo.UndefinedValue) 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(0, enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.find("123", enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.find_key([], enum.value)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.find_key([0], enum.value)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.find_key([1], enum.value)`, 0) testEnumModule(t, `out = enum.find_key([false,0,undefined,1], enum.value)`, 3) testEnumModule(t, `out = enum.find_key([1,2,3], enum.value)`, 0) testEnumModule(t, `out = enum.find_key({}, enum.value)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.find_key({a:0}, enum.value)`, tengo.UndefinedValue) testEnumModule(t, `out = enum.find_key({a:1}, enum.value)`, "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(0, enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.find_key("123", enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.map([], enum.value)`, ARR{}) testEnumModule(t, `out = enum.map([1,2,3], enum.value)`, ARR{1, 2, 3}) testEnumModule(t, `out = enum.map([1,2,3], enum.key)`, ARR{0, 1, 2}) testEnumModule(t, `out = enum.map([1,2,3], func(k,v) { return v*2 })`, ARR{2, 4, 6}) testEnumModule(t, `out = enum.map({}, enum.value)`, ARR{}) testEnumModule(t, `out = enum.map({a:1}, func(k,v) { return v*2 })`, ARR{2}) testEnumModule(t, `out = enum.map(0, enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined testEnumModule(t, `out = enum.map("123", enum.value)`, tengo.UndefinedValue) // non-enumerable: undefined } func testEnumModule(t *testing.T, input string, expected interface{}) { expectRun(t, `enum := import("enum"); `+input, Opts().Module("enum", stdlib.SourceModules["enum"]), expected) } func TestSrcModEnum(t *testing.T) { expectRun(t, ` x := import("enum") out = x.all([1, 2, 3], func(_, v) { return v >= 1 }) `, Opts().Stdlib(), true) expectRun(t, ` x := import("enum") out = x.all([1, 2, 3], func(_, v) { return v >= 2 }) `, Opts().Stdlib(), false) expectRun(t, ` x := import("enum") out = x.any([1, 2, 3], func(_, v) { return v >= 1 }) `, Opts().Stdlib(), true) expectRun(t, ` x := import("enum") out = x.any([1, 2, 3], func(_, v) { return v >= 2 }) `, Opts().Stdlib(), true) expectRun(t, ` x := import("enum") out = x.chunk([1, 2, 3], 1) `, Opts().Stdlib(), ARR{ARR{1}, ARR{2}, ARR{3}}) expectRun(t, ` x := import("enum") out = x.chunk([1, 2, 3], 2) `, Opts().Stdlib(), ARR{ARR{1, 2}, ARR{3}}) expectRun(t, ` x := import("enum") out = x.chunk([1, 2, 3], 3) `, Opts().Stdlib(), ARR{ARR{1, 2, 3}}) expectRun(t, ` x := import("enum") out = x.chunk([1, 2, 3], 4) `, Opts().Stdlib(), ARR{ARR{1, 2, 3}}) expectRun(t, ` x := import("enum") out = x.chunk([1, 2, 3, 4, 5, 6], 2) `, Opts().Stdlib(), ARR{ARR{1, 2}, ARR{3, 4}, ARR{5, 6}}) expectRun(t, ` x := import("enum") out = x.at([1, 2, 3], 0) `, Opts().Stdlib(), 1) } func TestVMStackOverflow(t *testing.T) { expectError(t, `f := func() { return f() + 1 }; f()`, nil, "stack overflow") } func TestString(t *testing.T) { expectRun(t, `out = "Hello World!"`, nil, "Hello World!") expectRun(t, `out = "Hello" + " " + "World!"`, nil, "Hello World!") expectRun(t, `out = "Hello" == "Hello"`, nil, true) expectRun(t, `out = "Hello" == "World"`, nil, false) expectRun(t, `out = "Hello" != "Hello"`, nil, false) expectRun(t, `out = "Hello" != "World"`, nil, true) expectRun(t, `out = "Hello" > "World"`, nil, false) expectRun(t, `out = "World" < "Hello"`, nil, false) expectRun(t, `out = "Hello" < "World"`, nil, true) expectRun(t, `out = "World" > "Hello"`, nil, true) expectRun(t, `out = "Hello" >= "World"`, nil, false) expectRun(t, `out = "Hello" <= "World"`, nil, true) expectRun(t, `out = "Hello" >= "Hello"`, nil, true) expectRun(t, `out = "World" <= "World"`, nil, true) // index operator str := "abcdef" strStr := `"abcdef"` strLen := 6 for idx := 0; idx < strLen; idx++ { expectRun(t, fmt.Sprintf("out = %s[%d]", strStr, idx), nil, str[idx]) expectRun(t, fmt.Sprintf("out = %s[0 + %d]", strStr, idx), nil, str[idx]) expectRun(t, fmt.Sprintf("out = %s[1 + %d - 1]", strStr, idx), nil, str[idx]) expectRun(t, fmt.Sprintf("idx := %d; out = %s[idx]", idx, strStr), nil, str[idx]) } expectRun(t, fmt.Sprintf("%s[%d]", strStr, -1), nil, tengo.UndefinedValue) expectRun(t, fmt.Sprintf("%s[%d]", strStr, strLen), nil, tengo.UndefinedValue) // slice operator for low := 0; low <= strLen; low++ { expectRun(t, fmt.Sprintf("out = %s[%d:%d]", strStr, low, low), nil, "") for high := low; high <= strLen; high++ { expectRun(t, fmt.Sprintf("out = %s[%d:%d]", strStr, low, high), nil, str[low:high]) expectRun(t, fmt.Sprintf("out = %s[0 + %d : 0 + %d]", strStr, low, high), nil, str[low:high]) expectRun(t, fmt.Sprintf("out = %s[1 + %d - 1 : 1 + %d - 1]", strStr, low, high), nil, str[low:high]) expectRun(t, fmt.Sprintf("out = %s[:%d]", strStr, high), nil, str[:high]) expectRun(t, fmt.Sprintf("out = %s[%d:]", strStr, low), nil, str[low:]) } } expectRun(t, fmt.Sprintf("out = %s[:]", strStr), nil, str[:]) expectRun(t, fmt.Sprintf("out = %s[:]", strStr), nil, str) expectRun(t, fmt.Sprintf("out = %s[%d:]", strStr, -1), nil, str) expectRun(t, fmt.Sprintf("out = %s[:%d]", strStr, strLen+1), nil, str) expectRun(t, fmt.Sprintf("out = %s[%d:%d]", strStr, 2, 2), nil, "") expectError(t, fmt.Sprintf("%s[:%d]", strStr, -1), nil, "invalid slice index") expectError(t, fmt.Sprintf("%s[%d:]", strStr, strLen+1), nil, "invalid slice index") expectError(t, fmt.Sprintf("%s[%d:%d]", strStr, 0, -1), nil, "invalid slice index") expectError(t, fmt.Sprintf("%s[%d:%d]", strStr, 2, 1), nil, "invalid slice index") // string concatenation with other types expectRun(t, `out = "foo" + 1`, nil, "foo1") // Float.String() returns the smallest number of digits // necessary such that ParseFloat will return f exactly. expectRun(t, `out = "foo" + 1.0`, nil, "foo1") // <- note '1' instead of '1.0' expectRun(t, `out = "foo" + 1.5`, nil, "foo1.5") expectRun(t, `out = "foo" + true`, nil, "footrue") expectRun(t, `out = "foo" + 'X'`, nil, "fooX") expectRun(t, `out = "foo" + error(5)`, nil, "fooerror: 5") expectRun(t, `out = "foo" + undefined`, nil, "foo") expectRun(t, `out = "foo" + [1,2,3]`, nil, "foo[1, 2, 3]") // also works with "+=" operator expectRun(t, `out = "foo"; out += 1.5`, nil, "foo1.5") // string concats works only when string is LHS expectError(t, `1 + "foo"`, nil, "invalid operation") expectError(t, `"foo" - "bar"`, nil, "invalid operation") } func TestTailCall(t *testing.T) { expectRun(t, ` fac := func(n, a) { if n == 1 { return a } return fac(n-1, n*a) } out = fac(5, 1)`, nil, 120) expectRun(t, ` fac := func(n, a) { if n == 1 { return a } x := {foo: fac} // indirection for test return x.foo(n-1, n*a) } out = fac(5, 1)`, nil, 120) expectRun(t, ` fib := func(x, s) { if x == 0 { return 0 + s } else if x == 1 { return 1 + s } return fib(x-1, fib(x-2, s)) } out = fib(15, 0)`, nil, 610) expectRun(t, ` fib := func(n, a, b) { if n == 0 { return a } else if n == 1 { return b } return fib(n-1, b, a + b) } out = fib(15, 0, 1)`, nil, 610) // global variable and no return value expectRun(t, ` out = 0 foo := func(a) { if a == 0 { return } out += a foo(a-1) } foo(10)`, nil, 55) expectRun(t, ` f1 := func() { f2 := 0 // TODO: this might be fixed in the future f2 = func(n, s) { if n == 0 { return s } return f2(n-1, n + s) } return f2(5, 0) } out = f1()`, nil, 15) // tail-call replacing loop // without tail-call optimization, this code will cause stack overflow expectRun(t, ` iter := func(n, max) { if n == max { return n } return iter(n+1, max) } out = iter(0, 9999) `, nil, 9999) expectRun(t, ` c := 0 iter := func(n, max) { if n == max { return } c++ iter(n+1, max) } iter(0, 9999) out = c `, nil, 9999) } // tail call with free vars func TestTailCallFreeVars(t *testing.T) { expectRun(t, ` func() { a := 10 f2 := 0 f2 = func(n, s) { if n == 0 { return s + a } return f2(n-1, n+s) } out = f2(5, 0) }()`, nil, 25) } func TestSpread(t *testing.T) { expectRun(t, ` f := func(...a) { return append(a, 3) } out = f([1, 2]...) `, nil, ARR{1, 2, 3}) expectRun(t, ` f := func(a, ...b) { return append([a], append(b, 3)...) } out = f([1, 2]...) `, nil, ARR{1, 2, 3}) expectRun(t, ` f := func(a, ...b) { return append(append([a], b), 3) } out = f(1, [2]...) `, nil, ARR{1, ARR{2}, 3}) expectRun(t, ` f1 := func(...a){ return append([3], a...) } f2 := func(a, ...b) { return f1(append([a], b...)...) } out = f2([1, 2]...) `, nil, ARR{3, 1, 2}) expectRun(t, ` f := func(a, ...b) { return func(...a) { return append([3], append(a, 4)...) }(a, b...) } out = f([1, 2]...) `, nil, ARR{3, 1, 2, 4}) expectRun(t, ` f := func(a, ...b) { c := append(b, 4) return func(){ return append(append([a], b...), c...) }() } out = f(1, immutable([2, 3])...) `, nil, ARR{1, 2, 3, 2, 3, 4}) expectError(t, `func(a) {}([1, 2]...)`, nil, "Runtime Error: wrong number of arguments: want=1, got=2") expectError(t, `func(a, b, c) {}([1, 2]...)`, nil, "Runtime Error: wrong number of arguments: want=3, got=2") } func TestSliceIndex(t *testing.T) { expectError(t, `undefined[:1]`, nil, "Runtime Error: not indexable") expectError(t, `123[-1:2]`, nil, "Runtime Error: not indexable") expectError(t, `{}[:]`, nil, "Runtime Error: not indexable") expectError(t, `a := 123[-1:2] ; a += 1`, nil, "Runtime Error: not indexable") } func expectRun( t *testing.T, input string, opts *testopts, expected interface{}, ) { if opts == nil { opts = Opts() } symbols := opts.symbols modules := opts.modules maxAllocs := opts.maxAllocs expectedObj := toObject(expected) if symbols == nil { symbols = make(map[string]tengo.Object) } symbols[testOut] = objectZeroCopy(expectedObj) // first pass: run the code normally { // parse file := parse(t, input) if file == nil { return } // compiler/VM res, trace, err := traceCompileRun(file, symbols, modules, maxAllocs) require.NoError(t, err, "\n"+strings.Join(trace, "\n")) require.Equal(t, expectedObj, res[testOut], "\n"+strings.Join(trace, "\n")) } // second pass: run the code as import module if !opts.skip2ndPass { file := parse(t, `out = import("__code__")`) if file == nil { return } expectedObj := toObject(expected) switch eo := expectedObj.(type) { case *tengo.Array: expectedObj = &tengo.ImmutableArray{Value: eo.Value} case *tengo.Map: expectedObj = &tengo.ImmutableMap{Value: eo.Value} } modules.AddSourceModule("__code__", []byte(fmt.Sprintf("out := undefined; %s; export out", input))) res, trace, err := traceCompileRun(file, symbols, modules, maxAllocs) require.NoError(t, err, "\n"+strings.Join(trace, "\n")) require.Equal(t, expectedObj, res[testOut], "\n"+strings.Join(trace, "\n")) } } func expectError( t *testing.T, input string, opts *testopts, expected string, ) { if opts == nil { opts = Opts() } symbols := opts.symbols modules := opts.modules maxAllocs := opts.maxAllocs expected = strings.TrimSpace(expected) if expected == "" { panic("expected must not be empty") } // parse program := parse(t, input) if program == nil { return } // compiler/VM _, trace, err := traceCompileRun(program, symbols, modules, maxAllocs) require.Error(t, err, "\n"+strings.Join(trace, "\n")) require.True(t, strings.Contains(err.Error(), expected), "expected error string: %s, got: %s\n%s", expected, err.Error(), strings.Join(trace, "\n")) } func expectErrorIs( t *testing.T, input string, opts *testopts, expected error, ) { if opts == nil { opts = Opts() } symbols := opts.symbols modules := opts.modules maxAllocs := opts.maxAllocs // parse program := parse(t, input) if program == nil { return } // compiler/VM _, trace, err := traceCompileRun(program, symbols, modules, maxAllocs) require.Error(t, err, "\n"+strings.Join(trace, "\n")) require.True(t, errors.Is(err, expected), "expected error is: %s, got: %s\n%s", expected.Error(), err.Error(), strings.Join(trace, "\n")) } func expectErrorAs( t *testing.T, input string, opts *testopts, expected interface{}, ) { if opts == nil { opts = Opts() } symbols := opts.symbols modules := opts.modules maxAllocs := opts.maxAllocs // parse program := parse(t, input) if program == nil { return } // compiler/VM _, trace, err := traceCompileRun(program, symbols, modules, maxAllocs) require.Error(t, err, "\n"+strings.Join(trace, "\n")) require.True(t, errors.As(err, expected), "expected error as: %v, got: %v\n%s", expected, err, strings.Join(trace, "\n")) } type vmTracer struct { Out []string } func (o *vmTracer) Write(p []byte) (n int, err error) { o.Out = append(o.Out, string(p)) return len(p), nil } func traceCompileRun( file *parser.File, symbols map[string]tengo.Object, modules *tengo.ModuleMap, maxAllocs int64, ) (res map[string]tengo.Object, trace []string, err error) { var v *tengo.VM defer func() { if e := recover(); e != nil { err = fmt.Errorf("panic: %v", e) // stack trace var stackTrace []string for i := 2; ; i += 1 { _, file, line, ok := _runtime.Caller(i) if !ok { break } stackTrace = append(stackTrace, fmt.Sprintf(" %s:%d", file, line)) } trace = append(trace, fmt.Sprintf("[Error Trace]\n\n %s\n", strings.Join(stackTrace, "\n "))) } }() globals := make([]tengo.Object, tengo.GlobalsSize) symTable := tengo.NewSymbolTable() for name, value := range symbols { sym := symTable.Define(name) // should not store pointer to 'value' variable // which is re-used in each iteration. valueCopy := value globals[sym.Index] = valueCopy } for idx, fn := range tengo.GetAllBuiltinFunctions() { symTable.DefineBuiltin(idx, fn.Name) } tr := &vmTracer{} c := tengo.NewCompiler(file.InputFile, symTable, nil, modules, tr) err = c.Compile(file) trace = append(trace, fmt.Sprintf("\n[Compiler Trace]\n\n%s", strings.Join(tr.Out, ""))) if err != nil { return } bytecode := c.Bytecode() bytecode.RemoveDuplicates() trace = append(trace, fmt.Sprintf("\n[Compiled Constants]\n\n%s", strings.Join(bytecode.FormatConstants(), "\n"))) trace = append(trace, fmt.Sprintf("\n[Compiled Instructions]\n\n%s\n", strings.Join(bytecode.FormatInstructions(), "\n"))) v = tengo.NewVM(bytecode, globals, maxAllocs) err = v.Run() { res = make(map[string]tengo.Object) for name := range symbols { sym, depth, ok := symTable.Resolve(name, false) if !ok || depth != 0 { err = fmt.Errorf("symbol not found: %s", name) return } res[name] = globals[sym.Index] } trace = append(trace, fmt.Sprintf("\n[Globals]\n\n%s", strings.Join(formatGlobals(globals), "\n"))) } if err == nil && !v.IsStackEmpty() { err = errors.New("non empty stack after execution") } return } func formatGlobals(globals []tengo.Object) (formatted []string) { for idx, global := range globals { if global == nil { return } formatted = append(formatted, fmt.Sprintf("[% 3d] %s (%s|%p)", idx, global.String(), reflect.TypeOf(global).Elem().Name(), global)) } return } func parse(t *testing.T, input string) *parser.File { testFileSet := parser.NewFileSet() testFile := testFileSet.AddFile("test", -1, len(input)) p := parser.NewParser(testFile, []byte(input), nil) file, err := p.ParseFile() require.NoError(t, err) return file } func errorObject(v interface{}) *tengo.Error { return &tengo.Error{Value: toObject(v)} } func toObject(v interface{}) tengo.Object { switch v := v.(type) { case tengo.Object: return v case string: return &tengo.String{Value: v} case int64: return &tengo.Int{Value: v} case int: // for convenience return &tengo.Int{Value: int64(v)} case bool: if v { return tengo.TrueValue } return tengo.FalseValue case rune: return &tengo.Char{Value: v} case byte: // for convenience return &tengo.Char{Value: rune(v)} case float64: return &tengo.Float{Value: v} case []byte: return &tengo.Bytes{Value: v} case MAP: objs := make(map[string]tengo.Object) for k, v := range v { objs[k] = toObject(v) } return &tengo.Map{Value: objs} case ARR: var objs []tengo.Object for _, e := range v { objs = append(objs, toObject(e)) } return &tengo.Array{Value: objs} case IMAP: objs := make(map[string]tengo.Object) for k, v := range v { objs[k] = toObject(v) } return &tengo.ImmutableMap{Value: objs} case IARR: var objs []tengo.Object for _, e := range v { objs = append(objs, toObject(e)) } return &tengo.ImmutableArray{Value: objs} } panic(fmt.Errorf("unknown type: %T", v)) } func objectZeroCopy(o tengo.Object) tengo.Object { switch o.(type) { case *tengo.Int: return &tengo.Int{} case *tengo.Float: return &tengo.Float{} case *tengo.Bool: return &tengo.Bool{} case *tengo.Char: return &tengo.Char{} case *tengo.String: return &tengo.String{} case *tengo.Array: return &tengo.Array{} case *tengo.Map: return &tengo.Map{} case *tengo.Undefined: return tengo.UndefinedValue case *tengo.Error: return &tengo.Error{} case *tengo.Bytes: return &tengo.Bytes{} case *tengo.ImmutableArray: return &tengo.ImmutableArray{} case *tengo.ImmutableMap: return &tengo.ImmutableMap{} case nil: panic("nil") default: panic(fmt.Errorf("unknown object type: %s", o.TypeName())) } }