Compiler optimization first iteration (#165)

* dead code elimination phase 1

* combine dead code elimination with return fix code

* remove last instruction tracking from compiler code (not needed)

* fix a symbol table block scope bug

* add some more tests
This commit is contained in:
Daniel 2019-03-24 02:23:38 -07:00 committed by GitHub
parent 01fe30f02a
commit b9c1c92d2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 415 additions and 97 deletions

View file

@ -6,7 +6,6 @@ import "github.com/d5/tengo/compiler/source"
// and the last two instructions that were emitted.
type CompilationScope struct {
instructions []byte
lastInstructions [2]EmittedInstruction
symbolInit map[string]bool
sourceMap map[int]source.Pos
}

View file

@ -6,6 +6,7 @@ import (
"io/ioutil"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/d5/tengo"
@ -292,6 +293,15 @@ func (c *Compiler) Compile(node ast.Node) error {
}
case *ast.BlockStmt:
if len(node.Stmts) == 0 {
return nil
}
c.symbolTable = c.symbolTable.Fork(true)
defer func() {
c.symbolTable = c.symbolTable.Parent(false)
}()
for _, stmt := range node.Stmts {
if err := c.Compile(stmt); err != nil {
return err
@ -404,8 +414,8 @@ func (c *Compiler) Compile(node ast.Node) error {
return err
}
// add OpReturn if function returns nothing
c.fixReturn(node)
// code optimization
c.optimizeFunc(node)
freeSymbols := c.symbolTable.FreeSymbols()
numLocals := c.symbolTable.MaxSymbols()
@ -688,33 +698,6 @@ func (c *Compiler) addInstruction(b []byte) int {
return posNewIns
}
func (c *Compiler) setLastInstruction(op Opcode, pos int) {
c.scopes[c.scopeIndex].lastInstructions[1] = c.scopes[c.scopeIndex].lastInstructions[0]
c.scopes[c.scopeIndex].lastInstructions[0].Opcode = op
c.scopes[c.scopeIndex].lastInstructions[0].Position = pos
}
func (c *Compiler) lastInstructionIs(op Opcode) bool {
if len(c.currentInstructions()) == 0 {
return false
}
return c.scopes[c.scopeIndex].lastInstructions[0].Opcode == op
}
func (c *Compiler) removeLastInstruction() {
lastPos := c.scopes[c.scopeIndex].lastInstructions[0].Position
if c.trace != nil {
c.printTrace(fmt.Sprintf("DELET %s",
FormatInstructions(c.scopes[c.scopeIndex].instructions[lastPos:], lastPos)[0]))
}
c.scopes[c.scopeIndex].instructions = c.currentInstructions()[:lastPos]
c.scopes[c.scopeIndex].lastInstructions[0] = c.scopes[c.scopeIndex].lastInstructions[1]
}
func (c *Compiler) replaceInstruction(pos int, inst []byte) {
copy(c.currentInstructions()[pos:], inst)
@ -731,36 +714,88 @@ func (c *Compiler) changeOperand(opPos int, operand ...int) {
c.replaceInstruction(opPos, inst)
}
// fixReturn appends "return" statement at the end of the function if
// 1) the function does not have a "return" statement at the end.
// 2) or, there are jump instructions that jump to the end of the function.
func (c *Compiler) fixReturn(node ast.Node) {
var appendReturn bool
if !c.lastInstructionIs(OpReturn) {
appendReturn = true
} else {
var lastOp Opcode
insts := c.scopes[c.scopeIndex].instructions
endPos := len(insts)
iterateInstructions(insts, func(pos int, opcode Opcode, operands []int) bool {
defer func() { lastOp = opcode }()
// optimizeFunc performs some code-level optimization for the current function instructions
// it removes unreachable (dead code) instructions and adds "returns" instruction if needed.
func (c *Compiler) optimizeFunc(node ast.Node) {
// any instructions between RETURN and the function end
// or instructions between RETURN and jump target position
// are considered as unreachable.
// pass 1. identify all jump destinations
var dsts []int
iterateInstructions(c.scopes[c.scopeIndex].instructions, func(pos int, opcode Opcode, operands []int) bool {
switch opcode {
case OpJump, OpJumpFalsy, OpAndJump, OpOrJump:
dst := operands[0]
if dst == endPos && lastOp != OpReturn {
appendReturn = true
return false
} else if dst > endPos {
panic(fmt.Errorf("wrong jump position: %d (end: %d)", dst, endPos))
}
dsts = append(dsts, operands[0])
}
return true
})
sort.Ints(dsts) // sort jump positions
var newInsts []byte
// pass 2. eliminate dead code
posMap := make(map[int]int) // old position to new position
var dstIdx int
var deadCode bool
iterateInstructions(c.scopes[c.scopeIndex].instructions, func(pos int, opcode Opcode, operands []int) bool {
switch {
case opcode == OpReturn:
if deadCode {
return true
}
deadCode = true
case dstIdx < len(dsts) && pos == dsts[dstIdx]:
dstIdx++
deadCode = false
case deadCode:
return true
}
posMap[pos] = len(newInsts)
newInsts = append(newInsts, MakeInstruction(opcode, operands...)...)
return true
})
// pass 3. update jump positions
var lastOp Opcode
var appendReturn bool
endPos := len(newInsts)
iterateInstructions(newInsts, func(pos int, opcode Opcode, operands []int) bool {
switch opcode {
case OpJump, OpJumpFalsy, OpAndJump, OpOrJump:
newDst, ok := posMap[operands[0]]
if ok {
copy(newInsts[pos:], MakeInstruction(opcode, newDst))
} else if endPos == operands[0] {
// there's a jump instruction that jumps to the end of function
// compiler should append "return".
appendReturn = true
} else {
panic(fmt.Errorf("invalid jump position: %d", newDst))
}
}
lastOp = opcode
return true
})
if lastOp != OpReturn {
appendReturn = true
}
// pass 4. update source map
newSourceMap := make(map[int]source.Pos)
for pos, srcPos := range c.scopes[c.scopeIndex].sourceMap {
newPos, ok := posMap[pos]
if ok {
newSourceMap[newPos] = srcPos
}
}
c.scopes[c.scopeIndex].instructions = newInsts
c.scopes[c.scopeIndex].sourceMap = newSourceMap
// append "return"
if appendReturn {
c.emit(node, OpReturn, 0)
}
@ -775,7 +810,6 @@ func (c *Compiler) emit(node ast.Node, opcode Opcode, operands ...int) int {
inst := MakeInstruction(opcode, operands...)
pos := c.addInstruction(inst)
c.scopes[c.scopeIndex].sourceMap[pos] = filePos
c.setLastInstruction(opcode, pos)
if c.trace != nil {
c.printTrace(fmt.Sprintf("EMIT %s",

View file

@ -49,7 +49,8 @@ func (c *Compiler) compileModule(node ast.Node, moduleName, modulePath string, s
return nil, err
}
moduleCompiler.fixReturn(node)
// code optimization
moduleCompiler.optimizeFunc(node)
compiledFunc := moduleCompiler.Bytecode().MainFunction
compiledFunc.NumLocals = symbolTable.MaxSymbols()

View file

@ -0,0 +1,124 @@
package compiler_test
import (
"testing"
"github.com/d5/tengo/compiler"
)
func TestCompilerDeadCode(t *testing.T) {
expect(t, `
func() {
a := 4
return a
b := 5 // dead code from here
c := a
return b
}`,
bytecode(
concat(
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
intObject(4),
intObject(5),
compiledFunction(0, 0,
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpDefineLocal, 0),
compiler.MakeInstruction(compiler.OpGetLocal, 0),
compiler.MakeInstruction(compiler.OpReturn, 1)))))
expect(t, `
func() {
if true {
return 5
a := 4 // dead code from here
b := a
return b
} else {
return 4
c := 5 // dead code from here
d := c
return d
}
}`, bytecode(
concat(
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
intObject(5),
intObject(4),
compiledFunction(0, 0,
compiler.MakeInstruction(compiler.OpTrue),
compiler.MakeInstruction(compiler.OpJumpFalsy, 9),
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpReturn, 1),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpReturn, 1)))))
expect(t, `
func() {
a := 1
for {
if a == 5 {
return 10
}
5 + 5
return 20
b := a
return b
}
}`, bytecode(
concat(
compiler.MakeInstruction(compiler.OpConstant, 4),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
intObject(1),
intObject(5),
intObject(10),
intObject(20),
compiledFunction(0, 0,
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpDefineLocal, 0),
compiler.MakeInstruction(compiler.OpGetLocal, 0),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpEqual),
compiler.MakeInstruction(compiler.OpJumpFalsy, 19),
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpReturn, 1),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpBinaryOp, 11),
compiler.MakeInstruction(compiler.OpPop),
compiler.MakeInstruction(compiler.OpConstant, 3),
compiler.MakeInstruction(compiler.OpReturn, 1)))))
expect(t, `
func() {
if true {
return 5
a := 4 // dead code from here
b := a
return b
} else {
return 4
c := 5 // dead code from here
d := c
return d
}
}`, bytecode(
concat(
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
intObject(5),
intObject(4),
compiledFunction(0, 0,
compiler.MakeInstruction(compiler.OpTrue),
compiler.MakeInstruction(compiler.OpJumpFalsy, 9),
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpReturn, 1),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpReturn, 1)))))
}

View file

@ -0,0 +1,69 @@
package compiler_test
import (
"testing"
"github.com/d5/tengo/compiler"
)
func TestCompilerScopes(t *testing.T) {
expect(t, `
if a := 1; a {
a = 2
b := a
} else {
a = 3
b := a
}`, bytecode(
concat(
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpSetGlobal, 0),
compiler.MakeInstruction(compiler.OpGetGlobal, 0),
compiler.MakeInstruction(compiler.OpJumpFalsy, 27),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpSetGlobal, 0),
compiler.MakeInstruction(compiler.OpGetGlobal, 0),
compiler.MakeInstruction(compiler.OpSetGlobal, 1),
compiler.MakeInstruction(compiler.OpJump, 39),
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpSetGlobal, 0),
compiler.MakeInstruction(compiler.OpGetGlobal, 0),
compiler.MakeInstruction(compiler.OpSetGlobal, 1)),
objectsArray(
intObject(1),
intObject(2),
intObject(3))))
expect(t, `
func() {
if a := 1; a {
a = 2
b := a
} else {
a = 3
b := a
}
}`, bytecode(
concat(
compiler.MakeInstruction(compiler.OpConstant, 3),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
intObject(1),
intObject(2),
intObject(3),
compiledFunction(0, 0,
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpDefineLocal, 0),
compiler.MakeInstruction(compiler.OpGetLocal, 0),
compiler.MakeInstruction(compiler.OpJumpFalsy, 22),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpSetLocal, 0),
compiler.MakeInstruction(compiler.OpGetLocal, 0),
compiler.MakeInstruction(compiler.OpDefineLocal, 1),
compiler.MakeInstruction(compiler.OpJump, 31),
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpSetLocal, 0),
compiler.MakeInstruction(compiler.OpGetLocal, 0),
compiler.MakeInstruction(compiler.OpDefineLocal, 1),
compiler.MakeInstruction(compiler.OpReturn, 0)))))
}

