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 967b0a05..2abd9ba5 100644 --- a/compiler.go +++ b/compiler.go @@ -67,8 +67,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 @@ -88,6 +87,8 @@ type compiler struct { ctxVM *vm // VM in which an eval() code is compiled codeScratchpad []instruction + + stringCache map[unistring.String]Value } type binding struct { @@ -384,6 +385,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{}, @@ -394,23 +418,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) @@ -982,6 +994,7 @@ func (c *compiler) compile(in *ast.Program, strict, inGlobal bool, evalVm *vm) { } scope.finaliseVarAlloc(0) + c.stringCache = nil } func (c *compiler) compileDeclList(v []*ast.VariableDeclaration, inFunc bool) { diff --git a/compiler_expr.go b/compiler_expr.go index b4e39898..477580ae 100644 --- a/compiler_expr.go +++ b/compiler_expr.go @@ -234,7 +234,7 @@ type compiledOptional 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) } } @@ -373,7 +373,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) } } @@ -1261,7 +1261,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) } } @@ -1272,15 +1272,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) @@ -1288,7 +1288,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) @@ -1296,7 +1296,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)) @@ -2450,7 +2450,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)) @@ -2467,7 +2467,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) @@ -2633,7 +2633,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 { @@ -2674,7 +2674,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 { @@ -2714,7 +2714,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_test.go b/compiler_test.go index 11ebaba7..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 = ` @@ -4697,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) { @@ -4743,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/vm.go b/vm.go index 29fffbeb..30b79331 100644 --- a/vm.go +++ b/vm.go @@ -897,10 +897,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++ } 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 + } +}