vm: implement slot-related opcodes

1. Slot is a new mechanism for storing variables during execution
which is more convenient than alt.stack. This commit implements
support for slot opcodes in both vm and compiler.
2. Remove old alt.stack opcodes.
3. Do not process globals at the start of every function, but instead
load them single time at main.
This commit is contained in:
Evgenii Stratonikov 2020-05-07 11:54:35 +03:00
parent a6271f6bf2
commit 0cb6dc47e4
12 changed files with 552 additions and 192 deletions

View file

@ -1,11 +1,14 @@
package compiler
import (
"errors"
"fmt"
"go/ast"
"go/constant"
"go/types"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"golang.org/x/tools/go/loader"
)
@ -47,6 +50,24 @@ func typeAndValueForField(fld *types.Var) (types.TypeAndValue, error) {
return types.TypeAndValue{}, nil
}
// newGlobal creates new global variable.
func (c *codegen) newGlobal(name string) {
c.globals[name] = len(c.globals)
}
// traverseGlobals visits and initializes global variables.
func (c *codegen) traverseGlobals(f ast.Node) {
n := countGlobals(f)
if n != 0 {
if n > 255 {
c.prog.BinWriter.Err = errors.New("too many global variables")
return
}
emit.Instruction(c.prog.BinWriter, opcode.INITSSLOT, []byte{byte(n)})
}
c.convertGlobals(f)
}
// countGlobals counts the global variables in the program to add
// them with the stack size of the function.
func countGlobals(f ast.Node) (i int64) {

View file

@ -43,6 +43,8 @@ type codegen struct {
// Current funcScope being converted.
scope *funcScope
globals map[string]int
// A mapping from label's names to their ids.
labels map[labelWithType]uint16
// A list of nested label names together with evaluation stack depth.
@ -82,6 +84,14 @@ type labelWithStackSize struct {
sz int
}
type varType int
const (
varGlobal varType = iota
varLocal
varArgument
)
// newLabel creates a new label to jump to
func (c *codegen) newLabel() (l uint16) {
li := len(c.l)
@ -147,34 +157,6 @@ func (c *codegen) convertBasicType(t types.TypeAndValue, typ *types.Basic) {
}
}
func (c *codegen) emitLoadLocal(name string) {
pos := c.scope.loadLocal(name)
if pos < 0 {
c.prog.Err = fmt.Errorf("cannot load local variable with position: %d", pos)
return
}
c.emitLoadLocalPos(pos)
}
func (c *codegen) emitLoadLocalPos(pos int) {
emit.Opcode(c.prog.BinWriter, opcode.DUPFROMALTSTACK)
emit.Int(c.prog.BinWriter, int64(pos))
emit.Opcode(c.prog.BinWriter, opcode.PICKITEM)
}
func (c *codegen) emitStoreLocal(pos int) {
emit.Opcode(c.prog.BinWriter, opcode.DUPFROMALTSTACK)
if pos < 0 {
c.prog.Err = fmt.Errorf("invalid position to store local: %d", pos)
return
}
emit.Int(c.prog.BinWriter, int64(pos))
emit.Opcode(c.prog.BinWriter, opcode.ROT)
emit.Opcode(c.prog.BinWriter, opcode.SETITEM)
}
func (c *codegen) emitLoadField(i int) {
emit.Int(c.prog.BinWriter, int64(i))
emit.Opcode(c.prog.BinWriter, opcode.PICKITEM)
@ -186,6 +168,81 @@ func (c *codegen) emitStoreStructField(i int) {
emit.Opcode(c.prog.BinWriter, opcode.SETITEM)
}
// getVarIndex returns variable type and position in corresponding slot,
// according to current scope.
func (c *codegen) getVarIndex(name string) (varType, int) {
if c.scope != nil {
if i, ok := c.scope.arguments[name]; ok {
return varArgument, i
} else if i, ok := c.scope.locals[name]; ok {
return varLocal, i
}
}
if i, ok := c.globals[name]; ok {
return varGlobal, i
}
return varLocal, c.scope.newVariable(varLocal, name)
}
func getBaseOpcode(t varType) (opcode.Opcode, opcode.Opcode) {
switch t {
case varGlobal:
return opcode.LDSFLD0, opcode.STSFLD0
case varLocal:
return opcode.LDLOC0, opcode.STLOC0
case varArgument:
return opcode.LDARG0, opcode.STARG0
default:
panic("invalid type")
}
}
// emitLoadVar loads specified variable to the evaluation stack.
func (c *codegen) emitLoadVar(name string) {
t, i := c.getVarIndex(name)
base, _ := getBaseOpcode(t)
if i < 7 {
emit.Opcode(c.prog.BinWriter, base+opcode.Opcode(i))
} else {
emit.Instruction(c.prog.BinWriter, base+7, []byte{byte(i)})
}
}
// emitStoreVar stores top value from the evaluation stack in the specified variable.
func (c *codegen) emitStoreVar(name string) {
t, i := c.getVarIndex(name)
_, base := getBaseOpcode(t)
if i < 7 {
emit.Opcode(c.prog.BinWriter, base+opcode.Opcode(i))
} else {
emit.Instruction(c.prog.BinWriter, base+7, []byte{byte(i)})
}
}
func (c *codegen) emitDefault(n ast.Expr) {
tv, ok := c.typeInfo.Types[n]
if !ok {
c.prog.Err = errors.New("invalid type")
return
}
if t, ok := tv.Type.(*types.Basic); ok {
info := t.Info()
switch {
case info&types.IsInteger != 0:
emit.Int(c.prog.BinWriter, 0)
case info&types.IsString != 0:
emit.Bytes(c.prog.BinWriter, []byte{})
case info&types.IsBoolean != 0:
emit.Bool(c.prog.BinWriter, false)
default:
emit.Opcode(c.prog.BinWriter, opcode.PUSHNULL)
}
return
}
emit.Opcode(c.prog.BinWriter, opcode.PUSHNULL)
}
// convertGlobals traverses the AST and only converts global declarations.
// If we call this in convertFuncDecl then it will load all global variables
// into the scope of the function.
@ -231,9 +288,17 @@ func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl) {
// All globals copied into the scope of the function need to be added
// to the stack size of the function.
emit.Int(c.prog.BinWriter, f.stackSize()+countGlobals(file))
emit.Opcode(c.prog.BinWriter, opcode.NEWARRAY)
emit.Opcode(c.prog.BinWriter, opcode.TOALTSTACK)
sizeLoc := f.countLocals()
if sizeLoc > 255 {
c.prog.Err = errors.New("maximum of 255 local variables is allowed")
}
sizeArg := f.countArgs()
if sizeArg > 255 {
c.prog.Err = errors.New("maximum of 255 local variables is allowed")
}
if sizeLoc != 0 || sizeArg != 0 {
emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{byte(sizeLoc), byte(sizeArg)})
}
// We need to handle methods, which in Go, is just syntactic sugar.
// The method receiver will be passed in as first argument.
@ -250,23 +315,18 @@ func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl) {
c.prog.Err = fmt.Errorf("method receives for non-struct types is not yet supported")
return
}
l := c.scope.newLocal(ident.Name)
c.emitStoreLocal(l)
// only create an argument here, it will be stored via INITSLOT
c.scope.newVariable(varArgument, ident.Name)
}
}
// Load the arguments in scope.
for _, arg := range decl.Type.Params.List {
for _, id := range arg.Names {
l := c.scope.newLocal(id.Name)
c.emitStoreLocal(l)
// only create an argument here, it will be stored via INITSLOT
c.scope.newVariable(varArgument, id.Name)
}
}
// Load in all the global variables in to the scope of the function.
// This is not necessary for syscalls.
if !isSyscall(f) {
c.convertGlobals(file)
}
ast.Walk(c, decl.Body)
@ -275,8 +335,6 @@ func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl) {
// This can be the case with void and named-return functions.
if !lastStmtIsReturn(decl) {
c.saveSequencePoint(decl.Body)
emit.Opcode(c.prog.BinWriter, opcode.FROMALTSTACK)
emit.Opcode(c.prog.BinWriter, opcode.DROP)
emit.Opcode(c.prog.BinWriter, opcode.RET)
}
@ -305,25 +363,32 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
switch t := spec.(type) {
case *ast.ValueSpec:
for _, id := range t.Names {
c.scope.newLocal(id.Name)
if c.scope == nil {
// it is a global declaration
c.newGlobal(id.Name)
} else {
c.scope.newLocal(id.Name)
}
c.registerDebugVariable(id.Name, t.Type)
}
if len(t.Values) != 0 {
for i, val := range t.Values {
ast.Walk(c, val)
l := c.scope.loadLocal(t.Names[i].Name)
c.emitStoreLocal(l)
c.emitStoreVar(t.Names[i].Name)
}
} else if c.isCompoundArrayType(t.Type) {
emit.Opcode(c.prog.BinWriter, opcode.PUSH0)
emit.Opcode(c.prog.BinWriter, opcode.NEWARRAY)
l := c.scope.loadLocal(t.Names[0].Name)
c.emitStoreLocal(l)
c.emitStoreVar(t.Names[0].Name)
} else if n, ok := c.isStructType(t.Type); ok {
emit.Int(c.prog.BinWriter, int64(n))
emit.Opcode(c.prog.BinWriter, opcode.NEWSTRUCT)
l := c.scope.loadLocal(t.Names[0].Name)
c.emitStoreLocal(l)
c.emitStoreVar(t.Names[0].Name)
} else {
for _, id := range t.Names {
c.emitDefault(t.Type)
c.emitStoreVar(id.Name)
}
}
}
}
@ -337,11 +402,10 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
case *ast.Ident:
switch n.Tok {
case token.ADD_ASSIGN, token.SUB_ASSIGN, token.MUL_ASSIGN, token.QUO_ASSIGN, token.REM_ASSIGN:
c.emitLoadLocal(t.Name)
c.emitLoadVar(t.Name)
ast.Walk(c, n.Rhs[0]) // can only add assign to 1 expr on the RHS
c.convertToken(n.Tok)
l := c.scope.loadLocal(t.Name)
c.emitStoreLocal(l)
c.emitStoreVar(t.Name)
case token.DEFINE:
if !multiRet {
c.registerDebugVariable(t.Name, n.Rhs[i])
@ -355,8 +419,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
if t.Name == "_" {
emit.Opcode(c.prog.BinWriter, opcode.DROP)
} else {
l := c.scope.loadLocal(t.Name)
c.emitStoreLocal(l)
c.emitStoreVar(t.Name)
}
}
@ -366,7 +429,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
ast.Walk(c, n.Rhs[i])
typ := c.typeInfo.ObjectOf(expr).Type().Underlying()
if strct, ok := typ.(*types.Struct); ok {
c.emitLoadLocal(expr.Name) // load the struct
c.emitLoadVar(expr.Name) // load the struct
i := indexOfStruct(strct, t.Sel.Name) // get the index of the field
c.emitStoreStructField(i) // store the field
}
@ -380,7 +443,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
case *ast.IndexExpr:
ast.Walk(c, n.Rhs[i])
name := t.X.(*ast.Ident).Name
c.emitLoadLocal(name)
c.emitLoadVar(name)
switch ind := t.Index.(type) {
case *ast.BasicLit:
indexStr := ind.Value
@ -391,7 +454,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
}
c.emitStoreStructField(index)
case *ast.Ident:
c.emitLoadLocal(ind.Name)
c.emitLoadVar(ind.Name)
emit.Opcode(c.prog.BinWriter, opcode.ROT)
emit.Opcode(c.prog.BinWriter, opcode.SETITEM)
default:
@ -404,7 +467,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
case *ast.SliceExpr:
name := n.X.(*ast.Ident).Name
c.emitLoadLocal(name)
c.emitLoadVar(name)
if n.Low != nil {
ast.Walk(c, n.Low)
@ -442,7 +505,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
for i := len(results.List) - 1; i >= 0; i-- {
names := results.List[i].Names
for j := len(names) - 1; j >= 0; j-- {
c.emitLoadLocal(names[j].Name)
c.emitLoadVar(names[j].Name)
}
}
}
@ -454,8 +517,6 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
}
c.saveSequencePoint(n)
emit.Opcode(c.prog.BinWriter, opcode.FROMALTSTACK)
emit.Opcode(c.prog.BinWriter, opcode.DROP) // Cleanup the stack.
emit.Opcode(c.prog.BinWriter, opcode.RET)
return nil
@ -557,7 +618,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
} else if tv := c.typeInfo.Types[n]; tv.Value != nil {
c.emitLoadConst(tv)
} else {
c.emitLoadLocal(n.Name)
c.emitLoadVar(n.Name)
}
return nil
@ -723,7 +784,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
c.convertBuiltin(n)
case name != "":
// Function was not found thus is can be only an invocation of func-typed variable.
c.emitLoadLocal(name)
c.emitLoadVar(name)
emit.Opcode(c.prog.BinWriter, opcode.CALLA)
case isSyscall(f):
c.convertSyscall(n, f.selector.Name, f.name)
@ -738,7 +799,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
case *ast.Ident:
typ := c.typeInfo.ObjectOf(t).Type().Underlying()
if strct, ok := typ.(*types.Struct); ok {
c.emitLoadLocal(t.Name) // load the struct
c.emitLoadVar(t.Name) // load the struct
i := indexOfStruct(strct, n.Sel.Name)
c.emitLoadField(i) // load the field
}
@ -777,8 +838,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
// for i := 0; i < 10; i++ {}
// Where the post stmt is ( i++ )
if ident, ok := n.X.(*ast.Ident); ok {
pos := c.scope.loadLocal(ident.Name)
c.emitStoreLocal(pos)
c.emitStoreVar(ident.Name)
}
return nil
@ -913,9 +973,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
if n.Key != nil {
emit.Opcode(c.prog.BinWriter, opcode.DUP)
pos := c.scope.loadLocal(n.Key.(*ast.Ident).Name)
c.emitStoreLocal(pos)
c.emitStoreVar(n.Key.(*ast.Ident).Name)
}
ast.Walk(c, n.Body)
@ -1315,6 +1373,8 @@ func (c *codegen) compile(info *buildInfo, pkg *loader.PackageInfo) error {
}
}
c.traverseGlobals(mainFile)
// convert the entry point first.
c.convertFuncDecl(mainFile, main)
@ -1354,6 +1414,7 @@ func newCodegen(info *buildInfo, pkg *loader.PackageInfo) *codegen {
l: []int{},
funcs: map[string]*funcScope{},
lambda: map[string]*funcScope{},
globals: map[string]int{},
labels: map[labelWithType]uint16{},
typeInfo: &pkg.Info,

View file

@ -115,6 +115,10 @@ type Event struct {
}
func (c *codegen) saveSequencePoint(n ast.Node) {
if c.scope == nil {
// do not save globals for now
return
}
fset := c.buildInfo.program.Fset
start := fset.Position(n.Pos())
end := fset.Position(n.End())
@ -143,6 +147,10 @@ func (c *codegen) emitDebugInfo() *DebugInfo {
}
func (c *codegen) registerDebugVariable(name string, expr ast.Expr) {
if c.scope == nil {
// do not save globals for now
return
}
typ := c.scTypeFromExpr(expr)
c.scope.variables = append(c.scope.variables, name+","+typ)
}

View file

@ -27,7 +27,8 @@ type funcScope struct {
variables []string
// Local variables
locals map[string]int
locals map[string]int
arguments map[string]int
// voidCalls are basically functions that return their value
// into nothing. The stack has their return value but there
@ -51,6 +52,7 @@ func newFuncScope(decl *ast.FuncDecl, label uint16) *funcScope {
decl: decl,
label: label,
locals: map[string]int{},
arguments: map[string]int{},
voidCalls: map[*ast.CallExpr]bool{},
variables: []string{},
i: -1,
@ -84,7 +86,7 @@ func (c *funcScope) analyzeVoidCalls(node ast.Node) bool {
return true
}
func (c *funcScope) stackSize() int64 {
func (c *funcScope) countLocals() int {
size := 0
ast.Inspect(c.decl, func(n ast.Node) bool {
switch n := n.(type) {
@ -110,28 +112,38 @@ func (c *funcScope) stackSize() int64 {
}
return true
})
return size
}
numArgs := len(c.decl.Type.Params.List)
// Also take care of struct methods recv: e.g. (t Token).Foo().
func (c *funcScope) countArgs() int {
n := c.decl.Type.Params.NumFields()
if c.decl.Recv != nil {
numArgs += len(c.decl.Recv.List)
n += c.decl.Recv.NumFields()
}
return n
}
func (c *funcScope) stackSize() int64 {
size := c.countLocals()
numArgs := c.countArgs()
return int64(size + numArgs + len(c.voidCalls))
}
// newVariable creates a new local variable or argument in the scope of the function.
func (c *funcScope) newVariable(t varType, name string) int {
c.i++
switch t {
case varLocal:
c.locals[name] = c.i
case varArgument:
c.arguments[name] = c.i
default:
panic("invalid type")
}
return c.i
}
// newLocal creates a new local variable into the scope of the function.
func (c *funcScope) newLocal(name string) int {
c.i++
c.locals[name] = c.i
return c.i
}
// loadLocal loads the position of a local variable inside the scope of the function.
func (c *funcScope) loadLocal(name string) int {
i, ok := c.locals[name]
if !ok {
// should emit a compiler warning.
return c.newLocal(name)
}
return i
return c.newVariable(varLocal, name)
}