View file

@ -358,17 +358,15 @@ func TestCompiler_Compile(t *testing.T) {
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpArray, 3),
compiler.MakeInstruction(compiler.OpConstant, 3),
compiler.MakeInstruction(compiler.OpConstant, 4),
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpBinaryOp, 11),
compiler.MakeInstruction(compiler.OpIndex),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
intObject(1),
intObject(2),
intObject(3),
intObject(1),
intObject(1))))
intObject(3))))
expect(t, `{a: 2}[2 - 1]`,
bytecode(
@ -376,15 +374,14 @@ func TestCompiler_Compile(t *testing.T) {
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpMap, 2),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpConstant, 3),
compiler.MakeInstruction(compiler.OpBinaryOp, 12),
compiler.MakeInstruction(compiler.OpIndex),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
stringObject("a"),
intObject(2),
intObject(2),
intObject(1))))
expect(t, `[1, 2, 3][:]`,
@ -411,15 +408,14 @@ func TestCompiler_Compile(t *testing.T) {
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpArray, 3),
compiler.MakeInstruction(compiler.OpConstant, 3),
compiler.MakeInstruction(compiler.OpConstant, 4),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpSliceIndex),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
intObject(1),
intObject(2),
intObject(3),
intObject(0),
intObject(2))))
intObject(0))))
expect(t, `[1, 2, 3][:2]`,
bytecode(
@ -429,14 +425,13 @@ func TestCompiler_Compile(t *testing.T) {
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpArray, 3),
compiler.MakeInstruction(compiler.OpNull),
compiler.MakeInstruction(compiler.OpConstant, 3),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpSliceIndex),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
intObject(1),
intObject(2),
intObject(3),
intObject(2))))
intObject(3))))
expect(t, `[1, 2, 3][0:]`,
bytecode(
@ -523,12 +518,11 @@ func TestCompiler_Compile(t *testing.T) {
intObject(2),
compiledFunction(0, 0,
compiler.MakeInstruction(compiler.OpTrue), // 0000
compiler.MakeInstruction(compiler.OpJumpFalsy, 12), // 0001
compiler.MakeInstruction(compiler.OpJumpFalsy, 9), // 0001
compiler.MakeInstruction(compiler.OpConstant, 0), // 0004
compiler.MakeInstruction(compiler.OpReturn, 1), // 0007
compiler.MakeInstruction(compiler.OpJump, 17), // 0008
compiler.MakeInstruction(compiler.OpConstant, 1), // 0011
compiler.MakeInstruction(compiler.OpReturn, 1))))) // 0014
compiler.MakeInstruction(compiler.OpConstant, 1), // 0009
compiler.MakeInstruction(compiler.OpReturn, 1))))) // 0012
expect(t, `func() { 1; if(true) { 2 } else { 3 }; 4 }`,
bytecode(
@ -879,21 +873,19 @@ func() {
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpSetGlobal, 0),
compiler.MakeInstruction(compiler.OpGetGlobal, 0),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpConstant, 0),
compiler.MakeInstruction(compiler.OpEqual),
compiler.MakeInstruction(compiler.OpAndJump, 23),
compiler.MakeInstruction(compiler.OpGetGlobal, 0),
compiler.MakeInstruction(compiler.OpConstant, 2),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpNotEqual),
compiler.MakeInstruction(compiler.OpOrJump, 34),
compiler.MakeInstruction(compiler.OpConstant, 3),
compiler.MakeInstruction(compiler.OpConstant, 1),
compiler.MakeInstruction(compiler.OpGetGlobal, 0),
compiler.MakeInstruction(compiler.OpBinaryOp, 39),
compiler.MakeInstruction(compiler.OpPop)),
objectsArray(
intObject(0),
intObject(0),
intObject(1),
intObject(1))))
expectError(t, `import("user1")`, "module 'user1' not found") // unknown module name
@ -1028,19 +1020,17 @@ func traceCompile(input string, symbols map[string]objects.Object) (res *compile
}
err = c.Compile(parsed)
res = c.Bytecode()
res.RemoveDuplicates()
{
trace = append(trace, fmt.Sprintf("Compiler Trace:\n%s", strings.Join(tr.Out, "")))
bytecode := c.Bytecode()
trace = append(trace, fmt.Sprintf("Compiled Constants:\n%s", strings.Join(bytecode.FormatConstants(), "\n")))
trace = append(trace, fmt.Sprintf("Compiled Instructions:\n%s\n", strings.Join(bytecode.FormatInstructions(), "\n")))
trace = append(trace, fmt.Sprintf("Compiled Constants:\n%s", strings.Join(res.FormatConstants(), "\n")))
trace = append(trace, fmt.Sprintf("Compiled Instructions:\n%s\n", strings.Join(res.FormatInstructions(), "\n")))
}
if err != nil {
return
}
res = c.Bytecode()
return
}

