Compare commits

...

23 commits

Author SHA1 Message Date
Max Kuznetsov
c461a7fe60
Fix: stop execution when trying to slice an unsupported type (#443) 2024-02-25 07:47:25 +00:00
lack
47062da36a
Add os.arch and os.platform (#437)
* add  and

* .ignore
2024-02-24 13:24:35 +00:00
shikbupt
e9b03930ce
fix the import expr string output (#440) 2024-02-24 13:22:39 +00:00
Max Kuznetsov
9d35005ffe
Throw a runtime error when trying to slice an unsupported type (#442) 2024-02-24 13:21:20 +00:00
wlxwlxwlx
18b953c7be
increase size of jump operands (#438)
* [OpJumpFalse, OpAndJump, OpOrJump, OpJump], these four instructions have been changed to use 4 bytes to avoid precision loss and panic when the number of instructions exceeds the maximum of 16 bits (65535)

* update test cases

* update test cases

---------

Co-authored-by: 王录祥 <wanglx@smartsteps.com>
2023-12-20 13:13:23 -05:00
E Sequeira
da09c300a5
Revert "[OpJumpFalse, OpAndJump, OpOrJump, OpJump], these four instructions have been changed to use 4 bytes to avoid precision loss and panic when the number of instructions exceeds the maximum of 16 bits (65535) (#433)" (#436)
This reverts commit 18424deb5a.
2023-12-05 20:52:53 +00:00
wlxwlxwlx
18424deb5a
[OpJumpFalse, OpAndJump, OpOrJump, OpJump], these four instructions have been changed to use 4 bytes to avoid precision loss and panic when the number of instructions exceeds the maximum of 16 bits (65535) (#433)
Co-authored-by: 王录祥 <wanglx@smartsteps.com>
2023-12-05 19:30:50 +00:00
Aman Gupta Karmani
92cbb9bff0
Use any instead of interface{} (#419) 2023-08-22 09:56:27 +05:30
liuxiong
0177bdb4e1
fix json.indent docs (#429) 2023-08-22 09:45:00 +05:30
E Sequeira
48308d45d9
fix json decode (#424)
* fix json decode
2023-06-26 08:40:49 +01:00
E Sequeira
55f2519b16
update go version to fix goreleaser (#422) 2023-04-23 15:17:21 +01:00
Adrian Lee
f90bc00af2
bugfix: parse expect error when mis match braces (#421)
* bugfix: parse expect error when mis match braces

Change-Id: Iba1ad456d5b020375bb0416f44fb54bc2e346d62

* fix wrong test case in vm_test

Change-Id: Idcdaf7ecc93eb61d374249d11282b8286557d30d

---------

Co-authored-by: lihongyuan <lihongyuan.adrian@bytedance.com>
2023-04-23 07:37:59 +01:00
Mikael Ganehag Brorsson
82b543fd98
Better support for TZ locations in the times module (#397)
* Handle panics by deferring recover as an error

* Check for type in recover handler

* Added support for times.in_location

* Added support for location (TZ) to times.date

* Updated documentation
2023-01-31 09:51:04 +00:00
Wannes
ecc3d9181e
Added more math consts (#404) 2023-01-30 08:12:28 +00:00
gufeijun
3f4245d962
fix: solve precision problem when decode bigint in json (#399) 2023-01-30 08:10:04 +00:00
skelly
2edd39e0c3
Update objects.md (#401) 2023-01-30 06:43:17 +00:00
E Sequeira
7108e9c50d
fix golint (#407) 2023-01-28 15:30:26 +00:00
Sean Chittenden
f980e7e724
Downgrade exclusive lock to shared lock when cloning a script (#400) 2023-01-04 05:32:27 +00:00
I Putu Susila
a419bfc93a
fix slice low/high index shadowing variables in vm (#394) 2022-09-26 08:19:00 +01:00
I Putu Susila
dfcfd6661c
Use Object.Copy when cloning globals in Compiled (#393) 2022-09-25 15:30:50 +01:00
shiyuge
8a3f5bdb11
fix: copy SourceMap in vm OpClosure branch (#392) 2022-09-24 16:48:53 +01:00
shiyuge
e338512259
fix: do not invert token.Less to token.Greater in compiler (#391)
* fix: do not invert token.Less to token.Great in compiler

* unittest: do not invert token.Less to token.Great in compiler
2022-08-29 17:02:14 +01:00
E Sequeira
6fc8053992
fix runtime panics on self assignment (#387) 2022-07-20 16:09:43 +01:00
27 changed files with 405 additions and 118 deletions

View file

@ -14,11 +14,11 @@ jobs:
- name: set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: 1.18
- name: run goreleaser
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -12,7 +12,7 @@ jobs:
- name: set up Go
uses: actions/setup-go@v1
with:
go-version: 1.14
go-version: 1.18
id: go
- name: set up Go module cache
uses: actions/cache@v1
@ -29,6 +29,6 @@ jobs:
- name: check out code
uses: actions/checkout@v2
- name: install golint
run: go get -u golang.org/x/lint/golint
run: go install golang.org/x/lint/golint@latest
- name: run tests
run: make test

4
.gitignore vendored
View file

@ -1 +1,3 @@
dist/
dist/
.idea

View file

@ -141,25 +141,7 @@ func (c *Compiler) Compile(node parser.Node) error {
if node.Token == token.LAnd || node.Token == token.LOr {
return c.compileLogical(node)
}
if node.Token == token.Less {
if err := c.Compile(node.RHS); err != nil {
return err
}
if err := c.Compile(node.LHS); err != nil {
return err
}
c.emit(node, parser.OpBinaryOp, int(token.Greater))
return nil
} else if node.Token == token.LessEq {
if err := c.Compile(node.RHS); err != nil {
return err
}
if err := c.Compile(node.LHS); err != nil {
return err
}
c.emit(node, parser.OpBinaryOp, int(token.GreaterEq))
return nil
}
if err := c.Compile(node.LHS); err != nil {
return err
}
@ -182,6 +164,10 @@ func (c *Compiler) Compile(node parser.Node) error {
c.emit(node, parser.OpBinaryOp, int(token.Greater))
case token.GreaterEq:
c.emit(node, parser.OpBinaryOp, int(token.GreaterEq))
case token.Less:
c.emit(node, parser.OpBinaryOp, int(token.Less))
case token.LessEq:
c.emit(node, parser.OpBinaryOp, int(token.LessEq))
case token.Equal:
c.emit(node, parser.OpEqual)
case token.NotEqual:
@ -692,12 +678,15 @@ func (c *Compiler) compileAssign(
return c.errorf(node, "operator ':=' not allowed with selector")
}
_, isFunc := rhs[0].(*parser.FuncLit)
symbol, depth, exists := c.symbolTable.Resolve(ident, false)
if op == token.Define {
if depth == 0 && exists {
return c.errorf(node, "'%s' redeclared in this block", ident)
}
symbol = c.symbolTable.Define(ident)
if isFunc {
symbol = c.symbolTable.Define(ident)
}
} else {
if !exists {
return c.errorf(node, "unresolved reference '%s'", ident)
@ -718,6 +707,10 @@ func (c *Compiler) compileAssign(
}
}
if op == token.Define && !isFunc {
symbol = c.symbolTable.Define(ident)
}
switch op {
case token.AddAssign:
c.emit(node, parser.OpBinaryOp, int(token.Add))

View file

@ -107,12 +107,12 @@ func TestCompiler_Compile(t *testing.T) {
concatInsts(
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpBinaryOp, 39),
tengo.MakeInstruction(parser.OpBinaryOp, 38),
tengo.MakeInstruction(parser.OpPop),
tengo.MakeInstruction(parser.OpSuspend)),
objectsArray(
intObject(2),
intObject(1))))
intObject(1),
intObject(2))))
expectCompile(t, `1 >= 2`,
bytecode(
@ -131,12 +131,12 @@ func TestCompiler_Compile(t *testing.T) {
concatInsts(
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpBinaryOp, 44),
tengo.MakeInstruction(parser.OpBinaryOp, 43),
tengo.MakeInstruction(parser.OpPop),
tengo.MakeInstruction(parser.OpSuspend)),
objectsArray(
intObject(2),
intObject(1))))
intObject(1),
intObject(2))))
expectCompile(t, `1 == 2`,
bytecode(
@ -204,11 +204,11 @@ func TestCompiler_Compile(t *testing.T) {
expectCompile(t, `if true { 10 }; 3333`,
bytecode(
concatInsts(
tengo.MakeInstruction(parser.OpTrue), // 0000
tengo.MakeInstruction(parser.OpJumpFalsy, 8), // 0001
tengo.MakeInstruction(parser.OpConstant, 0), // 0004
tengo.MakeInstruction(parser.OpPop), // 0007
tengo.MakeInstruction(parser.OpConstant, 1), // 0008
tengo.MakeInstruction(parser.OpTrue), // 0000
tengo.MakeInstruction(parser.OpJumpFalsy, 10), // 0001
tengo.MakeInstruction(parser.OpConstant, 0), // 0004
tengo.MakeInstruction(parser.OpPop), // 0007
tengo.MakeInstruction(parser.OpConstant, 1), // 0008
tengo.MakeInstruction(parser.OpPop),
tengo.MakeInstruction(parser.OpSuspend)), // 0011
objectsArray(
@ -219,10 +219,10 @@ func TestCompiler_Compile(t *testing.T) {
bytecode(
concatInsts(
tengo.MakeInstruction(parser.OpTrue), // 0000
tengo.MakeInstruction(parser.OpJumpFalsy, 11), // 0001
tengo.MakeInstruction(parser.OpJumpFalsy, 15), // 0001
tengo.MakeInstruction(parser.OpConstant, 0), // 0004
tengo.MakeInstruction(parser.OpPop), // 0007
tengo.MakeInstruction(parser.OpJump, 15), // 0008
tengo.MakeInstruction(parser.OpJump, 19), // 0008
tengo.MakeInstruction(parser.OpConstant, 1), // 0011
tengo.MakeInstruction(parser.OpPop), // 0014
tengo.MakeInstruction(parser.OpConstant, 2), // 0015
@ -577,12 +577,12 @@ func TestCompiler_Compile(t *testing.T) {
intObject(1),
intObject(2),
compiledFunction(0, 0,
tengo.MakeInstruction(parser.OpTrue), // 0000
tengo.MakeInstruction(parser.OpJumpFalsy, 9), // 0001
tengo.MakeInstruction(parser.OpConstant, 0), // 0004
tengo.MakeInstruction(parser.OpReturn, 1), // 0007
tengo.MakeInstruction(parser.OpConstant, 1), // 0009
tengo.MakeInstruction(parser.OpReturn, 1))))) // 0012
tengo.MakeInstruction(parser.OpTrue), // 0000
tengo.MakeInstruction(parser.OpJumpFalsy, 11), // 0001
tengo.MakeInstruction(parser.OpConstant, 0), // 0004
tengo.MakeInstruction(parser.OpReturn, 1), // 0007
tengo.MakeInstruction(parser.OpConstant, 1), // 0009
tengo.MakeInstruction(parser.OpReturn, 1))))) // 0012
expectCompile(t, `func() { 1; if(true) { 2 } else { 3 }; 4 }`,
bytecode(
@ -599,10 +599,10 @@ func TestCompiler_Compile(t *testing.T) {
tengo.MakeInstruction(parser.OpConstant, 0), // 0000
tengo.MakeInstruction(parser.OpPop), // 0003
tengo.MakeInstruction(parser.OpTrue), // 0004
tengo.MakeInstruction(parser.OpJumpFalsy, 15), // 0005
tengo.MakeInstruction(parser.OpJumpFalsy, 19), // 0005
tengo.MakeInstruction(parser.OpConstant, 1), // 0008
tengo.MakeInstruction(parser.OpPop), // 0011
tengo.MakeInstruction(parser.OpJump, 19), // 0012
tengo.MakeInstruction(parser.OpJump, 23), // 0012
tengo.MakeInstruction(parser.OpConstant, 2), // 0015
tengo.MakeInstruction(parser.OpPop), // 0018
tengo.MakeInstruction(parser.OpConstant, 3), // 0019
@ -929,10 +929,10 @@ func() {
concatInsts(
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpSetGlobal, 0),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpGetGlobal, 0),
tengo.MakeInstruction(parser.OpBinaryOp, 39),
tengo.MakeInstruction(parser.OpJumpFalsy, 31),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpBinaryOp, 38),
tengo.MakeInstruction(parser.OpJumpFalsy, 35),
tengo.MakeInstruction(parser.OpGetGlobal, 0),
tengo.MakeInstruction(parser.OpConstant, 2),
tengo.MakeInstruction(parser.OpBinaryOp, 11),
@ -954,7 +954,7 @@ func() {
tengo.MakeInstruction(parser.OpSetGlobal, 1),
tengo.MakeInstruction(parser.OpGetGlobal, 1),
tengo.MakeInstruction(parser.OpIteratorNext),
tengo.MakeInstruction(parser.OpJumpFalsy, 37),
tengo.MakeInstruction(parser.OpJumpFalsy, 41),
tengo.MakeInstruction(parser.OpGetGlobal, 1),
tengo.MakeInstruction(parser.OpIteratorKey),
tengo.MakeInstruction(parser.OpSetGlobal, 2),
@ -973,14 +973,14 @@ func() {
tengo.MakeInstruction(parser.OpGetGlobal, 0),
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpEqual),
tengo.MakeInstruction(parser.OpAndJump, 23),
tengo.MakeInstruction(parser.OpAndJump, 25),
tengo.MakeInstruction(parser.OpGetGlobal, 0),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpNotEqual),
tengo.MakeInstruction(parser.OpOrJump, 34),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpOrJump, 38),
tengo.MakeInstruction(parser.OpGetGlobal, 0),
tengo.MakeInstruction(parser.OpBinaryOp, 39),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpBinaryOp, 38),
tengo.MakeInstruction(parser.OpPop),
tengo.MakeInstruction(parser.OpSuspend)),
objectsArray(
@ -1023,6 +1023,8 @@ func TestCompilerErrorReport(t *testing.T) {
expectCompileError(t, `a = 1`,
"Compile Error: unresolved reference 'a'\n\tat test:1:1")
expectCompileError(t, `a := a`,
"Compile Error: unresolved reference 'a'\n\tat test:1:6")
expectCompileError(t, `a, b := 1, 2`,
"Compile Error: tuple assignment not allowed\n\tat test:1:1")
expectCompileError(t, `a.b := 1`,
@ -1087,7 +1089,7 @@ func() {
intObject(4),
compiledFunction(0, 0,
tengo.MakeInstruction(parser.OpTrue),
tengo.MakeInstruction(parser.OpJumpFalsy, 9),
tengo.MakeInstruction(parser.OpJumpFalsy, 11),
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpReturn, 1),
tengo.MakeInstruction(parser.OpConstant, 1),
@ -1121,7 +1123,7 @@ func() {
tengo.MakeInstruction(parser.OpGetLocal, 0),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpEqual),
tengo.MakeInstruction(parser.OpJumpFalsy, 19),
tengo.MakeInstruction(parser.OpJumpFalsy, 21),
tengo.MakeInstruction(parser.OpConstant, 2),
tengo.MakeInstruction(parser.OpReturn, 1),
tengo.MakeInstruction(parser.OpConstant, 1),
@ -1154,7 +1156,7 @@ func() {
intObject(4),
compiledFunction(0, 0,
tengo.MakeInstruction(parser.OpTrue),
tengo.MakeInstruction(parser.OpJumpFalsy, 9),
tengo.MakeInstruction(parser.OpJumpFalsy, 11),
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpReturn, 1),
tengo.MakeInstruction(parser.OpConstant, 1),
@ -1178,7 +1180,7 @@ func() {
intObject(123),
compiledFunction(0, 0,
tengo.MakeInstruction(parser.OpTrue),
tengo.MakeInstruction(parser.OpJumpFalsy, 6),
tengo.MakeInstruction(parser.OpJumpFalsy, 8),
tengo.MakeInstruction(parser.OpReturn, 0),
tengo.MakeInstruction(parser.OpReturn, 0),
tengo.MakeInstruction(parser.OpConstant, 0),
@ -1198,12 +1200,12 @@ if a := 1; a {
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpSetGlobal, 0),
tengo.MakeInstruction(parser.OpGetGlobal, 0),
tengo.MakeInstruction(parser.OpJumpFalsy, 27),
tengo.MakeInstruction(parser.OpJumpFalsy, 31),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpSetGlobal, 0),
tengo.MakeInstruction(parser.OpGetGlobal, 0),
tengo.MakeInstruction(parser.OpSetGlobal, 1),
tengo.MakeInstruction(parser.OpJump, 39),
tengo.MakeInstruction(parser.OpJump, 43),
tengo.MakeInstruction(parser.OpConstant, 2),
tengo.MakeInstruction(parser.OpSetGlobal, 0),
tengo.MakeInstruction(parser.OpGetGlobal, 0),
@ -1236,12 +1238,12 @@ func() {
tengo.MakeInstruction(parser.OpConstant, 0),
tengo.MakeInstruction(parser.OpDefineLocal, 0),
tengo.MakeInstruction(parser.OpGetLocal, 0),
tengo.MakeInstruction(parser.OpJumpFalsy, 22),
tengo.MakeInstruction(parser.OpJumpFalsy, 26),
tengo.MakeInstruction(parser.OpConstant, 1),
tengo.MakeInstruction(parser.OpSetLocal, 0),
tengo.MakeInstruction(parser.OpGetLocal, 0),
tengo.MakeInstruction(parser.OpDefineLocal, 1),
tengo.MakeInstruction(parser.OpJump, 31),
tengo.MakeInstruction(parser.OpJump, 35),
tengo.MakeInstruction(parser.OpConstant, 2),
tengo.MakeInstruction(parser.OpSetLocal, 0),
tengo.MakeInstruction(parser.OpGetLocal, 0),

View file

@ -297,9 +297,11 @@ to add `StringArray` to the script:
```golang
// script that uses 'my_list'
s := tengo.NewScript([]byte(`
print(my_list + "three")
fmt := import("fmt")
fmt.println(my_list, ", three")
`))
s.SetImports(stdlib.GetModuleMap("fmt"))
myList := &StringArray{Value: []string{"one", "two"}}
s.Add("my_list", myList) // add StringArray value 'my_list'
s.Run() // prints "one, two, three"

View file

@ -11,7 +11,7 @@ json := import("json")
- `encode(o object) => bytes`: Returns the JSON string (bytes) of the object.
Unlike Go's JSON package, this function does not HTML-escape texts, but, one
can use `html_escape` function if needed.
- `indent(b string/bytes) => bytes`: Returns an indented form of input JSON
- `indent(b string/bytes, prefix string, indent string) => bytes`: Returns an indented form of input JSON
bytes string.
- `html_escape(b string/bytes) => bytes`: Return an HTML-safe form of input
JSON bytes string.
@ -22,7 +22,7 @@ json := import("json")
json := import("json")
encoded := json.encode({a: 1, b: [2, 3, 4]}) // JSON-encoded bytes string
indentded := json.indent(encoded) // indented form
indentded := json.indent(encoded, "", " ") // indented form
html_safe := json.html_escape(encoded) // HTML escaped form
decoded := json.decode(encoded) // {a: 1, b: [2, 3, 4]}

View file

@ -18,6 +18,28 @@ math := import("math")
- `ln10`
- `ln10E`
Mathematical constants.
- `maxFloat32`
- `smallestNonzeroFloat32`
- `maxFloat64`
- `smallestNonzeroFloat64`
Floating-point limit values. Max is the largest finite value representable by the type. SmallestNonzero is the smallest positive, non-zero value representable by the type.
- `maxInt`
- `minInt`
- `maxInt8`
- `minInt8`
- `maxInt16`
- `minInt16`
- `maxInt32`
- `minInt32`
- `maxInt64`
- `minInt64`
Integer limit values.
## Functions
- `abs(x float) => float`: returns the absolute value of x.

View file

@ -6,6 +6,8 @@ os := import("os")
## Constants
- `platform`
- `arch`
- `o_rdonly`
- `o_wronly`
- `o_rdwr`

View file

@ -62,9 +62,10 @@ times := import("times")
duration.
- `month_string(month int) => string`: returns the English name of the month
("January", "February", ...).
- `date(year int, month int, day int, hour int, min int, sec int, nsec int) => time`:
returns the Time corresponding to "yyyy-mm-dd hh:mm:ss + nsec nanoseconds".
Current location is used.
- `date(year int, month int, day int, hour int, min int, sec int, nsec int, loc string) => time`:
returns the Time corresponding to "yyyy-mm-dd hh:mm:ss + nsec nanoseconds" in
the appropriate zone for that Time in the given (optional) location.
The Local time zone will be used if executed without specifying a location.
- `now() => time`: returns the current local time.
- `parse(format string, s string) => time`: parses a formatted string and
returns the time value it represents. The layout defines the format by
@ -116,5 +117,8 @@ times := import("times")
string "2006-01-02 15:04:05.999999999 -0700 MST".
- `is_zero(t time) => bool`: reports whether t represents the zero time
instant, January 1, year 1, 00:00:00 UTC.
- `in_location(t time, l string) => time`: returns a copy of t representing
the same time instant, but with the copy's location information set to l for
display purposes.
- `to_local(t time) => time`: returns t with the location set to local time.
- `to_utc(t time) => time`: returns t with the location set to UTC.

View file

@ -32,9 +32,9 @@ Here's a list of all available value types in Tengo.
| bytes | byte array | `[]byte` |
| error | [error](#error-values) value | - |
| time | time value | `time.Time` |
| array | value array _(mutable)_ | `[]interface{}` |
| array | value array _(mutable)_ | `[]any` |
| immutable array | [immutable](#immutable-values) array | - |
| map | value map with string keys _(mutable)_ | `map[string]interface{}` |
| map | value map with string keys _(mutable)_ | `map[string]any` |
| immutable map | [immutable](#immutable-values) map | - |
| undefined | [undefined](#undefined-values) value | - |
| function | [function](#function-values) value | - |

View file

@ -28,6 +28,12 @@ func MakeInstruction(opcode parser.Opcode, operands ...int) []byte {
n := uint16(o)
instruction[offset] = byte(n >> 8)
instruction[offset+1] = byte(n)
case 4:
n := uint32(o)
instruction[offset] = byte(n >> 24)
instruction[offset+1] = byte(n >> 16)
instruction[offset+2] = byte(n >> 8)
instruction[offset+3] = byte(n)
}
offset += width
}

View file

@ -351,7 +351,7 @@ func (e *ImportExpr) End() Pos {
}
func (e *ImportExpr) String() string {
return `import("` + e.ModuleName + `")"`
return `import("` + e.ModuleName + `")`
}
// IndexExpr represents an index expression.

View file

@ -106,10 +106,10 @@ var OpcodeOperands = [...][]int{
OpNotEqual: {},
OpMinus: {},
OpLNot: {},
OpJumpFalsy: {2},
OpAndJump: {2},
OpOrJump: {2},
OpJump: {2},
OpJumpFalsy: {4},
OpAndJump: {4},
OpOrJump: {4},
OpJump: {4},
OpNull: {},
OpGetGlobal: {2},
OpSetGlobal: {2},
@ -149,6 +149,8 @@ func ReadOperands(numOperands []int, ins []byte) (operands []int, offset int) {
operands = append(operands, int(ins[offset]))
case 2:
operands = append(operands, int(ins[offset+1])|int(ins[offset])<<8)
case 4:
operands = append(operands, int(ins[offset+3])|int(ins[offset+2])<<8|int(ins[offset+1])<<16|int(ins[offset])<<24)
}
offset += width
}

View file

@ -143,6 +143,7 @@ func (p *Parser) ParseFile() (file *File, err error) {
}
stmts := p.parseStmtList()
p.expect(token.EOF)
if p.errors.Len() > 0 {
return nil, p.errors.Err()
}

View file

@ -1631,6 +1631,20 @@ func TestParseFloat(t *testing.T) {
}
}
func TestMismatchBrace(t *testing.T) {
expectParseError(t, `
fmt := import("fmt")
out := 0
if 3 == 1 {
out = 1
}
} else {
out = 2
}
fmt.println(out)
`)
}
func TestParseNumberExpressions(t *testing.T) {
expectParse(t, `0x15e+2`, func(p pfn) []Stmt {
return stmts(

View file

@ -247,8 +247,8 @@ func (c *Compiled) RunContext(ctx context.Context) (err error) {
// Clone creates a new copy of Compiled. Cloned copies are safe for concurrent
// use by multiple goroutines.
func (c *Compiled) Clone() *Compiled {
c.lock.Lock()
defer c.lock.Unlock()
c.lock.RLock()
defer c.lock.RUnlock()
clone := &Compiled{
globalIndexes: c.globalIndexes,
@ -259,7 +259,7 @@ func (c *Compiled) Clone() *Compiled {
// copy global objects
for idx, g := range c.globals {
if g != nil {
clone.globals[idx] = g
clone.globals[idx] = g.Copy()
}
}
return clone

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"math/rand"
"strconv"
"strings"
"sync"
"testing"
@ -479,6 +480,99 @@ func TestCompiled_RunContext(t *testing.T) {
require.Equal(t, context.DeadlineExceeded, err)
}
func TestCompiled_CustomObject(t *testing.T) {
c := compile(t, `r := (t<130)`, M{"t": &customNumber{value: 123}})
compiledRun(t, c)
compiledGet(t, c, "r", true)
c = compile(t, `r := (t>13)`, M{"t": &customNumber{value: 123}})
compiledRun(t, c)
compiledGet(t, c, "r", true)
}
// customNumber is a user defined object that can compare to tengo.Int
// very shitty implementation, just to test that token.Less and token.Greater in BinaryOp works
type customNumber struct {
tengo.ObjectImpl
value int64
}
func (n *customNumber) TypeName() string {
return "Number"
}
func (n *customNumber) String() string {
return strconv.FormatInt(n.value, 10)
}
func (n *customNumber) BinaryOp(op token.Token, rhs tengo.Object) (tengo.Object, error) {
tengoInt, ok := rhs.(*tengo.Int)
if !ok {
return nil, tengo.ErrInvalidOperator
}
return n.binaryOpInt(op, tengoInt)
}
func (n *customNumber) binaryOpInt(op token.Token, rhs *tengo.Int) (tengo.Object, error) {
i := n.value
switch op {
case token.Less:
if i < rhs.Value {
return tengo.TrueValue, nil
}
return tengo.FalseValue, nil
case token.Greater:
if i > rhs.Value {
return tengo.TrueValue, nil
}
return tengo.FalseValue, nil
case token.LessEq:
if i <= rhs.Value {
return tengo.TrueValue, nil
}
return tengo.FalseValue, nil
case token.GreaterEq:
if i >= rhs.Value {
return tengo.TrueValue, nil
}
return tengo.FalseValue, nil
}
return nil, tengo.ErrInvalidOperator
}
func TestScript_ImportError(t *testing.T) {
m := `
exp := import("expression")
r := exp(ctx)
`
src := `
export func(ctx) {
closure := func() {
if ctx.actiontimes < 0 { // an error is thrown here because actiontimes is undefined
return true
}
return false
}
return closure()
}`
s := tengo.NewScript([]byte(m))
mods := tengo.NewModuleMap()
mods.AddSourceModule("expression", []byte(src))
s.SetImports(mods)
err := s.Add("ctx", map[string]interface{}{
"ctx": 12,
})
require.NoError(t, err)
_, err = s.Run()
require.True(t, strings.Contains(err.Error(), "expression:4:6"))
}
func compile(t *testing.T, input string, vars M) *tengo.Compiled {
s := tengo.NewScript([]byte(input))
for vn, vv := range vars {
@ -546,3 +640,28 @@ func compiledIsDefined(
) {
require.Equal(t, expected, c.IsDefined(name))
}
func TestCompiled_Clone(t *testing.T) {
script := tengo.NewScript([]byte(`
count += 1
data["b"] = 2
`))
err := script.Add("data", map[string]interface{}{"a": 1})
require.NoError(t, err)
err = script.Add("count", 1000)
require.NoError(t, err)
compiled, err := script.Compile()
require.NoError(t, err)
clone := compiled.Clone()
err = clone.RunContext(context.Background())
require.NoError(t, err)
require.Equal(t, 1000, compiled.Get("count").Int())
require.Equal(t, 1, len(compiled.Get("data").Map()))
require.Equal(t, 1001, clone.Get("count").Int())
require.Equal(t, 2, len(clone.Get("data").Map()))
}

View file

@ -62,9 +62,12 @@ func (d *decodeState) scanNext() {
// scanWhile processes bytes in d.data[d.off:] until it
// receives a scan code not equal to op.
func (d *decodeState) scanWhile(op int) {
func (d *decodeState) scanWhile(op int) (isFloat bool) {
s, data, i := &d.scan, d.data, d.off
for i < len(data) {
if data[i] == '.' || data[i] == 'e' || data[i] == 'E' {
isFloat = true
}
newOp := s.step(s, data[i])
i++
if newOp != op {
@ -76,6 +79,7 @@ func (d *decodeState) scanWhile(op int) {
d.off = len(data) + 1 // mark processed EOF with len+1
d.opcode = d.scan.eof()
return
}
func (d *decodeState) value() (tengo.Object, error) {
@ -185,7 +189,7 @@ func (d *decodeState) object() (tengo.Object, error) {
func (d *decodeState) literal() (tengo.Object, error) {
// All bytes inside literal return scanContinue op code.
start := d.readIndex()
d.scanWhile(scanContinue)
isFloat := d.scanWhile(scanContinue)
item := d.data[start:d.readIndex()]
@ -210,8 +214,12 @@ func (d *decodeState) literal() (tengo.Object, error) {
if c != '-' && (c < '0' || c > '9') {
panic(phasePanicMsg)
}
n, _ := strconv.ParseFloat(string(item), 10)
return &tengo.Float{Value: n}, nil
if isFloat {
n, _ := strconv.ParseFloat(string(item), 10)
return &tengo.Float{Value: n}, nil
}
n, _ := strconv.ParseInt(string(item), 10, 64)
return &tengo.Int{Value: n}, nil
}
}

View file

@ -57,6 +57,9 @@ func TestJSON(t *testing.T) {
"arr": ARR{1, 2, 3, "four"}})
testJSONEncodeDecode(t, MAP{"a": 0, "b": "bee",
"arr": ARR{1, 2, 3, MAP{"a": false, "b": 109.4}}})
testJSONEncodeDecode(t, MAP{"id1": 7075984636689534001, "id2": 7075984636689534002})
testJSONEncodeDecode(t, ARR{1e3, 1E7})
}
func TestDecode(t *testing.T) {

View file

@ -48,13 +48,13 @@ func TestJSON(t *testing.T) {
expect([]byte("[[\"bar\",1],[\"bar\",1]]"))
module(t, "json").call("decode", `5`).
expect(5.0)
expect(5)
module(t, "json").call("decode", `"foo"`).
expect("foo")
module(t, "json").call("decode", `[1,2,3,"bar"]`).
expect(ARR{1.0, 2.0, 3.0, "bar"})
expect(ARR{1, 2, 3, "bar"})
module(t, "json").call("decode", `{"foo":5}`).
expect(MAP{"foo": 5.0})
expect(MAP{"foo": 5})
module(t, "json").call("decode", `{"foo":2.5}`).
expect(MAP{"foo": 2.5})
module(t, "json").call("decode", `{"foo":true}`).
@ -62,7 +62,7 @@ func TestJSON(t *testing.T) {
module(t, "json").call("decode", `{"foo":"bar"}`).
expect(MAP{"foo": "bar"})
module(t, "json").call("decode", `{"foo":[1,2,3,"bar"]}`).
expect(MAP{"foo": ARR{1.0, 2.0, 3.0, "bar"}})
expect(MAP{"foo": ARR{1, 2, 3, "bar"}})
module(t, "json").
call("indent", []byte("{\"foo\":[\"bar\",1,1.8,56,true]}"), "", " ").

View file

@ -7,17 +7,31 @@ import (
)
var mathModule = map[string]tengo.Object{
"e": &tengo.Float{Value: math.E},
"pi": &tengo.Float{Value: math.Pi},
"phi": &tengo.Float{Value: math.Phi},
"sqrt2": &tengo.Float{Value: math.Sqrt2},
"sqrtE": &tengo.Float{Value: math.SqrtE},
"sqrtPi": &tengo.Float{Value: math.SqrtPi},
"sqrtPhi": &tengo.Float{Value: math.SqrtPhi},
"ln2": &tengo.Float{Value: math.Ln2},
"log2E": &tengo.Float{Value: math.Log2E},
"ln10": &tengo.Float{Value: math.Ln10},
"log10E": &tengo.Float{Value: math.Log10E},
"e": &tengo.Float{Value: math.E},
"pi": &tengo.Float{Value: math.Pi},
"phi": &tengo.Float{Value: math.Phi},
"sqrt2": &tengo.Float{Value: math.Sqrt2},
"sqrtE": &tengo.Float{Value: math.SqrtE},
"sqrtPi": &tengo.Float{Value: math.SqrtPi},
"sqrtPhi": &tengo.Float{Value: math.SqrtPhi},
"ln2": &tengo.Float{Value: math.Ln2},
"log2E": &tengo.Float{Value: math.Log2E},
"ln10": &tengo.Float{Value: math.Ln10},
"log10E": &tengo.Float{Value: math.Log10E},
"maxFloat32": &tengo.Float{Value: math.MaxFloat32},
"smallestNonzeroFloat32": &tengo.Float{Value: math.SmallestNonzeroFloat32},
"maxFloat64": &tengo.Float{Value: math.MaxFloat64},
"smallestNonzeroFloat64": &tengo.Float{Value: math.SmallestNonzeroFloat64},
"maxInt": &tengo.Int{Value: math.MaxInt},
"minInt": &tengo.Int{Value: math.MinInt},
"maxInt8": &tengo.Int{Value: math.MaxInt8},
"minInt8": &tengo.Int{Value: math.MinInt8},
"maxInt16": &tengo.Int{Value: math.MaxInt16},
"minInt16": &tengo.Int{Value: math.MinInt16},
"maxInt32": &tengo.Int{Value: math.MaxInt32},
"minInt32": &tengo.Int{Value: math.MinInt32},
"maxInt64": &tengo.Int{Value: math.MaxInt64},
"minInt64": &tengo.Int{Value: math.MinInt64},
"abs": &tengo.UserFunction{
Name: "abs",
Value: FuncAFRF(math.Abs),

View file

@ -6,11 +6,14 @@ import (
"io/ioutil"
"os"
"os/exec"
"runtime"
"github.com/d5/tengo/v2"
)
var osModule = map[string]tengo.Object{
"platform": &tengo.String{Value: runtime.GOOS},
"arch": &tengo.String{Value: runtime.GOARCH},
"o_rdonly": &tengo.Int{Value: int64(os.O_RDONLY)},
"o_wronly": &tengo.Int{Value: int64(os.O_WRONLY)},
"o_rdwr": &tengo.Int{Value: int64(os.O_RDWR)},

View file

@ -180,6 +180,10 @@ var timesModule = map[string]tengo.Object{
Name: "to_utc",
Value: timesToUTC,
}, // to_utc(time) => time
"in_location": &tengo.UserFunction{
Name: "in_location",
Value: timesInLocation,
}, // in_location(time, location) => time
}
func timesSleep(args ...tengo.Object) (ret tengo.Object, err error) {
@ -430,7 +434,7 @@ func timesDate(args ...tengo.Object) (
ret tengo.Object,
err error,
) {
if len(args) != 7 {
if len(args) < 7 || len(args) > 8 {
err = tengo.ErrWrongNumArguments
return
}
@ -499,9 +503,29 @@ func timesDate(args ...tengo.Object) (
return
}
var loc *time.Location
if len(args) == 8 {
i8, ok := tengo.ToString(args[7])
if !ok {
err = tengo.ErrInvalidArgumentType{
Name: "eighth",
Expected: "string(compatible)",
Found: args[7].TypeName(),
}
return
}
loc, err = time.LoadLocation(i8)
if err != nil {
ret = wrapError(err)
return
}
} else {
loc = time.Now().Location()
}
ret = &tengo.Time{
Value: time.Date(i1,
time.Month(i2), i3, i4, i5, i6, i7, time.Now().Location()),
time.Month(i2), i3, i4, i5, i6, i7, loc),
}
return
@ -1113,6 +1137,46 @@ func timesTimeLocation(args ...tengo.Object) (
return
}
func timesInLocation(args ...tengo.Object) (
ret tengo.Object,
err error,
) {
if len(args) != 2 {
err = tengo.ErrWrongNumArguments
return
}
t1, ok := tengo.ToTime(args[0])
if !ok {
err = tengo.ErrInvalidArgumentType{
Name: "first",
Expected: "time(compatible)",
Found: args[0].TypeName(),
}
return
}
s2, ok := tengo.ToString(args[1])
if !ok {
err = tengo.ErrInvalidArgumentType{
Name: "second",
Expected: "string(compatible)",
Found: args[1].TypeName(),
}
return
}
location, err := time.LoadLocation(s2)
if err != nil {
ret = wrapError(err)
return
}
ret = &tengo.Time{Value: t1.In(location)}
return
}
func timesTimeString(args ...tengo.Object) (ret tengo.Object, err error) {
if len(args) != 1 {
err = tengo.ErrWrongNumArguments

View file

@ -11,6 +11,8 @@ import (
func TestTimes(t *testing.T) {
time1 := time.Date(1982, 9, 28, 19, 21, 44, 999, time.Now().Location())
time2 := time.Now()
location, _ := time.LoadLocation("Pacific/Auckland")
time3 := time.Date(1982, 9, 28, 19, 21, 44, 999, location)
module(t, "times").call("sleep", 1).expect(tengo.UndefinedValue)
@ -35,6 +37,9 @@ func TestTimes(t *testing.T) {
module(t, "times").call("date", 1982, 9, 28, 19, 21, 44, 999).
expect(time1)
module(t, "times").call("date", 1982, 9, 28, 19, 21, 44, 999, "Pacific/Auckland").
expect(time3)
nowD := time.Until(module(t, "times").call("now").
o.(*tengo.Time).Value).Nanoseconds()
require.True(t, 0 > nowD && nowD > -100000000) // within 100ms
@ -80,4 +85,5 @@ func TestTimes(t *testing.T) {
module(t, "times").call("time_location", time1).
expect(time1.Location().String())
module(t, "times").call("time_string", time1).expect(time1.String())
module(t, "times").call("in_location", time1, location.String()).expect(time1.In(location))
}

38
vm.go
View file

@ -218,30 +218,30 @@ func (v *VM) run() {
return
}
case parser.OpJumpFalsy:
v.ip += 2
v.ip += 4
v.sp--
if v.stack[v.sp].IsFalsy() {
pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8
pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 | int(v.curInsts[v.ip-2])<<16 | int(v.curInsts[v.ip-3])<<24
v.ip = pos - 1
}
case parser.OpAndJump:
v.ip += 2
v.ip += 4
if v.stack[v.sp-1].IsFalsy() {
pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8
pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 | int(v.curInsts[v.ip-2])<<16 | int(v.curInsts[v.ip-3])<<24
v.ip = pos - 1
} else {
v.sp--
}
case parser.OpOrJump:
v.ip += 2
v.ip += 4
if v.stack[v.sp-1].IsFalsy() {
v.sp--
} else {
pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8
pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 | int(v.curInsts[v.ip-2])<<16 | int(v.curInsts[v.ip-3])<<24
v.ip = pos - 1
}
case parser.OpJump:
pos := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
pos := int(v.curInsts[v.ip+4]) | int(v.curInsts[v.ip+3])<<8 | int(v.curInsts[v.ip+2])<<16 | int(v.curInsts[v.ip+1])<<24
v.ip = pos - 1
case parser.OpSetGlobal:
v.ip += 2
@ -376,8 +376,8 @@ func (v *VM) run() {
var lowIdx int64
if low != UndefinedValue {
if low, ok := low.(*Int); ok {
lowIdx = low.Value
if lowInt, ok := low.(*Int); ok {
lowIdx = lowInt.Value
} else {
v.err = fmt.Errorf("invalid slice index type: %s",
low.TypeName())
@ -391,8 +391,8 @@ func (v *VM) run() {
var highIdx int64
if high == UndefinedValue {
highIdx = numElements
} else if high, ok := high.(*Int); ok {
highIdx = high.Value
} else if highInt, ok := high.(*Int); ok {
highIdx = highInt.Value
} else {
v.err = fmt.Errorf("invalid slice index type: %s",
high.TypeName())
@ -428,8 +428,8 @@ func (v *VM) run() {
var highIdx int64
if high == UndefinedValue {
highIdx = numElements
} else if high, ok := high.(*Int); ok {
highIdx = high.Value
} else if highInt, ok := high.(*Int); ok {
highIdx = highInt.Value
} else {
v.err = fmt.Errorf("invalid slice index type: %s",
high.TypeName())
@ -465,8 +465,8 @@ func (v *VM) run() {
var highIdx int64
if high == UndefinedValue {
highIdx = numElements
} else if high, ok := high.(*Int); ok {
highIdx = high.Value
} else if highInt, ok := high.(*Int); ok {
highIdx = highInt.Value
} else {
v.err = fmt.Errorf("invalid slice index type: %s",
high.TypeName())
@ -502,8 +502,8 @@ func (v *VM) run() {
var highIdx int64
if high == UndefinedValue {
highIdx = numElements
} else if high, ok := high.(*Int); ok {
highIdx = high.Value
} else if highInt, ok := high.(*Int); ok {
highIdx = highInt.Value
} else {
v.err = fmt.Errorf("invalid slice index type: %s",
high.TypeName())
@ -534,6 +534,9 @@ func (v *VM) run() {
}
v.stack[v.sp] = val
v.sp++
default:
v.err = fmt.Errorf("not indexable: %s", left.TypeName())
return
}
case parser.OpCall:
numArgs := int(v.curInsts[v.ip+1])
@ -767,6 +770,7 @@ func (v *VM) run() {
NumLocals: fn.NumLocals,
NumParameters: fn.NumParameters,
VarArgs: fn.VarArgs,
SourceMap: fn.SourceMap,
Free: free,
}
v.allocs--

View file

@ -1090,6 +1090,15 @@ export func() {
b := 5
return b + "foo"
}`), "Runtime Error: invalid operation: int + string\n\tat mod2:4:9")
expectError(t, `a := [1, 2, 3]; b := a[:"invalid"];`, nil,
"Runtime Error: invalid slice index type: string")
expectError(t, `a := immutable([4, 5, 6]); b := a[:false];`, nil,
"Runtime Error: invalid slice index type: bool")
expectError(t, `a := "hello"; b := a[:1.23];`, nil,
"Runtime Error: invalid slice index type: float")
expectError(t, `a := bytes("world"); b := a[:time(1)];`, nil,
"Runtime Error: invalid slice index type: time")
}
func TestVMErrorUnwrap(t *testing.T) {
@ -2745,10 +2754,10 @@ export func(a) {
Opts().Module("mod0", `if 1 { } else { export true }`),
tengo.UndefinedValue)
expectRun(t, `out = import("mod0")`,
Opts().Module("mod0", `for v:=0;;v++ { if v == 3 { export true } } }`),
Opts().Module("mod0", `for v:=0;;v++ { if v == 3 { export true } }`),
true)
expectRun(t, `out = import("mod0")`,
Opts().Module("mod0", `for v:=0;;v++ { if v == 3 { break } } }`),
Opts().Module("mod0", `for v:=0;;v++ { if v == 3 { break } }`),
tengo.UndefinedValue)
// duplicate compiled functions
@ -3625,6 +3634,13 @@ func TestSpread(t *testing.T) {
"Runtime Error: wrong number of arguments: want=3, got=2")
}
func TestSliceIndex(t *testing.T) {
expectError(t, `undefined[:1]`, nil, "Runtime Error: not indexable")
expectError(t, `123[-1:2]`, nil, "Runtime Error: not indexable")
expectError(t, `{}[:]`, nil, "Runtime Error: not indexable")
expectError(t, `a := 123[-1:2] ; a += 1`, nil, "Runtime Error: not indexable")
}
func expectRun(
t *testing.T,
input string,