Skip to content

Commit

Permalink
Optimised the handling of literal values during compilation. Bumped m…
Browse files Browse the repository at this point in the history
…inimum required Go version to 1.20. Closes #566.
  • Loading branch information
dop251 committed May 16, 2024
1 parent e401ed4 commit cba40bd
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 44 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------
Expand Down
41 changes: 27 additions & 14 deletions compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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{},
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
26 changes: 13 additions & 13 deletions compiler_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand All @@ -1272,31 +1272,31 @@ 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)
e.c.emit(_toString{})
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)
e.c.emit(_toString{})
stringCount++
}
if tail != "" {
e.c.emit(loadVal(e.c.p.defineLiteralValue(stringValueFromRaw(tail))))
e.c.emitLiteralString(stringValueFromRaw(tail))
stringCount++
}
e.c.emit(concatStrings(stringCount))
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
33 changes: 31 additions & 2 deletions compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"os"
"sync"
"testing"
"unsafe"

"github.com/dop251/goja/unistring"
)

const TESTLIB = `
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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, () => {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/dop251/goja

go 1.16
go 1.20

require (
github.com/dlclark/regexp2 v1.7.0
Expand Down
6 changes: 4 additions & 2 deletions vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++
}

Expand Down
27 changes: 16 additions & 11 deletions vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
}
Expand Down Expand Up @@ -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
}
}

0 comments on commit cba40bd

Please sign in to comment.