diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 32f14b9f..82a4094f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - go-version: [1.16.x, 1.x] + go-version: [1.20.x, 1.x] os: [ubuntu-latest] arch: ["", "386"] fail-fast: false diff --git a/README.md b/README.md index 3ad93dac..a4d0c09b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ performance. This project was largely inspired by [otto](https://github.com/robertkrimen/otto). -Minimum required Go version is 1.16. +The minimum required Go version is 1.20. Features -------- diff --git a/compiler.go b/compiler.go index 71a97839..aaa93888 100644 --- a/compiler.go +++ b/compiler.go @@ -68,8 +68,7 @@ type srcMapItem struct { // This representation is not linked to a runtime in any way and can be used concurrently. // It is always preferable to use a Program over a string when running code as it skips the compilation step. type Program struct { - code []instruction - values []Value + code []instruction funcName unistring.String src *file.File @@ -94,6 +93,8 @@ type compiler struct { ctxVM *vm // VM in which an eval() code is compiled codeScratchpad []instruction + + stringCache map[unistring.String]Value } func (c *compiler) getScriptOrModule() interface{} { @@ -407,6 +408,29 @@ func (c *compiler) popScope() { c.scope = c.scope.outer } +func (c *compiler) emitLiteralString(s String) { + key := s.string() + if c.stringCache == nil { + c.stringCache = make(map[unistring.String]Value) + } + internVal := c.stringCache[key] + if internVal == nil { + c.stringCache[key] = s + internVal = s + } + + c.emit(loadVal{internVal}) +} + +func (c *compiler) emitLiteralValue(v Value) { + if s, ok := v.(String); ok { + c.emitLiteralString(s) + return + } + + c.emit(loadVal{v}) +} + func newCompiler() *compiler { c := &compiler{ p: &Program{}, @@ -417,23 +441,11 @@ func newCompiler() *compiler { return c } -func (p *Program) defineLiteralValue(val Value) uint32 { - for idx, v := range p.values { - if v.SameAs(val) { - return uint32(idx) - } - } - idx := uint32(len(p.values)) - p.values = append(p.values, val) - return idx -} - func (p *Program) dumpCode(logger func(format string, args ...interface{})) { p._dumpCode("", logger) } func (p *Program) _dumpCode(indent string, logger func(format string, args ...interface{})) { - logger("values: %+v", p.values) dumpInitFields := func(initFields *Program) { i := indent + ">" logger("%s ---- init_fields:", i) @@ -1202,6 +1214,7 @@ func (c *compiler) compile(in *ast.Program, strict, inGlobal bool, evalVm *vm) { } scope.finaliseVarAlloc(0) + c.stringCache = nil } func (c *compiler) compileAmbiguousImport(name unistring.String) { diff --git a/compiler_expr.go b/compiler_expr.go index fa849c9b..2a8e71eb 100644 --- a/compiler_expr.go +++ b/compiler_expr.go @@ -241,7 +241,7 @@ type compiledDynamicImport struct { func (e *defaultDeleteExpr) emitGetter(putOnStack bool) { e.expr.emitGetter(false) if putOnStack { - e.c.emit(loadVal(e.c.p.defineLiteralValue(valueTrue))) + e.c.emitLiteralValue(valueTrue) } } @@ -380,7 +380,7 @@ func (e *baseCompiledExpr) addSrcMap() { func (e *constantExpr) emitGetter(putOnStack bool) { if putOnStack { e.addSrcMap() - e.c.emit(loadVal(e.c.p.defineLiteralValue(e.val))) + e.c.emitLiteralValue(e.val) } } @@ -1268,7 +1268,7 @@ func (e *compiledAssignExpr) emitGetter(putOnStack bool) { func (e *compiledLiteral) emitGetter(putOnStack bool) { if putOnStack { - e.c.emit(loadVal(e.c.p.defineLiteralValue(e.val))) + e.c.emitLiteralValue(e.val) } } @@ -1279,15 +1279,15 @@ func (e *compiledLiteral) constant() bool { func (e *compiledTemplateLiteral) emitGetter(putOnStack bool) { if e.tag == nil { if len(e.elements) == 0 { - e.c.emit(loadVal(e.c.p.defineLiteralValue(stringEmpty))) + e.c.emitLiteralString(stringEmpty) } else { tail := e.elements[len(e.elements)-1].Parsed if len(e.elements) == 1 { - e.c.emit(loadVal(e.c.p.defineLiteralValue(stringValueFromRaw(tail)))) + e.c.emitLiteralString(stringValueFromRaw(tail)) } else { stringCount := 0 if head := e.elements[0].Parsed; head != "" { - e.c.emit(loadVal(e.c.p.defineLiteralValue(stringValueFromRaw(head)))) + e.c.emitLiteralString(stringValueFromRaw(head)) stringCount++ } e.expressions[0].emitGetter(true) @@ -1295,7 +1295,7 @@ func (e *compiledTemplateLiteral) emitGetter(putOnStack bool) { stringCount++ for i := 1; i < len(e.elements)-1; i++ { if elt := e.elements[i].Parsed; elt != "" { - e.c.emit(loadVal(e.c.p.defineLiteralValue(stringValueFromRaw(elt)))) + e.c.emitLiteralString(stringValueFromRaw(elt)) stringCount++ } e.expressions[i].emitGetter(true) @@ -1303,7 +1303,7 @@ func (e *compiledTemplateLiteral) emitGetter(putOnStack bool) { stringCount++ } if tail != "" { - e.c.emit(loadVal(e.c.p.defineLiteralValue(stringValueFromRaw(tail)))) + e.c.emitLiteralString(stringValueFromRaw(tail)) stringCount++ } e.c.emit(concatStrings(stringCount)) @@ -2478,7 +2478,7 @@ func (c *compiler) emitThrow(v Value) { c.emit(loadDynamic(t)) msg := o.self.getStr("message", nil) if msg != nil { - c.emit(loadVal(c.p.defineLiteralValue(msg))) + c.emitLiteralValue(msg) c.emit(_new(1)) } else { c.emit(_new(0)) @@ -2495,7 +2495,7 @@ func (c *compiler) emitConst(expr compiledExpr, putOnStack bool) { v, ex := c.evalConst(expr) if ex == nil { if putOnStack { - c.emit(loadVal(c.p.defineLiteralValue(v))) + c.emitLiteralValue(v) } } else { c.emitThrow(ex.val) @@ -2661,7 +2661,7 @@ func (e *compiledLogicalOr) emitGetter(putOnStack bool) { e.c.emitExpr(e.right, putOnStack) } else { if putOnStack { - e.c.emit(loadVal(e.c.p.defineLiteralValue(v))) + e.c.emitLiteralValue(v) } } } else { @@ -2702,7 +2702,7 @@ func (e *compiledCoalesce) emitGetter(putOnStack bool) { e.c.emitExpr(e.right, putOnStack) } else { if putOnStack { - e.c.emit(loadVal(e.c.p.defineLiteralValue(v))) + e.c.emitLiteralValue(v) } } } else { @@ -2742,7 +2742,7 @@ func (e *compiledLogicalAnd) emitGetter(putOnStack bool) { if e.left.constant() { if v, ex := e.c.evalConst(e.left); ex == nil { if !v.ToBoolean() { - e.c.emit(loadVal(e.c.p.defineLiteralValue(v))) + e.c.emitLiteralValue(v) } else { e.c.emitExpr(e.right, putOnStack) } diff --git a/compiler_stmt.go b/compiler_stmt.go index e280f254..53de1b8b 100644 --- a/compiler_stmt.go +++ b/compiler_stmt.go @@ -742,7 +742,7 @@ func (c *compiler) compileReturnStatement(v *ast.ReturnStatement) { for b := c.block; b != nil; b = b.outer { switch b.typ { case blockTry: - c.emit(leaveTry{}) + c.emit(saveResult, leaveTry{}, loadResult) case blockLoopEnum: c.emit(enumPopClose) } diff --git a/compiler_test.go b/compiler_test.go index cb604997..05d82ca4 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -4,6 +4,9 @@ import ( "os" "sync" "testing" + "unsafe" + + "github.com/dop251/goja/unistring" ) const TESTLIB = ` @@ -1117,6 +1120,20 @@ func TestReturnOutOfTryNested(t *testing.T) { testScript(SCRIPT, intToValue(1), t) } +func TestReturnOutOfTryWithFinally(t *testing.T) { + const SCRIPT = ` + function test() { + try { + return 'Hello, world!'; + } finally { + const dummy = 'unexpected'; + } + } + test(); + ` + testScript(SCRIPT, asciiString("Hello, world!"), t) +} + func TestContinueLoop(t *testing.T) { const SCRIPT = ` function A() { @@ -4683,9 +4700,15 @@ func TestBadObjectKey(t *testing.T) { func TestConstantFolding(t *testing.T) { testValues := func(prg *Program, result Value, t *testing.T) { - if len(prg.values) != 1 || !prg.values[0].SameAs(result) { + values := make(map[unistring.String]struct{}) + for _, ins := range prg.code { + if lv, ok := ins.(loadVal); ok { + values[lv.v.string()] = struct{}{} + } + } + if len(values) != 1 { prg.dumpCode(t.Logf) - t.Fatalf("values: %v", prg.values) + t.Fatalf("values: %v", values) } } f := func(src string, result Value, t *testing.T) { @@ -4729,6 +4752,26 @@ func TestConstantFolding(t *testing.T) { }) } +func TestStringInterning(t *testing.T) { + const SCRIPT = ` + const str1 = "Test"; + function f() { + return "Test"; + } + [str1, f()]; + ` + vm := New() + res, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + str1 := res.(*Object).Get("0").String() + str2 := res.(*Object).Get("1").String() + if unsafe.StringData(str1) != unsafe.StringData(str2) { + t.Fatal("not interned") + } +} + func TestAssignBeforeInit(t *testing.T) { const SCRIPT = ` assert.throws(ReferenceError, () => { diff --git a/go.mod b/go.mod index e15019d0..4361fa0f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/dop251/goja -go 1.16 +go 1.20 require ( github.com/dlclark/regexp2 v1.7.0 diff --git a/runtime_test.go b/runtime_test.go index b084a4a3..a834428d 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -2325,15 +2325,32 @@ func TestStacktraceLocationThrowFromCatch(t *testing.T) { t.Fatal("Expected error") } stack := err.(*Exception).stack - if len(stack) != 2 { + if len(stack) != 3 { t.Fatalf("Unexpected stack len: %v", stack) } - if frame := stack[0]; frame.funcName != "main" || frame.pc != 29 { + if frame := stack[0]; frame.funcName != "f2" || frame.pc != 2 { t.Fatalf("Unexpected stack frame 0: %#v", frame) } - if frame := stack[1]; frame.funcName != "" || frame.pc != 7 { + if frame := stack[1]; frame.funcName != "main" || frame.pc != 17 { t.Fatalf("Unexpected stack frame 1: %#v", frame) } + if frame := stack[2]; frame.funcName != "" || frame.pc != 7 { + t.Fatalf("Unexpected stack frame 2: %#v", frame) + } +} + +func TestErrorStackRethrow(t *testing.T) { + const SCRIPT = ` + function f(e) { + throw e; + } + try { + f(new Error()); + } catch(e) { + assertStack(e, [["test.js", "", 6, 5]]); + } + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) } func TestStacktraceLocationThrowFromGo(t *testing.T) { diff --git a/vm.go b/vm.go index 00ef5a09..1673835a 100644 --- a/vm.go +++ b/vm.go @@ -901,10 +901,12 @@ func (vm *vm) toCallee(v Value) *Object { panic(vm.r.NewTypeError("Value is not an object: %s", v.toString())) } -type loadVal uint32 +type loadVal struct { + v Value +} func (l loadVal) exec(vm *vm) { - vm.push(vm.prg.values[l]) + vm.push(l.v) vm.pc++ } @@ -936,6 +938,15 @@ func (_saveResult) exec(vm *vm) { vm.pc++ } +type _loadResult struct{} + +var loadResult _loadResult + +func (_loadResult) exec(vm *vm) { + vm.push(vm.result) + vm.pc++ +} + type _clearResult struct{} var clearResult _clearResult @@ -4570,28 +4581,20 @@ var throw _throw func (_throw) exec(vm *vm) { v := vm.stack[vm.sp-1] - var ex *Exception + ex := &Exception{ + val: v, + } + if o, ok := v.(*Object); ok { if e, ok := o.self.(*errorObject); ok { if len(e.stack) > 0 { - frame0 := e.stack[0] - // If the Error was created immediately before throwing it (i.e. 'throw new Error(....)') - // avoid capturing the stack again by the reusing the stack from the Error. - // These stacks would be almost identical and the difference doesn't matter for debugging. - if frame0.prg == vm.prg && vm.pc-frame0.pc == 1 { - ex = &Exception{ - val: v, - stack: e.stack, - } - } + ex.stack = e.stack } } } - if ex == nil { - ex = &Exception{ - val: v, - stack: vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0), - } + + if ex.stack == nil { + ex.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0) } if ex = vm.handleThrow(ex); ex != nil { diff --git a/vm_test.go b/vm_test.go index f4a50a76..a88ff477 100644 --- a/vm_test.go +++ b/vm_test.go @@ -22,15 +22,14 @@ func TestVM1(t *testing.T) { vm := r.vm vm.prg = &Program{ - src: file.NewFile("dummy", "", 1), - values: []Value{valueInt(2), valueInt(3), asciiString("test")}, + src: file.NewFile("dummy", "", 1), code: []instruction{ &bindGlobal{vars: []unistring.String{"v"}}, newObject, setGlobal("v"), - loadVal(2), - loadVal(1), - loadVal(0), + loadVal{asciiString("test")}, + loadVal{valueInt(3)}, + loadVal{valueInt(2)}, add, setElem, pop, @@ -103,9 +102,7 @@ func BenchmarkVmNOP2(b *testing.B) { r.init() vm := r.vm - vm.prg = &Program{ - values: []Value{intToValue(2), intToValue(3)}, - } + vm.prg = &Program{} for i := 0; i < b.N; i++ { vm.pc = 0 @@ -152,10 +149,9 @@ func BenchmarkVm1(b *testing.B) { //ins2 := loadVal1(1) vm.prg = &Program{ - values: []Value{valueInt(2), valueInt(3)}, code: []instruction{ - loadVal(0), - loadVal(1), + loadVal{valueInt(2)}, + loadVal{valueInt(3)}, add, }, } @@ -278,3 +274,12 @@ func BenchmarkAssertInt(b *testing.B) { } } } + +func BenchmarkLoadVal(b *testing.B) { + var ins instruction + b.ReportAllocs() + for i := 0; i < b.N; i++ { + ins = loadVal{valueInt(1)} + _ = ins + } +}