View file

@ -64,9 +64,7 @@ func (t *SymbolTable) Resolve(name string) (symbol *Symbol, depth int, ok bool)
return
}
if !t.block {
depth++
}
// if symbol is defined in parent table and if it's not global/builtin
// then it's free variable.

View file

@ -83,12 +83,12 @@ func TestSymbolTable(t *testing.T) {
resolveExpect(t, global, "a", globalSymbol("a", 0), 0)
resolveExpect(t, local1, "d", localSymbol("d", 0), 0)
resolveExpect(t, local1, "a", globalSymbol("a", 0), 1)
resolveExpect(t, local3, "a", globalSymbol("a", 0), 2)
resolveExpect(t, local3, "d", freeSymbol("d", 0), 1)
resolveExpect(t, local3, "a", globalSymbol("a", 0), 3)
resolveExpect(t, local3, "d", freeSymbol("d", 0), 2)
resolveExpect(t, local3, "r", localSymbol("r", 1), 0)
resolveExpect(t, local2Block2, "k", localSymbol("k", 4), 0)
resolveExpect(t, local2Block2, "e", localSymbol("e", 0), 0)
resolveExpect(t, local2Block2, "b", globalSymbol("b", 1), 2)
resolveExpect(t, local2Block2, "e", localSymbol("e", 0), 1)
resolveExpect(t, local2Block2, "b", globalSymbol("b", 1), 3)
}
func symbol(name string, scope compiler.SymbolScope, index int) *compiler.Symbol {

103
runtime/vm_scopes_test.go Normal file
View file

@ -0,0 +1,103 @@
package runtime_test
import "testing"
func TestVMScopes(t *testing.T) {
// shadowed global variable
expect(t, `
c := 5
if a := 3; a {
c := 6
} else {
c := 7
}
out = c
`, nil, 5)
// shadowed local variable
expect(t, `
func() {
c := 5
if a := 3; a {
c := 6
} else {
c := 7
}
out = c
}()
`, nil, 5)
// 'b' is declared in 2 separate blocks
expect(t, `
c := 5
if a := 3; a {
b := 8
c = b
} else {
b := 9
c = b
}
out = c
`, nil, 8)
// shadowing inside for statement
expect(t, `
a := 4
b := 5
for i:=0;i<3;i++ {
b := 6
for j:=0;j<2;j++ {
b := 7
a = i*j
}
}
out = a`, nil, 2)
// shadowing variable declared in init statement
expect(t, `
if a := 5; a {
a := 6
out = a
}`, nil, 6)
expect(t, `
a := 4
if a := 5; a {
a := 6
out = a
}`, nil, 6)
expect(t, `
a := 4
if a := 0; a {
a := 6
out = a
} else {
a := 7
out = a
}`, nil, 7)
expect(t, `
a := 4
if a := 0; a {
out = a
} else {
out = a
}`, nil, 0)
// shadowing function level
expect(t, `
a := 5
func() {
a := 6
a = 7
}()
out = a
`, nil, 5)
expect(t, `
a := 5
func() {
if a := 7; true {
a = 8
}
}()
out = a
`, nil, 5)
}