Compiler update (#21)
* added seperate folders for cmd packages. * Fix netmodes in test + reverse bigint bytes * glide get deps * add, sub, mul, div * booleans * strings * binary expressions * if statements * function calls * composite literals (slice, array) * Added lots of test cases and update readme.
This commit is contained in:
parent
f7d57e4e49
commit
b257a06f3e
18 changed files with 1253 additions and 494 deletions
503
pkg/vm/compiler/compiler.go
Normal file
503
pkg/vm/compiler/compiler.go
Normal file
|
@ -0,0 +1,503 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/CityOfZion/neo-go/pkg/vm"
|
||||
)
|
||||
|
||||
const (
|
||||
outputExt = ".avm"
|
||||
// Identifier off the entry point function.
|
||||
mainIdent = "Main"
|
||||
)
|
||||
|
||||
// CallContext represents more details on function calls in the program.
|
||||
// It stores the position off where the call happend along with the
|
||||
// function it called. The compiler will store all function calls, so
|
||||
// it can update them later.
|
||||
type CallContext struct {
|
||||
pos int
|
||||
funcName string
|
||||
}
|
||||
|
||||
// A VarContext holds the info about the context of a variable in the program.
|
||||
type VarContext struct {
|
||||
name string
|
||||
tinfo types.TypeAndValue
|
||||
pos int
|
||||
}
|
||||
|
||||
func newVarContext(name string, tinfo types.TypeAndValue) *VarContext {
|
||||
return &VarContext{
|
||||
name: name,
|
||||
pos: -1,
|
||||
tinfo: tinfo,
|
||||
}
|
||||
}
|
||||
|
||||
// Compiler holds the output buffer of the compiled source.
|
||||
type Compiler struct {
|
||||
// Output extension of the file. Default .avm.
|
||||
OutputExt string
|
||||
|
||||
// scriptBuilder is responsible for all opcode writes.
|
||||
sb *ScriptBuilder
|
||||
|
||||
// map with all function contexts across the program.
|
||||
funcs map[string]*FuncContext
|
||||
|
||||
// list of function calls
|
||||
funcCalls []CallContext
|
||||
|
||||
// struct with info about decls, types, uses, ..
|
||||
typeInfo *types.Info
|
||||
}
|
||||
|
||||
// New returns a new compiler ready to compile smartcontracts.
|
||||
func New() *Compiler {
|
||||
return &Compiler{
|
||||
OutputExt: outputExt,
|
||||
sb: &ScriptBuilder{buf: new(bytes.Buffer)},
|
||||
funcs: map[string]*FuncContext{},
|
||||
funcCalls: []CallContext{},
|
||||
}
|
||||
}
|
||||
|
||||
// CompileSource will compile the source file into an avm format.
|
||||
func (c *Compiler) CompileSource(src string) error {
|
||||
file, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Compile(file)
|
||||
}
|
||||
|
||||
// LoadConst load a constant, if storeLocal is true it will store it on the position
|
||||
// of the VarContext.
|
||||
func (c *Compiler) loadConst(ctx *VarContext, storeLocal bool) {
|
||||
switch t := ctx.tinfo.Type.(type) {
|
||||
case *types.Basic:
|
||||
switch t.Kind() {
|
||||
case types.Int:
|
||||
val, _ := constant.Int64Val(ctx.tinfo.Value)
|
||||
c.sb.emitPushInt(val)
|
||||
case types.String:
|
||||
val := constant.StringVal(ctx.tinfo.Value)
|
||||
c.sb.emitPushString(val)
|
||||
case types.Bool, types.UntypedBool:
|
||||
val := constant.BoolVal(ctx.tinfo.Value)
|
||||
c.sb.emitPushBool(val)
|
||||
}
|
||||
default:
|
||||
log.Fatalf("compiler don't know how to handle this => %v", ctx)
|
||||
}
|
||||
|
||||
if storeLocal {
|
||||
c.storeLocal(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Load a local variable. The position of the VarContext is used to retrieve from
|
||||
// that position.
|
||||
func (c *Compiler) loadLocal(ctx *VarContext) {
|
||||
pos := int64(ctx.pos)
|
||||
if pos < 0 {
|
||||
log.Fatalf("want to load local %v but got invalid position => %d <=", ctx, pos)
|
||||
}
|
||||
|
||||
c.sb.emitPush(vm.OpFromAltStack)
|
||||
c.sb.emitPush(vm.OpDup)
|
||||
c.sb.emitPush(vm.OpToAltStack)
|
||||
|
||||
// push it's index on the stack
|
||||
c.sb.emitPushInt(pos)
|
||||
c.sb.emitPush(vm.OpPickItem)
|
||||
}
|
||||
|
||||
// Store a local variable on the stack. The position of the VarContext is used
|
||||
// to store at that position.
|
||||
func (c *Compiler) storeLocal(vctx *VarContext) {
|
||||
c.sb.emitPush(vm.OpFromAltStack)
|
||||
c.sb.emitPush(vm.OpDup)
|
||||
c.sb.emitPush(vm.OpToAltStack)
|
||||
|
||||
pos := int64(vctx.pos)
|
||||
if pos < 0 {
|
||||
log.Fatalf("want to store local %v but got invalid positionl => %d", vctx, pos)
|
||||
}
|
||||
|
||||
c.sb.emitPushInt(pos)
|
||||
c.sb.emitPushInt(2)
|
||||
c.sb.emitPush(vm.OpRoll)
|
||||
c.sb.emitPush(vm.OpSetItem)
|
||||
}
|
||||
|
||||
// Compile will compile from r into an avm format.
|
||||
func (c *Compiler) Compile(r io.Reader) error {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "", r, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf := types.Config{Importer: importer.Default()}
|
||||
typeInfo := &types.Info{
|
||||
Types: make(map[ast.Expr]types.TypeAndValue),
|
||||
Defs: make(map[*ast.Ident]types.Object),
|
||||
Uses: make(map[*ast.Ident]types.Object),
|
||||
Implicits: make(map[ast.Node]types.Object),
|
||||
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
||||
Scopes: make(map[ast.Node]*types.Scope),
|
||||
}
|
||||
|
||||
c.typeInfo = typeInfo
|
||||
|
||||
// Typechecker
|
||||
_, err = conf.Check("", fset, []*ast.File{f}, typeInfo)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var main *ast.FuncDecl
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
switch t := n.(type) {
|
||||
case *ast.FuncDecl:
|
||||
if t.Name.Name == mainIdent {
|
||||
main = t
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if main == nil {
|
||||
log.Fatal("could not find func main. did you forgot to declare it?")
|
||||
}
|
||||
|
||||
c.resolveFuncDecls(f)
|
||||
c.convertFuncDecl(main)
|
||||
|
||||
// Start building all declarations
|
||||
for _, decl := range f.Decls {
|
||||
switch t := decl.(type) {
|
||||
case *ast.GenDecl:
|
||||
case *ast.FuncDecl:
|
||||
if t.Name.Name != mainIdent {
|
||||
c.convertFuncDecl(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update all local function calls.
|
||||
c.updateFuncCalls()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateFuncCalls will update all local function calls that occured the program.
|
||||
func (c *Compiler) updateFuncCalls() {
|
||||
for _, ctx := range c.funcCalls {
|
||||
fun, ok := c.funcs[ctx.funcName]
|
||||
if !ok {
|
||||
log.Fatalf("could not resolve function %s", ctx.funcName)
|
||||
}
|
||||
// pos is the position of the call op, we need to add 1 to get the
|
||||
// start of the label.
|
||||
// for calculating the correct offset we need to subtract the target label
|
||||
// with the position the call occured.
|
||||
offset := fun.label - int16(ctx.pos)
|
||||
c.sb.updatePushCall(ctx.pos+1, offset)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Compiler) resolveFuncDecls(f *ast.File) {
|
||||
for _, decl := range f.Decls {
|
||||
switch t := decl.(type) {
|
||||
case *ast.GenDecl:
|
||||
case *ast.FuncDecl:
|
||||
if t.Name.Name != mainIdent {
|
||||
c.funcs[t.Name.Name] = newFuncContext(t, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Compiler) convertFuncDecl(decl *ast.FuncDecl) {
|
||||
ctx := newFuncContext(decl, c.currentPos())
|
||||
c.funcs[ctx.name] = ctx
|
||||
|
||||
// We need to write the the total stack size of the function first.
|
||||
// That size is the number of arguments + body operations that will be
|
||||
// pushed on the stack
|
||||
c.sb.emitPushInt(ctx.numStackOps())
|
||||
c.sb.emitPush(vm.OpNewArray)
|
||||
c.sb.emitPush(vm.OpToAltStack)
|
||||
|
||||
// Load the arguments into scope.
|
||||
for _, arg := range decl.Type.Params.List {
|
||||
name := arg.Names[0].Name
|
||||
ctx.args[name] = true
|
||||
vctx := ctx.newConst(name, c.getTypeInfo(arg.Type), true)
|
||||
c.storeLocal(vctx)
|
||||
}
|
||||
|
||||
for _, stmt := range decl.Body.List {
|
||||
c.convertStmt(ctx, stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Compiler) convertStmt(fctx *FuncContext, stmt ast.Stmt) {
|
||||
switch t := stmt.(type) {
|
||||
case *ast.AssignStmt:
|
||||
for i := 0; i < len(t.Lhs); i++ {
|
||||
lhs := t.Lhs[i].(*ast.Ident)
|
||||
|
||||
switch rhs := t.Rhs[i].(type) {
|
||||
case *ast.BasicLit:
|
||||
vctx := fctx.newConst(lhs.Name, c.getTypeInfo(t.Rhs[i]), true)
|
||||
c.loadConst(vctx, true)
|
||||
|
||||
case *ast.CompositeLit:
|
||||
// Write constants in reverse order on the stack.
|
||||
n := len(rhs.Elts)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
vctx := fctx.newConst("", c.getTypeInfo(rhs.Elts[i]), false)
|
||||
c.loadConst(vctx, false)
|
||||
}
|
||||
|
||||
c.sb.emitPushInt(int64(n))
|
||||
c.sb.emitPush(vm.OpPack)
|
||||
|
||||
vctx := fctx.newConst(lhs.Name, c.getTypeInfo(rhs), true)
|
||||
c.storeLocal(vctx)
|
||||
|
||||
case *ast.Ident:
|
||||
if isIdentBool(rhs) {
|
||||
vctx := fctx.newConst(lhs.Name, makeBoolFromIdent(rhs, c.typeInfo), true)
|
||||
c.loadConst(vctx, true)
|
||||
continue
|
||||
}
|
||||
|
||||
knownCtx := fctx.getContext(rhs.Name)
|
||||
c.loadLocal(knownCtx)
|
||||
newCtx := fctx.newConst(lhs.Name, c.getTypeInfo(rhs), true)
|
||||
c.storeLocal(newCtx)
|
||||
|
||||
default:
|
||||
c.convertExpr(fctx, t.Rhs[i])
|
||||
vctx := fctx.newConst(lhs.Name, c.getTypeInfo(t.Rhs[i]), true)
|
||||
c.storeLocal(vctx)
|
||||
}
|
||||
}
|
||||
|
||||
//Due to the design of the orginal VM, multiple return are not supported.
|
||||
case *ast.ReturnStmt:
|
||||
if len(t.Results) > 1 {
|
||||
log.Fatal("multiple returns not supported.")
|
||||
}
|
||||
|
||||
c.sb.emitPush(vm.OpJMP)
|
||||
c.sb.emitPush(vm.OpCode(0x03))
|
||||
c.sb.emitPush(vm.OpPush0)
|
||||
|
||||
c.convertExpr(fctx, t.Results[0])
|
||||
|
||||
c.sb.emitPush(vm.OpNOP)
|
||||
c.sb.emitPush(vm.OpFromAltStack)
|
||||
c.sb.emitPush(vm.OpDrop)
|
||||
c.sb.emitPush(vm.OpRET)
|
||||
|
||||
// TODO: this needs a rewrite ASAP.
|
||||
case *ast.IfStmt:
|
||||
c.convertExpr(fctx, t.Cond)
|
||||
|
||||
binExpr, ok := t.Cond.(*ast.BinaryExpr)
|
||||
if ok && binExpr.Op != token.LAND && binExpr.Op != token.LOR {
|
||||
// use a placeholder for the label.
|
||||
c.sb.emitJump(vm.OpJMPIFNOT, int16(0))
|
||||
// track our offset to update later subtract sizeOf int16.
|
||||
offset := int(c.currentPos()) - 2
|
||||
|
||||
defer func(offset int) {
|
||||
jumpTo := c.currentPos() + 1 - int16(offset)
|
||||
c.sb.updateJmpLabel(jumpTo, offset)
|
||||
}(offset)
|
||||
}
|
||||
|
||||
labelBeforeBlock := c.currentPos()
|
||||
// Process the block.
|
||||
for _, stmt := range t.Body.List {
|
||||
c.convertStmt(fctx, stmt)
|
||||
}
|
||||
|
||||
// if there are any labels we need to update.
|
||||
if len(fctx.jumpLabels) > 0 {
|
||||
for _, label := range fctx.jumpLabels {
|
||||
var pos int16
|
||||
if label.op == vm.OpJMPIF {
|
||||
pos = labelBeforeBlock + 1
|
||||
} else {
|
||||
pos = c.currentPos() + 1
|
||||
}
|
||||
jumpTo := pos - int16(label.offset)
|
||||
c.sb.updateJmpLabel(jumpTo, label.offset)
|
||||
}
|
||||
fctx.jumpLabels = []jumpLabel{}
|
||||
}
|
||||
|
||||
default:
|
||||
log.Fatalf("compiler has not implemented this statement => %v", reflect.TypeOf(t))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Compiler) convertExpr(fctx *FuncContext, expr ast.Expr) {
|
||||
switch t := expr.(type) {
|
||||
case *ast.BasicLit:
|
||||
vctx := fctx.newConst("", c.getTypeInfo(t), false)
|
||||
c.loadConst(vctx, false)
|
||||
|
||||
case *ast.Ident:
|
||||
if isIdentBool(t) {
|
||||
vctx := fctx.newConst(t.Name, makeBoolFromIdent(t, c.typeInfo), false)
|
||||
c.loadConst(vctx, false)
|
||||
return
|
||||
}
|
||||
if fctx.isArgument(t.Name) {
|
||||
vctx := fctx.getContext(t.Name)
|
||||
c.loadLocal(vctx)
|
||||
return
|
||||
}
|
||||
vctx := fctx.getContext(t.Name)
|
||||
c.loadLocal(vctx)
|
||||
|
||||
case *ast.CallExpr:
|
||||
fun := t.Fun.(*ast.Ident)
|
||||
fctx, ok := c.funcs[fun.Name]
|
||||
if !ok {
|
||||
log.Fatalf("could not resolve func %s", fun.Name)
|
||||
}
|
||||
|
||||
// handle the passed arguments.
|
||||
for _, arg := range t.Args {
|
||||
vctx := fctx.newConst("", c.getTypeInfo(arg), false)
|
||||
c.loadLocal(vctx)
|
||||
}
|
||||
|
||||
// c# compiler adds a NOP (0x61) before every function call. Dont think its relevant
|
||||
// and we could easily removed it, but to be consistent with the original compiler I
|
||||
// will put them in. ^^
|
||||
c.sb.emitPush(vm.OpNOP)
|
||||
|
||||
c.funcCalls = append(c.funcCalls, CallContext{int(c.currentPos()), fun.Name})
|
||||
c.sb.emitPushCall(0) // placeholder, update later.
|
||||
|
||||
case *ast.BinaryExpr:
|
||||
if t.Op == token.LAND || t.Op == token.LOR {
|
||||
c.convertExpr(fctx, t.X)
|
||||
|
||||
opJMP := vm.OpJMPIFNOT
|
||||
if t.Op == token.LOR {
|
||||
opJMP = vm.OpJMPIF
|
||||
}
|
||||
|
||||
if e, ok := t.X.(*ast.BinaryExpr); ok && e.Op != token.LAND && e.Op != token.LOR {
|
||||
c.sb.emitJump(opJMP, int16(0))
|
||||
fctx.addJump(opJMP, int(c.currentPos())-2)
|
||||
}
|
||||
|
||||
c.convertExpr(fctx, t.Y)
|
||||
c.sb.emitJump(vm.OpJMPIFNOT, int16(0))
|
||||
fctx.addJump(vm.OpJMPIFNOT, int(c.currentPos())-2)
|
||||
c.convertToken(t.Op)
|
||||
return
|
||||
}
|
||||
|
||||
// The AST package resolves all basic literals for us. If the typeinfo.Value is not nil
|
||||
// we know that the bin expr is resolved and needs no further action.
|
||||
// e.g. x := 2 + 2 + 2 will be resolved to 6.
|
||||
if tinfo := c.getTypeInfo(t); tinfo.Value != nil {
|
||||
vctx := fctx.newConst("", tinfo, false)
|
||||
c.loadConst(vctx, false)
|
||||
return
|
||||
}
|
||||
|
||||
c.convertExpr(fctx, t.X)
|
||||
c.convertExpr(fctx, t.Y)
|
||||
c.convertToken(t.Op)
|
||||
|
||||
default:
|
||||
log.Fatalf("compiler has not implemented this expr => %v", reflect.TypeOf(t))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Compiler) convertToken(tok token.Token) {
|
||||
switch tok {
|
||||
case token.ADD:
|
||||
c.sb.emitPush(vm.OpAdd)
|
||||
case token.SUB:
|
||||
c.sb.emitPush(vm.OpSub)
|
||||
case token.MUL:
|
||||
c.sb.emitPush(vm.OpMul)
|
||||
case token.QUO:
|
||||
c.sb.emitPush(vm.OpDiv)
|
||||
case token.LSS:
|
||||
c.sb.emitPush(vm.OpLT)
|
||||
case token.LEQ:
|
||||
c.sb.emitPush(vm.OpLTE)
|
||||
case token.GTR:
|
||||
c.sb.emitPush(vm.OpGT)
|
||||
case token.GEQ:
|
||||
c.sb.emitPush(vm.OpGTE)
|
||||
}
|
||||
}
|
||||
|
||||
// getTypeInfo return TypeAndValue for the given expression. If it could not resolve
|
||||
// the type value and type will be NIL.
|
||||
func (c *Compiler) getTypeInfo(expr ast.Expr) types.TypeAndValue {
|
||||
return c.typeInfo.Types[expr]
|
||||
}
|
||||
|
||||
// currentPos return the current position (address) of the latest opcode.
|
||||
func (c *Compiler) currentPos() int16 {
|
||||
return int16(c.sb.buf.Len())
|
||||
}
|
||||
|
||||
// Buffer returns the buffer of the builder as a io.Reader.
|
||||
func (c *Compiler) Buffer() *bytes.Buffer {
|
||||
return c.sb.buf
|
||||
}
|
||||
|
||||
// DumpOpcode dumps the current buffer, formatted with index, hex and opcode.
|
||||
func (c *Compiler) DumpOpcode() {
|
||||
c.sb.dumpOpcode()
|
||||
}
|
||||
|
||||
func makeBoolFromIdent(ident *ast.Ident, tinfo *types.Info) types.TypeAndValue {
|
||||
var b bool
|
||||
if ident.Name == "true" {
|
||||
b = true
|
||||
} else if ident.Name == "false" {
|
||||
b = false
|
||||
} else {
|
||||
log.Fatalf("givent identifier cannot be converted to a boolean => %s", ident.Name)
|
||||
}
|
||||
|
||||
return types.TypeAndValue{
|
||||
Type: tinfo.ObjectOf(ident).Type(),
|
||||
Value: constant.MakeBool(b),
|
||||
}
|
||||
}
|
||||
|
||||
func isIdentBool(ident *ast.Ident) bool {
|
||||
return ident.Name == "true" || ident.Name == "false"
|
||||
}
|
96
pkg/vm/compiler/func_context.go
Normal file
96
pkg/vm/compiler/func_context.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/types"
|
||||
"log"
|
||||
|
||||
"github.com/CityOfZion/neo-go/pkg/vm"
|
||||
)
|
||||
|
||||
type jumpLabel struct {
|
||||
offset int
|
||||
op vm.OpCode
|
||||
}
|
||||
|
||||
// A FuncContext represents details about a function in the program along withs its variables.
|
||||
type FuncContext struct {
|
||||
// The declaration tree of this function.
|
||||
decl *ast.FuncDecl
|
||||
// Identifier (name of the function in the program).
|
||||
name string
|
||||
// The scope of the function.
|
||||
scope map[string]*VarContext
|
||||
// Arguments of the function.
|
||||
args map[string]bool
|
||||
// Address (label) where the compiler can find this function when someone calls it.
|
||||
label int16
|
||||
// Counter for stored local variables.
|
||||
i int
|
||||
// This needs refactor along with the (if stmt)
|
||||
jumpLabels []jumpLabel
|
||||
}
|
||||
|
||||
func (f *FuncContext) addJump(op vm.OpCode, offset int) {
|
||||
f.jumpLabels = append(f.jumpLabels, jumpLabel{offset, op})
|
||||
}
|
||||
|
||||
func newFuncContext(decl *ast.FuncDecl, label int16) *FuncContext {
|
||||
return &FuncContext{
|
||||
decl: decl,
|
||||
label: int16(label),
|
||||
name: decl.Name.Name,
|
||||
scope: map[string]*VarContext{},
|
||||
args: map[string]bool{},
|
||||
jumpLabels: []jumpLabel{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FuncContext) newConst(name string, t types.TypeAndValue, needStore bool) *VarContext {
|
||||
ctx := &VarContext{
|
||||
name: name,
|
||||
tinfo: t,
|
||||
}
|
||||
if needStore {
|
||||
f.storeContext(ctx)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (f *FuncContext) numStackOps() int64 {
|
||||
ops := 0
|
||||
ast.Inspect(f.decl, func(n ast.Node) bool {
|
||||
switch n.(type) {
|
||||
case *ast.AssignStmt, *ast.ReturnStmt, *ast.IfStmt:
|
||||
ops++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
numArgs := len(f.decl.Type.Params.List)
|
||||
return int64(ops + numArgs)
|
||||
}
|
||||
|
||||
func (f *FuncContext) storeContext(ctx *VarContext) {
|
||||
ctx.pos = f.i
|
||||
f.scope[ctx.name] = ctx
|
||||
f.i++
|
||||
}
|
||||
|
||||
func (f *FuncContext) getContext(name string) *VarContext {
|
||||
ctx, ok := f.scope[name]
|
||||
if !ok {
|
||||
log.Fatalf("could not resolve variable %s", name)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (f *FuncContext) isRegistered(ctx *VarContext) bool {
|
||||
_, ok := f.scope[ctx.name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (f *FuncContext) isArgument(name string) bool {
|
||||
_, ok := f.args[name]
|
||||
return ok
|
||||
}
|
152
pkg/vm/compiler/script_builder.go
Normal file
152
pkg/vm/compiler/script_builder.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/CityOfZion/neo-go/pkg/util"
|
||||
"github.com/CityOfZion/neo-go/pkg/vm"
|
||||
)
|
||||
|
||||
// ScriptBuilder generates bytecode and will write all
|
||||
// generated bytecode into its internal buffer.
|
||||
type ScriptBuilder struct {
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) emit(op vm.OpCode, b []byte) error {
|
||||
if err := sb.buf.WriteByte(byte(op)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := sb.buf.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) emitPush(op vm.OpCode) error {
|
||||
return sb.buf.WriteByte(byte(op))
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) emitPushBool(b bool) error {
|
||||
if b {
|
||||
return sb.emitPush(vm.OpPushT)
|
||||
}
|
||||
return sb.emitPush(vm.OpPushF)
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) emitPushInt(i int64) error {
|
||||
if i == -1 {
|
||||
return sb.emitPush(vm.OpPushM1)
|
||||
}
|
||||
if i == 0 {
|
||||
return sb.emitPush(vm.OpPushF)
|
||||
}
|
||||
if i > 0 && i < 16 {
|
||||
val := vm.OpCode((int(vm.OpPush1) - 1 + int(i)))
|
||||
return sb.emitPush(val)
|
||||
}
|
||||
|
||||
bInt := big.NewInt(i)
|
||||
val := util.ToArrayReverse(bInt.Bytes())
|
||||
return sb.emitPushArray(val)
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) emitPushArray(b []byte) error {
|
||||
var (
|
||||
err error
|
||||
n = len(b)
|
||||
)
|
||||
|
||||
if n == 0 {
|
||||
return errors.New("0 bytes given in pushArray")
|
||||
}
|
||||
if n <= int(vm.OpPushBytes75) {
|
||||
return sb.emit(vm.OpCode(n), b)
|
||||
} else if n < 0x100 {
|
||||
err = sb.emit(vm.OpPushData1, []byte{byte(n)})
|
||||
} else if n < 0x10000 {
|
||||
buf := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(buf, uint16(n))
|
||||
err = sb.emit(vm.OpPushData2, buf)
|
||||
} else {
|
||||
buf := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(buf, uint32(n))
|
||||
err = sb.emit(vm.OpPushData4, buf)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = sb.buf.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) emitPushString(str string) error {
|
||||
return sb.emitPushArray([]byte(str))
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) emitSysCall(api string) error {
|
||||
lenAPI := len(api)
|
||||
if lenAPI == 0 {
|
||||
return errors.New("syscall argument cant be 0")
|
||||
}
|
||||
if lenAPI > 252 {
|
||||
return fmt.Errorf("invalid syscall argument: %s", api)
|
||||
}
|
||||
|
||||
bapi := []byte(api)
|
||||
args := make([]byte, lenAPI+1)
|
||||
args[0] = byte(lenAPI)
|
||||
copy(args, bapi[1:])
|
||||
return sb.emit(vm.OpSysCall, args)
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) emitPushCall(offset int16) error {
|
||||
buf := new(bytes.Buffer)
|
||||
binary.Write(buf, binary.LittleEndian, offset)
|
||||
return sb.emit(vm.OpCall, buf.Bytes())
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) emitJump(op vm.OpCode, offset int16) error {
|
||||
if op != vm.OpJMP && op != vm.OpJMPIF && op != vm.OpJMPIFNOT && op != vm.OpCall {
|
||||
return fmt.Errorf("invalid jump opcode: %v", op)
|
||||
}
|
||||
buf := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(buf, uint16(offset))
|
||||
return sb.emit(op, buf)
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) updateJmpLabel(label int16, offset int) error {
|
||||
sizeOfInt16 := 2
|
||||
if sizeOfInt16+offset >= sb.buf.Len() {
|
||||
return fmt.Errorf("cannot update label at offset %d", offset)
|
||||
}
|
||||
|
||||
b := make([]byte, sizeOfInt16)
|
||||
binary.LittleEndian.PutUint16(b, uint16(label))
|
||||
buf := sb.buf.Bytes()
|
||||
copy(buf[offset:offset+sizeOfInt16], b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) updatePushCall(offset int, label int16) {
|
||||
b := new(bytes.Buffer)
|
||||
binary.Write(b, binary.LittleEndian, label)
|
||||
|
||||
buf := sb.buf.Bytes()
|
||||
copy(buf[offset:offset+2], b.Bytes())
|
||||
}
|
||||
|
||||
func (sb *ScriptBuilder) dumpOpcode() {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
|
||||
buf := sb.buf.Bytes()
|
||||
|
||||
fmt.Fprintln(w, "INDEX\tOPCODE\tDESC")
|
||||
for i := 0; i < len(buf); i++ {
|
||||
fmt.Fprintf(w, "%d\t0x%2x\t%s\n", i, buf[i], vm.OpCode(buf[i]))
|
||||
}
|
||||
w.Flush()
|
||||
}
|
104
pkg/vm/compiler/script_builder_test.go
Normal file
104
pkg/vm/compiler/script_builder_test.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/CityOfZion/neo-go/pkg/util"
|
||||
"github.com/CityOfZion/neo-go/pkg/vm"
|
||||
)
|
||||
|
||||
func TestEmitPush(t *testing.T) {
|
||||
sb := &ScriptBuilder{buf: new(bytes.Buffer)}
|
||||
|
||||
if err := sb.emitPush(vm.OpPush1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sb.buf.Len() != 1 {
|
||||
t.Fatalf("expect buffer len of 1 got %d", sb.buf.Len())
|
||||
}
|
||||
}
|
||||
func TestEmitPushIntNeg(t *testing.T) {
|
||||
sb := &ScriptBuilder{buf: new(bytes.Buffer)}
|
||||
val := -1
|
||||
if err := sb.emitPushInt(int64(val)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want, have := vm.OpPushM1, vm.OpCode(sb.buf.Bytes()[0]); want != have {
|
||||
t.Fatalf("expected %v got %v", want, have)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitPushInt0(t *testing.T) {
|
||||
sb := &ScriptBuilder{buf: new(bytes.Buffer)}
|
||||
val := 0
|
||||
if err := sb.emitPushInt(int64(val)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want, have := vm.OpPushF, vm.OpCode(sb.buf.Bytes()[0]); want != have {
|
||||
t.Fatalf("expected %v got %v", want, have)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitPushInt1(t *testing.T) {
|
||||
sb := &ScriptBuilder{buf: new(bytes.Buffer)}
|
||||
val := 1
|
||||
if err := sb.emitPushInt(int64(val)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want, have := vm.OpPush1, vm.OpCode(sb.buf.Bytes()[0]); want != have {
|
||||
t.Fatalf("expected %v got %v", want, have)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitPushInt100(t *testing.T) {
|
||||
x := 100
|
||||
bigx := big.NewInt(int64(x))
|
||||
t.Log(bigx.Bytes())
|
||||
|
||||
sb := &ScriptBuilder{buf: new(bytes.Buffer)}
|
||||
val := 100
|
||||
if err := sb.emitPushInt(int64(val)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// len = 1
|
||||
if want, have := byte(0x01), byte(sb.buf.Bytes()[0]); want != have {
|
||||
t.Fatalf("expected %v got %v", want, have)
|
||||
}
|
||||
if want, have := byte(0x64), byte(sb.buf.Bytes()[1]); want != have {
|
||||
t.Fatalf("expected %v got %v", want, have)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitPush1000(t *testing.T) {
|
||||
sb := &ScriptBuilder{buf: new(bytes.Buffer)}
|
||||
val := 1000
|
||||
if err := sb.emitPushInt(int64(val)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bInt := big.NewInt(int64(val))
|
||||
if want, have := byte(len(bInt.Bytes())), byte(sb.buf.Bytes()[0]); want != have {
|
||||
t.Fatalf("expected %v got %v", want, have)
|
||||
}
|
||||
want := util.ToArrayReverse(bInt.Bytes()) // reverse
|
||||
have := sb.buf.Bytes()[1:]
|
||||
if bytes.Compare(want, have) != 0 {
|
||||
t.Fatalf("expected %v got %v", want, have)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitPushString(t *testing.T) {
|
||||
sb := &ScriptBuilder{buf: new(bytes.Buffer)}
|
||||
str := "anthdm"
|
||||
if err := sb.emitPushString(str); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want, have := byte(len(str)), sb.buf.Bytes()[0]; want != have {
|
||||
t.Fatalf("expected %v got %v", want, have)
|
||||
}
|
||||
want, have := []byte(str), sb.buf.Bytes()[1:]
|
||||
if bytes.Compare(want, have) != 0 {
|
||||
t.Fatalf("expected %v got %v", want, have)
|
||||
}
|
||||
}
|
72
pkg/vm/compiler/tests/binary_expr_test.go
Normal file
72
pkg/vm/compiler/tests/binary_expr_test.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package compiler_test
|
||||
|
||||
var binaryExprTestCases = []testCase{
|
||||
{
|
||||
"simple add",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 2 + 2
|
||||
return x
|
||||
}
|
||||
`,
|
||||
"52c56b546c766b00527ac46203006c766b00c3616c7566",
|
||||
},
|
||||
{
|
||||
"simple sub",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 2 - 2
|
||||
return x
|
||||
}
|
||||
`,
|
||||
"52c56b006c766b00527ac46203006c766b00c3616c7566",
|
||||
},
|
||||
{
|
||||
"simple div",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 2 / 2
|
||||
return x
|
||||
}
|
||||
`,
|
||||
"52c56b516c766b00527ac46203006c766b00c3616c7566",
|
||||
},
|
||||
{
|
||||
"simple mul",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 4 * 2
|
||||
return x
|
||||
}
|
||||
`,
|
||||
"52c56b586c766b00527ac46203006c766b00c3616c7566",
|
||||
},
|
||||
{
|
||||
"simple binary expr in return",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 2
|
||||
return 2 + x
|
||||
}
|
||||
`,
|
||||
"52c56b526c766b00527ac4620300526c766b00c393616c7566",
|
||||
},
|
||||
{
|
||||
"complex binary expr",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 4
|
||||
y := 8
|
||||
z := x + 2 + 2 - 8
|
||||
return y * z
|
||||
}
|
||||
`,
|
||||
"54c56b546c766b00527ac4586c766b51527ac46c766b00c35293529358946c766b52527ac46203006c766b51c36c766b52c395616c7566",
|
||||
},
|
||||
}
|
65
pkg/vm/compiler/tests/compiler_test.go
Normal file
65
pkg/vm/compiler/tests/compiler_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package compiler_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/CityOfZion/neo-go/pkg/vm"
|
||||
"github.com/CityOfZion/neo-go/pkg/vm/compiler"
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
src string
|
||||
result string
|
||||
}
|
||||
|
||||
func TestAllCases(t *testing.T) {
|
||||
testCases := []testCase{}
|
||||
testCases = append(testCases, stringTestCases...)
|
||||
testCases = append(testCases, binaryExprTestCases...)
|
||||
testCases = append(testCases, ifStatementTestCases...)
|
||||
testCases = append(testCases, functionCallTestCases...)
|
||||
|
||||
for _, tc := range testCases {
|
||||
c := compiler.New()
|
||||
if err := c.Compile(strings.NewReader(tc.src)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedResult, err := hex.DecodeString(tc.result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if bytes.Compare(c.Buffer().Bytes(), expectedResult) != 0 {
|
||||
t.Log(hex.EncodeToString(c.Buffer().Bytes()))
|
||||
want, _ := hex.DecodeString(tc.result)
|
||||
dumpOpCodeSideBySide(c.Buffer().Bytes(), want)
|
||||
t.Fatalf("compiling %s failed", tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dumpOpCodeSideBySide(have, want []byte) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
|
||||
fmt.Fprintln(w, "INDEX\tHAVE OPCODE\tDESC\tWANT OPCODE\tDESC\tDIFF")
|
||||
|
||||
for i := 0; i < len(have); i++ {
|
||||
if len(want) <= i {
|
||||
break
|
||||
}
|
||||
diff := ""
|
||||
if have[i] != want[i] {
|
||||
diff = "<<"
|
||||
}
|
||||
fmt.Fprintf(w, "%d\t0x%2x\t%s\t0x%2x\t%s\t%s\n",
|
||||
i, have[i], vm.OpCode(have[i]), want[i], vm.OpCode(want[i]), diff)
|
||||
}
|
||||
w.Flush()
|
||||
}
|
61
pkg/vm/compiler/tests/function_call_test.go
Normal file
61
pkg/vm/compiler/tests/function_call_test.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package compiler_test
|
||||
|
||||
var functionCallTestCases = []testCase{
|
||||
{
|
||||
"simple function call",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 10
|
||||
y := getSomeInteger()
|
||||
return x + y
|
||||
}
|
||||
|
||||
func getSomeInteger() int {
|
||||
x := 10
|
||||
return x
|
||||
}
|
||||
`,
|
||||
"53c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756652c56b5a6c766b00527ac46203006c766b00c3616c7566",
|
||||
},
|
||||
{
|
||||
"multiple function calls",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 10
|
||||
y := getSomeInteger()
|
||||
return x + y
|
||||
}
|
||||
|
||||
func getSomeInteger() int {
|
||||
x := 10
|
||||
y := getSomeOtherInt()
|
||||
return x + y
|
||||
}
|
||||
|
||||
func getSomeOtherInt() int {
|
||||
x := 8
|
||||
return x
|
||||
}
|
||||
`,
|
||||
"53c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756653c56b5a6c766b00527ac461651c006c766b51527ac46203006c766b00c36c766b51c393616c756652c56b586c766b00527ac46203006c766b00c3616c7566",
|
||||
},
|
||||
{
|
||||
"function call with arguments",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 10
|
||||
y := getSomeInteger(x)
|
||||
return y
|
||||
}
|
||||
|
||||
func getSomeInteger(x int) int {
|
||||
y := 8
|
||||
return x + y
|
||||
}
|
||||
`,
|
||||
"53c56b5a6c766b00527ac46c766b00c3616516006c766b51527ac46203006c766b51c3616c756653c56b6c766b00527ac4586c766b51527ac46203006c766b00c36c766b51c393616c7566",
|
||||
},
|
||||
}
|
74
pkg/vm/compiler/tests/if_statement_test.go
Normal file
74
pkg/vm/compiler/tests/if_statement_test.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package compiler_test
|
||||
|
||||
var ifStatementTestCases = []testCase{
|
||||
{
|
||||
"if statement LT",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 10
|
||||
if x < 100 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
`,
|
||||
"54c56b5a6c766b00527ac46c766b00c301649f640b0062030051616c756662030000616c7566",
|
||||
},
|
||||
{
|
||||
"if statement GT",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 10
|
||||
if x > 100 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
`,
|
||||
"54c56b5a6c766b00527ac46c766b00c30164a0640b0062030051616c756662030000616c7566",
|
||||
},
|
||||
{
|
||||
"if statement GTE",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 10
|
||||
if x >= 100 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
`,
|
||||
"54c56b5a6c766b00527ac46c766b00c30164a2640b0062030051616c756662030000616c7566",
|
||||
},
|
||||
{
|
||||
"complex if statement with LAND",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 10
|
||||
if x >= 10 && x <= 20 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
`,
|
||||
"54c56b5a6c766b00527ac46c766b00c35aa26416006c766b00c30114a1640b0062030051616c756662030000616c7566",
|
||||
},
|
||||
{
|
||||
"complex if statement with LOR",
|
||||
`
|
||||
package testcase
|
||||
func Main() int {
|
||||
x := 10
|
||||
if x >= 10 || x <= 20 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
`,
|
||||
"54c56b5a6c766b00527ac46c766b00c35aa2630e006c766b00c30114a1640b0062030051616c756662030000616c7566",
|
||||
},
|
||||
}
|
15
pkg/vm/compiler/tests/string_test.go
Normal file
15
pkg/vm/compiler/tests/string_test.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package compiler_test
|
||||
|
||||
var stringTestCases = []testCase{
|
||||
{
|
||||
"simple string",
|
||||
`
|
||||
package testcase
|
||||
func Main() string {
|
||||
x := "NEO"
|
||||
return x
|
||||
}
|
||||
`,
|
||||
"52c56b034e454f6c766b00527ac46203006c766b00c3616c7566",
|
||||
},
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue