├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── builtins.go ├── builtins_test.go ├── bytecode.go ├── bytecode_test.go ├── cmd ├── bench │ └── main.go └── tengo │ └── main.go ├── compiler.go ├── compiler_test.go ├── doc.go ├── docs ├── builtins.md ├── formatting.md ├── interoperability.md ├── objects.md ├── operators.md ├── runtime-types.md ├── stdlib-base64.md ├── stdlib-enum.md ├── stdlib-fmt.md ├── stdlib-hex.md ├── stdlib-json.md ├── stdlib-math.md ├── stdlib-os.md ├── stdlib-rand.md ├── stdlib-text.md ├── stdlib-times.md ├── stdlib.md ├── tengo-cli.md └── tutorial.md ├── errors.go ├── eval.go ├── eval_test.go ├── example_test.go ├── examples └── interoperability │ └── main.go ├── formatter.go ├── go.mod ├── go.sum ├── instructions.go ├── iterator.go ├── modules.go ├── objects.go ├── objects_test.go ├── parser ├── ast.go ├── ast_test.go ├── expr.go ├── file.go ├── opcodes.go ├── parser.go ├── parser_test.go ├── pos.go ├── scanner.go ├── scanner_test.go ├── source_file.go └── stmt.go ├── require └── require.go ├── script.go ├── script_test.go ├── stdlib ├── base64.go ├── base64_test.go ├── builtin_modules.go ├── errors.go ├── fmt.go ├── fmt_test.go ├── func_typedefs.go ├── func_typedefs_test.go ├── gensrcmods.go ├── hex.go ├── hex_test.go ├── json.go ├── json │ ├── decode.go │ ├── encode.go │ ├── json_test.go │ └── scanner.go ├── json_test.go ├── math.go ├── os.go ├── os_exec.go ├── os_file.go ├── os_process.go ├── os_test.go ├── rand.go ├── rand_test.go ├── source_modules.go ├── srcmod_enum.tengo ├── stdlib.go ├── stdlib_test.go ├── text.go ├── text_regexp.go ├── text_regexp_test.go ├── text_test.go ├── times.go └── times_test.go ├── symbol_table.go ├── symbol_table_test.go ├── tengo.go ├── tengo_test.go ├── testdata ├── cli │ ├── one.tengo │ ├── test.tengo │ ├── three.tengo │ └── two │ │ ├── five │ │ └── five.tengo │ │ ├── four │ │ └── four.tengo │ │ └── two.tengo └── issue286 │ ├── dos │ ├── cinco │ │ └── cinco.mshk │ ├── dos.mshk │ └── quatro │ │ └── quatro.mshk │ ├── test.mshk │ ├── tres.tengo │ └── uno.mshk ├── token └── token.go ├── variable.go ├── variable_test.go ├── vm.go └── vm_test.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: check out 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: set up Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: 1.18 18 | - name: run goreleaser 19 | uses: goreleaser/goreleaser-action@v5 20 | with: 21 | version: latest 22 | args: release --clean 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | name: build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: check out code 13 | uses: actions/checkout@v4 14 | - name: set up Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: 1.18 18 | - name: set up Go module cache 19 | uses: actions/cache@v4 20 | with: 21 | path: ~/go/pkg/mod 22 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 23 | restore-keys: | 24 | ${{ runner.os }}-go- 25 | - name: install golint 26 | run: go install golang.org/x/lint/golint@latest 27 | - name: run tests 28 | run: make test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | 3 | .idea -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | before: 4 | hooks: 5 | - go mod tidy 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | main: ./cmd/tengo/main.go 10 | goos: 11 | - darwin 12 | - linux 13 | - windows 14 | archives: 15 | - 16 | files: 17 | - none* 18 | checksum: 19 | name_template: 'checksums.txt' 20 | changelog: 21 | sort: asc 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Daniel Kang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | generate: 2 | go generate ./... 3 | 4 | lint: 5 | golint -set_exit_status ./... 6 | 7 | test: generate lint 8 | go test -race -cover ./... 9 | go run ./cmd/tengo -resolve ./testdata/cli/test.tengo 10 | 11 | fmt: 12 | go fmt ./... 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Tengo Language 2 | 3 | [![GoDoc](https://godoc.org/github.com/d5/tengo/v2?status.svg)](https://godoc.org/github.com/d5/tengo/v2) 4 | ![test](https://github.com/d5/tengo/workflows/test/badge.svg) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/d5/tengo)](https://goreportcard.com/report/github.com/d5/tengo) 6 | 7 | **Tengo is a small, dynamic, fast, secure script language for Go.** 8 | 9 | Tengo is **[fast](#benchmark)** and secure because it's compiled/executed as 10 | bytecode on stack-based VM that's written in native Go. 11 | 12 | ```golang 13 | /* The Tengo Language */ 14 | fmt := import("fmt") 15 | 16 | each := func(seq, fn) { 17 | for x in seq { fn(x) } 18 | } 19 | 20 | sum := func(init, seq) { 21 | each(seq, func(x) { init += x }) 22 | return init 23 | } 24 | 25 | fmt.println(sum(0, [1, 2, 3])) // "6" 26 | fmt.println(sum("", [1, 2, 3])) // "123" 27 | ``` 28 | 29 | > Test this Tengo code in the 30 | > [Tengo Playground](https://tengolang.com/?s=0c8d5d0d88f2795a7093d7f35ae12c3afa17bea3) 31 | 32 | ## Features 33 | 34 | - Simple and highly readable 35 | [Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md) 36 | - Dynamic typing with type coercion 37 | - Higher-order functions and closures 38 | - Immutable values 39 | - [Securely Embeddable](https://github.com/d5/tengo/blob/master/docs/interoperability.md) 40 | and [Extensible](https://github.com/d5/tengo/blob/master/docs/objects.md) 41 | - Compiler/runtime written in native Go _(no external deps or cgo)_ 42 | - Executable as a 43 | [standalone](https://github.com/d5/tengo/blob/master/docs/tengo-cli.md) 44 | language / REPL 45 | - Use cases: rules engine, [state machine](https://github.com/d5/go-fsm), 46 | data pipeline, [transpiler](https://github.com/d5/tengo2lua) 47 | 48 | ## Benchmark 49 | 50 | | | fib(35) | fibt(35) | Language (Type) | 51 | | :--- | ---: | ---: | :---: | 52 | | [**Tengo**](https://github.com/d5/tengo) | `2,315ms` | `3ms` | Tengo (VM) | 53 | | [go-lua](https://github.com/Shopify/go-lua) | `4,028ms` | `3ms` | Lua (VM) | 54 | | [GopherLua](https://github.com/yuin/gopher-lua) | `4,409ms` | `3ms` | Lua (VM) | 55 | | [goja](https://github.com/dop251/goja) | `5,194ms` | `4ms` | JavaScript (VM) | 56 | | [starlark-go](https://github.com/google/starlark-go) | `6,954ms` | `3ms` | Starlark (Interpreter) | 57 | | [gpython](https://github.com/go-python/gpython) | `11,324ms` | `4ms` | Python (Interpreter) | 58 | | [Yaegi](https://github.com/containous/yaegi) | `11,715ms` | `10ms` | Yaegi (Interpreter) | 59 | | [otto](https://github.com/robertkrimen/otto) | `48,539ms` | `6ms` | JavaScript (Interpreter) | 60 | | [Anko](https://github.com/mattn/anko) | `52,821ms` | `6ms` | Anko (Interpreter) | 61 | | - | - | - | - | 62 | | Go | `47ms` | `2ms` | Go (Native) | 63 | | Lua | `756ms` | `2ms` | Lua (Native) | 64 | | Python | `1,907ms` | `14ms` | Python2 (Native) | 65 | 66 | _* [fib(35)](https://github.com/d5/tengobench/blob/master/code/fib.tengo): 67 | Fibonacci(35)_ 68 | _* [fibt(35)](https://github.com/d5/tengobench/blob/master/code/fibtc.tengo): 69 | [tail-call](https://en.wikipedia.org/wiki/Tail_call) version of Fibonacci(35)_ 70 | _* **Go** does not read the source code from file, while all other cases do_ 71 | _* See [here](https://github.com/d5/tengobench) for commands/codes used_ 72 | 73 | ## Quick Start 74 | 75 | ``` 76 | go get github.com/d5/tengo/v2 77 | ``` 78 | 79 | A simple Go example code that compiles/runs Tengo script code with some input/output values: 80 | 81 | ```golang 82 | package main 83 | 84 | import ( 85 | "context" 86 | "fmt" 87 | 88 | "github.com/d5/tengo/v2" 89 | ) 90 | 91 | func main() { 92 | // create a new Script instance 93 | script := tengo.NewScript([]byte( 94 | `each := func(seq, fn) { 95 | for x in seq { fn(x) } 96 | } 97 | 98 | sum := 0 99 | mul := 1 100 | each([a, b, c, d], func(x) { 101 | sum += x 102 | mul *= x 103 | })`)) 104 | 105 | // set values 106 | _ = script.Add("a", 1) 107 | _ = script.Add("b", 9) 108 | _ = script.Add("c", 8) 109 | _ = script.Add("d", 4) 110 | 111 | // run the script 112 | compiled, err := script.RunContext(context.Background()) 113 | if err != nil { 114 | panic(err) 115 | } 116 | 117 | // retrieve values 118 | sum := compiled.Get("sum") 119 | mul := compiled.Get("mul") 120 | fmt.Println(sum, mul) // "22 288" 121 | } 122 | ``` 123 | 124 | 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: 125 | 126 | 127 | ```golang 128 | res, err := tengo.Eval(ctx, 129 | `input ? "success" : "fail"`, 130 | map[string]interface{}{"input": 1}) 131 | if err != nil { 132 | panic(err) 133 | } 134 | fmt.Println(res) // "success" 135 | ``` 136 | 137 | ## References 138 | 139 | - [Language Syntax](https://github.com/d5/tengo/blob/master/docs/tutorial.md) 140 | - [Object Types](https://github.com/d5/tengo/blob/master/docs/objects.md) 141 | - [Runtime Types](https://github.com/d5/tengo/blob/master/docs/runtime-types.md) 142 | and [Operators](https://github.com/d5/tengo/blob/master/docs/operators.md) 143 | - [Builtin Functions](https://github.com/d5/tengo/blob/master/docs/builtins.md) 144 | - [Interoperability](https://github.com/d5/tengo/blob/master/docs/interoperability.md) 145 | - [Tengo CLI](https://github.com/d5/tengo/blob/master/docs/tengo-cli.md) 146 | - [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md) 147 | - Syntax Highlighters: [VSCode](https://github.com/lissein/vscode-tengo), [Atom](https://github.com/d5/tengo-atom), [Vim](https://github.com/geseq/tengo-vim) 148 | - **Why the name Tengo?** It's from [1Q84](https://en.wikipedia.org/wiki/1Q84). 149 | 150 | 151 | -------------------------------------------------------------------------------- /bytecode.go: -------------------------------------------------------------------------------- 1 | package tengo 2 | 3 | import ( 4 | "encoding/gob" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | 9 | "github.com/d5/tengo/v2/parser" 10 | ) 11 | 12 | // Bytecode is a compiled instructions and constants. 13 | type Bytecode struct { 14 | FileSet *parser.SourceFileSet 15 | MainFunction *CompiledFunction 16 | Constants []Object 17 | } 18 | 19 | // Encode writes Bytecode data to the writer. 20 | func (b *Bytecode) Encode(w io.Writer) error { 21 | enc := gob.NewEncoder(w) 22 | if err := enc.Encode(b.FileSet); err != nil { 23 | return err 24 | } 25 | if err := enc.Encode(b.MainFunction); err != nil { 26 | return err 27 | } 28 | return enc.Encode(b.Constants) 29 | } 30 | 31 | // CountObjects returns the number of objects found in Constants. 32 | func (b *Bytecode) CountObjects() int { 33 | n := 0 34 | for _, c := range b.Constants { 35 | n += CountObjects(c) 36 | } 37 | return n 38 | } 39 | 40 | // FormatInstructions returns human readable string representations of 41 | // compiled instructions. 42 | func (b *Bytecode) FormatInstructions() []string { 43 | return FormatInstructions(b.MainFunction.Instructions, 0) 44 | } 45 | 46 | // FormatConstants returns human readable string representations of 47 | // compiled constants. 48 | func (b *Bytecode) FormatConstants() (output []string) { 49 | for cidx, cn := range b.Constants { 50 | switch cn := cn.(type) { 51 | case *CompiledFunction: 52 | output = append(output, fmt.Sprintf( 53 | "[% 3d] (Compiled Function|%p)", cidx, &cn)) 54 | for _, l := range FormatInstructions(cn.Instructions, 0) { 55 | output = append(output, fmt.Sprintf(" %s", l)) 56 | } 57 | default: 58 | output = append(output, fmt.Sprintf("[% 3d] %s (%s|%p)", 59 | cidx, cn, reflect.TypeOf(cn).Elem().Name(), &cn)) 60 | } 61 | } 62 | return 63 | } 64 | 65 | // Decode reads Bytecode data from the reader. 66 | func (b *Bytecode) Decode(r io.Reader, modules *ModuleMap) error { 67 | if modules == nil { 68 | modules = NewModuleMap() 69 | } 70 | 71 | dec := gob.NewDecoder(r) 72 | if err := dec.Decode(&b.FileSet); err != nil { 73 | return err 74 | } 75 | // TODO: files in b.FileSet.File does not have their 'set' field properly 76 | // set to b.FileSet as it's private field and not serialized by gob 77 | // encoder/decoder. 78 | if err := dec.Decode(&b.MainFunction); err != nil { 79 | return err 80 | } 81 | if err := dec.Decode(&b.Constants); err != nil { 82 | return err 83 | } 84 | for i, v := range b.Constants { 85 | fv, err := fixDecodedObject(v, modules) 86 | if err != nil { 87 | return err 88 | } 89 | b.Constants[i] = fv 90 | } 91 | return nil 92 | } 93 | 94 | // RemoveDuplicates finds and remove the duplicate values in Constants. 95 | // Note this function mutates Bytecode. 96 | func (b *Bytecode) RemoveDuplicates() { 97 | var deduped []Object 98 | 99 | indexMap := make(map[int]int) // mapping from old constant index to new index 100 | fns := make(map[*CompiledFunction]int) 101 | ints := make(map[int64]int) 102 | strings := make(map[string]int) 103 | floats := make(map[float64]int) 104 | chars := make(map[rune]int) 105 | immutableMaps := make(map[string]int) // for modules 106 | 107 | for curIdx, c := range b.Constants { 108 | switch c := c.(type) { 109 | case *CompiledFunction: 110 | if newIdx, ok := fns[c]; ok { 111 | indexMap[curIdx] = newIdx 112 | } else { 113 | newIdx = len(deduped) 114 | fns[c] = newIdx 115 | indexMap[curIdx] = newIdx 116 | deduped = append(deduped, c) 117 | } 118 | case *ImmutableMap: 119 | modName := inferModuleName(c) 120 | newIdx, ok := immutableMaps[modName] 121 | if modName != "" && ok { 122 | indexMap[curIdx] = newIdx 123 | } else { 124 | newIdx = len(deduped) 125 | immutableMaps[modName] = newIdx 126 | indexMap[curIdx] = newIdx 127 | deduped = append(deduped, c) 128 | } 129 | case *Int: 130 | if newIdx, ok := ints[c.Value]; ok { 131 | indexMap[curIdx] = newIdx 132 | } else { 133 | newIdx = len(deduped) 134 | ints[c.Value] = newIdx 135 | indexMap[curIdx] = newIdx 136 | deduped = append(deduped, c) 137 | } 138 | case *String: 139 | if newIdx, ok := strings[c.Value]; ok { 140 | indexMap[curIdx] = newIdx 141 | } else { 142 | newIdx = len(deduped) 143 | strings[c.Value] = newIdx 144 | indexMap[curIdx] = newIdx 145 | deduped = append(deduped, c) 146 | } 147 | case *Float: 148 | if newIdx, ok := floats[c.Value]; ok { 149 | indexMap[curIdx] = newIdx 150 | } else { 151 | newIdx = len(deduped) 152 | floats[c.Value] = newIdx 153 | indexMap[curIdx] = newIdx 154 | deduped = append(deduped, c) 155 | } 156 | case *Char: 157 | if newIdx, ok := chars[c.Value]; ok { 158 | indexMap[curIdx] = newIdx 159 | } else { 160 | newIdx = len(deduped) 161 | chars[c.Value] = newIdx 162 | indexMap[curIdx] = newIdx 163 | deduped = append(deduped, c) 164 | } 165 | default: 166 | panic(fmt.Errorf("unsupported top-level constant type: %s", 167 | c.TypeName())) 168 | } 169 | } 170 | 171 | // replace with de-duplicated constants 172 | b.Constants = deduped 173 | 174 | // update CONST instructions with new indexes 175 | // main function 176 | updateConstIndexes(b.MainFunction.Instructions, indexMap) 177 | // other compiled functions in constants 178 | for _, c := range b.Constants { 179 | switch c := c.(type) { 180 | case *CompiledFunction: 181 | updateConstIndexes(c.Instructions, indexMap) 182 | } 183 | } 184 | } 185 | 186 | func fixDecodedObject( 187 | o Object, 188 | modules *ModuleMap, 189 | ) (Object, error) { 190 | switch o := o.(type) { 191 | case *Bool: 192 | if o.IsFalsy() { 193 | return FalseValue, nil 194 | } 195 | return TrueValue, nil 196 | case *Undefined: 197 | return UndefinedValue, nil 198 | case *Array: 199 | for i, v := range o.Value { 200 | fv, err := fixDecodedObject(v, modules) 201 | if err != nil { 202 | return nil, err 203 | } 204 | o.Value[i] = fv 205 | } 206 | case *ImmutableArray: 207 | for i, v := range o.Value { 208 | fv, err := fixDecodedObject(v, modules) 209 | if err != nil { 210 | return nil, err 211 | } 212 | o.Value[i] = fv 213 | } 214 | case *Map: 215 | for k, v := range o.Value { 216 | fv, err := fixDecodedObject(v, modules) 217 | if err != nil { 218 | return nil, err 219 | } 220 | o.Value[k] = fv 221 | } 222 | case *ImmutableMap: 223 | modName := inferModuleName(o) 224 | if mod := modules.GetBuiltinModule(modName); mod != nil { 225 | return mod.AsImmutableMap(modName), nil 226 | } 227 | 228 | for k, v := range o.Value { 229 | // encoding of user function not supported 230 | if _, isUserFunction := v.(*UserFunction); isUserFunction { 231 | return nil, fmt.Errorf("user function not decodable") 232 | } 233 | 234 | fv, err := fixDecodedObject(v, modules) 235 | if err != nil { 236 | return nil, err 237 | } 238 | o.Value[k] = fv 239 | } 240 | } 241 | return o, nil 242 | } 243 | 244 | func updateConstIndexes(insts []byte, indexMap map[int]int) { 245 | i := 0 246 | for i < len(insts) { 247 | op := insts[i] 248 | numOperands := parser.OpcodeOperands[op] 249 | _, read := parser.ReadOperands(numOperands, insts[i+1:]) 250 | 251 | switch op { 252 | case parser.OpConstant: 253 | curIdx := int(insts[i+2]) | int(insts[i+1])<<8 254 | newIdx, ok := indexMap[curIdx] 255 | if !ok { 256 | panic(fmt.Errorf("constant index not found: %d", curIdx)) 257 | } 258 | copy(insts[i:], MakeInstruction(op, newIdx)) 259 | case parser.OpClosure: 260 | curIdx := int(insts[i+2]) | int(insts[i+1])<<8 261 | numFree := int(insts[i+3]) 262 | newIdx, ok := indexMap[curIdx] 263 | if !ok { 264 | panic(fmt.Errorf("constant index not found: %d", curIdx)) 265 | } 266 | copy(insts[i:], MakeInstruction(op, newIdx, numFree)) 267 | } 268 | 269 | i += 1 + read 270 | } 271 | } 272 | 273 | func inferModuleName(mod *ImmutableMap) string { 274 | if modName, ok := mod.Value["__module_name__"].(*String); ok { 275 | return modName.Value 276 | } 277 | return "" 278 | } 279 | 280 | func init() { 281 | gob.Register(&parser.SourceFileSet{}) 282 | gob.Register(&parser.SourceFile{}) 283 | gob.Register(&Array{}) 284 | gob.Register(&Bool{}) 285 | gob.Register(&Bytes{}) 286 | gob.Register(&Char{}) 287 | gob.Register(&CompiledFunction{}) 288 | gob.Register(&Error{}) 289 | gob.Register(&Float{}) 290 | gob.Register(&ImmutableArray{}) 291 | gob.Register(&ImmutableMap{}) 292 | gob.Register(&Int{}) 293 | gob.Register(&Map{}) 294 | gob.Register(&String{}) 295 | gob.Register(&Time{}) 296 | gob.Register(&Undefined{}) 297 | gob.Register(&UserFunction{}) 298 | } 299 | -------------------------------------------------------------------------------- /cmd/bench/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/d5/tengo/v2" 8 | "github.com/d5/tengo/v2/parser" 9 | ) 10 | 11 | func main() { 12 | runFib(35) 13 | runFibTC1(35) 14 | runFibTC2(35) 15 | } 16 | 17 | func runFib(n int) { 18 | start := time.Now() 19 | nativeResult := fib(n) 20 | nativeTime := time.Since(start) 21 | 22 | input := ` 23 | fib := func(x) { 24 | if x == 0 { 25 | return 0 26 | } else if x == 1 { 27 | return 1 28 | } 29 | 30 | return fib(x-1) + fib(x-2) 31 | } 32 | ` + fmt.Sprintf("out = fib(%d)", n) 33 | 34 | parseTime, compileTime, runTime, result, err := runBench([]byte(input)) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | if nativeResult != int(result.(*tengo.Int).Value) { 40 | panic(fmt.Errorf("wrong result: %d != %d", nativeResult, 41 | int(result.(*tengo.Int).Value))) 42 | } 43 | 44 | fmt.Println("-------------------------------------") 45 | fmt.Printf("fibonacci(%d)\n", n) 46 | fmt.Println("-------------------------------------") 47 | fmt.Printf("Result: %d\n", nativeResult) 48 | fmt.Printf("Go: %s\n", nativeTime) 49 | fmt.Printf("Parser: %s\n", parseTime) 50 | fmt.Printf("Compile: %s\n", compileTime) 51 | fmt.Printf("VM: %s\n", runTime) 52 | } 53 | 54 | func runFibTC1(n int) { 55 | start := time.Now() 56 | nativeResult := fibTC1(n, 0) 57 | nativeTime := time.Since(start) 58 | 59 | input := ` 60 | fib := func(x, s) { 61 | if x == 0 { 62 | return 0 + s 63 | } else if x == 1 { 64 | return 1 + s 65 | } 66 | 67 | return fib(x-1, fib(x-2, s)) 68 | } 69 | ` + fmt.Sprintf("out = fib(%d, 0)", n) 70 | 71 | parseTime, compileTime, runTime, result, err := runBench([]byte(input)) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | if nativeResult != int(result.(*tengo.Int).Value) { 77 | panic(fmt.Errorf("wrong result: %d != %d", nativeResult, 78 | int(result.(*tengo.Int).Value))) 79 | } 80 | 81 | fmt.Println("-------------------------------------") 82 | fmt.Printf("fibonacci(%d) (tail-call #1)\n", n) 83 | fmt.Println("-------------------------------------") 84 | fmt.Printf("Result: %d\n", nativeResult) 85 | fmt.Printf("Go: %s\n", nativeTime) 86 | fmt.Printf("Parser: %s\n", parseTime) 87 | fmt.Printf("Compile: %s\n", compileTime) 88 | fmt.Printf("VM: %s\n", runTime) 89 | } 90 | 91 | func runFibTC2(n int) { 92 | start := time.Now() 93 | nativeResult := fibTC2(n, 0, 1) 94 | nativeTime := time.Since(start) 95 | 96 | input := ` 97 | fib := func(x, a, b) { 98 | if x == 0 { 99 | return a 100 | } else if x == 1 { 101 | return b 102 | } 103 | 104 | return fib(x-1, b, a+b) 105 | } 106 | ` + fmt.Sprintf("out = fib(%d, 0, 1)", n) 107 | 108 | parseTime, compileTime, runTime, result, err := runBench([]byte(input)) 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | if nativeResult != int(result.(*tengo.Int).Value) { 114 | panic(fmt.Errorf("wrong result: %d != %d", nativeResult, 115 | int(result.(*tengo.Int).Value))) 116 | } 117 | 118 | fmt.Println("-------------------------------------") 119 | fmt.Printf("fibonacci(%d) (tail-call #2)\n", n) 120 | fmt.Println("-------------------------------------") 121 | fmt.Printf("Result: %d\n", nativeResult) 122 | fmt.Printf("Go: %s\n", nativeTime) 123 | fmt.Printf("Parser: %s\n", parseTime) 124 | fmt.Printf("Compile: %s\n", compileTime) 125 | fmt.Printf("VM: %s\n", runTime) 126 | } 127 | 128 | func fib(n int) int { 129 | if n == 0 { 130 | return 0 131 | } else if n == 1 { 132 | return 1 133 | } else { 134 | return fib(n-1) + fib(n-2) 135 | } 136 | } 137 | 138 | func fibTC1(n, s int) int { 139 | if n == 0 { 140 | return 0 + s 141 | } else if n == 1 { 142 | return 1 + s 143 | } 144 | return fibTC1(n-1, fibTC1(n-2, s)) 145 | } 146 | 147 | func fibTC2(n, a, b int) int { 148 | if n == 0 { 149 | return a 150 | } else if n == 1 { 151 | return b 152 | } else { 153 | return fibTC2(n-1, b, a+b) 154 | } 155 | } 156 | 157 | func runBench( 158 | input []byte, 159 | ) ( 160 | parseTime time.Duration, 161 | compileTime time.Duration, 162 | runTime time.Duration, 163 | result tengo.Object, 164 | err error, 165 | ) { 166 | var astFile *parser.File 167 | parseTime, astFile, err = parse(input) 168 | if err != nil { 169 | return 170 | } 171 | 172 | var bytecode *tengo.Bytecode 173 | compileTime, bytecode, err = compileFile(astFile) 174 | if err != nil { 175 | return 176 | } 177 | 178 | runTime, result, err = runVM(bytecode) 179 | 180 | return 181 | } 182 | 183 | func parse(input []byte) (time.Duration, *parser.File, error) { 184 | fileSet := parser.NewFileSet() 185 | inputFile := fileSet.AddFile("bench", -1, len(input)) 186 | 187 | start := time.Now() 188 | 189 | p := parser.NewParser(inputFile, input, nil) 190 | file, err := p.ParseFile() 191 | if err != nil { 192 | return time.Since(start), nil, err 193 | } 194 | 195 | return time.Since(start), file, nil 196 | } 197 | 198 | func compileFile(file *parser.File) (time.Duration, *tengo.Bytecode, error) { 199 | symTable := tengo.NewSymbolTable() 200 | symTable.Define("out") 201 | 202 | start := time.Now() 203 | 204 | c := tengo.NewCompiler(file.InputFile, symTable, nil, nil, nil) 205 | if err := c.Compile(file); err != nil { 206 | return time.Since(start), nil, err 207 | } 208 | 209 | bytecode := c.Bytecode() 210 | bytecode.RemoveDuplicates() 211 | 212 | return time.Since(start), bytecode, nil 213 | } 214 | 215 | func runVM( 216 | bytecode *tengo.Bytecode, 217 | ) (time.Duration, tengo.Object, error) { 218 | globals := make([]tengo.Object, tengo.GlobalsSize) 219 | 220 | start := time.Now() 221 | 222 | v := tengo.NewVM(bytecode, globals, -1) 223 | if err := v.Run(); err != nil { 224 | return time.Since(start), nil, err 225 | } 226 | 227 | return time.Since(start), globals[0], nil 228 | } 229 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // tengo is a small, dynamic, fast, secure script language for Go. 2 | 3 | package tengo 4 | -------------------------------------------------------------------------------- /docs/formatting.md: -------------------------------------------------------------------------------- 1 | # Formatting 2 | 3 | The format 'verbs' are derived from Go's but are simpler. 4 | 5 | ## The verbs 6 | 7 | ## General 8 | 9 | ``` 10 | %v the value in a default format 11 | %T a Go-syntax representation of the type of the value 12 | %% a literal percent sign; consumes no value 13 | ``` 14 | 15 | ## Boolean 16 | 17 | ``` 18 | %t the word true or false 19 | ``` 20 | 21 | ## Integer 22 | 23 | ``` 24 | %b base 2 25 | %c the character represented by the corresponding Unicode code point 26 | %d base 10 27 | %o base 8 28 | %O base 8 with 0o prefix 29 | %q a single-quoted character literal safely escaped with Go syntax. 30 | %x base 16, with lower-case letters for a-f 31 | %X base 16, with upper-case letters for A-F 32 | %U Unicode format: U+1234; same as "U+%04X" 33 | ``` 34 | 35 | ## Float 36 | 37 | ``` 38 | %b decimalless scientific notation with exponent a power of two, 39 | in the manner of Go's strconv.FormatFloat with the 'b' format, 40 | e.g. -123456p-78 41 | %e scientific notation, e.g. -1.234456e+78 42 | %E scientific notation, e.g. -1.234456E+78 43 | %f decimal point but no exponent, e.g. 123.456 44 | %F synonym for %f 45 | %g %e for large exponents, %f otherwise. Precision is discussed below. 46 | %G %E for large exponents, %F otherwise 47 | %x hexadecimal notation (with decimal power of two exponent), e.g. -0x1.23abcp+20 48 | %X upper-case hexadecimal notation, e.g. -0X1.23ABCP+20 49 | ``` 50 | 51 | ## String and Bytes 52 | 53 | ``` 54 | %s the uninterpreted bytes of the string or slice 55 | %q a double-quoted string safely escaped with Go syntax 56 | %x base 16, lower-case, two characters per byte 57 | %X base 16, upper-case, two characters per byte 58 | ``` 59 | 60 | ## Default format for %v 61 | 62 | ``` 63 | Bool: %t 64 | Int: %d 65 | Float: %g 66 | String: %s 67 | ``` 68 | 69 | ## Compound Objects 70 | 71 | ``` 72 | Array: [elem0 elem1 ...] 73 | Maps: {key1:value1 key2:value2 ...} 74 | ``` 75 | 76 | ## Width and Precision 77 | 78 | Width is specified by an optional decimal number immediately preceding the verb. 79 | If absent, the width is whatever is necessary to represent the value. 80 | 81 | Precision is specified after the (optional) width by a period followed by a 82 | decimal number. If no period is present, a default precision is used. A period 83 | with no following number specifies a precision of zero. 84 | Examples: 85 | ``` 86 | %f default width, default precision 87 | %9f width 9, default precision 88 | %.2f default width, precision 2 89 | %9.2f width 9, precision 2 90 | %9.f width 9, precision 0 91 | ``` 92 | 93 | Width and precision are measured in units of Unicode code points. Either or 94 | both of the flags may be replaced with the character '*', causing their values 95 | to be obtained from the next operand (preceding the one to format), which must 96 | be of type Int. 97 | 98 | For most values, width is the minimum number of runes to output, padding the 99 | formatted form with spaces if necessary. 100 | 101 | For Strings and Bytes, however, precision limits the length of the input to be 102 | formatted (not the size of the output), truncating if necessary. Normally it is 103 | measured in units of Unicode code points, but for these types when formatted 104 | with the %x or %X format it is measured in bytes. 105 | 106 | For floating-point values, width sets the minimum width of the field and 107 | precision sets the number of places after the decimal, if appropriate, except 108 | that for %g/%G precision sets the maximum number of significant digits 109 | (trailing zeros are removed). 110 | 111 | For example, given 12.345 the format %6.3f prints 12.345 while %.3g prints 12.3. 112 | 113 | The default precision for %e, %f and %#g is 6; for %g it is the smallest number 114 | of digits necessary to identify the value uniquely. 115 | 116 | For complex numbers, the width and precision apply to the two components 117 | independently and the result is parenthesized, so %f applied to 1.2+3.4i 118 | produces (1.200000+3.400000i). 119 | 120 | ## Other flags 121 | 122 | ``` 123 | + always print a sign for numeric values; 124 | guarantee ASCII-only output for %q (%+q) 125 | - pad with spaces on the right rather than the left (left-justify the field) 126 | # alternate format: add leading 0b for binary (%#b), 0 for octal (%#o), 127 | 0x or 0X for hex (%#x or %#X); 128 | for %q, print a raw (backquoted) string if strconv.CanBackquote returns true; 129 | always print a decimal point for %e, %E, %f, %F, %g and %G; 130 | do not remove trailing zeros for %g and %G; 131 | write e.g. U+0078 'x' if the character is printable for %U (%#U). 132 | ' ' (space) leave a space for elided sign in numbers (% d); 133 | put spaces between bytes printing strings or slices in hex (% x, % X) 134 | 0 pad with leading zeros rather than spaces; 135 | for numbers, this moves the padding after the sign 136 | ``` 137 | 138 | Flags are ignored by verbs that do not expect them. 139 | For example there is no alternate decimal format, so %#d and %d behave 140 | identically. 141 | 142 | -------------------------------------------------------------------------------- /docs/operators.md: -------------------------------------------------------------------------------- 1 | # Operators 2 | 3 | ## Int 4 | 5 | ### Equality 6 | 7 | - `(int) == (int) = (bool)`: equality 8 | - `(int) != (int) = (bool)`: inequality 9 | 10 | ### Arithmetic Operators 11 | 12 | - `(int) + (int) = (int)`: sum 13 | - `(int) - (int) = (int)`: difference 14 | - `(int) * (int) = (int)`: product 15 | - `(int) / (int) = (int)`: quotient 16 | - `(int) % (int) = (int)`: remainder 17 | - `(int) + (float) = (float)`: sum 18 | - `(int) - (float) = (float)`: difference 19 | - `(int) * (float) = (float)`: product 20 | - `(int) / (float) = (float)`: quotient 21 | - `(int) + (char) = (char)`: sum 22 | - `(int) - (char) = (char)`: difference 23 | 24 | ### Bitwise Operators 25 | 26 | - `(int) & (int) = (int)`: bitwise AND 27 | - `(int) | (int) = (int)`: bitwise OR 28 | - `(int) ^ (int) = (int)`: bitwise XOR 29 | - `(int) &^ (int) = (int)`: bitclear (AND NOT) 30 | - `(int) << (int) = (int)`: left shift 31 | - `(int) >> (int) = (int)`: right shift 32 | 33 | ### Comparison Operators 34 | 35 | - `(int) < (int) = (bool)`: less than 36 | - `(int) > (int) = (bool)`: greater than 37 | - `(int) <= (int) = (bool)`: less than or equal to 38 | - `(int) >= (int) = (bool)`: greater than or equal to 39 | - `(int) < (float) = (bool)`: less than 40 | - `(int) > (float) = (bool)`: greater than 41 | - `(int) <= (float) = (bool)`: less than or equal to 42 | - `(int) >= (float) = (bool)`: greater than or equal to 43 | - `(int) < (char) = (bool)`: less than 44 | - `(int) > (char) = (bool)`: greater than 45 | - `(int) <= (char) = (bool)`: less than or equal to 46 | - `(int) >= (char) = (bool)`: greater than or equal to 47 | 48 | ## Float 49 | 50 | ### Equality 51 | 52 | - `(float) == (float) = (bool)`: equality 53 | - `(float) != (float) = (bool)`: inequality 54 | 55 | ### Arithmetic Operators 56 | 57 | - `(float) + (float) = (float)`: sum 58 | - `(float) - (float) = (float)`: difference 59 | - `(float) * (float) = (float)`: product 60 | - `(float) / (float) = (float)`: quotient 61 | - `(float) + (int) = (int)`: sum 62 | - `(float) - (int) = (int)`: difference 63 | - `(float) * (int) = (int)`: product 64 | - `(float) / (int) = (int)`: quotient 65 | 66 | ### Comparison Operators 67 | 68 | - `(float) < (float) = (bool)`: less than 69 | - `(float) > (float) = (bool)`: greater than 70 | - `(float) <= (float) = (bool)`: less than or equal to 71 | - `(float) >= (float) = (bool)`: greater than or equal to 72 | - `(float) < (int) = (bool)`: less than 73 | - `(float) > (int) = (bool)`: greater than 74 | - `(float) <= (int) = (bool)`: less than or equal to 75 | - `(float) >= (int) = (bool)`: greater than or equal to 76 | 77 | ## String 78 | 79 | ### Equality 80 | 81 | - `(string) == (string) = (bool)`: equality 82 | - `(string) != (string) = (bool)`: inequality 83 | 84 | ### Concatenation 85 | 86 | - `(string) + (string) = (string)`: concatenation 87 | - `(string) + (other types) = (string)`: concatenation (after string-converted) 88 | 89 | ### Comparison Operators 90 | 91 | - `(string) < (string) = (bool)`: less than 92 | - `(string) > (string) = (bool)`: greater than 93 | - `(string) <= (string) = (bool)`: less than or equal to 94 | - `(string) >= (string) = (bool)`: greater than or equal to 95 | 96 | ## Char 97 | 98 | ### Equality 99 | 100 | - `(char) == (char) = (bool)`: equality 101 | - `(char) != (char) = (bool)`: inequality 102 | 103 | ### Arithmetic Operators 104 | 105 | - `(char) + (char) = (char)`: sum 106 | - `(char) - (char) = (char)`: difference 107 | - `(char) + (int) = (char)`: sum 108 | - `(char) - (int) = (char)`: difference 109 | 110 | ### Comparison Operators 111 | 112 | - `(char) < (char) = (bool)`: less than 113 | - `(char) > (char) = (bool)`: greater than 114 | - `(char) <= (char) = (bool)`: less than or equal to 115 | - `(char) >= (char) = (bool)`: greater than or equal to 116 | - `(char) < (int) = (bool)`: less than 117 | - `(char) > (int) = (bool)`: greater than 118 | - `(char) <= (int) = (bool)`: less than or equal to 119 | - `(char) >= (int) = (bool)`: greater than or equal to 120 | 121 | ## Bool 122 | 123 | ### Equality 124 | 125 | - `(bool) == (bool) = (bool)`: equality 126 | - `(bool) != (bool) = (bool)`: inequality 127 | 128 | ## Bytes 129 | 130 | ### Equality 131 | 132 | Test whether two byte array contain the same data. Uses 133 | [bytes.Compare](https://golang.org/pkg/bytes/#Compare) internally. 134 | 135 | - `(bytes) == (bytes) = (bool)`: equality 136 | - `(bytes) != (bytes) = (bool)`: inequality 137 | 138 | ## Time 139 | 140 | ### Equality 141 | 142 | Tests whether two times represent the same time instance. Uses 143 | [Time.Equal](https://golang.org/pkg/time/#Time.Equal) internally. 144 | 145 | - `(time) == (time) = (bool)`: equality 146 | - `(time) != (time) = (bool)`: inequality 147 | 148 | ### Arithmetic Operators 149 | 150 | - `(time) - (time) = (int)`: difference in nanoseconds (duration) 151 | - `(time) + (int) = (time)`: time + duration (nanoseconds) 152 | - `(time) - (int) = (time)`: time - duration (nanoseconds) 153 | 154 | ### Comparison Operators 155 | 156 | - `(time) < (time) = (bool)`: less than 157 | - `(time) > (time) = (bool)`: greater than 158 | - `(time) <= (time) = (bool)`: less than or equal to 159 | - `(time) >= (time) = (bool)`: greater than or equal to 160 | 161 | ## Array and ImmutableArray 162 | 163 | ### Equality 164 | 165 | Tests whether two _(immutable)_ arrays contain the same objects. 166 | 167 | - `(array) == (array) = (bool)`: equality 168 | - `(array) != (array) = (bool)`: inequality 169 | - `(array) == (immutable-array) = (bool)`: equality 170 | - `(array) != (immutable-array) = (bool)`: inequality 171 | - `(immutable-array) == (immutable-array) = (bool)`: equality 172 | - `(immutable-array) != (immutable-array) = (bool)`: inequality 173 | - `(immutable-array) == (array) = (bool)`: equality 174 | - `(immutable-array) != (array) = (bool)`: inequality 175 | 176 | ### Concatenation 177 | 178 | - `(array) + (array)`: return a concatenated array 179 | 180 | ## Map and ImmutableMap 181 | 182 | ### Equality 183 | 184 | Tests whether two _(immutable)_ maps contain the same key-objects. 185 | 186 | - `(map) == (map) = (bool)`: equality 187 | - `(map) != (map) = (bool)`: inequality 188 | - `(map) == (immutable-map) = (bool)`: equality 189 | - `(map) != (immutable-map) = (bool)`: inequality 190 | - `(immutable-map) == (immutable-map) = (bool)`: equality 191 | - `(immutable-map) != (immutable-map) = (bool)`: inequality 192 | - `(immutable-map) == (map) = (bool)`: equality 193 | - `(immutable-map) != (map) = (bool)`: inequality 194 | -------------------------------------------------------------------------------- /docs/runtime-types.md: -------------------------------------------------------------------------------- 1 | # Tengo Runtime Types 2 | 3 | - **Int**: signed 64bit integer 4 | - **String**: string 5 | - **Float**: 64bit floating point 6 | - **Bool**: boolean 7 | - **Char**: character (`rune` in Go) 8 | - **Bytes**: byte array (`[]byte` in Go) 9 | - **Array**: objects array (`[]Object` in Go) 10 | - **ImmutableArray**: immutable object array (`[]Object` in Go) 11 | - **Map**: objects map with string keys (`map[string]Object` in Go) 12 | - **ImmutableMap**: immutable object map with string keys (`map[string]Object` 13 | in Go) 14 | - **Time**: time (`time.Time` in Go) 15 | - **Error**: an error with underlying Object value of any type 16 | - **Undefined**: undefined 17 | 18 | ## Type Conversion/Coercion Table 19 | 20 | |src\dst |Int |String |Float |Bool |Char |Bytes |Array |Map |Time |Error |Undefined| 21 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 22 | |Int | - |_strconv_ |float64(v)|!IsFalsy()| rune(v)|**X**|**X**|**X**|_time.Unix()_|**X**|**X**| 23 | |String |_strconv_| - |_strconv_|!IsFalsy()|**X**|[]byte(s)|**X**|**X**|**X**|**X**|**X**| 24 | |Float |int64(f) |_strconv_ | - |!IsFalsy()|**X**|**X**|**X**|**X**|**X**|**X**|**X**| 25 | |Bool |1 / 0 |"true" / "false"|**X** | - |**X**|**X**|**X**|**X**|**X**|**X**|**X**| 26 | |Char |int64(c) |string(c) |**X** |!IsFalsy()| - |**X**|**X**|**X**|**X**|**X**|**X**| 27 | |Bytes |**X** |string(y)|**X** |!IsFalsy()|**X**| - |**X**|**X**|**X**|**X**|**X**| 28 | |Array |**X** |"[...]" |**X** |!IsFalsy()|**X**|**X**| - |**X**|**X**|**X**|**X**| 29 | |Map |**X** |"{...}" |**X** |!IsFalsy()|**X**|**X**|**X**| - |**X**|**X**|**X**| 30 | |Time |**X** |String() |**X** |!IsFalsy()|**X**|**X**|**X**|**X**| - |**X**|**X**| 31 | |Error |**X** |"error: ..." |**X** |false|**X**|**X**|**X**|**X**|**X**| - |**X**| 32 | |Undefined|**X** |**X**|**X** |false|**X**|**X**|**X**|**X**|**X**|**X**| - | 33 | 34 | _* **X**: No conversion; Typed value functions for `Variable` will 35 | return zero values._ 36 | _* strconv: converted using Go's conversion functions from `strconv` package._ 37 | _* IsFalsy(): use [Object.IsFalsy()](#objectisfalsy) function_ 38 | _* String(): use `Object.String()` function_ 39 | _* time.Unix(): use `time.Unix(v, 0)` to convert to Time_ 40 | 41 | ## Object.IsFalsy() 42 | 43 | `Object.IsFalsy()` interface method is used to determine if a given value 44 | should evaluate to `false` (e.g. for condition expression of `if` statement). 45 | 46 | - **Int**: `n == 0` 47 | - **String**: `len(s) == 0` 48 | - **Float**: `isNaN(f)` 49 | - **Bool**: `!b` 50 | - **Char**: `c == 0` 51 | - **Bytes**: `len(bytes) == 0` 52 | - **Array**: `len(arr) == 0` 53 | - **Map**: `len(map) == 0` 54 | - **Time**: `Time.IsZero()` 55 | - **Error**: `true` _(Error is always falsy)_ 56 | - **Undefined**: `true` _(Undefined is always falsy)_ 57 | 58 | ## Type Conversion Builtin Functions 59 | 60 | - `string(x)`: tries to convert `x` into string; returns `undefined` if failed 61 | - `int(x)`: tries to convert `x` into int; returns `undefined` if failed 62 | - `bool(x)`: tries to convert `x` into bool; returns `undefined` if failed 63 | - `float(x)`: tries to convert `x` into float; returns `undefined` if failed 64 | - `char(x)`: tries to convert `x` into char; returns `undefined` if failed 65 | - `bytes(x)`: tries to convert `x` into bytes; returns `undefined` if failed 66 | - `bytes(N)`: as a special case this will create a Bytes variable with the 67 | given size `N` (only if `N` is int) 68 | - `time(x)`: tries to convert `x` into time; returns `undefined` if failed 69 | - See [Builtins](https://github.com/d5/tengo/blob/master/docs/builtins.md) for 70 | the full list of builtin functions. 71 | 72 | ## Type Checking Builtin Functions 73 | 74 | - `is_string(x)`: returns `true` if `x` is string; `false` otherwise 75 | - `is_int(x)`: returns `true` if `x` is int; `false` otherwise 76 | - `is_bool(x)`: returns `true` if `x` is bool; `false` otherwise 77 | - `is_float(x)`: returns `true` if `x` is float; `false` otherwise 78 | - `is_char(x)`: returns `true` if `x` is char; `false` otherwise 79 | - `is_bytes(x)`: returns `true` if `x` is bytes; `false` otherwise 80 | - `is_array(x)`: return `true` if `x` is array; `false` otherwise 81 | - `is_immutable_array(x)`: return `true` if `x` is immutable array; `false` 82 | otherwise 83 | - `is_map(x)`: return `true` if `x` is map; `false` otherwise 84 | - `is_immutable_map(x)`: return `true` if `x` is immutable map; `false` 85 | otherwise 86 | - `is_time(x)`: return `true` if `x` is time; `false` otherwise 87 | - `is_error(x)`: returns `true` if `x` is error; `false` otherwise 88 | - `is_undefined(x)`: returns `true` if `x` is undefined; `false` otherwise 89 | - See [Builtins](https://github.com/d5/tengo/blob/master/docs/builtins.md) for 90 | the full list of builtin functions. 91 | -------------------------------------------------------------------------------- /docs/stdlib-base64.md: -------------------------------------------------------------------------------- 1 | # Module - "base64" 2 | 3 | ```golang 4 | base64 := import("base64") 5 | ``` 6 | 7 | ## Functions 8 | 9 | - `encode(src)`: returns the base64 encoding of src. 10 | - `decode(s)`: returns the bytes represented by the base64 string s. 11 | - `raw_encode(src)`: returns the base64 encoding of src but omits the padding. 12 | - `raw_decode(s)`: returns the bytes represented by the base64 string s which 13 | omits the padding. 14 | - `url_encode(src)`: returns the url-base64 encoding of src. 15 | - `url_decode(s)`: returns the bytes represented by the url-base64 string s. 16 | - `raw_url_encode(src)`: returns the url-base64 encoding of src but omits the 17 | padding. 18 | - `raw_url_decode(s)`: returns the bytes represented by the url-base64 string 19 | s which omits the padding. 20 | -------------------------------------------------------------------------------- /docs/stdlib-enum.md: -------------------------------------------------------------------------------- 1 | # Module - "enum" 2 | 3 | ```golang 4 | enum := import("enum") 5 | ``` 6 | 7 | ## Functions 8 | 9 | - `all(x, fn) => bool`: returns true if the given function `fn` evaluates to a 10 | truthy value on all of the items in `x`. It returns undefined if `x` is not 11 | enumerable. 12 | - `any(x, fn) => bool`: returns true if the given function `fn` evaluates to a 13 | truthy value on any of the items in `x`. It returns undefined if `x` is not 14 | enumerable. 15 | - `chunk(x, size) => [object]`: returns an array of elements split into groups 16 | the length of size. If `x` can't be split evenly, the final chunk will be the 17 | remaining elements. It returns undefined if `x` is not array. 18 | - `at(x, key) => object`: returns an element at the given index (if `x` is 19 | array) or key (if `x` is map). It returns undefined if `x` is not enumerable. 20 | - `each(x, fn)`: iterates over elements of `x` and invokes `fn` for each 21 | element. `fn` is invoked with two arguments: `key` and `value`. `key` is an 22 | int index if `x` is array. `key` is a string key if `x` is map. It does not 23 | iterate and returns undefined if `x` is not enumerable.` 24 | - `filter(x, fn) => [object]`: iterates over elements of `x`, returning an 25 | array of all elements `fn` returns truthy for. `fn` is invoked with two 26 | arguments: `key` and `value`. `key` is an int index if `x` is array. It returns 27 | undefined if `x` is not array. 28 | - `find(x, fn) => object`: iterates over elements of `x`, returning value of 29 | the first element `fn` returns truthy for. `fn` is invoked with two 30 | arguments: `key` and `value`. `key` is an int index if `x` is array. `key` is 31 | a string key if `x` is map. It returns undefined if `x` is not enumerable. 32 | - `find_key(x, fn) => int/string`: iterates over elements of `x`, returning key 33 | or index of the first element `fn` returns truthy for. `fn` is invoked with 34 | two arguments: `key` and `value`. `key` is an int index if `x` is array. 35 | `key` is a string key if `x` is map. It returns undefined if `x` is not 36 | enumerable. 37 | - `map(x, fn) => [object]`: creates an array of values by running each element 38 | in `x` through `fn`. `fn` is invoked with two arguments: `key` and `value`. 39 | `key` is an int index if `x` is array. `key` is a string key if `x` is map. 40 | It returns undefined if `x` is not enumerable. 41 | - `key(k, _) => object`: returns the first argument. 42 | - `value(_, v) => object`: returns the second argument. 43 | -------------------------------------------------------------------------------- /docs/stdlib-fmt.md: -------------------------------------------------------------------------------- 1 | # Module - "fmt" 2 | 3 | ```golang 4 | fmt := import("fmt") 5 | ``` 6 | 7 | ## Functions 8 | 9 | - `print(args...)`: Prints a string representation of the given variable to the 10 | standard output. Unlike Go's `fmt.Print` function, no spaces are added between 11 | the operands. 12 | - `println(args...)`: Prints a string representation of the given variable to 13 | the standard output with a newline appended. Unlike Go's `fmt.Println` 14 | function, no spaces are added between the operands. 15 | - `printf(format, args...)`: Prints a formatted string to the standard output. 16 | It does not append the newline character at the end. The first argument must 17 | a String object. See 18 | [this](https://github.com/d5/tengo/blob/master/docs/formatting.md) for more 19 | details on formatting. 20 | - `sprintf(format, args...)`: Returns a formatted string. Alias of the builtin 21 | function `format`. The first argument must be a String object. See 22 | [this](https://github.com/d5/tengo/blob/master/docs/formatting.md) for more 23 | details on formatting. 24 | -------------------------------------------------------------------------------- /docs/stdlib-hex.md: -------------------------------------------------------------------------------- 1 | # Module - "hex" 2 | 3 | ```golang 4 | hex := import("hex") 5 | ``` 6 | 7 | ## Functions 8 | 9 | - `encode(src)`: returns the hexadecimal encoding of src. 10 | - `decode(s)`: returns the bytes represented by the hexadecimal string s. 11 | -------------------------------------------------------------------------------- /docs/stdlib-json.md: -------------------------------------------------------------------------------- 1 | # Module - "json" 2 | 3 | ```golang 4 | json := import("json") 5 | ``` 6 | 7 | ## Functions 8 | 9 | - `decode(b string/bytes) => object`: Parses the JSON string and returns an 10 | object. 11 | - `encode(o object) => bytes`: Returns the JSON string (bytes) of the object. 12 | Unlike Go's JSON package, this function does not HTML-escape texts, but, one 13 | can use `html_escape` function if needed. 14 | - `indent(b string/bytes, prefix string, indent string) => bytes`: Returns an indented form of input JSON 15 | bytes string. 16 | - `html_escape(b string/bytes) => bytes`: Return an HTML-safe form of input 17 | JSON bytes string. 18 | 19 | ## Examples 20 | 21 | ```golang 22 | json := import("json") 23 | 24 | encoded := json.encode({a: 1, b: [2, 3, 4]}) // JSON-encoded bytes string 25 | indentded := json.indent(encoded, "", " ") // indented form 26 | html_safe := json.html_escape(encoded) // HTML escaped form 27 | 28 | decoded := json.decode(encoded) // {a: 1, b: [2, 3, 4]} 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/stdlib-math.md: -------------------------------------------------------------------------------- 1 | # Module - "math" 2 | 3 | ```golang 4 | math := import("math") 5 | ``` 6 | 7 | ## Constants 8 | 9 | - `e` 10 | - `pi` 11 | - `phi` 12 | - `sqrt2` 13 | - `sqrtE` 14 | - `sprtPi` 15 | - `sqrtPhi` 16 | - `ln2` 17 | - `log2E` 18 | - `ln10` 19 | - `ln10E` 20 | 21 | Mathematical constants. 22 | 23 | - `maxFloat32` 24 | - `smallestNonzeroFloat32` 25 | - `maxFloat64` 26 | - `smallestNonzeroFloat64` 27 | 28 | 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. 29 | 30 | - `maxInt` 31 | - `minInt` 32 | - `maxInt8` 33 | - `minInt8` 34 | - `maxInt16` 35 | - `minInt16` 36 | - `maxInt32` 37 | - `minInt32` 38 | - `maxInt64` 39 | - `minInt64` 40 | 41 | Integer limit values. 42 | 43 | ## Functions 44 | 45 | - `abs(x float) => float`: returns the absolute value of x. 46 | - `acos(x float) => float`: returns the arccosine, in radians, of x. 47 | - `acosh(x float) => float`: returns the inverse hyperbolic cosine of x. 48 | - `asin(x float) => float`: returns the arcsine, in radians, of x. 49 | - `asinh(x float) => float`: returns the inverse hyperbolic sine of x. 50 | - `atan(x float) => float`: returns the arctangent, in radians, of x. 51 | - `atan2(y float, xfloat) => float`: returns the arc tangent of y/x, using the 52 | signs of the two to determine the quadrant of the return value. 53 | - `atanh(x float) => float`: returns the inverse hyperbolic tangent of x. 54 | - `cbrt(x float) => float`: returns the cube root of x. 55 | - `ceil(x float) => float`: returns the least integer value greater than or 56 | equal to x. 57 | - `copysign(x float, y float) => float`: returns a value with the magnitude of 58 | x and the sign of y. 59 | - `cos(x float) => float`: returns the cosine of the radian argument x. 60 | - `cosh(x float) => float`: returns the hyperbolic cosine of x. 61 | - `dim(x float, y float) => float`: returns the maximum of x-y or 0. 62 | - `erf(x float) => float`: returns the error function of x. 63 | - `erfc(x float) => float`: returns the complementary error function of x. 64 | - `exp(x float) => float`: returns e**x, the base-e exponential of x. 65 | - `exp2(x float) => float`: returns 2**x, the base-2 exponential of x. 66 | - `expm1(x float) => float`: returns e**x - 1, the base-e exponential of x 67 | minus 1. It is more accurate than Exp(x) - 1 when x is near zero. 68 | - `floor(x float) => float`: returns the greatest integer value less than or 69 | equal to x. 70 | - `gamma(x float) => float`: returns the Gamma function of x. 71 | - `hypot(p float, q float) => float`: returns `Sqrt(p * p + q * q)`, taking care 72 | to avoid unnecessary overflow and underflow. 73 | - `ilogb(x float) => float`: returns the binary exponent of x as an integer. 74 | - `inf(sign int) => float`: returns positive infinity if sign >= 0, negative 75 | infinity if sign < 0. 76 | - `is_inf(f float, sign int) => float`: reports whether f is an infinity, 77 | according to sign. If sign > 0, IsInf reports whether f is positive infinity. 78 | If sign < 0, IsInf reports whether f is negative infinity. If sign == 0, 79 | IsInf reports whether f is either infinity. 80 | - `is_nan(f float) => float`: reports whether f is an IEEE 754 ``not-a-number'' 81 | value. 82 | - `j0(x float) => float`: returns the order-zero Bessel function of the first 83 | kind. 84 | - `j1(x float) => float`: returns the order-one Bessel function of the first 85 | kind. 86 | - `jn(n int, x float) => float`: returns the order-n Bessel function of the 87 | first kind. 88 | - `ldexp(frac float, exp int) => float`: is the inverse of frexp. It returns 89 | frac × 2**exp. 90 | - `log(x float) => float`: returns the natural logarithm of x. 91 | - `log10(x float) => float`: returns the decimal logarithm of x. 92 | - `log1p(x float) => float`: returns the natural logarithm of 1 plus its 93 | argument x. It is more accurate than Log(1 + x) when x is near zero. 94 | - `log2(x float) => float`: returns the binary logarithm of x. 95 | - `logb(x float) => float`: returns the binary exponent of x. 96 | - `max(x float, y float) => float`: returns the larger of x or y. 97 | - `min(x float, y float) => float`: returns the smaller of x or y. 98 | - `mod(x float, y float) => float`: returns the floating-point remainder of x/y. 99 | - `nan() => float`: returns an IEEE 754 ``not-a-number'' value. 100 | - `nextafter(x float, y float) => float`: returns the next representable 101 | float64 value after x towards y. 102 | - `pow(x float, y float) => float`: returns x**y, the base-x exponential of y. 103 | - `pow10(n int) => float`: returns 10**n, the base-10 exponential of n. 104 | - `remainder(x float, y float) => float`: returns the IEEE 754 floating-point 105 | remainder of x/y. 106 | - `signbit(x float) => float`: returns true if x is negative or negative zero. 107 | - `sin(x float) => float`: returns the sine of the radian argument x. 108 | - `sinh(x float) => float`: returns the hyperbolic sine of x. 109 | - `sqrt(x float) => float`: returns the square root of x. 110 | - `tan(x float) => float`: returns the tangent of the radian argument x. 111 | - `tanh(x float) => float`: returns the hyperbolic tangent of x. 112 | - `trunc(x float) => float`: returns the integer value of x. 113 | - `y0(x float) => float`: returns the order-zero Bessel function of the second 114 | kind. 115 | - `y1(x float) => float`: returns the order-one Bessel function of the second 116 | kind. 117 | - `yn(n int, x float) => float`: returns the order-n Bessel function of the 118 | second kind. 119 | -------------------------------------------------------------------------------- /docs/stdlib-rand.md: -------------------------------------------------------------------------------- 1 | # Module - "rand" 2 | 3 | ```golang 4 | rand := import("rand") 5 | ``` 6 | 7 | ## Functions 8 | 9 | - `seed(seed int)`: uses the provided seed value to initialize the default 10 | Source to a deterministic state. 11 | - `exp_float() => float`: returns an exponentially distributed float64 in the 12 | range (0, +math.MaxFloat64] with an exponential distribution whose rate 13 | parameter (lambda) is 1 and whose mean is 1/lambda (1) from the default 14 | Source. 15 | - `float() => float`: returns, as a float64, a pseudo-random number in 16 | [0.0,1.0) from the default Source. 17 | - `int() => int`: returns a non-negative pseudo-random 63-bit integer as an 18 | int64 from the default Source. 19 | - `intn(n int) => int`: returns, as an int64, a non-negative pseudo-random 20 | number in [0,n) from the default Source. It panics if n <= 0. 21 | - `norm_float) => float`: returns a normally distributed float64 in the range 22 | [-math.MaxFloat64, +math.MaxFloat64] with standard normal distribution 23 | (mean = 0, stddev = 1) from the default Source. 24 | - `perm(n int) => [int]`: returns, as a slice of n ints, a pseudo-random 25 | permutation of the integers [0,n) from the default Source. 26 | - `read(p bytes) => int/error`: generates len(p) random bytes from the default 27 | Source and writes them into p. It always returns len(p) and a nil error. 28 | - `rand(src_seed int) => Rand`: returns a new Rand that uses random values from 29 | src to generate other random values. 30 | 31 | ## Rand 32 | 33 | - `seed(seed int)`: uses the provided seed value to initialize the default 34 | Source to a deterministic state. 35 | - `exp_float() => float`: returns an exponentially distributed float64 in the 36 | range (0, +math.MaxFloat64] with an exponential distribution whose rate 37 | parameter (lambda) is 1 and whose mean is 1/lambda (1) from the default Source. 38 | - `float() => float`: returns, as a float64, a pseudo-random number in 39 | [0.0,1.0) from the default Source. 40 | - `int() => int`: returns a non-negative pseudo-random 63-bit integer as an 41 | int64 from the default Source. 42 | - `intn(n int) => int`: returns, as an int64, a non-negative pseudo-random 43 | number in [0,n) from the default Source. It panics if n <= 0. 44 | - `norm_float) => float`: returns a normally distributed float64 in the range 45 | [-math.MaxFloat64, +math.MaxFloat64] with standard normal distribution 46 | (mean = 0, stddev = 1) from the default Source. 47 | - `perm(n int) => [int]`: returns, as a slice of n ints, a pseudo-random 48 | permutation of the integers [0,n) from the default Source. 49 | - `read(p bytes) => int/error`: generates len(p) random bytes from the default 50 | Source and writes them into p. It always returns len(p) and a nil error. 51 | -------------------------------------------------------------------------------- /docs/stdlib-times.md: -------------------------------------------------------------------------------- 1 | # Module - "times" 2 | 3 | ```golang 4 | times := import("times") 5 | ``` 6 | 7 | ## Constants 8 | 9 | - `format_ansic`: time format "Mon Jan _2 15:04:05 2006" 10 | - `format_unix_date`: time format "Mon Jan _2 15:04:05 MST 2006" 11 | - `format_ruby_date`: time format "Mon Jan 02 15:04:05 -0700 2006" 12 | - `format_rfc822`: time format "02 Jan 06 15:04 MST" 13 | - `format_rfc822z`: time format "02 Jan 06 15:04 -0700" 14 | - `format_rfc850`: time format "Monday, 02-Jan-06 15:04:05 MST" 15 | - `format_rfc1123`: time format "Mon, 02 Jan 2006 15:04:05 MST" 16 | - `format_rfc1123z`: time format "Mon, 02 Jan 2006 15:04:05 -0700" 17 | - `format_rfc3339`: time format "2006-01-02T15:04:05Z07:00" 18 | - `format_rfc3339_nano`: time format "2006-01-02T15:04:05.999999999Z07:00" 19 | - `format_kitchen`: time format "3:04PM" 20 | - `format_stamp`: time format "Jan _2 15:04:05" 21 | - `format_stamp_milli`: time format "Jan _2 15:04:05.000" 22 | - `format_stamp_micro`: time format "Jan _2 15:04:05.000000" 23 | - `format_stamp_nano`: time format "Jan _2 15:04:05.000000000" 24 | - `nanosecond` 25 | - `microsecond` 26 | - `millisecond` 27 | - `second` 28 | - `minute` 29 | - `hour` 30 | - `january` 31 | - `february` 32 | - `march` 33 | - `april` 34 | - `may` 35 | - `june` 36 | - `july` 37 | - `august` 38 | - `september` 39 | - `october` 40 | - `november` 41 | - `december` 42 | 43 | ## Functions 44 | 45 | - `sleep(duration int)`: pauses the current goroutine for at least the duration 46 | d. A negative or zero duration causes Sleep to return immediately. 47 | - `parse_duration(s string) => int`: parses a duration string. A duration 48 | string is a possibly signed sequence of decimal numbers, each with optional 49 | fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time 50 | units are "ns", "us" (or "µs"), "ms", "s", "m", "h". 51 | - `since(t time) => int`: returns the time elapsed since t. 52 | - `until(t time) => int`: returns the duration until t. 53 | - `duration_hours(duration int) => float`: returns the duration as a floating 54 | point number of hours. 55 | - `duration_minutes(duration int) => float`: returns the duration as a floating 56 | point number of minutes. 57 | - `duration_nanoseconds(duration int) => int`: returns the duration as an 58 | integer of nanoseconds. 59 | - `duration_seconds(duration int) => float`: returns the duration as a floating 60 | point number of seconds. 61 | - `duration_string(duration int) => string`: returns a string representation of 62 | duration. 63 | - `month_string(month int) => string`: returns the English name of the month 64 | ("January", "February", ...). 65 | - `date(year int, month int, day int, hour int, min int, sec int, nsec int, loc string) => time`: 66 | returns the Time corresponding to "yyyy-mm-dd hh:mm:ss + nsec nanoseconds" in 67 | the appropriate zone for that Time in the given (optional) location. 68 | The Local time zone will be used if executed without specifying a location. 69 | - `now() => time`: returns the current local time. 70 | - `parse(format string, s string) => time`: parses a formatted string and 71 | returns the time value it represents. The layout defines the format by 72 | showing how the reference time, defined to be "Mon Jan 2 15:04:05 -0700 MST 73 | 2006" would be interpreted if it were the value; it serves as an example of 74 | the input format. The same interpretation will then be made to the input 75 | string. 76 | - `unix(sec int, nsec int) => time`: returns the local Time corresponding to 77 | the given Unix time, sec seconds and nsec nanoseconds since January 1, 78 | 1970 UTC. 79 | - `add(t time, duration int) => time`: returns the time t+d. 80 | - `add_date(t time, years int, months int, days int) => time`: returns the time 81 | corresponding to adding the given number of years, months, and days to t. For 82 | example, AddDate(-1, 2, 3) applied to January 1, 2011 returns March 4, 2010. 83 | - `sub(t time, u time) => int`: returns the duration t-u. 84 | - `after(t time, u time) => bool`: reports whether the time instant t is after 85 | u. 86 | - `before(t time, u time) => bool`: reports whether the time instant t is 87 | before u. 88 | - `time_year(t time) => int`: returns the year in which t occurs. 89 | - `time_month(t time) => int`: returns the month of the year specified by t. 90 | - `time_day(t time) => int`: returns the day of the month specified by t. 91 | - `time_weekday(t time) => int`: returns the day of the week specified by t. 92 | - `time_hour(t time) => int`: returns the hour within the day specified by t, 93 | in the range [0, 23]. 94 | - `time_minute(t time) => int`: returns the minute offset within the hour 95 | specified by t, in the range [0, 59]. 96 | - `time_second(t time) => int`: returns the second offset within the minute 97 | specified by t, in the range [0, 59]. 98 | - `time_nanosecond(t time) => int`: returns the nanosecond offset within the 99 | second specified by t, in the range [0, 999999999]. 100 | - `time_unix(t time) => int`: returns t as a Unix time, the number of seconds 101 | elapsed since January 1, 1970 UTC. The result does not depend on the location 102 | associated with t. 103 | - `time_unix_nano(t time) => int`: returns t as a Unix time, the number of 104 | nanoseconds elapsed since January 1, 1970 UTC. The result is undefined if the 105 | Unix time in nanoseconds cannot be represented by an int64 (a date before the 106 | year 1678 or after 2262). Note that this means the result of calling UnixNano 107 | on the zero Time is undefined. The result does not depend on the location 108 | associated with t. 109 | - `time_format(t time, format) => string`: returns a textual representation of 110 | he time value formatted according to layout, which defines the format by 111 | showing how the reference time, defined to be "Mon Jan 2 15:04:05 -0700 MST 112 | 2006" would be displayed if it were the value; it serves as an example of the 113 | desired output. The same display rules will then be applied to the time value. 114 | - `time_location(t time) => string`: returns the time zone name associated with 115 | t. 116 | - `time_string(t time) => string`: returns the time formatted using the format 117 | string "2006-01-02 15:04:05.999999999 -0700 MST". 118 | - `is_zero(t time) => bool`: reports whether t represents the zero time 119 | instant, January 1, year 1, 00:00:00 UTC. 120 | - `in_location(t time, l string) => time`: returns a copy of t representing 121 | the same time instant, but with the copy's location information set to l for 122 | display purposes. 123 | - `to_local(t time) => time`: returns t with the location set to local time. 124 | - `to_utc(t time) => time`: returns t with the location set to UTC. 125 | -------------------------------------------------------------------------------- /docs/stdlib.md: -------------------------------------------------------------------------------- 1 | # Standard Library 2 | 3 | - [os](https://github.com/d5/tengo/blob/master/docs/stdlib-os.md): 4 | platform-independent interface to operating system functionality. 5 | - [text](https://github.com/d5/tengo/blob/master/docs/stdlib-text.md): regular 6 | expressions, string conversion, and manipulation 7 | - [math](https://github.com/d5/tengo/blob/master/docs/stdlib-math.md): 8 | mathematical constants and functions 9 | - [times](https://github.com/d5/tengo/blob/master/docs/stdlib-times.md): 10 | time-related functions 11 | - [rand](https://github.com/d5/tengo/blob/master/docs/stdlib-rand.md): 12 | random functions 13 | - [fmt](https://github.com/d5/tengo/blob/master/docs/stdlib-fmt.md): 14 | formatting functions 15 | - [json](https://github.com/d5/tengo/blob/master/docs/stdlib-json.md): JSON 16 | functions 17 | - [enum](https://github.com/d5/tengo/blob/master/docs/stdlib-enum.md): 18 | Enumeration functions 19 | - [hex](https://github.com/d5/tengo/blob/master/docs/stdlib-hex.md): hex 20 | encoding and decoding functions 21 | - [base64](https://github.com/d5/tengo/blob/master/docs/stdlib-base64.md): 22 | base64 encoding and decoding functions 23 | -------------------------------------------------------------------------------- /docs/tengo-cli.md: -------------------------------------------------------------------------------- 1 | # Tengo CLI Tool 2 | 3 | Tengo is designed as an embedding script language for Go, but, it can also be 4 | compiled and executed as native binary using `tengo` CLI tool. 5 | 6 | ## Installing Tengo CLI 7 | 8 | To install `tengo` tool, run: 9 | 10 | ```bash 11 | go get github.com/d5/tengo/cmd/tengo 12 | ``` 13 | 14 | Or, you can download the precompiled binaries from 15 | [here](https://github.com/d5/tengo/releases/latest). 16 | 17 | ## Compiling and Executing Tengo Code 18 | 19 | You can directly execute the Tengo source code by running `tengo` tool with 20 | your Tengo source file (`*.tengo`). 21 | 22 | ```bash 23 | tengo myapp.tengo 24 | ``` 25 | 26 | Or, you can compile the code into a binary file and execute it later. 27 | 28 | ```bash 29 | tengo -o myapp myapp.tengo # compile 'myapp.tengo' into binary file 'myapp' 30 | tengo myapp # execute the compiled binary `myapp` 31 | ``` 32 | 33 | Or, you can make tengo source file executable 34 | 35 | ```bash 36 | # copy tengo executable to a dir where PATH environment variable includes 37 | cp tengo /usr/local/bin/ 38 | 39 | # add shebang line to source file 40 | cat > myapp.tengo << EOF 41 | #!/usr/local/bin/tengo 42 | fmt := import("fmt") 43 | fmt.println("Hello World!") 44 | EOF 45 | 46 | # make myapp.tengo file executable 47 | chmod +x myapp.tengo 48 | 49 | # run your script 50 | ./myapp.tengo 51 | ``` 52 | 53 | **Note: Your source file must have `.tengo` extension.** 54 | 55 | ## Resolving Relative Import Paths 56 | 57 | If there are tengo source module files which are imported with relative import 58 | paths, CLI has `-resolve` flag. Flag enables to import a module relative to 59 | importing file. This behavior will be default at version 3. 60 | 61 | ## Tengo REPL 62 | 63 | You can run Tengo [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) 64 | if you run `tengo` with no arguments. 65 | 66 | ```bash 67 | tengo 68 | ``` 69 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package tengo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | // ErrStackOverflow is a stack overflow error. 10 | ErrStackOverflow = errors.New("stack overflow") 11 | 12 | // ErrObjectAllocLimit is an objects allocation limit error. 13 | ErrObjectAllocLimit = errors.New("object allocation limit exceeded") 14 | 15 | // ErrIndexOutOfBounds is an error where a given index is out of the 16 | // bounds. 17 | ErrIndexOutOfBounds = errors.New("index out of bounds") 18 | 19 | // ErrInvalidIndexType represents an invalid index type. 20 | ErrInvalidIndexType = errors.New("invalid index type") 21 | 22 | // ErrInvalidIndexValueType represents an invalid index value type. 23 | ErrInvalidIndexValueType = errors.New("invalid index value type") 24 | 25 | // ErrInvalidIndexOnError represents an invalid index on error. 26 | ErrInvalidIndexOnError = errors.New("invalid index on error") 27 | 28 | // ErrInvalidOperator represents an error for invalid operator usage. 29 | ErrInvalidOperator = errors.New("invalid operator") 30 | 31 | // ErrWrongNumArguments represents a wrong number of arguments error. 32 | ErrWrongNumArguments = errors.New("wrong number of arguments") 33 | 34 | // ErrBytesLimit represents an error where the size of bytes value exceeds 35 | // the limit. 36 | ErrBytesLimit = errors.New("exceeding bytes size limit") 37 | 38 | // ErrStringLimit represents an error where the size of string value 39 | // exceeds the limit. 40 | ErrStringLimit = errors.New("exceeding string size limit") 41 | 42 | // ErrNotIndexable is an error where an Object is not indexable. 43 | ErrNotIndexable = errors.New("not indexable") 44 | 45 | // ErrNotIndexAssignable is an error where an Object is not index 46 | // assignable. 47 | ErrNotIndexAssignable = errors.New("not index-assignable") 48 | 49 | // ErrNotImplemented is an error where an Object has not implemented a 50 | // required method. 51 | ErrNotImplemented = errors.New("not implemented") 52 | 53 | // ErrInvalidRangeStep is an error where the step parameter is less than or equal to 0 when using builtin range function. 54 | ErrInvalidRangeStep = errors.New("range step must be greater than 0") 55 | ) 56 | 57 | // ErrInvalidArgumentType represents an invalid argument value type error. 58 | type ErrInvalidArgumentType struct { 59 | Name string 60 | Expected string 61 | Found string 62 | } 63 | 64 | func (e ErrInvalidArgumentType) Error() string { 65 | return fmt.Sprintf("invalid type for argument '%s': expected %s, found %s", 66 | e.Name, e.Expected, e.Found) 67 | } 68 | -------------------------------------------------------------------------------- /eval.go: -------------------------------------------------------------------------------- 1 | package tengo 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Eval compiles and executes given expr with params, and returns an 10 | // evaluated value. expr must be an expression. Otherwise it will fail to 11 | // compile. Expression must not use or define variable "__res__" as it's 12 | // reserved for the internal usage. 13 | func Eval( 14 | ctx context.Context, 15 | expr string, 16 | params map[string]interface{}, 17 | ) (interface{}, error) { 18 | expr = strings.TrimSpace(expr) 19 | if expr == "" { 20 | return nil, fmt.Errorf("empty expression") 21 | } 22 | 23 | script := NewScript([]byte(fmt.Sprintf("__res__ := (%s)", expr))) 24 | for pk, pv := range params { 25 | err := script.Add(pk, pv) 26 | if err != nil { 27 | return nil, fmt.Errorf("script add: %w", err) 28 | } 29 | } 30 | compiled, err := script.RunContext(ctx) 31 | if err != nil { 32 | return nil, fmt.Errorf("script run: %w", err) 33 | } 34 | return compiled.Get("__res__").Value(), nil 35 | } 36 | -------------------------------------------------------------------------------- /eval_test.go: -------------------------------------------------------------------------------- 1 | package tengo_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/d5/tengo/v2" 8 | "github.com/d5/tengo/v2/require" 9 | ) 10 | 11 | func TestEval(t *testing.T) { 12 | eval := func( 13 | expr string, 14 | params map[string]interface{}, 15 | expected interface{}, 16 | ) { 17 | ctx := context.Background() 18 | actual, err := tengo.Eval(ctx, expr, params) 19 | require.NoError(t, err) 20 | require.Equal(t, expected, actual) 21 | } 22 | 23 | eval(`undefined`, nil, nil) 24 | eval(`1`, nil, int64(1)) 25 | eval(`19 + 23`, nil, int64(42)) 26 | eval(`"foo bar"`, nil, "foo bar") 27 | eval(`[1, 2, 3][1]`, nil, int64(2)) 28 | 29 | eval( 30 | `5 + p`, 31 | map[string]interface{}{ 32 | "p": 7, 33 | }, 34 | int64(12), 35 | ) 36 | eval( 37 | `"seven is " + p`, 38 | map[string]interface{}{ 39 | "p": 7, 40 | }, 41 | "seven is 7", 42 | ) 43 | eval( 44 | `"" + a + b`, 45 | map[string]interface{}{ 46 | "a": 7, 47 | "b": " is seven", 48 | }, 49 | "7 is seven", 50 | ) 51 | 52 | eval( 53 | `a ? "success" : "fail"`, 54 | map[string]interface{}{ 55 | "a": 1, 56 | }, 57 | "success", 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package tengo_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/d5/tengo/v2" 8 | ) 9 | 10 | func Example() { 11 | // Tengo script code 12 | src := ` 13 | each := func(seq, fn) { 14 | for x in seq { fn(x) } 15 | } 16 | 17 | sum := 0 18 | mul := 1 19 | each([a, b, c, d], func(x) { 20 | sum += x 21 | mul *= x 22 | })` 23 | 24 | // create a new Script instance 25 | script := tengo.NewScript([]byte(src)) 26 | 27 | // set values 28 | _ = script.Add("a", 1) 29 | _ = script.Add("b", 9) 30 | _ = script.Add("c", 8) 31 | _ = script.Add("d", 4) 32 | 33 | // run the script 34 | compiled, err := script.RunContext(context.Background()) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | // retrieve values 40 | sum := compiled.Get("sum") 41 | mul := compiled.Get("mul") 42 | fmt.Println(sum, mul) 43 | 44 | // Output: 45 | // 22 288 46 | } 47 | -------------------------------------------------------------------------------- /examples/interoperability/main.go: -------------------------------------------------------------------------------- 1 | /*An example to demonstrate an alternative way to run tengo functions from go. 2 | */ 3 | package main 4 | 5 | import ( 6 | "container/list" 7 | "context" 8 | "errors" 9 | "fmt" 10 | "math/rand" 11 | "sync" 12 | "time" 13 | 14 | "github.com/d5/tengo/v2" 15 | ) 16 | 17 | // CallArgs holds function name to be executed and its required parameters with 18 | // a channel to listen result of function. 19 | type CallArgs struct { 20 | Func string 21 | Params []tengo.Object 22 | Result chan<- tengo.Object 23 | } 24 | 25 | // NewGoProxy creates GoProxy object. 26 | func NewGoProxy(ctx context.Context) *GoProxy { 27 | mod := new(GoProxy) 28 | mod.ctx = ctx 29 | mod.callbacks = make(map[string]tengo.Object) 30 | mod.callChan = make(chan *CallArgs, 1) 31 | mod.moduleMap = map[string]tengo.Object{ 32 | "next": &tengo.UserFunction{Value: mod.next}, 33 | "register": &tengo.UserFunction{Value: mod.register}, 34 | "args": &tengo.UserFunction{Value: mod.args}, 35 | } 36 | mod.tasks = list.New() 37 | return mod 38 | } 39 | 40 | // GoProxy is a builtin tengo module to register tengo functions and run them. 41 | type GoProxy struct { 42 | tengo.ObjectImpl 43 | ctx context.Context 44 | moduleMap map[string]tengo.Object 45 | callbacks map[string]tengo.Object 46 | callChan chan *CallArgs 47 | tasks *list.List 48 | mtx sync.Mutex 49 | } 50 | 51 | // TypeName returns type name. 52 | func (mod *GoProxy) TypeName() string { 53 | return "GoProxy" 54 | } 55 | 56 | func (mod *GoProxy) String() string { 57 | m := tengo.ImmutableMap{Value: mod.moduleMap} 58 | return m.String() 59 | } 60 | 61 | // ModuleMap returns a map to add a builtin tengo module. 62 | func (mod *GoProxy) ModuleMap() map[string]tengo.Object { 63 | return mod.moduleMap 64 | } 65 | 66 | // CallChan returns call channel which expects arguments to run a tengo 67 | // function. 68 | func (mod *GoProxy) CallChan() chan<- *CallArgs { 69 | return mod.callChan 70 | } 71 | 72 | func (mod *GoProxy) next(args ...tengo.Object) (tengo.Object, error) { 73 | mod.mtx.Lock() 74 | defer mod.mtx.Unlock() 75 | select { 76 | case <-mod.ctx.Done(): 77 | return tengo.FalseValue, nil 78 | case args := <-mod.callChan: 79 | if args != nil { 80 | mod.tasks.PushBack(args) 81 | } 82 | return tengo.TrueValue, nil 83 | } 84 | } 85 | 86 | func (mod *GoProxy) register(args ...tengo.Object) (tengo.Object, error) { 87 | if len(args) == 0 { 88 | return nil, tengo.ErrWrongNumArguments 89 | } 90 | mod.mtx.Lock() 91 | defer mod.mtx.Unlock() 92 | 93 | switch v := args[0].(type) { 94 | case *tengo.Map: 95 | mod.callbacks = v.Value 96 | case *tengo.ImmutableMap: 97 | mod.callbacks = v.Value 98 | default: 99 | return nil, tengo.ErrInvalidArgumentType{ 100 | Name: "first", 101 | Expected: "map", 102 | Found: args[0].TypeName(), 103 | } 104 | } 105 | return tengo.UndefinedValue, nil 106 | } 107 | 108 | func (mod *GoProxy) args(args ...tengo.Object) (tengo.Object, error) { 109 | mod.mtx.Lock() 110 | defer mod.mtx.Unlock() 111 | 112 | if mod.tasks.Len() == 0 { 113 | return tengo.UndefinedValue, nil 114 | } 115 | el := mod.tasks.Front() 116 | callArgs, ok := el.Value.(*CallArgs) 117 | if !ok || callArgs == nil { 118 | return nil, errors.New("invalid call arguments") 119 | } 120 | mod.tasks.Remove(el) 121 | f, ok := mod.callbacks[callArgs.Func] 122 | if !ok { 123 | return tengo.UndefinedValue, nil 124 | } 125 | compiledFunc, ok := f.(*tengo.CompiledFunction) 126 | if !ok { 127 | return tengo.UndefinedValue, nil 128 | } 129 | params := callArgs.Params 130 | if params == nil { 131 | params = make([]tengo.Object, 0) 132 | } 133 | // callable.VarArgs implementation is omitted. 134 | return &tengo.ImmutableMap{ 135 | Value: map[string]tengo.Object{ 136 | "result": &tengo.UserFunction{ 137 | Value: func(args ...tengo.Object) (tengo.Object, error) { 138 | if len(args) > 0 { 139 | callArgs.Result <- args[0] 140 | return tengo.UndefinedValue, nil 141 | } 142 | callArgs.Result <- &tengo.Error{ 143 | Value: &tengo.String{ 144 | Value: tengo.ErrWrongNumArguments.Error()}, 145 | } 146 | return tengo.UndefinedValue, nil 147 | }}, 148 | "num_params": &tengo.Int{Value: int64(compiledFunc.NumParameters)}, 149 | "callable": compiledFunc, 150 | "params": &tengo.Array{Value: params}, 151 | }, 152 | }, nil 153 | } 154 | 155 | // ProxySource is a tengo script to handle bidirectional arguments flow between 156 | // go and pure tengo functions. Note: you should add more if conditions for 157 | // different number of parameters. 158 | // TODO: handle variadic functions. 159 | var ProxySource = ` 160 | export func(args) { 161 | if is_undefined(args) { 162 | return 163 | } 164 | callable := args.callable 165 | if is_undefined(callable) { 166 | return 167 | } 168 | result := args.result 169 | num_params := args.num_params 170 | v := undefined 171 | // add more else if conditions for different number of parameters. 172 | if num_params == 0 { 173 | v = callable() 174 | } else if num_params == 1 { 175 | v = callable(args.params[0]) 176 | } else if num_params == 2 { 177 | v = callable(args.params[0], args.params[1]) 178 | } else if num_params == 3 { 179 | v = callable(args.params[0], args.params[1], args.params[2]) 180 | } 181 | result(v) 182 | } 183 | ` 184 | 185 | func main() { 186 | src := ` 187 | // goproxy and proxy must be imported. 188 | goproxy := import("goproxy") 189 | proxy := import("proxy") 190 | 191 | global := 0 192 | 193 | callbacks := { 194 | sum: func(a, b) { 195 | return a + b 196 | }, 197 | multiply: func(a, b) { 198 | return a * b 199 | }, 200 | increment: func() { 201 | global++ 202 | return global 203 | } 204 | } 205 | 206 | // Register callbacks to call them in goproxy loop. 207 | goproxy.register(callbacks) 208 | 209 | // goproxy loop waits for new call requests and run them with the help of 210 | // "proxy" source module. Cancelling the context breaks the loop. 211 | for goproxy.next() { 212 | proxy(goproxy.args()) 213 | } 214 | ` 215 | // 5 seconds context timeout is enough for an example. 216 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 217 | defer cancel() 218 | script := tengo.NewScript([]byte(src)) 219 | moduleMap := tengo.NewModuleMap() 220 | goproxy := NewGoProxy(ctx) 221 | // register modules 222 | moduleMap.AddBuiltinModule("goproxy", goproxy.ModuleMap()) 223 | moduleMap.AddSourceModule("proxy", []byte(ProxySource)) 224 | script.SetImports(moduleMap) 225 | 226 | compiled, err := script.Compile() 227 | if err != nil { 228 | panic(err) 229 | } 230 | 231 | // call "sum", "multiply", "increment" functions from tengo in a new goroutine 232 | go func() { 233 | callChan := goproxy.CallChan() 234 | result := make(chan tengo.Object, 1) 235 | // TODO: check tengo error from result channel. 236 | loop: 237 | for { 238 | select { 239 | case <-ctx.Done(): 240 | break loop 241 | default: 242 | } 243 | fmt.Println("Calling tengo sum function") 244 | i1, i2 := rand.Int63n(100), rand.Int63n(100) 245 | callChan <- &CallArgs{Func: "sum", 246 | Params: []tengo.Object{&tengo.Int{Value: i1}, 247 | &tengo.Int{Value: i2}}, 248 | Result: result, 249 | } 250 | v := <-result 251 | fmt.Printf("%d + %d = %v\n", i1, i2, v) 252 | 253 | fmt.Println("Calling tengo multiply function") 254 | i1, i2 = rand.Int63n(20), rand.Int63n(20) 255 | callChan <- &CallArgs{Func: "multiply", 256 | Params: []tengo.Object{&tengo.Int{Value: i1}, 257 | &tengo.Int{Value: i2}}, 258 | Result: result, 259 | } 260 | v = <-result 261 | fmt.Printf("%d * %d = %v\n", i1, i2, v) 262 | 263 | fmt.Println("Calling tengo increment function") 264 | callChan <- &CallArgs{Func: "increment", Result: result} 265 | v = <-result 266 | fmt.Printf("increment = %v\n", v) 267 | time.Sleep(1 * time.Second) 268 | } 269 | }() 270 | 271 | if err := compiled.RunContext(ctx); err != nil { 272 | fmt.Println(err) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/d5/tengo/v2 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d5/tengo/e78e18a0f6f90ab659edc5fe5e8b812825ccb5a4/go.sum -------------------------------------------------------------------------------- /instructions.go: -------------------------------------------------------------------------------- 1 | package tengo 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/d5/tengo/v2/parser" 7 | ) 8 | 9 | // MakeInstruction returns a bytecode for an opcode and the operands. 10 | func MakeInstruction(opcode parser.Opcode, operands ...int) []byte { 11 | numOperands := parser.OpcodeOperands[opcode] 12 | 13 | totalLen := 1 14 | for _, w := range numOperands { 15 | totalLen += w 16 | } 17 | 18 | instruction := make([]byte, totalLen) 19 | instruction[0] = opcode 20 | 21 | offset := 1 22 | for i, o := range operands { 23 | width := numOperands[i] 24 | switch width { 25 | case 1: 26 | instruction[offset] = byte(o) 27 | case 2: 28 | n := uint16(o) 29 | instruction[offset] = byte(n >> 8) 30 | instruction[offset+1] = byte(n) 31 | case 4: 32 | n := uint32(o) 33 | instruction[offset] = byte(n >> 24) 34 | instruction[offset+1] = byte(n >> 16) 35 | instruction[offset+2] = byte(n >> 8) 36 | instruction[offset+3] = byte(n) 37 | } 38 | offset += width 39 | } 40 | return instruction 41 | } 42 | 43 | // FormatInstructions returns string representation of bytecode instructions. 44 | func FormatInstructions(b []byte, posOffset int) []string { 45 | var out []string 46 | 47 | i := 0 48 | for i < len(b) { 49 | numOperands := parser.OpcodeOperands[b[i]] 50 | operands, read := parser.ReadOperands(numOperands, b[i+1:]) 51 | 52 | switch len(numOperands) { 53 | case 0: 54 | out = append(out, fmt.Sprintf("%04d %-7s", 55 | posOffset+i, parser.OpcodeNames[b[i]])) 56 | case 1: 57 | out = append(out, fmt.Sprintf("%04d %-7s %-5d", 58 | posOffset+i, parser.OpcodeNames[b[i]], operands[0])) 59 | case 2: 60 | out = append(out, fmt.Sprintf("%04d %-7s %-5d %-5d", 61 | posOffset+i, parser.OpcodeNames[b[i]], 62 | operands[0], operands[1])) 63 | } 64 | i += 1 + read 65 | } 66 | return out 67 | } 68 | -------------------------------------------------------------------------------- /iterator.go: -------------------------------------------------------------------------------- 1 | package tengo 2 | 3 | // Iterator represents an iterator for underlying data type. 4 | type Iterator interface { 5 | Object 6 | 7 | // Next returns true if there are more elements to iterate. 8 | Next() bool 9 | 10 | // Key returns the key or index value of the current element. 11 | Key() Object 12 | 13 | // Value returns the value of the current element. 14 | Value() Object 15 | } 16 | 17 | // ArrayIterator is an iterator for an array. 18 | type ArrayIterator struct { 19 | ObjectImpl 20 | v []Object 21 | i int 22 | l int 23 | } 24 | 25 | // TypeName returns the name of the type. 26 | func (i *ArrayIterator) TypeName() string { 27 | return "array-iterator" 28 | } 29 | 30 | func (i *ArrayIterator) String() string { 31 | return "" 32 | } 33 | 34 | // IsFalsy returns true if the value of the type is falsy. 35 | func (i *ArrayIterator) IsFalsy() bool { 36 | return true 37 | } 38 | 39 | // Equals returns true if the value of the type is equal to the value of 40 | // another object. 41 | func (i *ArrayIterator) Equals(Object) bool { 42 | return false 43 | } 44 | 45 | // Copy returns a copy of the type. 46 | func (i *ArrayIterator) Copy() Object { 47 | return &ArrayIterator{v: i.v, i: i.i, l: i.l} 48 | } 49 | 50 | // Next returns true if there are more elements to iterate. 51 | func (i *ArrayIterator) Next() bool { 52 | i.i++ 53 | return i.i <= i.l 54 | } 55 | 56 | // Key returns the key or index value of the current element. 57 | func (i *ArrayIterator) Key() Object { 58 | return &Int{Value: int64(i.i - 1)} 59 | } 60 | 61 | // Value returns the value of the current element. 62 | func (i *ArrayIterator) Value() Object { 63 | return i.v[i.i-1] 64 | } 65 | 66 | // BytesIterator represents an iterator for a string. 67 | type BytesIterator struct { 68 | ObjectImpl 69 | v []byte 70 | i int 71 | l int 72 | } 73 | 74 | // TypeName returns the name of the type. 75 | func (i *BytesIterator) TypeName() string { 76 | return "bytes-iterator" 77 | } 78 | 79 | func (i *BytesIterator) String() string { 80 | return "" 81 | } 82 | 83 | // Equals returns true if the value of the type is equal to the value of 84 | // another object. 85 | func (i *BytesIterator) Equals(Object) bool { 86 | return false 87 | } 88 | 89 | // Copy returns a copy of the type. 90 | func (i *BytesIterator) Copy() Object { 91 | return &BytesIterator{v: i.v, i: i.i, l: i.l} 92 | } 93 | 94 | // Next returns true if there are more elements to iterate. 95 | func (i *BytesIterator) Next() bool { 96 | i.i++ 97 | return i.i <= i.l 98 | } 99 | 100 | // Key returns the key or index value of the current element. 101 | func (i *BytesIterator) Key() Object { 102 | return &Int{Value: int64(i.i - 1)} 103 | } 104 | 105 | // Value returns the value of the current element. 106 | func (i *BytesIterator) Value() Object { 107 | return &Int{Value: int64(i.v[i.i-1])} 108 | } 109 | 110 | // MapIterator represents an iterator for the map. 111 | type MapIterator struct { 112 | ObjectImpl 113 | v map[string]Object 114 | k []string 115 | i int 116 | l int 117 | } 118 | 119 | // TypeName returns the name of the type. 120 | func (i *MapIterator) TypeName() string { 121 | return "map-iterator" 122 | } 123 | 124 | func (i *MapIterator) String() string { 125 | return "" 126 | } 127 | 128 | // IsFalsy returns true if the value of the type is falsy. 129 | func (i *MapIterator) IsFalsy() bool { 130 | return true 131 | } 132 | 133 | // Equals returns true if the value of the type is equal to the value of 134 | // another object. 135 | func (i *MapIterator) Equals(Object) bool { 136 | return false 137 | } 138 | 139 | // Copy returns a copy of the type. 140 | func (i *MapIterator) Copy() Object { 141 | return &MapIterator{v: i.v, k: i.k, i: i.i, l: i.l} 142 | } 143 | 144 | // Next returns true if there are more elements to iterate. 145 | func (i *MapIterator) Next() bool { 146 | i.i++ 147 | return i.i <= i.l 148 | } 149 | 150 | // Key returns the key or index value of the current element. 151 | func (i *MapIterator) Key() Object { 152 | k := i.k[i.i-1] 153 | return &String{Value: k} 154 | } 155 | 156 | // Value returns the value of the current element. 157 | func (i *MapIterator) Value() Object { 158 | k := i.k[i.i-1] 159 | return i.v[k] 160 | } 161 | 162 | // StringIterator represents an iterator for a string. 163 | type StringIterator struct { 164 | ObjectImpl 165 | v []rune 166 | i int 167 | l int 168 | } 169 | 170 | // TypeName returns the name of the type. 171 | func (i *StringIterator) TypeName() string { 172 | return "string-iterator" 173 | } 174 | 175 | func (i *StringIterator) String() string { 176 | return "" 177 | } 178 | 179 | // IsFalsy returns true if the value of the type is falsy. 180 | func (i *StringIterator) IsFalsy() bool { 181 | return true 182 | } 183 | 184 | // Equals returns true if the value of the type is equal to the value of 185 | // another object. 186 | func (i *StringIterator) Equals(Object) bool { 187 | return false 188 | } 189 | 190 | // Copy returns a copy of the type. 191 | func (i *StringIterator) Copy() Object { 192 | return &StringIterator{v: i.v, i: i.i, l: i.l} 193 | } 194 | 195 | // Next returns true if there are more elements to iterate. 196 | func (i *StringIterator) Next() bool { 197 | i.i++ 198 | return i.i <= i.l 199 | } 200 | 201 | // Key returns the key or index value of the current element. 202 | func (i *StringIterator) Key() Object { 203 | return &Int{Value: int64(i.i - 1)} 204 | } 205 | 206 | // Value returns the value of the current element. 207 | func (i *StringIterator) Value() Object { 208 | return &Char{Value: i.v[i.i-1]} 209 | } 210 | -------------------------------------------------------------------------------- /modules.go: -------------------------------------------------------------------------------- 1 | package tengo 2 | 3 | // Importable interface represents importable module instance. 4 | type Importable interface { 5 | // Import should return either an Object or module source code ([]byte). 6 | Import(moduleName string) (interface{}, error) 7 | } 8 | 9 | // ModuleGetter enables implementing dynamic module loading. 10 | type ModuleGetter interface { 11 | Get(name string) Importable 12 | } 13 | 14 | // ModuleMap represents a set of named modules. Use NewModuleMap to create a 15 | // new module map. 16 | type ModuleMap struct { 17 | m map[string]Importable 18 | } 19 | 20 | // NewModuleMap creates a new module map. 21 | func NewModuleMap() *ModuleMap { 22 | return &ModuleMap{ 23 | m: make(map[string]Importable), 24 | } 25 | } 26 | 27 | // Add adds an import module. 28 | func (m *ModuleMap) Add(name string, module Importable) { 29 | m.m[name] = module 30 | } 31 | 32 | // AddBuiltinModule adds a builtin module. 33 | func (m *ModuleMap) AddBuiltinModule(name string, attrs map[string]Object) { 34 | m.m[name] = &BuiltinModule{Attrs: attrs} 35 | } 36 | 37 | // AddSourceModule adds a source module. 38 | func (m *ModuleMap) AddSourceModule(name string, src []byte) { 39 | m.m[name] = &SourceModule{Src: src} 40 | } 41 | 42 | // Remove removes a named module. 43 | func (m *ModuleMap) Remove(name string) { 44 | delete(m.m, name) 45 | } 46 | 47 | // Get returns an import module identified by name. It returns if the name is 48 | // not found. 49 | func (m *ModuleMap) Get(name string) Importable { 50 | return m.m[name] 51 | } 52 | 53 | // GetBuiltinModule returns a builtin module identified by name. It returns 54 | // if the name is not found or the module is not a builtin module. 55 | func (m *ModuleMap) GetBuiltinModule(name string) *BuiltinModule { 56 | mod, _ := m.m[name].(*BuiltinModule) 57 | return mod 58 | } 59 | 60 | // GetSourceModule returns a source module identified by name. It returns if 61 | // the name is not found or the module is not a source module. 62 | func (m *ModuleMap) GetSourceModule(name string) *SourceModule { 63 | mod, _ := m.m[name].(*SourceModule) 64 | return mod 65 | } 66 | 67 | // Copy creates a copy of the module map. 68 | func (m *ModuleMap) Copy() *ModuleMap { 69 | c := &ModuleMap{ 70 | m: make(map[string]Importable), 71 | } 72 | for name, mod := range m.m { 73 | c.m[name] = mod 74 | } 75 | return c 76 | } 77 | 78 | // Len returns the number of named modules. 79 | func (m *ModuleMap) Len() int { 80 | return len(m.m) 81 | } 82 | 83 | // AddMap adds named modules from another module map. 84 | func (m *ModuleMap) AddMap(o *ModuleMap) { 85 | for name, mod := range o.m { 86 | m.m[name] = mod 87 | } 88 | } 89 | 90 | // SourceModule is an importable module that's written in Tengo. 91 | type SourceModule struct { 92 | Src []byte 93 | } 94 | 95 | // Import returns a module source code. 96 | func (m *SourceModule) Import(_ string) (interface{}, error) { 97 | return m.Src, nil 98 | } 99 | -------------------------------------------------------------------------------- /parser/ast.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const ( 8 | nullRep = "" 9 | ) 10 | 11 | // Node represents a node in the AST. 12 | type Node interface { 13 | // Pos returns the position of first character belonging to the node. 14 | Pos() Pos 15 | // End returns the position of first character immediately after the node. 16 | End() Pos 17 | // String returns a string representation of the node. 18 | String() string 19 | } 20 | 21 | // IdentList represents a list of identifiers. 22 | type IdentList struct { 23 | LParen Pos 24 | VarArgs bool 25 | List []*Ident 26 | RParen Pos 27 | } 28 | 29 | // Pos returns the position of first character belonging to the node. 30 | func (n *IdentList) Pos() Pos { 31 | if n.LParen.IsValid() { 32 | return n.LParen 33 | } 34 | if len(n.List) > 0 { 35 | return n.List[0].Pos() 36 | } 37 | return NoPos 38 | } 39 | 40 | // End returns the position of first character immediately after the node. 41 | func (n *IdentList) End() Pos { 42 | if n.RParen.IsValid() { 43 | return n.RParen + 1 44 | } 45 | if l := len(n.List); l > 0 { 46 | return n.List[l-1].End() 47 | } 48 | return NoPos 49 | } 50 | 51 | // NumFields returns the number of fields. 52 | func (n *IdentList) NumFields() int { 53 | if n == nil { 54 | return 0 55 | } 56 | return len(n.List) 57 | } 58 | 59 | func (n *IdentList) String() string { 60 | var list []string 61 | for i, e := range n.List { 62 | if n.VarArgs && i == len(n.List)-1 { 63 | list = append(list, "..."+e.String()) 64 | } else { 65 | list = append(list, e.String()) 66 | } 67 | } 68 | return "(" + strings.Join(list, ", ") + ")" 69 | } 70 | -------------------------------------------------------------------------------- /parser/ast_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/d5/tengo/v2/parser" 7 | ) 8 | 9 | func TestIdentListString(t *testing.T) { 10 | identListVar := &parser.IdentList{ 11 | List: []*parser.Ident{ 12 | {Name: "a"}, 13 | {Name: "b"}, 14 | {Name: "c"}, 15 | }, 16 | VarArgs: true, 17 | } 18 | 19 | expectedVar := "(a, b, ...c)" 20 | if str := identListVar.String(); str != expectedVar { 21 | t.Fatalf("expected string of %#v to be %s, got %s", 22 | identListVar, expectedVar, str) 23 | } 24 | 25 | identList := &parser.IdentList{ 26 | List: []*parser.Ident{ 27 | {Name: "a"}, 28 | {Name: "b"}, 29 | {Name: "c"}, 30 | }, 31 | VarArgs: false, 32 | } 33 | 34 | expected := "(a, b, c)" 35 | if str := identList.String(); str != expected { 36 | t.Fatalf("expected string of %#v to be %s, got %s", 37 | identList, expected, str) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /parser/file.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // File represents a file unit. 8 | type File struct { 9 | InputFile *SourceFile 10 | Stmts []Stmt 11 | } 12 | 13 | // Pos returns the position of first character belonging to the node. 14 | func (n *File) Pos() Pos { 15 | return Pos(n.InputFile.Base) 16 | } 17 | 18 | // End returns the position of first character immediately after the node. 19 | func (n *File) End() Pos { 20 | return Pos(n.InputFile.Base + n.InputFile.Size) 21 | } 22 | 23 | func (n *File) String() string { 24 | var stmts []string 25 | for _, e := range n.Stmts { 26 | stmts = append(stmts, e.String()) 27 | } 28 | return strings.Join(stmts, "; ") 29 | } 30 | -------------------------------------------------------------------------------- /parser/opcodes.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // Opcode represents a single byte operation code. 4 | type Opcode = byte 5 | 6 | // List of opcodes 7 | const ( 8 | OpConstant Opcode = iota // Load constant 9 | OpBComplement // bitwise complement 10 | OpPop // Pop 11 | OpTrue // Push true 12 | OpFalse // Push false 13 | OpEqual // Equal == 14 | OpNotEqual // Not equal != 15 | OpMinus // Minus - 16 | OpLNot // Logical not ! 17 | OpJumpFalsy // Jump if falsy 18 | OpAndJump // Logical AND jump 19 | OpOrJump // Logical OR jump 20 | OpJump // Jump 21 | OpNull // Push null 22 | OpArray // Array object 23 | OpMap // Map object 24 | OpError // Error object 25 | OpImmutable // Immutable object 26 | OpIndex // Index operation 27 | OpSliceIndex // Slice operation 28 | OpCall // Call function 29 | OpReturn // Return 30 | OpGetGlobal // Get global variable 31 | OpSetGlobal // Set global variable 32 | OpSetSelGlobal // Set global variable using selectors 33 | OpGetLocal // Get local variable 34 | OpSetLocal // Set local variable 35 | OpDefineLocal // Define local variable 36 | OpSetSelLocal // Set local variable using selectors 37 | OpGetFreePtr // Get free variable pointer object 38 | OpGetFree // Get free variables 39 | OpSetFree // Set free variables 40 | OpGetLocalPtr // Get local variable as a pointer 41 | OpSetSelFree // Set free variables using selectors 42 | OpGetBuiltin // Get builtin function 43 | OpClosure // Push closure 44 | OpIteratorInit // Iterator init 45 | OpIteratorNext // Iterator next 46 | OpIteratorKey // Iterator key 47 | OpIteratorValue // Iterator value 48 | OpBinaryOp // Binary operation 49 | OpSuspend // Suspend VM 50 | ) 51 | 52 | // OpcodeNames are string representation of opcodes. 53 | var OpcodeNames = [...]string{ 54 | OpConstant: "CONST", 55 | OpPop: "POP", 56 | OpTrue: "TRUE", 57 | OpFalse: "FALSE", 58 | OpBComplement: "NEG", 59 | OpEqual: "EQL", 60 | OpNotEqual: "NEQ", 61 | OpMinus: "NEG", 62 | OpLNot: "NOT", 63 | OpJumpFalsy: "JMPF", 64 | OpAndJump: "ANDJMP", 65 | OpOrJump: "ORJMP", 66 | OpJump: "JMP", 67 | OpNull: "NULL", 68 | OpGetGlobal: "GETG", 69 | OpSetGlobal: "SETG", 70 | OpSetSelGlobal: "SETSG", 71 | OpArray: "ARR", 72 | OpMap: "MAP", 73 | OpError: "ERROR", 74 | OpImmutable: "IMMUT", 75 | OpIndex: "INDEX", 76 | OpSliceIndex: "SLICE", 77 | OpCall: "CALL", 78 | OpReturn: "RET", 79 | OpGetLocal: "GETL", 80 | OpSetLocal: "SETL", 81 | OpDefineLocal: "DEFL", 82 | OpSetSelLocal: "SETSL", 83 | OpGetBuiltin: "BUILTIN", 84 | OpClosure: "CLOSURE", 85 | OpGetFreePtr: "GETFP", 86 | OpGetFree: "GETF", 87 | OpSetFree: "SETF", 88 | OpGetLocalPtr: "GETLP", 89 | OpSetSelFree: "SETSF", 90 | OpIteratorInit: "ITER", 91 | OpIteratorNext: "ITNXT", 92 | OpIteratorKey: "ITKEY", 93 | OpIteratorValue: "ITVAL", 94 | OpBinaryOp: "BINARYOP", 95 | OpSuspend: "SUSPEND", 96 | } 97 | 98 | // OpcodeOperands is the number of operands. 99 | var OpcodeOperands = [...][]int{ 100 | OpConstant: {2}, 101 | OpPop: {}, 102 | OpTrue: {}, 103 | OpFalse: {}, 104 | OpBComplement: {}, 105 | OpEqual: {}, 106 | OpNotEqual: {}, 107 | OpMinus: {}, 108 | OpLNot: {}, 109 | OpJumpFalsy: {4}, 110 | OpAndJump: {4}, 111 | OpOrJump: {4}, 112 | OpJump: {4}, 113 | OpNull: {}, 114 | OpGetGlobal: {2}, 115 | OpSetGlobal: {2}, 116 | OpSetSelGlobal: {2, 1}, 117 | OpArray: {2}, 118 | OpMap: {2}, 119 | OpError: {}, 120 | OpImmutable: {}, 121 | OpIndex: {}, 122 | OpSliceIndex: {}, 123 | OpCall: {1, 1}, 124 | OpReturn: {1}, 125 | OpGetLocal: {1}, 126 | OpSetLocal: {1}, 127 | OpDefineLocal: {1}, 128 | OpSetSelLocal: {1, 1}, 129 | OpGetBuiltin: {1}, 130 | OpClosure: {2, 1}, 131 | OpGetFreePtr: {1}, 132 | OpGetFree: {1}, 133 | OpSetFree: {1}, 134 | OpGetLocalPtr: {1}, 135 | OpSetSelFree: {1, 1}, 136 | OpIteratorInit: {}, 137 | OpIteratorNext: {}, 138 | OpIteratorKey: {}, 139 | OpIteratorValue: {}, 140 | OpBinaryOp: {1}, 141 | OpSuspend: {}, 142 | } 143 | 144 | // ReadOperands reads operands from the bytecode. 145 | func ReadOperands(numOperands []int, ins []byte) (operands []int, offset int) { 146 | for _, width := range numOperands { 147 | switch width { 148 | case 1: 149 | operands = append(operands, int(ins[offset])) 150 | case 2: 151 | operands = append(operands, int(ins[offset+1])|int(ins[offset])<<8) 152 | case 4: 153 | operands = append(operands, int(ins[offset+3])|int(ins[offset+2])<<8|int(ins[offset+1])<<16|int(ins[offset])<<24) 154 | } 155 | offset += width 156 | } 157 | return 158 | } 159 | -------------------------------------------------------------------------------- /parser/pos.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // Pos represents a position in the file set. 4 | type Pos int 5 | 6 | // NoPos represents an invalid position. 7 | const NoPos Pos = 0 8 | 9 | // IsValid returns true if the position is valid. 10 | func (p Pos) IsValid() bool { 11 | return p != NoPos 12 | } 13 | -------------------------------------------------------------------------------- /parser/scanner_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/d5/tengo/v2/parser" 11 | "github.com/d5/tengo/v2/require" 12 | "github.com/d5/tengo/v2/token" 13 | ) 14 | 15 | var testFileSet = parser.NewFileSet() 16 | 17 | type scanResult struct { 18 | Token token.Token 19 | Literal string 20 | Line int 21 | Column int 22 | } 23 | 24 | func TestScanner_Scan(t *testing.T) { 25 | var testCases = [...]struct { 26 | token token.Token 27 | literal string 28 | }{ 29 | {token.Comment, "/* a comment */"}, 30 | {token.Comment, "// a comment \n"}, 31 | {token.Comment, "/*\r*/"}, 32 | {token.Comment, "/**\r/*/"}, 33 | {token.Comment, "/**\r\r/*/"}, 34 | {token.Comment, "//\r\n"}, 35 | {token.Ident, "foobar"}, 36 | {token.Ident, "a۰۱۸"}, 37 | {token.Ident, "foo६४"}, 38 | {token.Ident, "bar9876"}, 39 | {token.Ident, "ŝ"}, 40 | {token.Ident, "ŝfoo"}, 41 | {token.Int, "0"}, 42 | {token.Int, "1"}, 43 | {token.Int, "123456789012345678890"}, 44 | {token.Int, "01234567"}, 45 | {token.Int, "0xcafebabe"}, 46 | {token.Float, "0."}, 47 | {token.Float, ".0"}, 48 | {token.Float, "3.14159265"}, 49 | {token.Float, "1e0"}, 50 | {token.Float, "1e+100"}, 51 | {token.Float, "1e-100"}, 52 | {token.Float, "2.71828e-1000"}, 53 | {token.Char, "'a'"}, 54 | {token.Char, "'\\000'"}, 55 | {token.Char, "'\\xFF'"}, 56 | {token.Char, "'\\uff16'"}, 57 | {token.Char, "'\\U0000ff16'"}, 58 | {token.String, "`foobar`"}, 59 | {token.String, "`" + `foo 60 | bar` + 61 | "`", 62 | }, 63 | {token.String, "`\r`"}, 64 | {token.String, "`foo\r\nbar`"}, 65 | {token.Add, "+"}, 66 | {token.Sub, "-"}, 67 | {token.Mul, "*"}, 68 | {token.Quo, "/"}, 69 | {token.Rem, "%"}, 70 | {token.And, "&"}, 71 | {token.Or, "|"}, 72 | {token.Xor, "^"}, 73 | {token.Shl, "<<"}, 74 | {token.Shr, ">>"}, 75 | {token.AndNot, "&^"}, 76 | {token.AddAssign, "+="}, 77 | {token.SubAssign, "-="}, 78 | {token.MulAssign, "*="}, 79 | {token.QuoAssign, "/="}, 80 | {token.RemAssign, "%="}, 81 | {token.AndAssign, "&="}, 82 | {token.OrAssign, "|="}, 83 | {token.XorAssign, "^="}, 84 | {token.ShlAssign, "<<="}, 85 | {token.ShrAssign, ">>="}, 86 | {token.AndNotAssign, "&^="}, 87 | {token.LAnd, "&&"}, 88 | {token.LOr, "||"}, 89 | {token.Inc, "++"}, 90 | {token.Dec, "--"}, 91 | {token.Equal, "=="}, 92 | {token.Less, "<"}, 93 | {token.Greater, ">"}, 94 | {token.Assign, "="}, 95 | {token.Not, "!"}, 96 | {token.NotEqual, "!="}, 97 | {token.LessEq, "<="}, 98 | {token.GreaterEq, ">="}, 99 | {token.Define, ":="}, 100 | {token.Ellipsis, "..."}, 101 | {token.LParen, "("}, 102 | {token.LBrack, "["}, 103 | {token.LBrace, "{"}, 104 | {token.Comma, ","}, 105 | {token.Period, "."}, 106 | {token.RParen, ")"}, 107 | {token.RBrack, "]"}, 108 | {token.RBrace, "}"}, 109 | {token.Semicolon, ";"}, 110 | {token.Colon, ":"}, 111 | {token.Break, "break"}, 112 | {token.Continue, "continue"}, 113 | {token.Else, "else"}, 114 | {token.For, "for"}, 115 | {token.Func, "func"}, 116 | {token.If, "if"}, 117 | {token.Return, "return"}, 118 | {token.Export, "export"}, 119 | } 120 | 121 | // combine 122 | var lines []string 123 | var lineSum int 124 | lineNos := make([]int, len(testCases)) 125 | columnNos := make([]int, len(testCases)) 126 | for i, tc := range testCases { 127 | // add 0-2 lines before each test case 128 | emptyLines := rand.Intn(3) 129 | for j := 0; j < emptyLines; j++ { 130 | lines = append(lines, strings.Repeat(" ", rand.Intn(10))) 131 | } 132 | 133 | // add test case line with some whitespaces around it 134 | emptyColumns := rand.Intn(10) 135 | lines = append(lines, fmt.Sprintf("%s%s%s", 136 | strings.Repeat(" ", emptyColumns), 137 | tc.literal, 138 | strings.Repeat(" ", rand.Intn(10)))) 139 | 140 | lineNos[i] = lineSum + emptyLines + 1 141 | lineSum += emptyLines + countLines(tc.literal) 142 | columnNos[i] = emptyColumns + 1 143 | } 144 | 145 | // expected results 146 | var expected []scanResult 147 | var expectedSkipComments []scanResult 148 | for i, tc := range testCases { 149 | // expected literal 150 | var expectedLiteral string 151 | switch tc.token { 152 | case token.Comment: 153 | // strip CRs in comments 154 | expectedLiteral = string(parser.StripCR([]byte(tc.literal), 155 | tc.literal[1] == '*')) 156 | 157 | //-style comment literal doesn't contain newline 158 | if expectedLiteral[1] == '/' { 159 | expectedLiteral = expectedLiteral[:len(expectedLiteral)-1] 160 | } 161 | case token.Ident: 162 | expectedLiteral = tc.literal 163 | case token.Semicolon: 164 | expectedLiteral = ";" 165 | default: 166 | if tc.token.IsLiteral() { 167 | // strip CRs in raw string 168 | expectedLiteral = tc.literal 169 | if expectedLiteral[0] == '`' { 170 | expectedLiteral = string(parser.StripCR( 171 | []byte(expectedLiteral), false)) 172 | } 173 | } else if tc.token.IsKeyword() { 174 | expectedLiteral = tc.literal 175 | } 176 | } 177 | 178 | res := scanResult{ 179 | Token: tc.token, 180 | Literal: expectedLiteral, 181 | Line: lineNos[i], 182 | Column: columnNos[i], 183 | } 184 | 185 | expected = append(expected, res) 186 | if tc.token != token.Comment { 187 | expectedSkipComments = append(expectedSkipComments, res) 188 | } 189 | } 190 | 191 | scanExpect(t, strings.Join(lines, "\n"), 192 | parser.ScanComments|parser.DontInsertSemis, expected...) 193 | scanExpect(t, strings.Join(lines, "\n"), 194 | parser.DontInsertSemis, expectedSkipComments...) 195 | } 196 | 197 | func TestStripCR(t *testing.T) { 198 | for _, tc := range []struct { 199 | input string 200 | expect string 201 | }{ 202 | {"//\n", "//\n"}, 203 | {"//\r\n", "//\n"}, 204 | {"//\r\r\r\n", "//\n"}, 205 | {"//\r*\r/\r\n", "//*/\n"}, 206 | {"/**/", "/**/"}, 207 | {"/*\r/*/", "/*/*/"}, 208 | {"/*\r*/", "/**/"}, 209 | {"/**\r/*/", "/**\r/*/"}, 210 | {"/*\r/\r*\r/*/", "/*/*\r/*/"}, 211 | {"/*\r\r\r\r*/", "/**/"}, 212 | } { 213 | actual := string(parser.StripCR([]byte(tc.input), 214 | len(tc.input) >= 2 && tc.input[1] == '*')) 215 | require.Equal(t, tc.expect, actual) 216 | } 217 | } 218 | 219 | func scanExpect( 220 | t *testing.T, 221 | input string, 222 | mode parser.ScanMode, 223 | expected ...scanResult, 224 | ) { 225 | testFile := testFileSet.AddFile("test", -1, len(input)) 226 | 227 | s := parser.NewScanner( 228 | testFile, 229 | []byte(input), 230 | func(_ parser.SourceFilePos, msg string) { require.Fail(t, msg) }, 231 | mode) 232 | 233 | for idx, e := range expected { 234 | tok, literal, pos := s.Scan() 235 | 236 | filePos := testFile.Position(pos) 237 | 238 | require.Equal(t, e.Token, tok, "[%d] expected: %s, actual: %s", 239 | idx, e.Token.String(), tok.String()) 240 | require.Equal(t, e.Literal, literal) 241 | require.Equal(t, e.Line, filePos.Line) 242 | require.Equal(t, e.Column, filePos.Column) 243 | } 244 | 245 | tok, _, _ := s.Scan() 246 | require.Equal(t, token.EOF, tok, "more tokens left") 247 | require.Equal(t, 0, s.ErrorCount()) 248 | } 249 | 250 | func countLines(s string) int { 251 | if s == "" { 252 | return 0 253 | } 254 | n := 1 255 | for i := 0; i < len(s); i++ { 256 | if s[i] == '\n' { 257 | n++ 258 | } 259 | } 260 | return n 261 | } 262 | 263 | func init() { 264 | rand.Seed(time.Now().UnixNano()) 265 | } 266 | -------------------------------------------------------------------------------- /parser/source_file.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | // SourceFilePos represents a position information in the file. 9 | type SourceFilePos struct { 10 | Filename string // filename, if any 11 | Offset int // offset, starting at 0 12 | Line int // line number, starting at 1 13 | Column int // column number, starting at 1 (byte count) 14 | } 15 | 16 | // IsValid returns true if the position is valid. 17 | func (p SourceFilePos) IsValid() bool { 18 | return p.Line > 0 19 | } 20 | 21 | // String returns a string in one of several forms: 22 | // 23 | // file:line:column valid position with file name 24 | // file:line valid position with file name but no column (column == 0) 25 | // line:column valid position without file name 26 | // line valid position without file name and no column (column == 0) 27 | // file invalid position with file name 28 | // - invalid position without file name 29 | // 30 | func (p SourceFilePos) String() string { 31 | s := p.Filename 32 | if p.IsValid() { 33 | if s != "" { 34 | s += ":" 35 | } 36 | s += fmt.Sprintf("%d", p.Line) 37 | if p.Column != 0 { 38 | s += fmt.Sprintf(":%d", p.Column) 39 | } 40 | } 41 | if s == "" { 42 | s = "-" 43 | } 44 | return s 45 | } 46 | 47 | // SourceFileSet represents a set of source files. 48 | type SourceFileSet struct { 49 | Base int // base offset for the next file 50 | Files []*SourceFile // list of files in the order added to the set 51 | LastFile *SourceFile // cache of last file looked up 52 | } 53 | 54 | // NewFileSet creates a new file set. 55 | func NewFileSet() *SourceFileSet { 56 | return &SourceFileSet{ 57 | Base: 1, // 0 == NoPos 58 | } 59 | } 60 | 61 | // AddFile adds a new file in the file set. 62 | func (s *SourceFileSet) AddFile(filename string, base, size int) *SourceFile { 63 | if base < 0 { 64 | base = s.Base 65 | } 66 | if base < s.Base || size < 0 { 67 | panic("illegal base or size") 68 | } 69 | f := &SourceFile{ 70 | set: s, 71 | Name: filename, 72 | Base: base, 73 | Size: size, 74 | Lines: []int{0}, 75 | } 76 | base += size + 1 // +1 because EOF also has a position 77 | if base < 0 { 78 | panic("offset overflow (> 2G of source code in file set)") 79 | } 80 | 81 | // add the file to the file set 82 | s.Base = base 83 | s.Files = append(s.Files, f) 84 | s.LastFile = f 85 | return f 86 | } 87 | 88 | // File returns the file that contains the position p. If no such file is 89 | // found (for instance for p == NoPos), the result is nil. 90 | func (s *SourceFileSet) File(p Pos) (f *SourceFile) { 91 | if p != NoPos { 92 | f = s.file(p) 93 | } 94 | return 95 | } 96 | 97 | // Position converts a SourcePos p in the fileset into a SourceFilePos value. 98 | func (s *SourceFileSet) Position(p Pos) (pos SourceFilePos) { 99 | if p != NoPos { 100 | if f := s.file(p); f != nil { 101 | return f.position(p) 102 | } 103 | } 104 | return 105 | } 106 | 107 | func (s *SourceFileSet) file(p Pos) *SourceFile { 108 | // common case: p is in last file 109 | f := s.LastFile 110 | if f != nil && f.Base <= int(p) && int(p) <= f.Base+f.Size { 111 | return f 112 | } 113 | 114 | // p is not in last file - search all files 115 | if i := searchFiles(s.Files, int(p)); i >= 0 { 116 | f := s.Files[i] 117 | 118 | // f.base <= int(p) by definition of searchFiles 119 | if int(p) <= f.Base+f.Size { 120 | s.LastFile = f // race is ok - s.last is only a cache 121 | return f 122 | } 123 | } 124 | return nil 125 | } 126 | 127 | func searchFiles(a []*SourceFile, x int) int { 128 | return sort.Search(len(a), func(i int) bool { return a[i].Base > x }) - 1 129 | } 130 | 131 | // SourceFile represents a source file. 132 | type SourceFile struct { 133 | // SourceFile set for the file 134 | set *SourceFileSet 135 | // SourceFile name as provided to AddFile 136 | Name string 137 | // SourcePos value range for this file is [base...base+size] 138 | Base int 139 | // SourceFile size as provided to AddFile 140 | Size int 141 | // Lines contains the offset of the first character for each line 142 | // (the first entry is always 0) 143 | Lines []int 144 | } 145 | 146 | // Set returns SourceFileSet. 147 | func (f *SourceFile) Set() *SourceFileSet { 148 | return f.set 149 | } 150 | 151 | // LineCount returns the current number of lines. 152 | func (f *SourceFile) LineCount() int { 153 | return len(f.Lines) 154 | } 155 | 156 | // AddLine adds a new line. 157 | func (f *SourceFile) AddLine(offset int) { 158 | i := len(f.Lines) 159 | if (i == 0 || f.Lines[i-1] < offset) && offset < f.Size { 160 | f.Lines = append(f.Lines, offset) 161 | } 162 | } 163 | 164 | // LineStart returns the position of the first character in the line. 165 | func (f *SourceFile) LineStart(line int) Pos { 166 | if line < 1 { 167 | panic("illegal line number (line numbering starts at 1)") 168 | } 169 | if line > len(f.Lines) { 170 | panic("illegal line number") 171 | } 172 | return Pos(f.Base + f.Lines[line-1]) 173 | } 174 | 175 | // FileSetPos returns the position in the file set. 176 | func (f *SourceFile) FileSetPos(offset int) Pos { 177 | if offset > f.Size { 178 | panic("illegal file offset") 179 | } 180 | return Pos(f.Base + offset) 181 | } 182 | 183 | // Offset translates the file set position into the file offset. 184 | func (f *SourceFile) Offset(p Pos) int { 185 | if int(p) < f.Base || int(p) > f.Base+f.Size { 186 | panic("illegal SourcePos value") 187 | } 188 | return int(p) - f.Base 189 | } 190 | 191 | // Position translates the file set position into the file position. 192 | func (f *SourceFile) Position(p Pos) (pos SourceFilePos) { 193 | if p != NoPos { 194 | if int(p) < f.Base || int(p) > f.Base+f.Size { 195 | panic("illegal SourcePos value") 196 | } 197 | pos = f.position(p) 198 | } 199 | return 200 | } 201 | 202 | func (f *SourceFile) position(p Pos) (pos SourceFilePos) { 203 | offset := int(p) - f.Base 204 | pos.Offset = offset 205 | pos.Filename, pos.Line, pos.Column = f.unpack(offset) 206 | return 207 | } 208 | 209 | func (f *SourceFile) unpack(offset int) (filename string, line, column int) { 210 | filename = f.Name 211 | if i := searchInts(f.Lines, offset); i >= 0 { 212 | line, column = i+1, offset-f.Lines[i]+1 213 | } 214 | return 215 | } 216 | 217 | func searchInts(a []int, x int) int { 218 | // This function body is a manually inlined version of: 219 | // return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1 220 | i, j := 0, len(a) 221 | for i < j { 222 | h := i + (j-i)/2 // avoid overflow when computing h 223 | // i ≤ h < j 224 | if a[h] <= x { 225 | i = h + 1 226 | } else { 227 | j = h 228 | } 229 | } 230 | return i - 1 231 | } 232 | -------------------------------------------------------------------------------- /stdlib/base64.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | var base64Module = map[string]tengo.Object{ 10 | "encode": &tengo.UserFunction{ 11 | Value: FuncAYRS(base64.StdEncoding.EncodeToString), 12 | }, 13 | "decode": &tengo.UserFunction{ 14 | Value: FuncASRYE(base64.StdEncoding.DecodeString), 15 | }, 16 | "raw_encode": &tengo.UserFunction{ 17 | Value: FuncAYRS(base64.RawStdEncoding.EncodeToString), 18 | }, 19 | "raw_decode": &tengo.UserFunction{ 20 | Value: FuncASRYE(base64.RawStdEncoding.DecodeString), 21 | }, 22 | "url_encode": &tengo.UserFunction{ 23 | Value: FuncAYRS(base64.URLEncoding.EncodeToString), 24 | }, 25 | "url_decode": &tengo.UserFunction{ 26 | Value: FuncASRYE(base64.URLEncoding.DecodeString), 27 | }, 28 | "raw_url_encode": &tengo.UserFunction{ 29 | Value: FuncAYRS(base64.RawURLEncoding.EncodeToString), 30 | }, 31 | "raw_url_decode": &tengo.UserFunction{ 32 | Value: FuncASRYE(base64.RawURLEncoding.DecodeString), 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /stdlib/base64_test.go: -------------------------------------------------------------------------------- 1 | package stdlib_test 2 | 3 | import "testing" 4 | 5 | var base64Bytes1 = []byte{ 6 | 0x06, 0xAC, 0x76, 0x1B, 0x1D, 0x6A, 0xFA, 0x9D, 0xB1, 0xA0, 7 | } 8 | 9 | const ( 10 | base64Std = "Bqx2Gx1q+p2xoA==" 11 | base64URL = "Bqx2Gx1q-p2xoA==" 12 | base64RawStd = "Bqx2Gx1q+p2xoA" 13 | base64RawURL = "Bqx2Gx1q-p2xoA" 14 | ) 15 | 16 | func TestBase64(t *testing.T) { 17 | module(t, `base64`).call("encode", base64Bytes1).expect(base64Std) 18 | module(t, `base64`).call("decode", base64Std).expect(base64Bytes1) 19 | module(t, `base64`).call("url_encode", base64Bytes1).expect(base64URL) 20 | module(t, `base64`).call("url_decode", base64URL).expect(base64Bytes1) 21 | module(t, `base64`).call("raw_encode", base64Bytes1).expect(base64RawStd) 22 | module(t, `base64`).call("raw_decode", base64RawStd).expect(base64Bytes1) 23 | module(t, `base64`).call("raw_url_encode", base64Bytes1). 24 | expect(base64RawURL) 25 | module(t, `base64`).call("raw_url_decode", base64RawURL). 26 | expect(base64Bytes1) 27 | } 28 | -------------------------------------------------------------------------------- /stdlib/builtin_modules.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "github.com/d5/tengo/v2" 5 | ) 6 | 7 | // BuiltinModules are builtin type standard library modules. 8 | var BuiltinModules = map[string]map[string]tengo.Object{ 9 | "math": mathModule, 10 | "os": osModule, 11 | "text": textModule, 12 | "times": timesModule, 13 | "rand": randModule, 14 | "fmt": fmtModule, 15 | "json": jsonModule, 16 | "base64": base64Module, 17 | "hex": hexModule, 18 | } 19 | -------------------------------------------------------------------------------- /stdlib/errors.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "github.com/d5/tengo/v2" 5 | ) 6 | 7 | func wrapError(err error) tengo.Object { 8 | if err == nil { 9 | return tengo.TrueValue 10 | } 11 | return &tengo.Error{Value: &tengo.String{Value: err.Error()}} 12 | } 13 | -------------------------------------------------------------------------------- /stdlib/fmt.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | var fmtModule = map[string]tengo.Object{ 10 | "print": &tengo.UserFunction{Name: "print", Value: fmtPrint}, 11 | "printf": &tengo.UserFunction{Name: "printf", Value: fmtPrintf}, 12 | "println": &tengo.UserFunction{Name: "println", Value: fmtPrintln}, 13 | "sprintf": &tengo.UserFunction{Name: "sprintf", Value: fmtSprintf}, 14 | } 15 | 16 | func fmtPrint(args ...tengo.Object) (ret tengo.Object, err error) { 17 | printArgs, err := getPrintArgs(args...) 18 | if err != nil { 19 | return nil, err 20 | } 21 | _, _ = fmt.Print(printArgs...) 22 | return nil, nil 23 | } 24 | 25 | func fmtPrintf(args ...tengo.Object) (ret tengo.Object, err error) { 26 | numArgs := len(args) 27 | if numArgs == 0 { 28 | return nil, tengo.ErrWrongNumArguments 29 | } 30 | 31 | format, ok := args[0].(*tengo.String) 32 | if !ok { 33 | return nil, tengo.ErrInvalidArgumentType{ 34 | Name: "format", 35 | Expected: "string", 36 | Found: args[0].TypeName(), 37 | } 38 | } 39 | if numArgs == 1 { 40 | fmt.Print(format) 41 | return nil, nil 42 | } 43 | 44 | s, err := tengo.Format(format.Value, args[1:]...) 45 | if err != nil { 46 | return nil, err 47 | } 48 | fmt.Print(s) 49 | return nil, nil 50 | } 51 | 52 | func fmtPrintln(args ...tengo.Object) (ret tengo.Object, err error) { 53 | printArgs, err := getPrintArgs(args...) 54 | if err != nil { 55 | return nil, err 56 | } 57 | printArgs = append(printArgs, "\n") 58 | _, _ = fmt.Print(printArgs...) 59 | return nil, nil 60 | } 61 | 62 | func fmtSprintf(args ...tengo.Object) (ret tengo.Object, err error) { 63 | numArgs := len(args) 64 | if numArgs == 0 { 65 | return nil, tengo.ErrWrongNumArguments 66 | } 67 | 68 | format, ok := args[0].(*tengo.String) 69 | if !ok { 70 | return nil, tengo.ErrInvalidArgumentType{ 71 | Name: "format", 72 | Expected: "string", 73 | Found: args[0].TypeName(), 74 | } 75 | } 76 | if numArgs == 1 { 77 | // okay to return 'format' directly as String is immutable 78 | return format, nil 79 | } 80 | s, err := tengo.Format(format.Value, args[1:]...) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return &tengo.String{Value: s}, nil 85 | } 86 | 87 | func getPrintArgs(args ...tengo.Object) ([]interface{}, error) { 88 | var printArgs []interface{} 89 | l := 0 90 | for _, arg := range args { 91 | s, _ := tengo.ToString(arg) 92 | slen := len(s) 93 | // make sure length does not exceed the limit 94 | if l+slen > tengo.MaxStringLen { 95 | return nil, tengo.ErrStringLimit 96 | } 97 | l += slen 98 | printArgs = append(printArgs, s) 99 | } 100 | return printArgs, nil 101 | } 102 | -------------------------------------------------------------------------------- /stdlib/fmt_test.go: -------------------------------------------------------------------------------- 1 | package stdlib_test 2 | 3 | import "testing" 4 | 5 | func TestFmtSprintf(t *testing.T) { 6 | module(t, `fmt`).call("sprintf", "").expect("") 7 | module(t, `fmt`).call("sprintf", "foo").expect("foo") 8 | module(t, `fmt`).call("sprintf", `foo %d %v %s`, 1, 2, "bar"). 9 | expect("foo 1 2 bar") 10 | module(t, `fmt`).call("sprintf", "foo %v", ARR{1, "bar", true}). 11 | expect(`foo [1, "bar", true]`) 12 | module(t, `fmt`).call("sprintf", "foo %v %d", ARR{1, "bar", true}, 19). 13 | expect(`foo [1, "bar", true] 19`) 14 | module(t, `fmt`). 15 | call("sprintf", "foo %v", MAP{"a": IMAP{"b": IMAP{"c": ARR{1, 2, 3}}}}). 16 | expect(`foo {a: {b: {c: [1, 2, 3]}}}`) 17 | module(t, `fmt`).call("sprintf", "%v", IARR{1, IARR{2, IARR{3, 4}}}). 18 | expect(`[1, [2, [3, 4]]]`) 19 | } 20 | -------------------------------------------------------------------------------- /stdlib/gensrcmods.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "io/ioutil" 8 | "log" 9 | "regexp" 10 | "strconv" 11 | ) 12 | 13 | var tengoModFileRE = regexp.MustCompile(`^srcmod_(\w+).tengo$`) 14 | 15 | func main() { 16 | modules := make(map[string]string) 17 | 18 | // enumerate all Tengo module files 19 | files, err := ioutil.ReadDir(".") 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | for _, file := range files { 24 | m := tengoModFileRE.FindStringSubmatch(file.Name()) 25 | if m != nil { 26 | modName := m[1] 27 | 28 | src, err := ioutil.ReadFile(file.Name()) 29 | if err != nil { 30 | log.Fatalf("file '%s' read error: %s", 31 | file.Name(), err.Error()) 32 | } 33 | 34 | modules[modName] = string(src) 35 | } 36 | } 37 | 38 | var out bytes.Buffer 39 | out.WriteString(`// Code generated using gensrcmods.go; DO NOT EDIT. 40 | 41 | package stdlib 42 | 43 | // SourceModules are source type standard library modules. 44 | var SourceModules = map[string]string{` + "\n") 45 | for modName, modSrc := range modules { 46 | out.WriteString("\t\"" + modName + "\": " + 47 | strconv.Quote(modSrc) + ",\n") 48 | } 49 | out.WriteString("}\n") 50 | 51 | const target = "source_modules.go" 52 | if err := ioutil.WriteFile(target, out.Bytes(), 0644); err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /stdlib/hex.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | var hexModule = map[string]tengo.Object{ 10 | "encode": &tengo.UserFunction{Value: FuncAYRS(hex.EncodeToString)}, 11 | "decode": &tengo.UserFunction{Value: FuncASRYE(hex.DecodeString)}, 12 | } 13 | -------------------------------------------------------------------------------- /stdlib/hex_test.go: -------------------------------------------------------------------------------- 1 | package stdlib_test 2 | 3 | import "testing" 4 | 5 | var hexBytes1 = []byte{ 6 | 0x06, 0xAC, 0x76, 0x1B, 0x1D, 0x6A, 0xFA, 0x9D, 0xB1, 0xA0, 7 | } 8 | 9 | const hex1 = "06ac761b1d6afa9db1a0" 10 | 11 | func TestHex(t *testing.T) { 12 | module(t, `hex`).call("encode", hexBytes1).expect(hex1) 13 | module(t, `hex`).call("decode", hex1).expect(hexBytes1) 14 | } 15 | -------------------------------------------------------------------------------- /stdlib/json.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "bytes" 5 | gojson "encoding/json" 6 | 7 | "github.com/d5/tengo/v2" 8 | "github.com/d5/tengo/v2/stdlib/json" 9 | ) 10 | 11 | var jsonModule = map[string]tengo.Object{ 12 | "decode": &tengo.UserFunction{ 13 | Name: "decode", 14 | Value: jsonDecode, 15 | }, 16 | "encode": &tengo.UserFunction{ 17 | Name: "encode", 18 | Value: jsonEncode, 19 | }, 20 | "indent": &tengo.UserFunction{ 21 | Name: "encode", 22 | Value: jsonIndent, 23 | }, 24 | "html_escape": &tengo.UserFunction{ 25 | Name: "html_escape", 26 | Value: jsonHTMLEscape, 27 | }, 28 | } 29 | 30 | func jsonDecode(args ...tengo.Object) (ret tengo.Object, err error) { 31 | if len(args) != 1 { 32 | return nil, tengo.ErrWrongNumArguments 33 | } 34 | 35 | switch o := args[0].(type) { 36 | case *tengo.Bytes: 37 | v, err := json.Decode(o.Value) 38 | if err != nil { 39 | return &tengo.Error{ 40 | Value: &tengo.String{Value: err.Error()}, 41 | }, nil 42 | } 43 | return v, nil 44 | case *tengo.String: 45 | v, err := json.Decode([]byte(o.Value)) 46 | if err != nil { 47 | return &tengo.Error{ 48 | Value: &tengo.String{Value: err.Error()}, 49 | }, nil 50 | } 51 | return v, nil 52 | default: 53 | return nil, tengo.ErrInvalidArgumentType{ 54 | Name: "first", 55 | Expected: "bytes/string", 56 | Found: args[0].TypeName(), 57 | } 58 | } 59 | } 60 | 61 | func jsonEncode(args ...tengo.Object) (ret tengo.Object, err error) { 62 | if len(args) != 1 { 63 | return nil, tengo.ErrWrongNumArguments 64 | } 65 | 66 | b, err := json.Encode(args[0]) 67 | if err != nil { 68 | return &tengo.Error{Value: &tengo.String{Value: err.Error()}}, nil 69 | } 70 | 71 | return &tengo.Bytes{Value: b}, nil 72 | } 73 | 74 | func jsonIndent(args ...tengo.Object) (ret tengo.Object, err error) { 75 | if len(args) != 3 { 76 | return nil, tengo.ErrWrongNumArguments 77 | } 78 | 79 | prefix, ok := tengo.ToString(args[1]) 80 | if !ok { 81 | return nil, tengo.ErrInvalidArgumentType{ 82 | Name: "prefix", 83 | Expected: "string(compatible)", 84 | Found: args[1].TypeName(), 85 | } 86 | } 87 | 88 | indent, ok := tengo.ToString(args[2]) 89 | if !ok { 90 | return nil, tengo.ErrInvalidArgumentType{ 91 | Name: "indent", 92 | Expected: "string(compatible)", 93 | Found: args[2].TypeName(), 94 | } 95 | } 96 | 97 | switch o := args[0].(type) { 98 | case *tengo.Bytes: 99 | var dst bytes.Buffer 100 | err := gojson.Indent(&dst, o.Value, prefix, indent) 101 | if err != nil { 102 | return &tengo.Error{ 103 | Value: &tengo.String{Value: err.Error()}, 104 | }, nil 105 | } 106 | return &tengo.Bytes{Value: dst.Bytes()}, nil 107 | case *tengo.String: 108 | var dst bytes.Buffer 109 | err := gojson.Indent(&dst, []byte(o.Value), prefix, indent) 110 | if err != nil { 111 | return &tengo.Error{ 112 | Value: &tengo.String{Value: err.Error()}, 113 | }, nil 114 | } 115 | return &tengo.Bytes{Value: dst.Bytes()}, nil 116 | default: 117 | return nil, tengo.ErrInvalidArgumentType{ 118 | Name: "first", 119 | Expected: "bytes/string", 120 | Found: args[0].TypeName(), 121 | } 122 | } 123 | } 124 | 125 | func jsonHTMLEscape(args ...tengo.Object) (ret tengo.Object, err error) { 126 | if len(args) != 1 { 127 | return nil, tengo.ErrWrongNumArguments 128 | } 129 | 130 | switch o := args[0].(type) { 131 | case *tengo.Bytes: 132 | var dst bytes.Buffer 133 | gojson.HTMLEscape(&dst, o.Value) 134 | return &tengo.Bytes{Value: dst.Bytes()}, nil 135 | case *tengo.String: 136 | var dst bytes.Buffer 137 | gojson.HTMLEscape(&dst, []byte(o.Value)) 138 | return &tengo.Bytes{Value: dst.Bytes()}, nil 139 | default: 140 | return nil, tengo.ErrInvalidArgumentType{ 141 | Name: "first", 142 | Expected: "bytes/string", 143 | Found: args[0].TypeName(), 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /stdlib/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json_test 2 | 3 | import ( 4 | gojson "encoding/json" 5 | "testing" 6 | 7 | "github.com/d5/tengo/v2" 8 | "github.com/d5/tengo/v2/require" 9 | "github.com/d5/tengo/v2/stdlib/json" 10 | ) 11 | 12 | type ARR = []interface{} 13 | type MAP = map[string]interface{} 14 | 15 | func TestJSON(t *testing.T) { 16 | testJSONEncodeDecode(t, nil) 17 | 18 | testJSONEncodeDecode(t, 0) 19 | testJSONEncodeDecode(t, 1) 20 | testJSONEncodeDecode(t, -1) 21 | testJSONEncodeDecode(t, 1984) 22 | testJSONEncodeDecode(t, -1984) 23 | 24 | testJSONEncodeDecode(t, 0.0) 25 | testJSONEncodeDecode(t, 1.0) 26 | testJSONEncodeDecode(t, -1.0) 27 | testJSONEncodeDecode(t, 19.84) 28 | testJSONEncodeDecode(t, -19.84) 29 | 30 | testJSONEncodeDecode(t, "") 31 | testJSONEncodeDecode(t, "foo") 32 | testJSONEncodeDecode(t, "foo bar") 33 | testJSONEncodeDecode(t, "foo \"bar\"") 34 | // See: https://github.com/d5/tengo/issues/268 35 | testJSONEncodeDecode(t, "1\u001C04") 36 | testJSONEncodeDecode(t, "çığöşü") 37 | testJSONEncodeDecode(t, "ç1\u001C04IĞÖŞÜ") 38 | testJSONEncodeDecode(t, "错误测试") 39 | 40 | testJSONEncodeDecode(t, true) 41 | testJSONEncodeDecode(t, false) 42 | 43 | testJSONEncodeDecode(t, ARR{}) 44 | testJSONEncodeDecode(t, ARR{0}) 45 | testJSONEncodeDecode(t, ARR{false}) 46 | testJSONEncodeDecode(t, ARR{1, 2, 3, 47 | "four", false}) 48 | testJSONEncodeDecode(t, ARR{1, 2, 3, 49 | "four", false, MAP{"a": 0, "b": "bee", "bool": true}}) 50 | 51 | testJSONEncodeDecode(t, MAP{}) 52 | testJSONEncodeDecode(t, MAP{"a": 0}) 53 | testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee"}) 54 | testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee", "bool": true}) 55 | 56 | testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee", 57 | "arr": ARR{1, 2, 3, "four"}}) 58 | testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee", 59 | "arr": ARR{1, 2, 3, MAP{"a": false, "b": 109.4}}}) 60 | 61 | testJSONEncodeDecode(t, MAP{"id1": 7075984636689534001, "id2": 7075984636689534002}) 62 | testJSONEncodeDecode(t, ARR{1e3, 1E7}) 63 | } 64 | 65 | func TestDecode(t *testing.T) { 66 | testDecodeError(t, `{`) 67 | testDecodeError(t, `}`) 68 | testDecodeError(t, `{}a`) 69 | testDecodeError(t, `{{}`) 70 | testDecodeError(t, `{}}`) 71 | testDecodeError(t, `[`) 72 | testDecodeError(t, `]`) 73 | testDecodeError(t, `[]a`) 74 | testDecodeError(t, `[[]`) 75 | testDecodeError(t, `[]]`) 76 | testDecodeError(t, `"`) 77 | testDecodeError(t, `"abc`) 78 | testDecodeError(t, `abc"`) 79 | testDecodeError(t, `.123`) 80 | testDecodeError(t, `123.`) 81 | testDecodeError(t, `1.2.3`) 82 | testDecodeError(t, `'a'`) 83 | testDecodeError(t, `true, false`) 84 | testDecodeError(t, `{"a:"b"}`) 85 | testDecodeError(t, `{a":"b"}`) 86 | testDecodeError(t, `{"a":"b":"c"}`) 87 | } 88 | 89 | func testDecodeError(t *testing.T, input string) { 90 | _, err := json.Decode([]byte(input)) 91 | require.Error(t, err) 92 | } 93 | 94 | func testJSONEncodeDecode(t *testing.T, v interface{}) { 95 | o, err := tengo.FromInterface(v) 96 | require.NoError(t, err) 97 | 98 | b, err := json.Encode(o) 99 | require.NoError(t, err) 100 | 101 | a, err := json.Decode(b) 102 | require.NoError(t, err, string(b)) 103 | 104 | vj, err := gojson.Marshal(v) 105 | require.NoError(t, err) 106 | 107 | aj, err := gojson.Marshal(tengo.ToInterface(a)) 108 | require.NoError(t, err) 109 | 110 | require.Equal(t, vj, aj) 111 | } 112 | -------------------------------------------------------------------------------- /stdlib/json_test.go: -------------------------------------------------------------------------------- 1 | package stdlib_test 2 | 3 | import "testing" 4 | 5 | func TestJSON(t *testing.T) { 6 | module(t, "json").call("encode", 5). 7 | expect([]byte("5")) 8 | module(t, "json").call("encode", "foobar"). 9 | expect([]byte(`"foobar"`)) 10 | module(t, "json").call("encode", MAP{"foo": 5}). 11 | expect([]byte("{\"foo\":5}")) 12 | module(t, "json").call("encode", IMAP{"foo": 5}). 13 | expect([]byte("{\"foo\":5}")) 14 | module(t, "json").call("encode", ARR{1, 2, 3}). 15 | expect([]byte("[1,2,3]")) 16 | module(t, "json").call("encode", IARR{1, 2, 3}). 17 | expect([]byte("[1,2,3]")) 18 | module(t, "json").call("encode", MAP{"foo": "bar"}). 19 | expect([]byte("{\"foo\":\"bar\"}")) 20 | module(t, "json").call("encode", MAP{"foo": 1.8}). 21 | expect([]byte("{\"foo\":1.8}")) 22 | module(t, "json").call("encode", MAP{"foo": true}). 23 | expect([]byte("{\"foo\":true}")) 24 | module(t, "json").call("encode", MAP{"foo": '8'}). 25 | expect([]byte("{\"foo\":56}")) 26 | module(t, "json").call("encode", MAP{"foo": []byte("foo")}). 27 | expect([]byte("{\"foo\":\"Zm9v\"}")) // json encoding returns []byte as base64 encoded string 28 | module(t, "json"). 29 | call("encode", MAP{"foo": ARR{"bar", 1, 1.8, '8', true}}). 30 | expect([]byte("{\"foo\":[\"bar\",1,1.8,56,true]}")) 31 | module(t, "json"). 32 | call("encode", MAP{"foo": IARR{"bar", 1, 1.8, '8', true}}). 33 | expect([]byte("{\"foo\":[\"bar\",1,1.8,56,true]}")) 34 | module(t, "json"). 35 | call("encode", MAP{"foo": ARR{ARR{"bar", 1}, ARR{"bar", 1}}}). 36 | expect([]byte("{\"foo\":[[\"bar\",1],[\"bar\",1]]}")) 37 | module(t, "json"). 38 | call("encode", MAP{"foo": MAP{"string": "bar"}}). 39 | expect([]byte("{\"foo\":{\"string\":\"bar\"}}")) 40 | module(t, "json"). 41 | call("encode", MAP{"foo": IMAP{"string": "bar"}}). 42 | expect([]byte("{\"foo\":{\"string\":\"bar\"}}")) 43 | module(t, "json"). 44 | call("encode", MAP{"foo": MAP{"map1": MAP{"string": "bar"}}}). 45 | expect([]byte("{\"foo\":{\"map1\":{\"string\":\"bar\"}}}")) 46 | module(t, "json"). 47 | call("encode", ARR{ARR{"bar", 1}, ARR{"bar", 1}}). 48 | expect([]byte("[[\"bar\",1],[\"bar\",1]]")) 49 | 50 | module(t, "json").call("decode", `5`). 51 | expect(5) 52 | module(t, "json").call("decode", `"foo"`). 53 | expect("foo") 54 | module(t, "json").call("decode", `[1,2,3,"bar"]`). 55 | expect(ARR{1, 2, 3, "bar"}) 56 | module(t, "json").call("decode", `{"foo":5}`). 57 | expect(MAP{"foo": 5}) 58 | module(t, "json").call("decode", `{"foo":2.5}`). 59 | expect(MAP{"foo": 2.5}) 60 | module(t, "json").call("decode", `{"foo":true}`). 61 | expect(MAP{"foo": true}) 62 | module(t, "json").call("decode", `{"foo":"bar"}`). 63 | expect(MAP{"foo": "bar"}) 64 | module(t, "json").call("decode", `{"foo":[1,2,3,"bar"]}`). 65 | expect(MAP{"foo": ARR{1, 2, 3, "bar"}}) 66 | 67 | module(t, "json"). 68 | call("indent", []byte("{\"foo\":[\"bar\",1,1.8,56,true]}"), "", " "). 69 | expect([]byte(`{ 70 | "foo": [ 71 | "bar", 72 | 1, 73 | 1.8, 74 | 56, 75 | true 76 | ] 77 | }`)) 78 | 79 | module(t, "json"). 80 | call("html_escape", []byte( 81 | `{"M":"foo &`+"\xe2\x80\xa8 \xe2\x80\xa9"+`"}`)). 82 | expect([]byte( 83 | `{"M":"\u003chtml\u003efoo \u0026\u2028 \u2029\u003c/html\u003e"}`)) 84 | } 85 | -------------------------------------------------------------------------------- /stdlib/math.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | var mathModule = map[string]tengo.Object{ 10 | "e": &tengo.Float{Value: math.E}, 11 | "pi": &tengo.Float{Value: math.Pi}, 12 | "phi": &tengo.Float{Value: math.Phi}, 13 | "sqrt2": &tengo.Float{Value: math.Sqrt2}, 14 | "sqrtE": &tengo.Float{Value: math.SqrtE}, 15 | "sqrtPi": &tengo.Float{Value: math.SqrtPi}, 16 | "sqrtPhi": &tengo.Float{Value: math.SqrtPhi}, 17 | "ln2": &tengo.Float{Value: math.Ln2}, 18 | "log2E": &tengo.Float{Value: math.Log2E}, 19 | "ln10": &tengo.Float{Value: math.Ln10}, 20 | "log10E": &tengo.Float{Value: math.Log10E}, 21 | "maxFloat32": &tengo.Float{Value: math.MaxFloat32}, 22 | "smallestNonzeroFloat32": &tengo.Float{Value: math.SmallestNonzeroFloat32}, 23 | "maxFloat64": &tengo.Float{Value: math.MaxFloat64}, 24 | "smallestNonzeroFloat64": &tengo.Float{Value: math.SmallestNonzeroFloat64}, 25 | "maxInt": &tengo.Int{Value: math.MaxInt}, 26 | "minInt": &tengo.Int{Value: math.MinInt}, 27 | "maxInt8": &tengo.Int{Value: math.MaxInt8}, 28 | "minInt8": &tengo.Int{Value: math.MinInt8}, 29 | "maxInt16": &tengo.Int{Value: math.MaxInt16}, 30 | "minInt16": &tengo.Int{Value: math.MinInt16}, 31 | "maxInt32": &tengo.Int{Value: math.MaxInt32}, 32 | "minInt32": &tengo.Int{Value: math.MinInt32}, 33 | "maxInt64": &tengo.Int{Value: math.MaxInt64}, 34 | "minInt64": &tengo.Int{Value: math.MinInt64}, 35 | "abs": &tengo.UserFunction{ 36 | Name: "abs", 37 | Value: FuncAFRF(math.Abs), 38 | }, 39 | "acos": &tengo.UserFunction{ 40 | Name: "acos", 41 | Value: FuncAFRF(math.Acos), 42 | }, 43 | "acosh": &tengo.UserFunction{ 44 | Name: "acosh", 45 | Value: FuncAFRF(math.Acosh), 46 | }, 47 | "asin": &tengo.UserFunction{ 48 | Name: "asin", 49 | Value: FuncAFRF(math.Asin), 50 | }, 51 | "asinh": &tengo.UserFunction{ 52 | Name: "asinh", 53 | Value: FuncAFRF(math.Asinh), 54 | }, 55 | "atan": &tengo.UserFunction{ 56 | Name: "atan", 57 | Value: FuncAFRF(math.Atan), 58 | }, 59 | "atan2": &tengo.UserFunction{ 60 | Name: "atan2", 61 | Value: FuncAFFRF(math.Atan2), 62 | }, 63 | "atanh": &tengo.UserFunction{ 64 | Name: "atanh", 65 | Value: FuncAFRF(math.Atanh), 66 | }, 67 | "cbrt": &tengo.UserFunction{ 68 | Name: "cbrt", 69 | Value: FuncAFRF(math.Cbrt), 70 | }, 71 | "ceil": &tengo.UserFunction{ 72 | Name: "ceil", 73 | Value: FuncAFRF(math.Ceil), 74 | }, 75 | "copysign": &tengo.UserFunction{ 76 | Name: "copysign", 77 | Value: FuncAFFRF(math.Copysign), 78 | }, 79 | "cos": &tengo.UserFunction{ 80 | Name: "cos", 81 | Value: FuncAFRF(math.Cos), 82 | }, 83 | "cosh": &tengo.UserFunction{ 84 | Name: "cosh", 85 | Value: FuncAFRF(math.Cosh), 86 | }, 87 | "dim": &tengo.UserFunction{ 88 | Name: "dim", 89 | Value: FuncAFFRF(math.Dim), 90 | }, 91 | "erf": &tengo.UserFunction{ 92 | Name: "erf", 93 | Value: FuncAFRF(math.Erf), 94 | }, 95 | "erfc": &tengo.UserFunction{ 96 | Name: "erfc", 97 | Value: FuncAFRF(math.Erfc), 98 | }, 99 | "exp": &tengo.UserFunction{ 100 | Name: "exp", 101 | Value: FuncAFRF(math.Exp), 102 | }, 103 | "exp2": &tengo.UserFunction{ 104 | Name: "exp2", 105 | Value: FuncAFRF(math.Exp2), 106 | }, 107 | "expm1": &tengo.UserFunction{ 108 | Name: "expm1", 109 | Value: FuncAFRF(math.Expm1), 110 | }, 111 | "floor": &tengo.UserFunction{ 112 | Name: "floor", 113 | Value: FuncAFRF(math.Floor), 114 | }, 115 | "gamma": &tengo.UserFunction{ 116 | Name: "gamma", 117 | Value: FuncAFRF(math.Gamma), 118 | }, 119 | "hypot": &tengo.UserFunction{ 120 | Name: "hypot", 121 | Value: FuncAFFRF(math.Hypot), 122 | }, 123 | "ilogb": &tengo.UserFunction{ 124 | Name: "ilogb", 125 | Value: FuncAFRI(math.Ilogb), 126 | }, 127 | "inf": &tengo.UserFunction{ 128 | Name: "inf", 129 | Value: FuncAIRF(math.Inf), 130 | }, 131 | "is_inf": &tengo.UserFunction{ 132 | Name: "is_inf", 133 | Value: FuncAFIRB(math.IsInf), 134 | }, 135 | "is_nan": &tengo.UserFunction{ 136 | Name: "is_nan", 137 | Value: FuncAFRB(math.IsNaN), 138 | }, 139 | "j0": &tengo.UserFunction{ 140 | Name: "j0", 141 | Value: FuncAFRF(math.J0), 142 | }, 143 | "j1": &tengo.UserFunction{ 144 | Name: "j1", 145 | Value: FuncAFRF(math.J1), 146 | }, 147 | "jn": &tengo.UserFunction{ 148 | Name: "jn", 149 | Value: FuncAIFRF(math.Jn), 150 | }, 151 | "ldexp": &tengo.UserFunction{ 152 | Name: "ldexp", 153 | Value: FuncAFIRF(math.Ldexp), 154 | }, 155 | "log": &tengo.UserFunction{ 156 | Name: "log", 157 | Value: FuncAFRF(math.Log), 158 | }, 159 | "log10": &tengo.UserFunction{ 160 | Name: "log10", 161 | Value: FuncAFRF(math.Log10), 162 | }, 163 | "log1p": &tengo.UserFunction{ 164 | Name: "log1p", 165 | Value: FuncAFRF(math.Log1p), 166 | }, 167 | "log2": &tengo.UserFunction{ 168 | Name: "log2", 169 | Value: FuncAFRF(math.Log2), 170 | }, 171 | "logb": &tengo.UserFunction{ 172 | Name: "logb", 173 | Value: FuncAFRF(math.Logb), 174 | }, 175 | "max": &tengo.UserFunction{ 176 | Name: "max", 177 | Value: FuncAFFRF(math.Max), 178 | }, 179 | "min": &tengo.UserFunction{ 180 | Name: "min", 181 | Value: FuncAFFRF(math.Min), 182 | }, 183 | "mod": &tengo.UserFunction{ 184 | Name: "mod", 185 | Value: FuncAFFRF(math.Mod), 186 | }, 187 | "nan": &tengo.UserFunction{ 188 | Name: "nan", 189 | Value: FuncARF(math.NaN), 190 | }, 191 | "nextafter": &tengo.UserFunction{ 192 | Name: "nextafter", 193 | Value: FuncAFFRF(math.Nextafter), 194 | }, 195 | "pow": &tengo.UserFunction{ 196 | Name: "pow", 197 | Value: FuncAFFRF(math.Pow), 198 | }, 199 | "pow10": &tengo.UserFunction{ 200 | Name: "pow10", 201 | Value: FuncAIRF(math.Pow10), 202 | }, 203 | "remainder": &tengo.UserFunction{ 204 | Name: "remainder", 205 | Value: FuncAFFRF(math.Remainder), 206 | }, 207 | "signbit": &tengo.UserFunction{ 208 | Name: "signbit", 209 | Value: FuncAFRB(math.Signbit), 210 | }, 211 | "sin": &tengo.UserFunction{ 212 | Name: "sin", 213 | Value: FuncAFRF(math.Sin), 214 | }, 215 | "sinh": &tengo.UserFunction{ 216 | Name: "sinh", 217 | Value: FuncAFRF(math.Sinh), 218 | }, 219 | "sqrt": &tengo.UserFunction{ 220 | Name: "sqrt", 221 | Value: FuncAFRF(math.Sqrt), 222 | }, 223 | "tan": &tengo.UserFunction{ 224 | Name: "tan", 225 | Value: FuncAFRF(math.Tan), 226 | }, 227 | "tanh": &tengo.UserFunction{ 228 | Name: "tanh", 229 | Value: FuncAFRF(math.Tanh), 230 | }, 231 | "trunc": &tengo.UserFunction{ 232 | Name: "trunc", 233 | Value: FuncAFRF(math.Trunc), 234 | }, 235 | "y0": &tengo.UserFunction{ 236 | Name: "y0", 237 | Value: FuncAFRF(math.Y0), 238 | }, 239 | "y1": &tengo.UserFunction{ 240 | Name: "y1", 241 | Value: FuncAFRF(math.Y1), 242 | }, 243 | "yn": &tengo.UserFunction{ 244 | Name: "yn", 245 | Value: FuncAIFRF(math.Yn), 246 | }, 247 | } 248 | -------------------------------------------------------------------------------- /stdlib/os_exec.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | func makeOSExecCommand(cmd *exec.Cmd) *tengo.ImmutableMap { 10 | return &tengo.ImmutableMap{ 11 | Value: map[string]tengo.Object{ 12 | // combined_output() => bytes/error 13 | "combined_output": &tengo.UserFunction{ 14 | Name: "combined_output", 15 | Value: FuncARYE(cmd.CombinedOutput), 16 | }, 17 | // output() => bytes/error 18 | "output": &tengo.UserFunction{ 19 | Name: "output", 20 | Value: FuncARYE(cmd.Output), 21 | }, // 22 | // run() => error 23 | "run": &tengo.UserFunction{ 24 | Name: "run", 25 | Value: FuncARE(cmd.Run), 26 | }, // 27 | // start() => error 28 | "start": &tengo.UserFunction{ 29 | Name: "start", 30 | Value: FuncARE(cmd.Start), 31 | }, // 32 | // wait() => error 33 | "wait": &tengo.UserFunction{ 34 | Name: "wait", 35 | Value: FuncARE(cmd.Wait), 36 | }, // 37 | // set_path(path string) 38 | "set_path": &tengo.UserFunction{ 39 | Name: "set_path", 40 | Value: func(args ...tengo.Object) (tengo.Object, error) { 41 | if len(args) != 1 { 42 | return nil, tengo.ErrWrongNumArguments 43 | } 44 | s1, ok := tengo.ToString(args[0]) 45 | if !ok { 46 | return nil, tengo.ErrInvalidArgumentType{ 47 | Name: "first", 48 | Expected: "string(compatible)", 49 | Found: args[0].TypeName(), 50 | } 51 | } 52 | cmd.Path = s1 53 | return tengo.UndefinedValue, nil 54 | }, 55 | }, 56 | // set_dir(dir string) 57 | "set_dir": &tengo.UserFunction{ 58 | Name: "set_dir", 59 | Value: func(args ...tengo.Object) (tengo.Object, error) { 60 | if len(args) != 1 { 61 | return nil, tengo.ErrWrongNumArguments 62 | } 63 | s1, ok := tengo.ToString(args[0]) 64 | if !ok { 65 | return nil, tengo.ErrInvalidArgumentType{ 66 | Name: "first", 67 | Expected: "string(compatible)", 68 | Found: args[0].TypeName(), 69 | } 70 | } 71 | cmd.Dir = s1 72 | return tengo.UndefinedValue, nil 73 | }, 74 | }, 75 | // set_env(env array(string)) 76 | "set_env": &tengo.UserFunction{ 77 | Name: "set_env", 78 | Value: func(args ...tengo.Object) (tengo.Object, error) { 79 | if len(args) != 1 { 80 | return nil, tengo.ErrWrongNumArguments 81 | } 82 | 83 | var env []string 84 | var err error 85 | switch arg0 := args[0].(type) { 86 | case *tengo.Array: 87 | env, err = stringArray(arg0.Value, "first") 88 | if err != nil { 89 | return nil, err 90 | } 91 | case *tengo.ImmutableArray: 92 | env, err = stringArray(arg0.Value, "first") 93 | if err != nil { 94 | return nil, err 95 | } 96 | default: 97 | return nil, tengo.ErrInvalidArgumentType{ 98 | Name: "first", 99 | Expected: "array", 100 | Found: arg0.TypeName(), 101 | } 102 | } 103 | cmd.Env = env 104 | return tengo.UndefinedValue, nil 105 | }, 106 | }, 107 | // process() => imap(process) 108 | "process": &tengo.UserFunction{ 109 | Name: "process", 110 | Value: func(args ...tengo.Object) (tengo.Object, error) { 111 | if len(args) != 0 { 112 | return nil, tengo.ErrWrongNumArguments 113 | } 114 | return makeOSProcess(cmd.Process), nil 115 | }, 116 | }, 117 | }, 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /stdlib/os_file.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | func makeOSFile(file *os.File) *tengo.ImmutableMap { 10 | return &tengo.ImmutableMap{ 11 | Value: map[string]tengo.Object{ 12 | // chdir() => true/error 13 | "chdir": &tengo.UserFunction{ 14 | Name: "chdir", 15 | Value: FuncARE(file.Chdir), 16 | }, // 17 | // chown(uid int, gid int) => true/error 18 | "chown": &tengo.UserFunction{ 19 | Name: "chown", 20 | Value: FuncAIIRE(file.Chown), 21 | }, // 22 | // close() => error 23 | "close": &tengo.UserFunction{ 24 | Name: "close", 25 | Value: FuncARE(file.Close), 26 | }, // 27 | // name() => string 28 | "name": &tengo.UserFunction{ 29 | Name: "name", 30 | Value: FuncARS(file.Name), 31 | }, // 32 | // readdirnames(n int) => array(string)/error 33 | "readdirnames": &tengo.UserFunction{ 34 | Name: "readdirnames", 35 | Value: FuncAIRSsE(file.Readdirnames), 36 | }, // 37 | // sync() => error 38 | "sync": &tengo.UserFunction{ 39 | Name: "sync", 40 | Value: FuncARE(file.Sync), 41 | }, // 42 | // write(bytes) => int/error 43 | "write": &tengo.UserFunction{ 44 | Name: "write", 45 | Value: FuncAYRIE(file.Write), 46 | }, // 47 | // write(string) => int/error 48 | "write_string": &tengo.UserFunction{ 49 | Name: "write_string", 50 | Value: FuncASRIE(file.WriteString), 51 | }, // 52 | // read(bytes) => int/error 53 | "read": &tengo.UserFunction{ 54 | Name: "read", 55 | Value: FuncAYRIE(file.Read), 56 | }, // 57 | // chmod(mode int) => error 58 | "chmod": &tengo.UserFunction{ 59 | Name: "chmod", 60 | Value: func(args ...tengo.Object) (tengo.Object, error) { 61 | if len(args) != 1 { 62 | return nil, tengo.ErrWrongNumArguments 63 | } 64 | i1, ok := tengo.ToInt64(args[0]) 65 | if !ok { 66 | return nil, tengo.ErrInvalidArgumentType{ 67 | Name: "first", 68 | Expected: "int(compatible)", 69 | Found: args[0].TypeName(), 70 | } 71 | } 72 | return wrapError(file.Chmod(os.FileMode(i1))), nil 73 | }, 74 | }, 75 | // seek(offset int, whence int) => int/error 76 | "seek": &tengo.UserFunction{ 77 | Name: "seek", 78 | Value: func(args ...tengo.Object) (tengo.Object, error) { 79 | if len(args) != 2 { 80 | return nil, tengo.ErrWrongNumArguments 81 | } 82 | i1, ok := tengo.ToInt64(args[0]) 83 | if !ok { 84 | return nil, tengo.ErrInvalidArgumentType{ 85 | Name: "first", 86 | Expected: "int(compatible)", 87 | Found: args[0].TypeName(), 88 | } 89 | } 90 | i2, ok := tengo.ToInt(args[1]) 91 | if !ok { 92 | return nil, tengo.ErrInvalidArgumentType{ 93 | Name: "second", 94 | Expected: "int(compatible)", 95 | Found: args[1].TypeName(), 96 | } 97 | } 98 | res, err := file.Seek(i1, i2) 99 | if err != nil { 100 | return wrapError(err), nil 101 | } 102 | return &tengo.Int{Value: res}, nil 103 | }, 104 | }, 105 | // stat() => imap(fileinfo)/error 106 | "stat": &tengo.UserFunction{ 107 | Name: "stat", 108 | Value: func(args ...tengo.Object) (tengo.Object, error) { 109 | if len(args) != 0 { 110 | return nil, tengo.ErrWrongNumArguments 111 | } 112 | return osStat(&tengo.String{Value: file.Name()}) 113 | }, 114 | }, 115 | }, 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /stdlib/os_process.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | 7 | "github.com/d5/tengo/v2" 8 | ) 9 | 10 | func makeOSProcessState(state *os.ProcessState) *tengo.ImmutableMap { 11 | return &tengo.ImmutableMap{ 12 | Value: map[string]tengo.Object{ 13 | "exited": &tengo.UserFunction{ 14 | Name: "exited", 15 | Value: FuncARB(state.Exited), 16 | }, 17 | "pid": &tengo.UserFunction{ 18 | Name: "pid", 19 | Value: FuncARI(state.Pid), 20 | }, 21 | "string": &tengo.UserFunction{ 22 | Name: "string", 23 | Value: FuncARS(state.String), 24 | }, 25 | "success": &tengo.UserFunction{ 26 | Name: "success", 27 | Value: FuncARB(state.Success), 28 | }, 29 | }, 30 | } 31 | } 32 | 33 | func makeOSProcess(proc *os.Process) *tengo.ImmutableMap { 34 | return &tengo.ImmutableMap{ 35 | Value: map[string]tengo.Object{ 36 | "kill": &tengo.UserFunction{ 37 | Name: "kill", 38 | Value: FuncARE(proc.Kill), 39 | }, 40 | "release": &tengo.UserFunction{ 41 | Name: "release", 42 | Value: FuncARE(proc.Release), 43 | }, 44 | "signal": &tengo.UserFunction{ 45 | Name: "signal", 46 | Value: func(args ...tengo.Object) (tengo.Object, error) { 47 | if len(args) != 1 { 48 | return nil, tengo.ErrWrongNumArguments 49 | } 50 | i1, ok := tengo.ToInt64(args[0]) 51 | if !ok { 52 | return nil, tengo.ErrInvalidArgumentType{ 53 | Name: "first", 54 | Expected: "int(compatible)", 55 | Found: args[0].TypeName(), 56 | } 57 | } 58 | return wrapError(proc.Signal(syscall.Signal(i1))), nil 59 | }, 60 | }, 61 | "wait": &tengo.UserFunction{ 62 | Name: "wait", 63 | Value: func(args ...tengo.Object) (tengo.Object, error) { 64 | if len(args) != 0 { 65 | return nil, tengo.ErrWrongNumArguments 66 | } 67 | state, err := proc.Wait() 68 | if err != nil { 69 | return wrapError(err), nil 70 | } 71 | return makeOSProcessState(state), nil 72 | }, 73 | }, 74 | }, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /stdlib/os_test.go: -------------------------------------------------------------------------------- 1 | package stdlib_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/d5/tengo/v2" 9 | "github.com/d5/tengo/v2/require" 10 | ) 11 | 12 | func TestReadFile(t *testing.T) { 13 | content := []byte("the quick brown fox jumps over the lazy dog") 14 | tf, err := ioutil.TempFile("", "test") 15 | require.NoError(t, err) 16 | defer func() { _ = os.Remove(tf.Name()) }() 17 | 18 | _, err = tf.Write(content) 19 | require.NoError(t, err) 20 | _ = tf.Close() 21 | 22 | module(t, "os").call("read_file", tf.Name()). 23 | expect(&tengo.Bytes{Value: content}) 24 | } 25 | 26 | func TestReadFileArgs(t *testing.T) { 27 | module(t, "os").call("read_file").expectError() 28 | } 29 | func TestFileStatArgs(t *testing.T) { 30 | module(t, "os").call("stat").expectError() 31 | } 32 | 33 | func TestFileStatFile(t *testing.T) { 34 | content := []byte("the quick brown fox jumps over the lazy dog") 35 | tf, err := ioutil.TempFile("", "test") 36 | require.NoError(t, err) 37 | defer func() { _ = os.Remove(tf.Name()) }() 38 | 39 | _, err = tf.Write(content) 40 | require.NoError(t, err) 41 | _ = tf.Close() 42 | 43 | stat, err := os.Stat(tf.Name()) 44 | if err != nil { 45 | t.Logf("could not get tmp file stat: %s", err) 46 | return 47 | } 48 | 49 | module(t, "os").call("stat", tf.Name()).expect(&tengo.ImmutableMap{ 50 | Value: map[string]tengo.Object{ 51 | "name": &tengo.String{Value: stat.Name()}, 52 | "mtime": &tengo.Time{Value: stat.ModTime()}, 53 | "size": &tengo.Int{Value: stat.Size()}, 54 | "mode": &tengo.Int{Value: int64(stat.Mode())}, 55 | "directory": tengo.FalseValue, 56 | }, 57 | }) 58 | } 59 | 60 | func TestFileStatDir(t *testing.T) { 61 | td, err := ioutil.TempDir("", "test") 62 | require.NoError(t, err) 63 | defer func() { _ = os.RemoveAll(td) }() 64 | 65 | stat, err := os.Stat(td) 66 | require.NoError(t, err) 67 | 68 | module(t, "os").call("stat", td).expect(&tengo.ImmutableMap{ 69 | Value: map[string]tengo.Object{ 70 | "name": &tengo.String{Value: stat.Name()}, 71 | "mtime": &tengo.Time{Value: stat.ModTime()}, 72 | "size": &tengo.Int{Value: stat.Size()}, 73 | "mode": &tengo.Int{Value: int64(stat.Mode())}, 74 | "directory": tengo.TrueValue, 75 | }, 76 | }) 77 | } 78 | 79 | func TestOSExpandEnv(t *testing.T) { 80 | curMaxStringLen := tengo.MaxStringLen 81 | defer func() { tengo.MaxStringLen = curMaxStringLen }() 82 | tengo.MaxStringLen = 12 83 | 84 | _ = os.Setenv("TENGO", "FOO BAR") 85 | module(t, "os").call("expand_env", "$TENGO").expect("FOO BAR") 86 | 87 | _ = os.Setenv("TENGO", "FOO") 88 | module(t, "os").call("expand_env", "$TENGO $TENGO").expect("FOO FOO") 89 | 90 | _ = os.Setenv("TENGO", "123456789012") 91 | module(t, "os").call("expand_env", "$TENGO").expect("123456789012") 92 | 93 | _ = os.Setenv("TENGO", "1234567890123") 94 | module(t, "os").call("expand_env", "$TENGO").expectError() 95 | 96 | _ = os.Setenv("TENGO", "123456") 97 | module(t, "os").call("expand_env", "$TENGO$TENGO").expect("123456123456") 98 | 99 | _ = os.Setenv("TENGO", "123456") 100 | module(t, "os").call("expand_env", "${TENGO}${TENGO}"). 101 | expect("123456123456") 102 | 103 | _ = os.Setenv("TENGO", "123456") 104 | module(t, "os").call("expand_env", "$TENGO $TENGO").expectError() 105 | 106 | _ = os.Setenv("TENGO", "123456") 107 | module(t, "os").call("expand_env", "${TENGO} ${TENGO}").expectError() 108 | } 109 | -------------------------------------------------------------------------------- /stdlib/rand.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | var randModule = map[string]tengo.Object{ 10 | "int": &tengo.UserFunction{ 11 | Name: "int", 12 | Value: FuncARI64(rand.Int63), 13 | }, 14 | "float": &tengo.UserFunction{ 15 | Name: "float", 16 | Value: FuncARF(rand.Float64), 17 | }, 18 | "intn": &tengo.UserFunction{ 19 | Name: "intn", 20 | Value: FuncAI64RI64(rand.Int63n), 21 | }, 22 | "exp_float": &tengo.UserFunction{ 23 | Name: "exp_float", 24 | Value: FuncARF(rand.ExpFloat64), 25 | }, 26 | "norm_float": &tengo.UserFunction{ 27 | Name: "norm_float", 28 | Value: FuncARF(rand.NormFloat64), 29 | }, 30 | "perm": &tengo.UserFunction{ 31 | Name: "perm", 32 | Value: FuncAIRIs(rand.Perm), 33 | }, 34 | "seed": &tengo.UserFunction{ 35 | Name: "seed", 36 | Value: FuncAI64R(rand.Seed), 37 | }, 38 | "read": &tengo.UserFunction{ 39 | Name: "read", 40 | Value: func(args ...tengo.Object) (ret tengo.Object, err error) { 41 | if len(args) != 1 { 42 | return nil, tengo.ErrWrongNumArguments 43 | } 44 | y1, ok := args[0].(*tengo.Bytes) 45 | if !ok { 46 | return nil, tengo.ErrInvalidArgumentType{ 47 | Name: "first", 48 | Expected: "bytes", 49 | Found: args[0].TypeName(), 50 | } 51 | } 52 | res, err := rand.Read(y1.Value) 53 | if err != nil { 54 | ret = wrapError(err) 55 | return 56 | } 57 | return &tengo.Int{Value: int64(res)}, nil 58 | }, 59 | }, 60 | "rand": &tengo.UserFunction{ 61 | Name: "rand", 62 | Value: func(args ...tengo.Object) (tengo.Object, error) { 63 | if len(args) != 1 { 64 | return nil, tengo.ErrWrongNumArguments 65 | } 66 | i1, ok := tengo.ToInt64(args[0]) 67 | if !ok { 68 | return nil, tengo.ErrInvalidArgumentType{ 69 | Name: "first", 70 | Expected: "int(compatible)", 71 | Found: args[0].TypeName(), 72 | } 73 | } 74 | src := rand.NewSource(i1) 75 | return randRand(rand.New(src)), nil 76 | }, 77 | }, 78 | } 79 | 80 | func randRand(r *rand.Rand) *tengo.ImmutableMap { 81 | return &tengo.ImmutableMap{ 82 | Value: map[string]tengo.Object{ 83 | "int": &tengo.UserFunction{ 84 | Name: "int", 85 | Value: FuncARI64(r.Int63), 86 | }, 87 | "float": &tengo.UserFunction{ 88 | Name: "float", 89 | Value: FuncARF(r.Float64), 90 | }, 91 | "intn": &tengo.UserFunction{ 92 | Name: "intn", 93 | Value: FuncAI64RI64(r.Int63n), 94 | }, 95 | "exp_float": &tengo.UserFunction{ 96 | Name: "exp_float", 97 | Value: FuncARF(r.ExpFloat64), 98 | }, 99 | "norm_float": &tengo.UserFunction{ 100 | Name: "norm_float", 101 | Value: FuncARF(r.NormFloat64), 102 | }, 103 | "perm": &tengo.UserFunction{ 104 | Name: "perm", 105 | Value: FuncAIRIs(r.Perm), 106 | }, 107 | "seed": &tengo.UserFunction{ 108 | Name: "seed", 109 | Value: FuncAI64R(r.Seed), 110 | }, 111 | "read": &tengo.UserFunction{ 112 | Name: "read", 113 | Value: func(args ...tengo.Object) ( 114 | ret tengo.Object, 115 | err error, 116 | ) { 117 | if len(args) != 1 { 118 | return nil, tengo.ErrWrongNumArguments 119 | } 120 | y1, ok := args[0].(*tengo.Bytes) 121 | if !ok { 122 | return nil, tengo.ErrInvalidArgumentType{ 123 | Name: "first", 124 | Expected: "bytes", 125 | Found: args[0].TypeName(), 126 | } 127 | } 128 | res, err := r.Read(y1.Value) 129 | if err != nil { 130 | ret = wrapError(err) 131 | return 132 | } 133 | return &tengo.Int{Value: int64(res)}, nil 134 | }, 135 | }, 136 | }, 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /stdlib/rand_test.go: -------------------------------------------------------------------------------- 1 | package stdlib_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "github.com/d5/tengo/v2" 8 | "github.com/d5/tengo/v2/require" 9 | ) 10 | 11 | func TestRand(t *testing.T) { 12 | var seed int64 = 1234 13 | r := rand.New(rand.NewSource(seed)) 14 | 15 | module(t, "rand").call("seed", seed).expect(tengo.UndefinedValue) 16 | module(t, "rand").call("int").expect(r.Int63()) 17 | module(t, "rand").call("float").expect(r.Float64()) 18 | module(t, "rand").call("intn", 111).expect(r.Int63n(111)) 19 | module(t, "rand").call("exp_float").expect(r.ExpFloat64()) 20 | module(t, "rand").call("norm_float").expect(r.NormFloat64()) 21 | module(t, "rand").call("perm", 10).expect(r.Perm(10)) 22 | 23 | buf1 := make([]byte, 10) 24 | buf2 := &tengo.Bytes{Value: make([]byte, 10)} 25 | n, _ := r.Read(buf1) 26 | module(t, "rand").call("read", buf2).expect(n) 27 | require.Equal(t, buf1, buf2.Value) 28 | 29 | seed = 9191 30 | r = rand.New(rand.NewSource(seed)) 31 | randObj := module(t, "rand").call("rand", seed) 32 | randObj.call("seed", seed).expect(tengo.UndefinedValue) 33 | randObj.call("int").expect(r.Int63()) 34 | randObj.call("float").expect(r.Float64()) 35 | randObj.call("intn", 111).expect(r.Int63n(111)) 36 | randObj.call("exp_float").expect(r.ExpFloat64()) 37 | randObj.call("norm_float").expect(r.NormFloat64()) 38 | randObj.call("perm", 10).expect(r.Perm(10)) 39 | 40 | buf1 = make([]byte, 12) 41 | buf2 = &tengo.Bytes{Value: make([]byte, 12)} 42 | n, _ = r.Read(buf1) 43 | randObj.call("read", buf2).expect(n) 44 | require.Equal(t, buf1, buf2.Value) 45 | } 46 | -------------------------------------------------------------------------------- /stdlib/source_modules.go: -------------------------------------------------------------------------------- 1 | // Code generated using gensrcmods.go; DO NOT EDIT. 2 | 3 | package stdlib 4 | 5 | // SourceModules are source type standard library modules. 6 | var SourceModules = map[string]string{ 7 | "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", 8 | } 9 | -------------------------------------------------------------------------------- /stdlib/srcmod_enum.tengo: -------------------------------------------------------------------------------- 1 | is_enumerable := func(x) { 2 | return is_array(x) || is_map(x) || is_immutable_array(x) || is_immutable_map(x) 3 | } 4 | 5 | is_array_like := func(x) { 6 | return is_array(x) || is_immutable_array(x) 7 | } 8 | 9 | export { 10 | // all returns true if the given function `fn` evaluates to a truthy value on 11 | // all of the items in `x`. It returns undefined if `x` is not enumerable. 12 | all: func(x, fn) { 13 | if !is_enumerable(x) { return undefined } 14 | 15 | for k, v in x { 16 | if !fn(k, v) { return false } 17 | } 18 | 19 | return true 20 | }, 21 | // any returns true if the given function `fn` evaluates to a truthy value on 22 | // any of the items in `x`. It returns undefined if `x` is not enumerable. 23 | any: func(x, fn) { 24 | if !is_enumerable(x) { return undefined } 25 | 26 | for k, v in x { 27 | if fn(k, v) { return true } 28 | } 29 | 30 | return false 31 | }, 32 | // chunk returns an array of elements split into groups the length of size. 33 | // If `x` can't be split evenly, the final chunk will be the remaining elements. 34 | // It returns undefined if `x` is not array. 35 | chunk: func(x, size) { 36 | if !is_array_like(x) || !size { return undefined } 37 | 38 | numElements := len(x) 39 | if !numElements { return [] } 40 | 41 | res := [] 42 | idx := 0 43 | for idx < numElements { 44 | res = append(res, x[idx:idx+size]) 45 | idx += size 46 | } 47 | 48 | return res 49 | }, 50 | // at returns an element at the given index (if `x` is array) or 51 | // key (if `x` is map). It returns undefined if `x` is not enumerable. 52 | at: func(x, key) { 53 | if !is_enumerable(x) { return undefined } 54 | 55 | if is_array_like(x) { 56 | if !is_int(key) { return undefined } 57 | } else { 58 | if !is_string(key) { return undefined } 59 | } 60 | 61 | return x[key] 62 | }, 63 | // each iterates over elements of `x` and invokes `fn` for each element. `fn` is 64 | // invoked with two arguments: `key` and `value`. `key` is an int index 65 | // if `x` is array. `key` is a string key if `x` is map. It does not iterate 66 | // and returns undefined if `x` is not enumerable. 67 | each: func(x, fn) { 68 | if !is_enumerable(x) { return undefined } 69 | 70 | for k, v in x { 71 | fn(k, v) 72 | } 73 | }, 74 | // filter iterates over elements of `x`, returning an array of all elements `fn` 75 | // returns truthy for. `fn` is invoked with two arguments: `key` and `value`. 76 | // `key` is an int index if `x` is array. It returns undefined if `x` is not array. 77 | filter: func(x, fn) { 78 | if !is_array_like(x) { return undefined } 79 | 80 | dst := [] 81 | for k, v in x { 82 | if fn(k, v) { dst = append(dst, v) } 83 | } 84 | 85 | return dst 86 | }, 87 | // find iterates over elements of `x`, returning value of the first element `fn` 88 | // returns truthy for. `fn` is invoked with two arguments: `key` and `value`. 89 | // `key` is an int index if `x` is array. `key` is a string key if `x` is map. 90 | // It returns undefined if `x` is not enumerable. 91 | find: func(x, fn) { 92 | if !is_enumerable(x) { return undefined } 93 | 94 | for k, v in x { 95 | if fn(k, v) { return v } 96 | } 97 | }, 98 | // find_key iterates over elements of `x`, returning key or index of the first 99 | // element `fn` returns truthy for. `fn` is invoked with two arguments: `key` 100 | // and `value`. `key` is an int index if `x` is array. `key` is a string key if 101 | // `x` is map. It returns undefined if `x` is not enumerable. 102 | find_key: func(x, fn) { 103 | if !is_enumerable(x) { return undefined } 104 | 105 | for k, v in x { 106 | if fn(k, v) { return k } 107 | } 108 | }, 109 | // map creates an array of values by running each element in `x` through `fn`. 110 | // `fn` is invoked with two arguments: `key` and `value`. `key` is an int index 111 | // if `x` is array. `key` is a string key if `x` is map. It returns undefined 112 | // if `x` is not enumerable. 113 | map: func(x, fn) { 114 | if !is_enumerable(x) { return undefined } 115 | 116 | dst := [] 117 | for k, v in x { 118 | dst = append(dst, fn(k, v)) 119 | } 120 | 121 | return dst 122 | }, 123 | // key returns the first argument. 124 | key: func(k, _) { return k }, 125 | // value returns the second argument. 126 | value: func(_, v) { return v } 127 | } 128 | -------------------------------------------------------------------------------- /stdlib/stdlib.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | //go:generate go run gensrcmods.go 4 | 5 | import ( 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | // AllModuleNames returns a list of all default module names. 10 | func AllModuleNames() []string { 11 | var names []string 12 | for name := range BuiltinModules { 13 | names = append(names, name) 14 | } 15 | for name := range SourceModules { 16 | names = append(names, name) 17 | } 18 | return names 19 | } 20 | 21 | // GetModuleMap returns the module map that includes all modules 22 | // for the given module names. 23 | func GetModuleMap(names ...string) *tengo.ModuleMap { 24 | modules := tengo.NewModuleMap() 25 | for _, name := range names { 26 | if mod := BuiltinModules[name]; mod != nil { 27 | modules.AddBuiltinModule(name, mod) 28 | } 29 | if mod := SourceModules[name]; mod != "" { 30 | modules.AddSourceModule(name, []byte(mod)) 31 | } 32 | } 33 | return modules 34 | } 35 | -------------------------------------------------------------------------------- /stdlib/stdlib_test.go: -------------------------------------------------------------------------------- 1 | package stdlib_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/d5/tengo/v2" 9 | "github.com/d5/tengo/v2/require" 10 | "github.com/d5/tengo/v2/stdlib" 11 | ) 12 | 13 | type ARR = []interface{} 14 | type MAP = map[string]interface{} 15 | type IARR []interface{} 16 | type IMAP map[string]interface{} 17 | 18 | func TestAllModuleNames(t *testing.T) { 19 | names := stdlib.AllModuleNames() 20 | require.Equal(t, 21 | len(stdlib.BuiltinModules)+len(stdlib.SourceModules), 22 | len(names)) 23 | } 24 | 25 | func TestModulesRun(t *testing.T) { 26 | // os.File 27 | expect(t, ` 28 | os := import("os") 29 | out := "" 30 | 31 | write_file := func(filename, data) { 32 | file := os.create(filename) 33 | if !file { return file } 34 | 35 | if res := file.write(bytes(data)); is_error(res) { 36 | return res 37 | } 38 | 39 | return file.close() 40 | } 41 | 42 | read_file := func(filename) { 43 | file := os.open(filename) 44 | if !file { return file } 45 | 46 | data := bytes(100) 47 | cnt := file.read(data) 48 | if is_error(cnt) { 49 | return cnt 50 | } 51 | 52 | file.close() 53 | return data[:cnt] 54 | } 55 | 56 | if write_file("./temp", "foobar") { 57 | out = string(read_file("./temp")) 58 | } 59 | 60 | os.remove("./temp") 61 | `, "foobar") 62 | 63 | // exec.command 64 | expect(t, ` 65 | out := "" 66 | os := import("os") 67 | cmd := os.exec("echo", "foo", "bar") 68 | if !is_error(cmd) { 69 | out = cmd.output() 70 | } 71 | `, []byte("foo bar\n")) 72 | 73 | } 74 | 75 | func TestGetModules(t *testing.T) { 76 | mods := stdlib.GetModuleMap() 77 | require.Equal(t, 0, mods.Len()) 78 | 79 | mods = stdlib.GetModuleMap("os") 80 | require.Equal(t, 1, mods.Len()) 81 | require.NotNil(t, mods.Get("os")) 82 | 83 | mods = stdlib.GetModuleMap("os", "rand") 84 | require.Equal(t, 2, mods.Len()) 85 | require.NotNil(t, mods.Get("os")) 86 | require.NotNil(t, mods.Get("rand")) 87 | 88 | mods = stdlib.GetModuleMap("text", "text") 89 | require.Equal(t, 1, mods.Len()) 90 | require.NotNil(t, mods.Get("text")) 91 | 92 | mods = stdlib.GetModuleMap("nonexisting", "text") 93 | require.Equal(t, 1, mods.Len()) 94 | require.NotNil(t, mods.Get("text")) 95 | } 96 | 97 | type callres struct { 98 | t *testing.T 99 | o interface{} 100 | e error 101 | } 102 | 103 | func (c callres) call(funcName string, args ...interface{}) callres { 104 | if c.e != nil { 105 | return c 106 | } 107 | 108 | var oargs []tengo.Object 109 | for _, v := range args { 110 | oargs = append(oargs, object(v)) 111 | } 112 | 113 | switch o := c.o.(type) { 114 | case *tengo.BuiltinModule: 115 | m, ok := o.Attrs[funcName] 116 | if !ok { 117 | return callres{t: c.t, e: fmt.Errorf( 118 | "function not found: %s", funcName)} 119 | } 120 | 121 | f, ok := m.(*tengo.UserFunction) 122 | if !ok { 123 | return callres{t: c.t, e: fmt.Errorf( 124 | "non-callable: %s", funcName)} 125 | } 126 | 127 | res, err := f.Value(oargs...) 128 | return callres{t: c.t, o: res, e: err} 129 | case *tengo.UserFunction: 130 | res, err := o.Value(oargs...) 131 | return callres{t: c.t, o: res, e: err} 132 | case *tengo.ImmutableMap: 133 | m, ok := o.Value[funcName] 134 | if !ok { 135 | return callres{t: c.t, e: fmt.Errorf("function not found: %s", funcName)} 136 | } 137 | 138 | f, ok := m.(*tengo.UserFunction) 139 | if !ok { 140 | return callres{t: c.t, e: fmt.Errorf("non-callable: %s", funcName)} 141 | } 142 | 143 | res, err := f.Value(oargs...) 144 | return callres{t: c.t, o: res, e: err} 145 | default: 146 | panic(fmt.Errorf("unexpected object: %v (%T)", o, o)) 147 | } 148 | } 149 | 150 | func (c callres) expect(expected interface{}, msgAndArgs ...interface{}) { 151 | require.NoError(c.t, c.e, msgAndArgs...) 152 | require.Equal(c.t, object(expected), c.o, msgAndArgs...) 153 | } 154 | 155 | func (c callres) expectError() { 156 | require.Error(c.t, c.e) 157 | } 158 | 159 | func module(t *testing.T, moduleName string) callres { 160 | mod := stdlib.GetModuleMap(moduleName).GetBuiltinModule(moduleName) 161 | if mod == nil { 162 | return callres{t: t, e: fmt.Errorf("module not found: %s", moduleName)} 163 | } 164 | 165 | return callres{t: t, o: mod} 166 | } 167 | 168 | func object(v interface{}) tengo.Object { 169 | switch v := v.(type) { 170 | case tengo.Object: 171 | return v 172 | case string: 173 | return &tengo.String{Value: v} 174 | case int64: 175 | return &tengo.Int{Value: v} 176 | case int: // for convenience 177 | return &tengo.Int{Value: int64(v)} 178 | case bool: 179 | if v { 180 | return tengo.TrueValue 181 | } 182 | return tengo.FalseValue 183 | case rune: 184 | return &tengo.Char{Value: v} 185 | case byte: // for convenience 186 | return &tengo.Char{Value: rune(v)} 187 | case float64: 188 | return &tengo.Float{Value: v} 189 | case []byte: 190 | return &tengo.Bytes{Value: v} 191 | case MAP: 192 | objs := make(map[string]tengo.Object) 193 | for k, v := range v { 194 | objs[k] = object(v) 195 | } 196 | 197 | return &tengo.Map{Value: objs} 198 | case ARR: 199 | var objs []tengo.Object 200 | for _, e := range v { 201 | objs = append(objs, object(e)) 202 | } 203 | 204 | return &tengo.Array{Value: objs} 205 | case IMAP: 206 | objs := make(map[string]tengo.Object) 207 | for k, v := range v { 208 | objs[k] = object(v) 209 | } 210 | 211 | return &tengo.ImmutableMap{Value: objs} 212 | case IARR: 213 | var objs []tengo.Object 214 | for _, e := range v { 215 | objs = append(objs, object(e)) 216 | } 217 | 218 | return &tengo.ImmutableArray{Value: objs} 219 | case time.Time: 220 | return &tengo.Time{Value: v} 221 | case []int: 222 | var objs []tengo.Object 223 | for _, e := range v { 224 | objs = append(objs, &tengo.Int{Value: int64(e)}) 225 | } 226 | 227 | return &tengo.Array{Value: objs} 228 | } 229 | 230 | panic(fmt.Errorf("unknown type: %T", v)) 231 | } 232 | 233 | func expect(t *testing.T, input string, expected interface{}) { 234 | s := tengo.NewScript([]byte(input)) 235 | s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) 236 | c, err := s.Run() 237 | require.NoError(t, err) 238 | require.NotNil(t, c) 239 | v := c.Get("out") 240 | require.NotNil(t, v) 241 | require.Equal(t, expected, v.Value()) 242 | } 243 | -------------------------------------------------------------------------------- /stdlib/text_regexp.go: -------------------------------------------------------------------------------- 1 | package stdlib 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | func makeTextRegexp(re *regexp.Regexp) *tengo.ImmutableMap { 10 | return &tengo.ImmutableMap{ 11 | Value: map[string]tengo.Object{ 12 | // match(text) => bool 13 | "match": &tengo.UserFunction{ 14 | Value: func(args ...tengo.Object) ( 15 | ret tengo.Object, 16 | err error, 17 | ) { 18 | if len(args) != 1 { 19 | err = tengo.ErrWrongNumArguments 20 | return 21 | } 22 | 23 | s1, ok := tengo.ToString(args[0]) 24 | if !ok { 25 | err = tengo.ErrInvalidArgumentType{ 26 | Name: "first", 27 | Expected: "string(compatible)", 28 | Found: args[0].TypeName(), 29 | } 30 | return 31 | } 32 | 33 | if re.MatchString(s1) { 34 | ret = tengo.TrueValue 35 | } else { 36 | ret = tengo.FalseValue 37 | } 38 | 39 | return 40 | }, 41 | }, 42 | 43 | // find(text) => array(array({text:,begin:,end:}))/undefined 44 | // find(text, maxCount) => array(array({text:,begin:,end:}))/undefined 45 | "find": &tengo.UserFunction{ 46 | Value: func(args ...tengo.Object) ( 47 | ret tengo.Object, 48 | err error, 49 | ) { 50 | numArgs := len(args) 51 | if numArgs != 1 && numArgs != 2 { 52 | err = tengo.ErrWrongNumArguments 53 | return 54 | } 55 | 56 | s1, ok := tengo.ToString(args[0]) 57 | if !ok { 58 | err = tengo.ErrInvalidArgumentType{ 59 | Name: "first", 60 | Expected: "string(compatible)", 61 | Found: args[0].TypeName(), 62 | } 63 | return 64 | } 65 | 66 | if numArgs == 1 { 67 | m := re.FindStringSubmatchIndex(s1) 68 | if m == nil { 69 | ret = tengo.UndefinedValue 70 | return 71 | } 72 | 73 | arr := &tengo.Array{} 74 | for i := 0; i < len(m); i += 2 { 75 | arr.Value = append(arr.Value, 76 | &tengo.ImmutableMap{ 77 | Value: map[string]tengo.Object{ 78 | "text": &tengo.String{ 79 | Value: s1[m[i]:m[i+1]], 80 | }, 81 | "begin": &tengo.Int{ 82 | Value: int64(m[i]), 83 | }, 84 | "end": &tengo.Int{ 85 | Value: int64(m[i+1]), 86 | }, 87 | }}) 88 | } 89 | 90 | ret = &tengo.Array{Value: []tengo.Object{arr}} 91 | 92 | return 93 | } 94 | 95 | i2, ok := tengo.ToInt(args[1]) 96 | if !ok { 97 | err = tengo.ErrInvalidArgumentType{ 98 | Name: "second", 99 | Expected: "int(compatible)", 100 | Found: args[1].TypeName(), 101 | } 102 | return 103 | } 104 | m := re.FindAllStringSubmatchIndex(s1, i2) 105 | if m == nil { 106 | ret = tengo.UndefinedValue 107 | return 108 | } 109 | 110 | arr := &tengo.Array{} 111 | for _, m := range m { 112 | subMatch := &tengo.Array{} 113 | for i := 0; i < len(m); i += 2 { 114 | subMatch.Value = append(subMatch.Value, 115 | &tengo.ImmutableMap{ 116 | Value: map[string]tengo.Object{ 117 | "text": &tengo.String{ 118 | Value: s1[m[i]:m[i+1]], 119 | }, 120 | "begin": &tengo.Int{ 121 | Value: int64(m[i]), 122 | }, 123 | "end": &tengo.Int{ 124 | Value: int64(m[i+1]), 125 | }, 126 | }}) 127 | } 128 | 129 | arr.Value = append(arr.Value, subMatch) 130 | } 131 | 132 | ret = arr 133 | 134 | return 135 | }, 136 | }, 137 | 138 | // replace(src, repl) => string 139 | "replace": &tengo.UserFunction{ 140 | Value: func(args ...tengo.Object) ( 141 | ret tengo.Object, 142 | err error, 143 | ) { 144 | if len(args) != 2 { 145 | err = tengo.ErrWrongNumArguments 146 | return 147 | } 148 | 149 | s1, ok := tengo.ToString(args[0]) 150 | if !ok { 151 | err = tengo.ErrInvalidArgumentType{ 152 | Name: "first", 153 | Expected: "string(compatible)", 154 | Found: args[0].TypeName(), 155 | } 156 | return 157 | } 158 | 159 | s2, ok := tengo.ToString(args[1]) 160 | if !ok { 161 | err = tengo.ErrInvalidArgumentType{ 162 | Name: "second", 163 | Expected: "string(compatible)", 164 | Found: args[1].TypeName(), 165 | } 166 | return 167 | } 168 | 169 | s, ok := doTextRegexpReplace(re, s1, s2) 170 | if !ok { 171 | return nil, tengo.ErrStringLimit 172 | } 173 | 174 | ret = &tengo.String{Value: s} 175 | 176 | return 177 | }, 178 | }, 179 | 180 | // split(text) => array(string) 181 | // split(text, maxCount) => array(string) 182 | "split": &tengo.UserFunction{ 183 | Value: func(args ...tengo.Object) ( 184 | ret tengo.Object, 185 | err error, 186 | ) { 187 | numArgs := len(args) 188 | if numArgs != 1 && numArgs != 2 { 189 | err = tengo.ErrWrongNumArguments 190 | return 191 | } 192 | 193 | s1, ok := tengo.ToString(args[0]) 194 | if !ok { 195 | err = tengo.ErrInvalidArgumentType{ 196 | Name: "first", 197 | Expected: "string(compatible)", 198 | Found: args[0].TypeName(), 199 | } 200 | return 201 | } 202 | 203 | var i2 = -1 204 | if numArgs > 1 { 205 | i2, ok = tengo.ToInt(args[1]) 206 | if !ok { 207 | err = tengo.ErrInvalidArgumentType{ 208 | Name: "second", 209 | Expected: "int(compatible)", 210 | Found: args[1].TypeName(), 211 | } 212 | return 213 | } 214 | } 215 | 216 | arr := &tengo.Array{} 217 | for _, s := range re.Split(s1, i2) { 218 | arr.Value = append(arr.Value, 219 | &tengo.String{Value: s}) 220 | } 221 | 222 | ret = arr 223 | 224 | return 225 | }, 226 | }, 227 | }, 228 | } 229 | } 230 | 231 | // Size-limit checking implementation of regexp.ReplaceAllString. 232 | func doTextRegexpReplace(re *regexp.Regexp, src, repl string) (string, bool) { 233 | idx := 0 234 | out := "" 235 | for _, m := range re.FindAllStringSubmatchIndex(src, -1) { 236 | var exp []byte 237 | exp = re.ExpandString(exp, repl, src, m) 238 | if len(out)+m[0]-idx+len(exp) > tengo.MaxStringLen { 239 | return "", false 240 | } 241 | out += src[idx:m[0]] + string(exp) 242 | idx = m[1] 243 | } 244 | if idx < len(src) { 245 | if len(out)+len(src)-idx > tengo.MaxStringLen { 246 | return "", false 247 | } 248 | out += src[idx:] 249 | } 250 | return out, true 251 | } 252 | -------------------------------------------------------------------------------- /stdlib/text_regexp_test.go: -------------------------------------------------------------------------------- 1 | package stdlib_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/d5/tengo/v2" 7 | ) 8 | 9 | func TestTextREAlternation(t *testing.T) { 10 | module(t, "text").call("re_find", "([a-zA-Z])|([0-9])", "a").expect(ARR{ 11 | ARR{ 12 | IMAP{"text": "a", "begin": 0, "end": 1}, 13 | IMAP{"text": "a", "begin": 0, "end": 1}, 14 | }, 15 | }, "alternation with letter") 16 | 17 | module(t, "text").call("re_find", "([a-zA-Z])|([0-9])", "5").expect(ARR{ 18 | ARR{ 19 | IMAP{"text": "5", "begin": 0, "end": 1}, 20 | IMAP{"text": "5", "begin": 0, "end": 1}, 21 | }, 22 | }, "alternation with number") 23 | 24 | module(t, "text").call("re_find", "([a-zA-Z])|([0-9])", "").expect(tengo.UndefinedValue, "empty input") 25 | 26 | module(t, "text").call("re_find", "([a-zA-Z])|([0-9])", "!").expect(tengo.UndefinedValue, "non-matching input") 27 | 28 | module(t, "text").call("re_find", "(?:([a-zA-Z])|([0-9]))+", "a5b").expect(ARR{ 29 | ARR{ 30 | IMAP{"text": "a5b", "begin": 0, "end": 3}, 31 | IMAP{"text": "b", "begin": 2, "end": 3}, 32 | IMAP{"text": "5", "begin": 1, "end": 2}, 33 | }, 34 | }, "multiple alternations") 35 | 36 | module(t, "text").call("re_find", "(foo)|(bar)|(baz)", "foo").expect(ARR{ 37 | ARR{ 38 | IMAP{"text": "foo", "begin": 0, "end": 3}, 39 | IMAP{"text": "foo", "begin": 0, "end": 3}, 40 | }, 41 | }, "multiple groups with non-matches") 42 | 43 | module(t, "text").call("re_find", "((cat)|(dog))((run)|(walk))", "catrun").expect(ARR{ 44 | ARR{ 45 | IMAP{"text": "catrun", "begin": 0, "end": 6}, 46 | IMAP{"text": "cat", "begin": 0, "end": 3}, 47 | IMAP{"text": "cat", "begin": 0, "end": 3}, 48 | IMAP{"text": "run", "begin": 3, "end": 6}, 49 | IMAP{"text": "run", "begin": 3, "end": 6}, 50 | }, 51 | }, "nested groups with alternation") 52 | } 53 | -------------------------------------------------------------------------------- /stdlib/times_test.go: -------------------------------------------------------------------------------- 1 | package stdlib_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/d5/tengo/v2" 8 | "github.com/d5/tengo/v2/require" 9 | ) 10 | 11 | func TestTimes(t *testing.T) { 12 | time1 := time.Date(1982, 9, 28, 19, 21, 44, 999, time.Now().Location()) 13 | time2 := time.Now() 14 | location, _ := time.LoadLocation("Pacific/Auckland") 15 | time3 := time.Date(1982, 9, 28, 19, 21, 44, 999, location) 16 | 17 | module(t, "times").call("sleep", 1).expect(tengo.UndefinedValue) 18 | 19 | require.True(t, module(t, "times"). 20 | call("since", time.Now().Add(-time.Hour)). 21 | o.(*tengo.Int).Value > 3600000000000) 22 | require.True(t, module(t, "times"). 23 | call("until", time.Now().Add(time.Hour)). 24 | o.(*tengo.Int).Value < 3600000000000) 25 | 26 | module(t, "times").call("parse_duration", "1ns").expect(1) 27 | module(t, "times").call("parse_duration", "1ms").expect(1000000) 28 | module(t, "times").call("parse_duration", "1h").expect(3600000000000) 29 | module(t, "times").call("duration_hours", 1800000000000).expect(0.5) 30 | module(t, "times").call("duration_minutes", 1800000000000).expect(30.0) 31 | module(t, "times").call("duration_nanoseconds", 100).expect(100) 32 | module(t, "times").call("duration_seconds", 1000000).expect(0.001) 33 | module(t, "times").call("duration_string", 1800000000000).expect("30m0s") 34 | 35 | module(t, "times").call("month_string", 1).expect("January") 36 | module(t, "times").call("month_string", 12).expect("December") 37 | 38 | module(t, "times").call("date", 1982, 9, 28, 19, 21, 44, 999). 39 | expect(time1) 40 | module(t, "times").call("date", 1982, 9, 28, 19, 21, 44, 999, "Pacific/Auckland"). 41 | expect(time3) 42 | 43 | nowD := time.Until(module(t, "times").call("now"). 44 | o.(*tengo.Time).Value).Nanoseconds() 45 | require.True(t, 0 > nowD && nowD > -100000000) // within 100ms 46 | parsed, _ := time.Parse(time.RFC3339, "1982-09-28T19:21:44+07:00") 47 | module(t, "times"). 48 | call("parse", time.RFC3339, "1982-09-28T19:21:44+07:00"). 49 | expect(parsed) 50 | module(t, "times"). 51 | call("unix", 1234325, 94493). 52 | expect(time.Unix(1234325, 94493)) 53 | 54 | module(t, "times").call("add", time2, 3600000000000). 55 | expect(time2.Add(time.Duration(3600000000000))) 56 | module(t, "times").call("sub", time2, time2.Add(-time.Hour)). 57 | expect(3600000000000) 58 | module(t, "times").call("add_date", time2, 1, 2, 3). 59 | expect(time2.AddDate(1, 2, 3)) 60 | module(t, "times").call("after", time2, time2.Add(time.Hour)). 61 | expect(false) 62 | module(t, "times").call("after", time2, time2.Add(-time.Hour)). 63 | expect(true) 64 | module(t, "times").call("before", time2, time2.Add(time.Hour)). 65 | expect(true) 66 | module(t, "times").call("before", time2, time2.Add(-time.Hour)). 67 | expect(false) 68 | 69 | module(t, "times").call("time_year", time1).expect(time1.Year()) 70 | module(t, "times").call("time_month", time1).expect(int(time1.Month())) 71 | module(t, "times").call("time_day", time1).expect(time1.Day()) 72 | module(t, "times").call("time_hour", time1).expect(time1.Hour()) 73 | module(t, "times").call("time_minute", time1).expect(time1.Minute()) 74 | module(t, "times").call("time_second", time1).expect(time1.Second()) 75 | module(t, "times").call("time_nanosecond", time1). 76 | expect(time1.Nanosecond()) 77 | module(t, "times").call("time_unix", time1).expect(time1.Unix()) 78 | module(t, "times").call("time_unix_nano", time1).expect(time1.UnixNano()) 79 | module(t, "times").call("time_format", time1, time.RFC3339). 80 | expect(time1.Format(time.RFC3339)) 81 | module(t, "times").call("is_zero", time1).expect(false) 82 | module(t, "times").call("is_zero", time.Time{}).expect(true) 83 | module(t, "times").call("to_local", time1).expect(time1.Local()) 84 | module(t, "times").call("to_utc", time1).expect(time1.UTC()) 85 | module(t, "times").call("time_location", time1). 86 | expect(time1.Location().String()) 87 | module(t, "times").call("time_string", time1).expect(time1.String()) 88 | module(t, "times").call("in_location", time1, location.String()).expect(time1.In(location)) 89 | } 90 | -------------------------------------------------------------------------------- /symbol_table.go: -------------------------------------------------------------------------------- 1 | package tengo 2 | 3 | // SymbolScope represents a symbol scope. 4 | type SymbolScope string 5 | 6 | // List of symbol scopes 7 | const ( 8 | ScopeGlobal SymbolScope = "GLOBAL" 9 | ScopeLocal SymbolScope = "LOCAL" 10 | ScopeBuiltin SymbolScope = "BUILTIN" 11 | ScopeFree SymbolScope = "FREE" 12 | ) 13 | 14 | // Symbol represents a symbol in the symbol table. 15 | type Symbol struct { 16 | Name string 17 | Scope SymbolScope 18 | Index int 19 | LocalAssigned bool // if the local symbol is assigned at least once 20 | } 21 | 22 | // SymbolTable represents a symbol table. 23 | type SymbolTable struct { 24 | parent *SymbolTable 25 | block bool 26 | store map[string]*Symbol 27 | numDefinition int 28 | maxDefinition int 29 | freeSymbols []*Symbol 30 | builtinSymbols []*Symbol 31 | } 32 | 33 | // NewSymbolTable creates a SymbolTable. 34 | func NewSymbolTable() *SymbolTable { 35 | return &SymbolTable{ 36 | store: make(map[string]*Symbol), 37 | } 38 | } 39 | 40 | // Define adds a new symbol in the current scope. 41 | func (t *SymbolTable) Define(name string) *Symbol { 42 | symbol := &Symbol{Name: name, Index: t.nextIndex()} 43 | t.numDefinition++ 44 | 45 | if t.Parent(true) == nil { 46 | symbol.Scope = ScopeGlobal 47 | 48 | // if symbol is defined in a block of global scope, symbol index must 49 | // be tracked at the root-level table instead. 50 | if p := t.parent; p != nil { 51 | for p.parent != nil { 52 | p = p.parent 53 | } 54 | t.numDefinition-- 55 | p.numDefinition++ 56 | } 57 | 58 | } else { 59 | symbol.Scope = ScopeLocal 60 | } 61 | t.store[name] = symbol 62 | t.updateMaxDefs(symbol.Index + 1) 63 | return symbol 64 | } 65 | 66 | // DefineBuiltin adds a symbol for builtin function. 67 | func (t *SymbolTable) DefineBuiltin(index int, name string) *Symbol { 68 | if t.parent != nil { 69 | return t.parent.DefineBuiltin(index, name) 70 | } 71 | 72 | symbol := &Symbol{ 73 | Name: name, 74 | Index: index, 75 | Scope: ScopeBuiltin, 76 | } 77 | t.store[name] = symbol 78 | t.builtinSymbols = append(t.builtinSymbols, symbol) 79 | return symbol 80 | } 81 | 82 | // Resolve resolves a symbol with a given name. 83 | func (t *SymbolTable) Resolve( 84 | name string, 85 | recur bool, 86 | ) (*Symbol, int, bool) { 87 | symbol, ok := t.store[name] 88 | if ok { 89 | // symbol can be used if 90 | if symbol.Scope != ScopeLocal || // it's not of local scope, OR, 91 | symbol.LocalAssigned || // it's assigned at least once, OR, 92 | recur { // it's defined in higher level 93 | return symbol, 0, true 94 | } 95 | } 96 | 97 | if t.parent == nil { 98 | return nil, 0, false 99 | } 100 | 101 | symbol, depth, ok := t.parent.Resolve(name, true) 102 | if !ok { 103 | return nil, 0, false 104 | } 105 | depth++ 106 | 107 | // if symbol is defined in parent table and if it's not global/builtin 108 | // then it's free variable. 109 | if !t.block && depth > 0 && 110 | symbol.Scope != ScopeGlobal && 111 | symbol.Scope != ScopeBuiltin { 112 | return t.defineFree(symbol), depth, true 113 | } 114 | return symbol, depth, true 115 | } 116 | 117 | // Fork creates a new symbol table for a new scope. 118 | func (t *SymbolTable) Fork(block bool) *SymbolTable { 119 | return &SymbolTable{ 120 | store: make(map[string]*Symbol), 121 | parent: t, 122 | block: block, 123 | } 124 | } 125 | 126 | // Parent returns the outer scope of the current symbol table. 127 | func (t *SymbolTable) Parent(skipBlock bool) *SymbolTable { 128 | if skipBlock && t.block { 129 | return t.parent.Parent(skipBlock) 130 | } 131 | return t.parent 132 | } 133 | 134 | // MaxSymbols returns the total number of symbols defined in the scope. 135 | func (t *SymbolTable) MaxSymbols() int { 136 | return t.maxDefinition 137 | } 138 | 139 | // FreeSymbols returns free symbols for the scope. 140 | func (t *SymbolTable) FreeSymbols() []*Symbol { 141 | return t.freeSymbols 142 | } 143 | 144 | // BuiltinSymbols returns builtin symbols for the scope. 145 | func (t *SymbolTable) BuiltinSymbols() []*Symbol { 146 | if t.parent != nil { 147 | return t.parent.BuiltinSymbols() 148 | } 149 | return t.builtinSymbols 150 | } 151 | 152 | // Names returns the name of all the symbols. 153 | func (t *SymbolTable) Names() []string { 154 | var names []string 155 | for name := range t.store { 156 | names = append(names, name) 157 | } 158 | return names 159 | } 160 | 161 | func (t *SymbolTable) nextIndex() int { 162 | if t.block { 163 | return t.parent.nextIndex() + t.numDefinition 164 | } 165 | return t.numDefinition 166 | } 167 | 168 | func (t *SymbolTable) updateMaxDefs(numDefs int) { 169 | if numDefs > t.maxDefinition { 170 | t.maxDefinition = numDefs 171 | } 172 | if t.block { 173 | t.parent.updateMaxDefs(numDefs) 174 | } 175 | } 176 | 177 | func (t *SymbolTable) defineFree(original *Symbol) *Symbol { 178 | // TODO: should we check duplicates? 179 | t.freeSymbols = append(t.freeSymbols, original) 180 | symbol := &Symbol{ 181 | Name: original.Name, 182 | Index: len(t.freeSymbols) - 1, 183 | Scope: ScopeFree, 184 | } 185 | t.store[original.Name] = symbol 186 | return symbol 187 | } 188 | -------------------------------------------------------------------------------- /symbol_table_test.go: -------------------------------------------------------------------------------- 1 | package tengo_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/d5/tengo/v2" 7 | "github.com/d5/tengo/v2/require" 8 | ) 9 | 10 | func TestSymbolTable(t *testing.T) { 11 | /* 12 | GLOBAL 13 | [0] a 14 | [1] b 15 | 16 | LOCAL 1 17 | [0] d 18 | 19 | LOCAL 2 20 | [0] e 21 | [1] f 22 | 23 | LOCAL 2 BLOCK 1 24 | [2] g 25 | [3] h 26 | 27 | LOCAL 2 BLOCK 2 28 | [2] i 29 | [3] j 30 | [4] k 31 | 32 | LOCAL 1 BLOCK 1 33 | [1] l 34 | [2] m 35 | [3] n 36 | [4] o 37 | [5] p 38 | 39 | LOCAL 3 40 | [0] q 41 | [1] r 42 | */ 43 | 44 | global := symbolTable() 45 | require.Equal(t, globalSymbol("a", 0), global.Define("a")) 46 | require.Equal(t, globalSymbol("b", 1), global.Define("b")) 47 | 48 | local1 := global.Fork(false) 49 | require.Equal(t, localSymbol("d", 0), local1.Define("d")) 50 | 51 | local1Block1 := local1.Fork(true) 52 | require.Equal(t, localSymbol("l", 1), local1Block1.Define("l")) 53 | require.Equal(t, localSymbol("m", 2), local1Block1.Define("m")) 54 | require.Equal(t, localSymbol("n", 3), local1Block1.Define("n")) 55 | require.Equal(t, localSymbol("o", 4), local1Block1.Define("o")) 56 | require.Equal(t, localSymbol("p", 5), local1Block1.Define("p")) 57 | 58 | local2 := local1.Fork(false) 59 | require.Equal(t, localSymbol("e", 0), local2.Define("e")) 60 | require.Equal(t, localSymbol("f", 1), local2.Define("f")) 61 | 62 | local2Block1 := local2.Fork(true) 63 | require.Equal(t, localSymbol("g", 2), local2Block1.Define("g")) 64 | require.Equal(t, localSymbol("h", 3), local2Block1.Define("h")) 65 | 66 | local2Block2 := local2.Fork(true) 67 | require.Equal(t, localSymbol("i", 2), local2Block2.Define("i")) 68 | require.Equal(t, localSymbol("j", 3), local2Block2.Define("j")) 69 | require.Equal(t, localSymbol("k", 4), local2Block2.Define("k")) 70 | 71 | local3 := local1Block1.Fork(false) 72 | require.Equal(t, localSymbol("q", 0), local3.Define("q")) 73 | require.Equal(t, localSymbol("r", 1), local3.Define("r")) 74 | 75 | require.Equal(t, 2, global.MaxSymbols()) 76 | require.Equal(t, 6, local1.MaxSymbols()) 77 | require.Equal(t, 6, local1Block1.MaxSymbols()) 78 | require.Equal(t, 5, local2.MaxSymbols()) 79 | require.Equal(t, 4, local2Block1.MaxSymbols()) 80 | require.Equal(t, 5, local2Block2.MaxSymbols()) 81 | require.Equal(t, 2, local3.MaxSymbols()) 82 | 83 | resolveExpect(t, global, "a", globalSymbol("a", 0), 0) 84 | resolveExpect(t, local1, "d", localSymbol("d", 0), 0) 85 | resolveExpect(t, local1, "a", globalSymbol("a", 0), 1) 86 | resolveExpect(t, local3, "a", globalSymbol("a", 0), 3) 87 | resolveExpect(t, local3, "d", freeSymbol("d", 0), 2) 88 | resolveExpect(t, local3, "r", localSymbol("r", 1), 0) 89 | resolveExpect(t, local2Block2, "k", localSymbol("k", 4), 0) 90 | resolveExpect(t, local2Block2, "e", localSymbol("e", 0), 1) 91 | resolveExpect(t, local2Block2, "b", globalSymbol("b", 1), 3) 92 | } 93 | 94 | func symbol( 95 | name string, 96 | scope tengo.SymbolScope, 97 | index int, 98 | ) *tengo.Symbol { 99 | return &tengo.Symbol{ 100 | Name: name, 101 | Scope: scope, 102 | Index: index, 103 | } 104 | } 105 | 106 | func globalSymbol(name string, index int) *tengo.Symbol { 107 | return symbol(name, tengo.ScopeGlobal, index) 108 | } 109 | 110 | func localSymbol(name string, index int) *tengo.Symbol { 111 | return symbol(name, tengo.ScopeLocal, index) 112 | } 113 | 114 | func freeSymbol(name string, index int) *tengo.Symbol { 115 | return symbol(name, tengo.ScopeFree, index) 116 | } 117 | 118 | func symbolTable() *tengo.SymbolTable { 119 | return tengo.NewSymbolTable() 120 | } 121 | 122 | func resolveExpect( 123 | t *testing.T, 124 | symbolTable *tengo.SymbolTable, 125 | name string, 126 | expectedSymbol *tengo.Symbol, 127 | expectedDepth int, 128 | ) { 129 | actualSymbol, actualDepth, ok := symbolTable.Resolve(name, true) 130 | require.True(t, ok) 131 | require.Equal(t, expectedSymbol, actualSymbol) 132 | require.Equal(t, expectedDepth, actualDepth) 133 | } 134 | -------------------------------------------------------------------------------- /tengo.go: -------------------------------------------------------------------------------- 1 | package tengo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | var ( 11 | // MaxStringLen is the maximum byte-length for string value. Note this 12 | // limit applies to all compiler/VM instances in the process. 13 | MaxStringLen = 2147483647 14 | 15 | // MaxBytesLen is the maximum length for bytes value. Note this limit 16 | // applies to all compiler/VM instances in the process. 17 | MaxBytesLen = 2147483647 18 | ) 19 | 20 | const ( 21 | // GlobalsSize is the maximum number of global variables for a VM. 22 | GlobalsSize = 1024 23 | 24 | // StackSize is the maximum stack size for a VM. 25 | StackSize = 2048 26 | 27 | // MaxFrames is the maximum number of function frames for a VM. 28 | MaxFrames = 1024 29 | 30 | // SourceFileExtDefault is the default extension for source files. 31 | SourceFileExtDefault = ".tengo" 32 | ) 33 | 34 | // CallableFunc is a function signature for the callable functions. 35 | type CallableFunc = func(args ...Object) (ret Object, err error) 36 | 37 | // CountObjects returns the number of objects that a given object o contains. 38 | // For scalar value types, it will always be 1. For compound value types, 39 | // this will include its elements and all of their elements recursively. 40 | func CountObjects(o Object) (c int) { 41 | c = 1 42 | switch o := o.(type) { 43 | case *Array: 44 | for _, v := range o.Value { 45 | c += CountObjects(v) 46 | } 47 | case *ImmutableArray: 48 | for _, v := range o.Value { 49 | c += CountObjects(v) 50 | } 51 | case *Map: 52 | for _, v := range o.Value { 53 | c += CountObjects(v) 54 | } 55 | case *ImmutableMap: 56 | for _, v := range o.Value { 57 | c += CountObjects(v) 58 | } 59 | case *Error: 60 | c += CountObjects(o.Value) 61 | } 62 | return 63 | } 64 | 65 | // ToString will try to convert object o to string value. 66 | func ToString(o Object) (v string, ok bool) { 67 | if o == UndefinedValue { 68 | return 69 | } 70 | ok = true 71 | if str, isStr := o.(*String); isStr { 72 | v = str.Value 73 | } else { 74 | v = o.String() 75 | } 76 | return 77 | } 78 | 79 | // ToInt will try to convert object o to int value. 80 | func ToInt(o Object) (v int, ok bool) { 81 | switch o := o.(type) { 82 | case *Int: 83 | v = int(o.Value) 84 | ok = true 85 | case *Float: 86 | v = int(o.Value) 87 | ok = true 88 | case *Char: 89 | v = int(o.Value) 90 | ok = true 91 | case *Bool: 92 | if o == TrueValue { 93 | v = 1 94 | } 95 | ok = true 96 | case *String: 97 | c, err := strconv.ParseInt(o.Value, 10, 64) 98 | if err == nil { 99 | v = int(c) 100 | ok = true 101 | } 102 | } 103 | return 104 | } 105 | 106 | // ToInt64 will try to convert object o to int64 value. 107 | func ToInt64(o Object) (v int64, ok bool) { 108 | switch o := o.(type) { 109 | case *Int: 110 | v = o.Value 111 | ok = true 112 | case *Float: 113 | v = int64(o.Value) 114 | ok = true 115 | case *Char: 116 | v = int64(o.Value) 117 | ok = true 118 | case *Bool: 119 | if o == TrueValue { 120 | v = 1 121 | } 122 | ok = true 123 | case *String: 124 | c, err := strconv.ParseInt(o.Value, 10, 64) 125 | if err == nil { 126 | v = c 127 | ok = true 128 | } 129 | } 130 | return 131 | } 132 | 133 | // ToFloat64 will try to convert object o to float64 value. 134 | func ToFloat64(o Object) (v float64, ok bool) { 135 | switch o := o.(type) { 136 | case *Int: 137 | v = float64(o.Value) 138 | ok = true 139 | case *Float: 140 | v = o.Value 141 | ok = true 142 | case *String: 143 | c, err := strconv.ParseFloat(o.Value, 64) 144 | if err == nil { 145 | v = c 146 | ok = true 147 | } 148 | } 149 | return 150 | } 151 | 152 | // ToBool will try to convert object o to bool value. 153 | func ToBool(o Object) (v bool, ok bool) { 154 | ok = true 155 | v = !o.IsFalsy() 156 | return 157 | } 158 | 159 | // ToRune will try to convert object o to rune value. 160 | func ToRune(o Object) (v rune, ok bool) { 161 | switch o := o.(type) { 162 | case *Int: 163 | v = rune(o.Value) 164 | ok = true 165 | case *Char: 166 | v = o.Value 167 | ok = true 168 | } 169 | return 170 | } 171 | 172 | // ToByteSlice will try to convert object o to []byte value. 173 | func ToByteSlice(o Object) (v []byte, ok bool) { 174 | switch o := o.(type) { 175 | case *Bytes: 176 | v = o.Value 177 | ok = true 178 | case *String: 179 | v = []byte(o.Value) 180 | ok = true 181 | } 182 | return 183 | } 184 | 185 | // ToTime will try to convert object o to time.Time value. 186 | func ToTime(o Object) (v time.Time, ok bool) { 187 | switch o := o.(type) { 188 | case *Time: 189 | v = o.Value 190 | ok = true 191 | case *Int: 192 | v = time.Unix(o.Value, 0) 193 | ok = true 194 | } 195 | return 196 | } 197 | 198 | // ToInterface attempts to convert an object o to an interface{} value 199 | func ToInterface(o Object) (res interface{}) { 200 | switch o := o.(type) { 201 | case *Int: 202 | res = o.Value 203 | case *String: 204 | res = o.Value 205 | case *Float: 206 | res = o.Value 207 | case *Bool: 208 | res = o == TrueValue 209 | case *Char: 210 | res = o.Value 211 | case *Bytes: 212 | res = o.Value 213 | case *Array: 214 | res = make([]interface{}, len(o.Value)) 215 | for i, val := range o.Value { 216 | res.([]interface{})[i] = ToInterface(val) 217 | } 218 | case *ImmutableArray: 219 | res = make([]interface{}, len(o.Value)) 220 | for i, val := range o.Value { 221 | res.([]interface{})[i] = ToInterface(val) 222 | } 223 | case *Map: 224 | res = make(map[string]interface{}) 225 | for key, v := range o.Value { 226 | res.(map[string]interface{})[key] = ToInterface(v) 227 | } 228 | case *ImmutableMap: 229 | res = make(map[string]interface{}) 230 | for key, v := range o.Value { 231 | res.(map[string]interface{})[key] = ToInterface(v) 232 | } 233 | case *Time: 234 | res = o.Value 235 | case *Error: 236 | res = errors.New(o.String()) 237 | case *Undefined: 238 | res = nil 239 | case Object: 240 | return o 241 | } 242 | return 243 | } 244 | 245 | // FromInterface will attempt to convert an interface{} v to a Tengo Object 246 | func FromInterface(v interface{}) (Object, error) { 247 | switch v := v.(type) { 248 | case nil: 249 | return UndefinedValue, nil 250 | case string: 251 | if len(v) > MaxStringLen { 252 | return nil, ErrStringLimit 253 | } 254 | return &String{Value: v}, nil 255 | case int64: 256 | return &Int{Value: v}, nil 257 | case int: 258 | return &Int{Value: int64(v)}, nil 259 | case bool: 260 | if v { 261 | return TrueValue, nil 262 | } 263 | return FalseValue, nil 264 | case rune: 265 | return &Char{Value: v}, nil 266 | case byte: 267 | return &Char{Value: rune(v)}, nil 268 | case float64: 269 | return &Float{Value: v}, nil 270 | case []byte: 271 | if len(v) > MaxBytesLen { 272 | return nil, ErrBytesLimit 273 | } 274 | return &Bytes{Value: v}, nil 275 | case error: 276 | return &Error{Value: &String{Value: v.Error()}}, nil 277 | case map[string]Object: 278 | return &Map{Value: v}, nil 279 | case map[string]interface{}: 280 | kv := make(map[string]Object) 281 | for vk, vv := range v { 282 | vo, err := FromInterface(vv) 283 | if err != nil { 284 | return nil, err 285 | } 286 | kv[vk] = vo 287 | } 288 | return &Map{Value: kv}, nil 289 | case []Object: 290 | return &Array{Value: v}, nil 291 | case []interface{}: 292 | arr := make([]Object, len(v)) 293 | for i, e := range v { 294 | vo, err := FromInterface(e) 295 | if err != nil { 296 | return nil, err 297 | } 298 | arr[i] = vo 299 | } 300 | return &Array{Value: arr}, nil 301 | case time.Time: 302 | return &Time{Value: v}, nil 303 | case Object: 304 | return v, nil 305 | case CallableFunc: 306 | return &UserFunction{Value: v}, nil 307 | } 308 | return nil, fmt.Errorf("cannot convert to object: %T", v) 309 | } 310 | -------------------------------------------------------------------------------- /tengo_test.go: -------------------------------------------------------------------------------- 1 | package tengo_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | 8 | "github.com/d5/tengo/v2" 9 | "github.com/d5/tengo/v2/parser" 10 | "github.com/d5/tengo/v2/require" 11 | ) 12 | 13 | func TestInstructions_String(t *testing.T) { 14 | assertInstructionString(t, 15 | [][]byte{ 16 | tengo.MakeInstruction(parser.OpConstant, 1), 17 | tengo.MakeInstruction(parser.OpConstant, 2), 18 | tengo.MakeInstruction(parser.OpConstant, 65535), 19 | }, 20 | `0000 CONST 1 21 | 0003 CONST 2 22 | 0006 CONST 65535`) 23 | 24 | assertInstructionString(t, 25 | [][]byte{ 26 | tengo.MakeInstruction(parser.OpBinaryOp, 11), 27 | tengo.MakeInstruction(parser.OpConstant, 2), 28 | tengo.MakeInstruction(parser.OpConstant, 65535), 29 | }, 30 | `0000 BINARYOP 11 31 | 0002 CONST 2 32 | 0005 CONST 65535`) 33 | 34 | assertInstructionString(t, 35 | [][]byte{ 36 | tengo.MakeInstruction(parser.OpBinaryOp, 11), 37 | tengo.MakeInstruction(parser.OpGetLocal, 1), 38 | tengo.MakeInstruction(parser.OpConstant, 2), 39 | tengo.MakeInstruction(parser.OpConstant, 65535), 40 | }, 41 | `0000 BINARYOP 11 42 | 0002 GETL 1 43 | 0004 CONST 2 44 | 0007 CONST 65535`) 45 | } 46 | 47 | func TestMakeInstruction(t *testing.T) { 48 | makeInstruction(t, []byte{parser.OpConstant, 0, 0}, 49 | parser.OpConstant, 0) 50 | makeInstruction(t, []byte{parser.OpConstant, 0, 1}, 51 | parser.OpConstant, 1) 52 | makeInstruction(t, []byte{parser.OpConstant, 255, 254}, 53 | parser.OpConstant, 65534) 54 | makeInstruction(t, []byte{parser.OpPop}, parser.OpPop) 55 | makeInstruction(t, []byte{parser.OpTrue}, parser.OpTrue) 56 | makeInstruction(t, []byte{parser.OpFalse}, parser.OpFalse) 57 | } 58 | 59 | func TestNumObjects(t *testing.T) { 60 | testCountObjects(t, &tengo.Array{}, 1) 61 | testCountObjects(t, &tengo.Array{Value: []tengo.Object{ 62 | &tengo.Int{Value: 1}, 63 | &tengo.Int{Value: 2}, 64 | &tengo.Array{Value: []tengo.Object{ 65 | &tengo.Int{Value: 3}, 66 | &tengo.Int{Value: 4}, 67 | &tengo.Int{Value: 5}, 68 | }}, 69 | }}, 7) 70 | testCountObjects(t, tengo.TrueValue, 1) 71 | testCountObjects(t, tengo.FalseValue, 1) 72 | testCountObjects(t, &tengo.BuiltinFunction{}, 1) 73 | testCountObjects(t, &tengo.Bytes{Value: []byte("foobar")}, 1) 74 | testCountObjects(t, &tengo.Char{Value: '가'}, 1) 75 | testCountObjects(t, &tengo.CompiledFunction{}, 1) 76 | testCountObjects(t, &tengo.Error{Value: &tengo.Int{Value: 5}}, 2) 77 | testCountObjects(t, &tengo.Float{Value: 19.84}, 1) 78 | testCountObjects(t, &tengo.ImmutableArray{Value: []tengo.Object{ 79 | &tengo.Int{Value: 1}, 80 | &tengo.Int{Value: 2}, 81 | &tengo.ImmutableArray{Value: []tengo.Object{ 82 | &tengo.Int{Value: 3}, 83 | &tengo.Int{Value: 4}, 84 | &tengo.Int{Value: 5}, 85 | }}, 86 | }}, 7) 87 | testCountObjects(t, &tengo.ImmutableMap{ 88 | Value: map[string]tengo.Object{ 89 | "k1": &tengo.Int{Value: 1}, 90 | "k2": &tengo.Int{Value: 2}, 91 | "k3": &tengo.Array{Value: []tengo.Object{ 92 | &tengo.Int{Value: 3}, 93 | &tengo.Int{Value: 4}, 94 | &tengo.Int{Value: 5}, 95 | }}, 96 | }}, 7) 97 | testCountObjects(t, &tengo.Int{Value: 1984}, 1) 98 | testCountObjects(t, &tengo.Map{Value: map[string]tengo.Object{ 99 | "k1": &tengo.Int{Value: 1}, 100 | "k2": &tengo.Int{Value: 2}, 101 | "k3": &tengo.Array{Value: []tengo.Object{ 102 | &tengo.Int{Value: 3}, 103 | &tengo.Int{Value: 4}, 104 | &tengo.Int{Value: 5}, 105 | }}, 106 | }}, 7) 107 | testCountObjects(t, &tengo.String{Value: "foo bar"}, 1) 108 | testCountObjects(t, &tengo.Time{Value: time.Now()}, 1) 109 | testCountObjects(t, tengo.UndefinedValue, 1) 110 | } 111 | 112 | func testCountObjects(t *testing.T, o tengo.Object, expected int) { 113 | require.Equal(t, expected, tengo.CountObjects(o)) 114 | } 115 | 116 | func assertInstructionString( 117 | t *testing.T, 118 | instructions [][]byte, 119 | expected string, 120 | ) { 121 | concatted := make([]byte, 0) 122 | for _, e := range instructions { 123 | concatted = append(concatted, e...) 124 | } 125 | require.Equal(t, expected, strings.Join( 126 | tengo.FormatInstructions(concatted, 0), "\n")) 127 | } 128 | 129 | func makeInstruction( 130 | t *testing.T, 131 | expected []byte, 132 | opcode parser.Opcode, 133 | operands ...int, 134 | ) { 135 | inst := tengo.MakeInstruction(opcode, operands...) 136 | require.Equal(t, expected, inst) 137 | } 138 | -------------------------------------------------------------------------------- /testdata/cli/one.tengo: -------------------------------------------------------------------------------- 1 | 2 | export { 3 | fn: func(a) { 4 | two := import("two/two") 5 | return two.fn(a, "one") 6 | } 7 | } -------------------------------------------------------------------------------- /testdata/cli/test.tengo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tengo 2 | 3 | os := import("os") 4 | one := import("one") 5 | fmt := import("fmt") 6 | text := import("text") 7 | expected := ["test", "one", "two", "three", "four", "five"] 8 | expected = text.join(expected, " ") 9 | if v := one.fn("test"); v != expected { 10 | fmt.printf("relative import test error:\n\texpected: %v\n\tgot : %v\n", 11 | expected, v) 12 | os.exit(1) 13 | } 14 | args := text.join(os.args(), " ") 15 | fmt.println("ok\t", args) 16 | -------------------------------------------------------------------------------- /testdata/cli/three.tengo: -------------------------------------------------------------------------------- 1 | export { 2 | fn: func(a, b, c) { 3 | four := import("./two/four/four.tengo") 4 | return four.fn(a, b, c, "three") 5 | } 6 | } -------------------------------------------------------------------------------- /testdata/cli/two/five/five.tengo: -------------------------------------------------------------------------------- 1 | export { 2 | fn: func(...args) { 3 | text := import("text") 4 | args = append(args, "five") 5 | return text.join(args, " ") 6 | } 7 | } -------------------------------------------------------------------------------- /testdata/cli/two/four/four.tengo: -------------------------------------------------------------------------------- 1 | export { 2 | fn: func(a, b, c, d) { 3 | five := import("../five/five") 4 | return five.fn(a, b, c, d, "four") 5 | } 6 | } -------------------------------------------------------------------------------- /testdata/cli/two/two.tengo: -------------------------------------------------------------------------------- 1 | export { 2 | fn: func(a, b) { 3 | three := import("../three") 4 | return three.fn(a, b, "two") 5 | } 6 | } -------------------------------------------------------------------------------- /testdata/issue286/dos/cinco/cinco.mshk: -------------------------------------------------------------------------------- 1 | export { 2 | fn: func(...args) { 3 | text := import("text") 4 | args = append(args, "cinco") 5 | 6 | return text.join(args, " ") 7 | } 8 | } -------------------------------------------------------------------------------- /testdata/issue286/dos/dos.mshk: -------------------------------------------------------------------------------- 1 | export { 2 | fn: func(a, b) { 3 | tres := import("../tres") 4 | 5 | return tres.fn(a, b, "dos") 6 | } 7 | } -------------------------------------------------------------------------------- /testdata/issue286/dos/quatro/quatro.mshk: -------------------------------------------------------------------------------- 1 | export { 2 | fn: func(a, b, c, d) { 3 | cinco := import("../cinco/cinco") 4 | 5 | return cinco.fn(a, b, c, d, "quatro") 6 | } 7 | } -------------------------------------------------------------------------------- /testdata/issue286/test.mshk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tengo 2 | // This is a test of custom extension for issue #286 and PR #350. 3 | // Which allows the tengo library to use custom extension names for the 4 | // source files. 5 | // 6 | // This test should pass if the interpreter's tengo.Compiler.SetImportExt() 7 | // was set as `c.SetImportExt(".tengo", ".mshk")`. 8 | 9 | os := import("os") 10 | uno := import("uno") // it will search uno.tengo and uno.mshk 11 | fmt := import("fmt") 12 | text := import("text") 13 | 14 | expected := ["test", "uno", "dos", "tres", "quatro", "cinco"] 15 | expected = text.join(expected, " ") 16 | if v := uno.fn("test"); v != expected { 17 | fmt.printf("relative import test error:\n\texpected: %v\n\tgot : %v\n", 18 | expected, v) 19 | os.exit(1) 20 | } 21 | 22 | args := text.join(os.args(), " ") 23 | fmt.println("ok\t", args) 24 | -------------------------------------------------------------------------------- /testdata/issue286/tres.tengo: -------------------------------------------------------------------------------- 1 | export { 2 | fn: func(a, b, c) { 3 | quatro := import("./dos/quatro/quatro.mshk") 4 | return quatro.fn(a, b, c, "tres") 5 | } 6 | } -------------------------------------------------------------------------------- /testdata/issue286/uno.mshk: -------------------------------------------------------------------------------- 1 | export { 2 | fn: func(a) { 3 | dos := import("dos/dos") 4 | return dos.fn(a, "uno") 5 | } 6 | } -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import "strconv" 4 | 5 | var keywords map[string]Token 6 | 7 | // Token represents a token. 8 | type Token int 9 | 10 | // List of tokens 11 | const ( 12 | Illegal Token = iota 13 | EOF 14 | Comment 15 | _literalBeg 16 | Ident 17 | Int 18 | Float 19 | Char 20 | String 21 | _literalEnd 22 | _operatorBeg 23 | Add // + 24 | Sub // - 25 | Mul // * 26 | Quo // / 27 | Rem // % 28 | And // & 29 | Or // | 30 | Xor // ^ 31 | Shl // << 32 | Shr // >> 33 | AndNot // &^ 34 | AddAssign // += 35 | SubAssign // -= 36 | MulAssign // *= 37 | QuoAssign // /= 38 | RemAssign // %= 39 | AndAssign // &= 40 | OrAssign // |= 41 | XorAssign // ^= 42 | ShlAssign // <<= 43 | ShrAssign // >>= 44 | AndNotAssign // &^= 45 | LAnd // && 46 | LOr // || 47 | Inc // ++ 48 | Dec // -- 49 | Equal // == 50 | Less // < 51 | Greater // > 52 | Assign // = 53 | Not // ! 54 | NotEqual // != 55 | LessEq // <= 56 | GreaterEq // >= 57 | Define // := 58 | Ellipsis // ... 59 | LParen // ( 60 | LBrack // [ 61 | LBrace // { 62 | Comma // , 63 | Period // . 64 | RParen // ) 65 | RBrack // ] 66 | RBrace // } 67 | Semicolon // ; 68 | Colon // : 69 | Question // ? 70 | _operatorEnd 71 | _keywordBeg 72 | Break 73 | Continue 74 | Else 75 | For 76 | Func 77 | Error 78 | Immutable 79 | If 80 | Return 81 | Export 82 | True 83 | False 84 | In 85 | Undefined 86 | Import 87 | _keywordEnd 88 | ) 89 | 90 | var tokens = [...]string{ 91 | Illegal: "ILLEGAL", 92 | EOF: "EOF", 93 | Comment: "COMMENT", 94 | Ident: "IDENT", 95 | Int: "INT", 96 | Float: "FLOAT", 97 | Char: "CHAR", 98 | String: "STRING", 99 | Add: "+", 100 | Sub: "-", 101 | Mul: "*", 102 | Quo: "/", 103 | Rem: "%", 104 | And: "&", 105 | Or: "|", 106 | Xor: "^", 107 | Shl: "<<", 108 | Shr: ">>", 109 | AndNot: "&^", 110 | AddAssign: "+=", 111 | SubAssign: "-=", 112 | MulAssign: "*=", 113 | QuoAssign: "/=", 114 | RemAssign: "%=", 115 | AndAssign: "&=", 116 | OrAssign: "|=", 117 | XorAssign: "^=", 118 | ShlAssign: "<<=", 119 | ShrAssign: ">>=", 120 | AndNotAssign: "&^=", 121 | LAnd: "&&", 122 | LOr: "||", 123 | Inc: "++", 124 | Dec: "--", 125 | Equal: "==", 126 | Less: "<", 127 | Greater: ">", 128 | Assign: "=", 129 | Not: "!", 130 | NotEqual: "!=", 131 | LessEq: "<=", 132 | GreaterEq: ">=", 133 | Define: ":=", 134 | Ellipsis: "...", 135 | LParen: "(", 136 | LBrack: "[", 137 | LBrace: "{", 138 | Comma: ",", 139 | Period: ".", 140 | RParen: ")", 141 | RBrack: "]", 142 | RBrace: "}", 143 | Semicolon: ";", 144 | Colon: ":", 145 | Question: "?", 146 | Break: "break", 147 | Continue: "continue", 148 | Else: "else", 149 | For: "for", 150 | Func: "func", 151 | Error: "error", 152 | Immutable: "immutable", 153 | If: "if", 154 | Return: "return", 155 | Export: "export", 156 | True: "true", 157 | False: "false", 158 | In: "in", 159 | Undefined: "undefined", 160 | Import: "import", 161 | } 162 | 163 | func (tok Token) String() string { 164 | s := "" 165 | 166 | if 0 <= tok && tok < Token(len(tokens)) { 167 | s = tokens[tok] 168 | } 169 | 170 | if s == "" { 171 | s = "token(" + strconv.Itoa(int(tok)) + ")" 172 | } 173 | 174 | return s 175 | } 176 | 177 | // LowestPrec represents lowest operator precedence. 178 | const LowestPrec = 0 179 | 180 | // Precedence returns the precedence for the operator token. 181 | func (tok Token) Precedence() int { 182 | switch tok { 183 | case LOr: 184 | return 1 185 | case LAnd: 186 | return 2 187 | case Equal, NotEqual, Less, LessEq, Greater, GreaterEq: 188 | return 3 189 | case Add, Sub, Or, Xor: 190 | return 4 191 | case Mul, Quo, Rem, Shl, Shr, And, AndNot: 192 | return 5 193 | } 194 | return LowestPrec 195 | } 196 | 197 | // IsLiteral returns true if the token is a literal. 198 | func (tok Token) IsLiteral() bool { 199 | return _literalBeg < tok && tok < _literalEnd 200 | } 201 | 202 | // IsOperator returns true if the token is an operator. 203 | func (tok Token) IsOperator() bool { 204 | return _operatorBeg < tok && tok < _operatorEnd 205 | } 206 | 207 | // IsKeyword returns true if the token is a keyword. 208 | func (tok Token) IsKeyword() bool { 209 | return _keywordBeg < tok && tok < _keywordEnd 210 | } 211 | 212 | // Lookup returns corresponding keyword if ident is a keyword. 213 | func Lookup(ident string) Token { 214 | if tok, isKeyword := keywords[ident]; isKeyword { 215 | return tok 216 | } 217 | return Ident 218 | } 219 | 220 | func init() { 221 | keywords = make(map[string]Token) 222 | for i := _keywordBeg + 1; i < _keywordEnd; i++ { 223 | keywords[tokens[i]] = i 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /variable.go: -------------------------------------------------------------------------------- 1 | package tengo 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Variable is a user-defined variable for the script. 8 | type Variable struct { 9 | name string 10 | value Object 11 | } 12 | 13 | // NewVariable creates a Variable. 14 | func NewVariable(name string, value interface{}) (*Variable, error) { 15 | obj, err := FromInterface(value) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return &Variable{ 20 | name: name, 21 | value: obj, 22 | }, nil 23 | } 24 | 25 | // Name returns the name of the variable. 26 | func (v *Variable) Name() string { 27 | return v.name 28 | } 29 | 30 | // Value returns an empty interface of the variable value. 31 | func (v *Variable) Value() interface{} { 32 | return ToInterface(v.value) 33 | } 34 | 35 | // ValueType returns the name of the value type. 36 | func (v *Variable) ValueType() string { 37 | return v.value.TypeName() 38 | } 39 | 40 | // Int returns int value of the variable value. 41 | // It returns 0 if the value is not convertible to int. 42 | func (v *Variable) Int() int { 43 | c, _ := ToInt(v.value) 44 | return c 45 | } 46 | 47 | // Int64 returns int64 value of the variable value. It returns 0 if the value 48 | // is not convertible to int64. 49 | func (v *Variable) Int64() int64 { 50 | c, _ := ToInt64(v.value) 51 | return c 52 | } 53 | 54 | // Float returns float64 value of the variable value. It returns 0.0 if the 55 | // value is not convertible to float64. 56 | func (v *Variable) Float() float64 { 57 | c, _ := ToFloat64(v.value) 58 | return c 59 | } 60 | 61 | // Char returns rune value of the variable value. It returns 0 if the value is 62 | // not convertible to rune. 63 | func (v *Variable) Char() rune { 64 | c, _ := ToRune(v.value) 65 | return c 66 | } 67 | 68 | // Bool returns bool value of the variable value. It returns 0 if the value is 69 | // not convertible to bool. 70 | func (v *Variable) Bool() bool { 71 | c, _ := ToBool(v.value) 72 | return c 73 | } 74 | 75 | // Array returns []interface value of the variable value. It returns 0 if the 76 | // value is not convertible to []interface. 77 | func (v *Variable) Array() []interface{} { 78 | switch val := v.value.(type) { 79 | case *Array: 80 | var arr []interface{} 81 | for _, e := range val.Value { 82 | arr = append(arr, ToInterface(e)) 83 | } 84 | return arr 85 | } 86 | return nil 87 | } 88 | 89 | // Map returns map[string]interface{} value of the variable value. It returns 90 | // 0 if the value is not convertible to map[string]interface{}. 91 | func (v *Variable) Map() map[string]interface{} { 92 | switch val := v.value.(type) { 93 | case *Map: 94 | kv := make(map[string]interface{}) 95 | for mk, mv := range val.Value { 96 | kv[mk] = ToInterface(mv) 97 | } 98 | return kv 99 | } 100 | return nil 101 | } 102 | 103 | // String returns string value of the variable value. It returns 0 if the value 104 | // is not convertible to string. 105 | func (v *Variable) String() string { 106 | c, _ := ToString(v.value) 107 | return c 108 | } 109 | 110 | // Bytes returns a byte slice of the variable value. It returns nil if the 111 | // value is not convertible to byte slice. 112 | func (v *Variable) Bytes() []byte { 113 | c, _ := ToByteSlice(v.value) 114 | return c 115 | } 116 | 117 | // Error returns an error if the underlying value is error object. If not, 118 | // this returns nil. 119 | func (v *Variable) Error() error { 120 | err, ok := v.value.(*Error) 121 | if ok { 122 | return errors.New(err.String()) 123 | } 124 | return nil 125 | } 126 | 127 | // Object returns an underlying Object of the variable value. Note that 128 | // returned Object is a copy of an actual Object used in the script. 129 | func (v *Variable) Object() Object { 130 | return v.value 131 | } 132 | 133 | // IsUndefined returns true if the underlying value is undefined. 134 | func (v *Variable) IsUndefined() bool { 135 | return v.value == UndefinedValue 136 | } 137 | -------------------------------------------------------------------------------- /variable_test.go: -------------------------------------------------------------------------------- 1 | package tengo_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/d5/tengo/v2" 7 | "github.com/d5/tengo/v2/require" 8 | ) 9 | 10 | type VariableTest struct { 11 | Name string 12 | Value interface{} 13 | ValueType string 14 | IntValue int 15 | Int64Value int64 16 | FloatValue float64 17 | CharValue rune 18 | BoolValue bool 19 | StringValue string 20 | Object tengo.Object 21 | IsUndefined bool 22 | } 23 | 24 | func TestVariable(t *testing.T) { 25 | vars := []VariableTest{ 26 | { 27 | Name: "a", 28 | Value: int64(1), 29 | ValueType: "int", 30 | IntValue: 1, 31 | Int64Value: 1, 32 | FloatValue: 1.0, 33 | CharValue: rune(1), 34 | BoolValue: true, 35 | StringValue: "1", 36 | Object: &tengo.Int{Value: 1}, 37 | }, 38 | { 39 | Name: "b", 40 | Value: "52.11", 41 | ValueType: "string", 42 | FloatValue: 52.11, 43 | StringValue: "52.11", 44 | BoolValue: true, 45 | Object: &tengo.String{Value: "52.11"}, 46 | }, 47 | { 48 | Name: "c", 49 | Value: true, 50 | ValueType: "bool", 51 | IntValue: 1, 52 | Int64Value: 1, 53 | FloatValue: 0, 54 | BoolValue: true, 55 | StringValue: "true", 56 | Object: tengo.TrueValue, 57 | }, 58 | { 59 | Name: "d", 60 | Value: nil, 61 | ValueType: "undefined", 62 | Object: tengo.UndefinedValue, 63 | IsUndefined: true, 64 | }, 65 | } 66 | 67 | for _, tc := range vars { 68 | v, err := tengo.NewVariable(tc.Name, tc.Value) 69 | require.NoError(t, err) 70 | require.Equal(t, tc.Value, v.Value(), "Name: %s", tc.Name) 71 | require.Equal(t, tc.ValueType, v.ValueType(), "Name: %s", tc.Name) 72 | require.Equal(t, tc.IntValue, v.Int(), "Name: %s", tc.Name) 73 | require.Equal(t, tc.Int64Value, v.Int64(), "Name: %s", tc.Name) 74 | require.Equal(t, tc.FloatValue, v.Float(), "Name: %s", tc.Name) 75 | require.Equal(t, tc.CharValue, v.Char(), "Name: %s", tc.Name) 76 | require.Equal(t, tc.BoolValue, v.Bool(), "Name: %s", tc.Name) 77 | require.Equal(t, tc.StringValue, v.String(), "Name: %s", tc.Name) 78 | require.Equal(t, tc.Object, v.Object(), "Name: %s", tc.Name) 79 | require.Equal(t, tc.IsUndefined, v.IsUndefined(), "Name: %s", tc.Name) 80 | } 81 | } 82 | --------------------------------------------------------------------------------