compiler: move it up from vm
It really deserves it, I think. Especially given that it doesn't have any direct usage of `vm` package now.
This commit is contained in:
parent
31add423a8
commit
852e6a335b
12 changed files with 7 additions and 12 deletions
212
pkg/compiler/analysis.go
Normal file
212
pkg/compiler/analysis.go
Normal file
|
@ -0,0 +1,212 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/types"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
var (
|
||||
// Go language builtin functions and custom builtin utility functions.
|
||||
builtinFuncs = []string{
|
||||
"len", "append", "SHA256",
|
||||
"SHA1", "Hash256", "Hash160",
|
||||
"FromAddress", "Equals",
|
||||
}
|
||||
)
|
||||
|
||||
// typeAndValueForField returns a zero initialized typeAndValue for the given type.Var.
|
||||
func typeAndValueForField(fld *types.Var) (types.TypeAndValue, error) {
|
||||
switch t := fld.Type().(type) {
|
||||
case *types.Basic:
|
||||
switch t.Kind() {
|
||||
case types.Int:
|
||||
return types.TypeAndValue{
|
||||
Type: t,
|
||||
Value: constant.MakeInt64(0),
|
||||
}, nil
|
||||
case types.String:
|
||||
return types.TypeAndValue{
|
||||
Type: t,
|
||||
Value: constant.MakeString(""),
|
||||
}, nil
|
||||
case types.Bool, types.UntypedBool:
|
||||
return types.TypeAndValue{
|
||||
Type: t,
|
||||
Value: constant.MakeBool(false),
|
||||
}, nil
|
||||
default:
|
||||
return types.TypeAndValue{}, fmt.Errorf("could not initialize struct field %s to zero, type: %s", fld.Name(), t)
|
||||
}
|
||||
}
|
||||
return types.TypeAndValue{}, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
ast.Inspect(f, func(node ast.Node) bool {
|
||||
switch node.(type) {
|
||||
// Skip all function declarations.
|
||||
case *ast.FuncDecl:
|
||||
return false
|
||||
// After skipping all funcDecls we are sure that each value spec
|
||||
// is a global declared variable or constant.
|
||||
case *ast.ValueSpec:
|
||||
i++
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// isIdentBool looks if the given ident is a boolean.
|
||||
func isIdentBool(ident *ast.Ident) bool {
|
||||
return ident.Name == "true" || ident.Name == "false"
|
||||
}
|
||||
|
||||
// makeBoolFromIdent creates a bool type from an *ast.Ident.
|
||||
func makeBoolFromIdent(ident *ast.Ident, tinfo *types.Info) (types.TypeAndValue, error) {
|
||||
var b bool
|
||||
switch ident.Name {
|
||||
case "true":
|
||||
b = true
|
||||
case "false":
|
||||
b = false
|
||||
default:
|
||||
return types.TypeAndValue{}, fmt.Errorf("givent identifier cannot be converted to a boolean => %s", ident.Name)
|
||||
}
|
||||
return types.TypeAndValue{
|
||||
Type: tinfo.ObjectOf(ident).Type(),
|
||||
Value: constant.MakeBool(b),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolveEntryPoint returns the function declaration of the entrypoint and the corresponding file.
|
||||
func resolveEntryPoint(entry string, pkg *loader.PackageInfo) (*ast.FuncDecl, *ast.File) {
|
||||
var (
|
||||
main *ast.FuncDecl
|
||||
file *ast.File
|
||||
)
|
||||
for _, f := range pkg.Files {
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
switch t := n.(type) {
|
||||
case *ast.FuncDecl:
|
||||
if t.Name.Name == entry {
|
||||
main = t
|
||||
file = f
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return main, file
|
||||
}
|
||||
|
||||
// indexOfStruct returns the index of the given field inside that struct.
|
||||
// If the struct does not contain that field it will return -1.
|
||||
func indexOfStruct(strct *types.Struct, fldName string) int {
|
||||
for i := 0; i < strct.NumFields(); i++ {
|
||||
if strct.Field(i).Name() == fldName {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type funcUsage map[string]bool
|
||||
|
||||
func (f funcUsage) funcUsed(name string) bool {
|
||||
_, ok := f[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// hasReturnStmt looks if the given FuncDecl has a return statement.
|
||||
func hasReturnStmt(decl ast.Node) (b bool) {
|
||||
ast.Inspect(decl, func(node ast.Node) bool {
|
||||
if _, ok := node.(*ast.ReturnStmt); ok {
|
||||
b = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func analyzeFuncUsage(pkgs map[*types.Package]*loader.PackageInfo) funcUsage {
|
||||
usage := funcUsage{}
|
||||
|
||||
for _, pkg := range pkgs {
|
||||
for _, f := range pkg.Files {
|
||||
ast.Inspect(f, func(node ast.Node) bool {
|
||||
switch n := node.(type) {
|
||||
case *ast.CallExpr:
|
||||
switch t := n.Fun.(type) {
|
||||
case *ast.Ident:
|
||||
usage[t.Name] = true
|
||||
case *ast.SelectorExpr:
|
||||
usage[t.Sel.Name] = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
func isBuiltin(expr ast.Expr) bool {
|
||||
var name string
|
||||
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
name = t.Name
|
||||
case *ast.SelectorExpr:
|
||||
name = t.Sel.Name
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
for _, n := range builtinFuncs {
|
||||
if name == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isByteArray(lit *ast.CompositeLit, tInfo *types.Info) bool {
|
||||
if len(lit.Elts) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
typ := tInfo.Types[lit.Elts[0]].Type.Underlying()
|
||||
switch t := typ.(type) {
|
||||
case *types.Basic:
|
||||
switch t.Kind() {
|
||||
case types.Byte:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isSyscall(fun *funcScope) bool {
|
||||
if fun.selector == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := syscalls[fun.selector.Name][fun.name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func isByteArrayType(t types.Type) bool {
|
||||
return t.String() == "[]byte"
|
||||
}
|
||||
|
||||
func isStringType(t types.Type) bool {
|
||||
return t.String() == "string"
|
||||
}
|
882
pkg/compiler/codegen.go
Normal file
882
pkg/compiler/codegen.go
Normal file
|
@ -0,0 +1,882 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/CityOfZion/neo-go/pkg/crypto"
|
||||
"github.com/CityOfZion/neo-go/pkg/io"
|
||||
"github.com/CityOfZion/neo-go/pkg/vm/opcode"
|
||||
)
|
||||
|
||||
// The identifier of the entry function. Default set to Main.
|
||||
const mainIdent = "Main"
|
||||
|
||||
type codegen struct {
|
||||
// Information about the program with all its dependencies.
|
||||
buildInfo *buildInfo
|
||||
|
||||
// prog holds the output buffer.
|
||||
prog *io.BufBinWriter
|
||||
|
||||
// Type information.
|
||||
typeInfo *types.Info
|
||||
|
||||
// A mapping of func identifiers with their scope.
|
||||
funcs map[string]*funcScope
|
||||
|
||||
// Current funcScope being converted.
|
||||
scope *funcScope
|
||||
|
||||
// Label table for recording jump destinations.
|
||||
l []int
|
||||
}
|
||||
|
||||
// newLabel creates a new label to jump to
|
||||
func (c *codegen) newLabel() (l int) {
|
||||
l = len(c.l)
|
||||
c.l = append(c.l, -1)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *codegen) setLabel(l int) {
|
||||
c.l[l] = c.pc() + 1
|
||||
}
|
||||
|
||||
// pc returns the program offset off the last instruction.
|
||||
func (c *codegen) pc() int {
|
||||
return c.prog.Len() - 1
|
||||
}
|
||||
|
||||
func (c *codegen) emitLoadConst(t types.TypeAndValue) {
|
||||
if c.prog.Err != nil {
|
||||
return
|
||||
}
|
||||
switch typ := t.Type.Underlying().(type) {
|
||||
case *types.Basic:
|
||||
switch typ.Kind() {
|
||||
case types.Int, types.UntypedInt, types.Uint:
|
||||
val, _ := constant.Int64Val(t.Value)
|
||||
emitInt(c.prog.BinWriter, val)
|
||||
case types.String, types.UntypedString:
|
||||
val := constant.StringVal(t.Value)
|
||||
emitString(c.prog.BinWriter, val)
|
||||
case types.Bool, types.UntypedBool:
|
||||
val := constant.BoolVal(t.Value)
|
||||
emitBool(c.prog.BinWriter, val)
|
||||
case types.Byte:
|
||||
val, _ := constant.Int64Val(t.Value)
|
||||
b := byte(val)
|
||||
emitBytes(c.prog.BinWriter, []byte{b})
|
||||
default:
|
||||
c.prog.Err = fmt.Errorf("compiler doesn't know how to convert this basic type: %v", t)
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.prog.Err = fmt.Errorf("compiler doesn't know how to convert this constant: %v", t)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
emitOpcode(c.prog.BinWriter, opcode.DUPFROMALTSTACK)
|
||||
emitInt(c.prog.BinWriter, int64(pos))
|
||||
emitOpcode(c.prog.BinWriter, opcode.PICKITEM)
|
||||
}
|
||||
|
||||
func (c *codegen) emitStoreLocal(pos int) {
|
||||
emitOpcode(c.prog.BinWriter, opcode.DUPFROMALTSTACK)
|
||||
|
||||
if pos < 0 {
|
||||
c.prog.Err = fmt.Errorf("invalid position to store local: %d", pos)
|
||||
return
|
||||
}
|
||||
|
||||
emitInt(c.prog.BinWriter, int64(pos))
|
||||
emitInt(c.prog.BinWriter, 2)
|
||||
emitOpcode(c.prog.BinWriter, opcode.ROLL)
|
||||
emitOpcode(c.prog.BinWriter, opcode.SETITEM)
|
||||
}
|
||||
|
||||
func (c *codegen) emitLoadField(i int) {
|
||||
emitInt(c.prog.BinWriter, int64(i))
|
||||
emitOpcode(c.prog.BinWriter, opcode.PICKITEM)
|
||||
}
|
||||
|
||||
func (c *codegen) emitStoreStructField(i int) {
|
||||
emitInt(c.prog.BinWriter, int64(i))
|
||||
emitOpcode(c.prog.BinWriter, opcode.ROT)
|
||||
emitOpcode(c.prog.BinWriter, opcode.SETITEM)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *codegen) convertGlobals(f ast.Node) {
|
||||
ast.Inspect(f, func(node ast.Node) bool {
|
||||
switch n := node.(type) {
|
||||
case *ast.FuncDecl:
|
||||
return false
|
||||
case *ast.GenDecl:
|
||||
ast.Walk(c, n)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl) {
|
||||
var (
|
||||
f *funcScope
|
||||
ok bool
|
||||
)
|
||||
|
||||
f, ok = c.funcs[decl.Name.Name]
|
||||
if ok {
|
||||
// If this function is a syscall we will not convert it to bytecode.
|
||||
if isSyscall(f) {
|
||||
return
|
||||
}
|
||||
c.setLabel(f.label)
|
||||
} else {
|
||||
f = c.newFunc(decl)
|
||||
}
|
||||
|
||||
c.scope = f
|
||||
ast.Inspect(decl, c.scope.analyzeVoidCalls) // @OPTIMIZE
|
||||
|
||||
// All globals copied into the scope of the function need to be added
|
||||
// to the stack size of the function.
|
||||
emitInt(c.prog.BinWriter, f.stackSize()+countGlobals(file))
|
||||
emitOpcode(c.prog.BinWriter, opcode.NEWARRAY)
|
||||
emitOpcode(c.prog.BinWriter, opcode.TOALTSTACK)
|
||||
|
||||
// We need to handle methods, which in Go, is just syntactic sugar.
|
||||
// The method receiver will be passed in as first argument.
|
||||
// We check if this declaration has a receiver and load it into scope.
|
||||
//
|
||||
// FIXME: For now we will hard cast this to a struct. We can later fine tune this
|
||||
// to support other types.
|
||||
if decl.Recv != nil {
|
||||
for _, arg := range decl.Recv.List {
|
||||
ident := arg.Names[0]
|
||||
// Currently only method receives for struct types is supported.
|
||||
_, ok := c.typeInfo.Defs[ident].Type().Underlying().(*types.Struct)
|
||||
if !ok {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Load the arguments in scope.
|
||||
for _, arg := range decl.Type.Params.List {
|
||||
name := arg.Names[0].Name // for now.
|
||||
l := c.scope.newLocal(name)
|
||||
c.emitStoreLocal(l)
|
||||
}
|
||||
// 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)
|
||||
|
||||
// If this function returns the void (no return stmt) we will cleanup its junk on the stack.
|
||||
if !hasReturnStmt(decl) {
|
||||
emitOpcode(c.prog.BinWriter, opcode.FROMALTSTACK)
|
||||
emitOpcode(c.prog.BinWriter, opcode.DROP)
|
||||
emitOpcode(c.prog.BinWriter, opcode.RET)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||
if c.prog.Err != nil {
|
||||
return nil
|
||||
}
|
||||
switch n := node.(type) {
|
||||
|
||||
// General declarations.
|
||||
// var (
|
||||
// x = 2
|
||||
// )
|
||||
case *ast.GenDecl:
|
||||
for _, spec := range n.Specs {
|
||||
switch t := spec.(type) {
|
||||
case *ast.ValueSpec:
|
||||
for i, val := range t.Values {
|
||||
ast.Walk(c, val)
|
||||
l := c.scope.newLocal(t.Names[i].Name)
|
||||
c.emitStoreLocal(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
case *ast.AssignStmt:
|
||||
for i := 0; i < len(n.Lhs); i++ {
|
||||
switch t := n.Lhs[i].(type) {
|
||||
case *ast.Ident:
|
||||
switch n.Tok {
|
||||
case token.ADD_ASSIGN, token.SUB_ASSIGN, token.MUL_ASSIGN, token.QUO_ASSIGN:
|
||||
c.emitLoadLocal(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)
|
||||
default:
|
||||
ast.Walk(c, n.Rhs[i])
|
||||
l := c.scope.loadLocal(t.Name)
|
||||
c.emitStoreLocal(l)
|
||||
}
|
||||
|
||||
case *ast.SelectorExpr:
|
||||
switch expr := t.X.(type) {
|
||||
case *ast.Ident:
|
||||
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
|
||||
i := indexOfStruct(strct, t.Sel.Name) // get the index of the field
|
||||
c.emitStoreStructField(i) // store the field
|
||||
}
|
||||
default:
|
||||
c.prog.Err = fmt.Errorf("nested selector assigns not supported yet")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Assignments to index expressions.
|
||||
// slice[0] = 10
|
||||
case *ast.IndexExpr:
|
||||
ast.Walk(c, n.Rhs[i])
|
||||
name := t.X.(*ast.Ident).Name
|
||||
c.emitLoadLocal(name)
|
||||
// For now storm only supports basic index operations. Hence we
|
||||
// cast this to an *ast.BasicLit (1, 2 , 3)
|
||||
indexStr := t.Index.(*ast.BasicLit).Value
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
c.prog.Err = fmt.Errorf("failed to convert slice index to integer")
|
||||
return nil
|
||||
}
|
||||
c.emitStoreStructField(index)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
case *ast.ReturnStmt:
|
||||
if len(n.Results) > 1 {
|
||||
c.prog.Err = fmt.Errorf("multiple returns not supported")
|
||||
return nil
|
||||
}
|
||||
|
||||
l := c.newLabel()
|
||||
c.setLabel(l)
|
||||
|
||||
if len(n.Results) > 0 {
|
||||
ast.Walk(c, n.Results[0])
|
||||
}
|
||||
|
||||
emitOpcode(c.prog.BinWriter, opcode.FROMALTSTACK)
|
||||
emitOpcode(c.prog.BinWriter, opcode.DROP) // Cleanup the stack.
|
||||
emitOpcode(c.prog.BinWriter, opcode.RET)
|
||||
return nil
|
||||
|
||||
case *ast.IfStmt:
|
||||
lIf := c.newLabel()
|
||||
lElse := c.newLabel()
|
||||
lElseEnd := c.newLabel()
|
||||
|
||||
if n.Cond != nil {
|
||||
ast.Walk(c, n.Cond)
|
||||
emitJmp(c.prog.BinWriter, opcode.JMPIFNOT, int16(lElse))
|
||||
}
|
||||
|
||||
c.setLabel(lIf)
|
||||
ast.Walk(c, n.Body)
|
||||
if n.Else != nil {
|
||||
emitJmp(c.prog.BinWriter, opcode.JMP, int16(lElseEnd))
|
||||
}
|
||||
|
||||
c.setLabel(lElse)
|
||||
if n.Else != nil {
|
||||
ast.Walk(c, n.Else)
|
||||
}
|
||||
c.setLabel(lElseEnd)
|
||||
return nil
|
||||
|
||||
case *ast.BasicLit:
|
||||
c.emitLoadConst(c.typeInfo.Types[n])
|
||||
return nil
|
||||
|
||||
case *ast.Ident:
|
||||
if isIdentBool(n) {
|
||||
value, err := makeBoolFromIdent(n, c.typeInfo)
|
||||
if err != nil {
|
||||
c.prog.Err = err
|
||||
return nil
|
||||
}
|
||||
c.emitLoadConst(value)
|
||||
} else {
|
||||
c.emitLoadLocal(n.Name)
|
||||
}
|
||||
return nil
|
||||
|
||||
case *ast.CompositeLit:
|
||||
var typ types.Type
|
||||
|
||||
switch t := n.Type.(type) {
|
||||
case *ast.Ident:
|
||||
typ = c.typeInfo.ObjectOf(t).Type().Underlying()
|
||||
case *ast.SelectorExpr:
|
||||
typ = c.typeInfo.ObjectOf(t.Sel).Type().Underlying()
|
||||
default:
|
||||
ln := len(n.Elts)
|
||||
// ByteArrays needs a different approach than normal arrays.
|
||||
if isByteArray(n, c.typeInfo) {
|
||||
c.convertByteArray(n)
|
||||
return nil
|
||||
}
|
||||
for i := ln - 1; i >= 0; i-- {
|
||||
c.emitLoadConst(c.typeInfo.Types[n.Elts[i]])
|
||||
}
|
||||
emitInt(c.prog.BinWriter, int64(ln))
|
||||
emitOpcode(c.prog.BinWriter, opcode.PACK)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch typ.(type) {
|
||||
case *types.Struct:
|
||||
c.convertStruct(n)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case *ast.BinaryExpr:
|
||||
switch n.Op {
|
||||
case token.LAND:
|
||||
ast.Walk(c, n.X)
|
||||
emitJmp(c.prog.BinWriter, opcode.JMPIFNOT, int16(len(c.l)-1))
|
||||
ast.Walk(c, n.Y)
|
||||
return nil
|
||||
|
||||
case token.LOR:
|
||||
ast.Walk(c, n.X)
|
||||
emitJmp(c.prog.BinWriter, opcode.JMPIF, int16(len(c.l)-3))
|
||||
ast.Walk(c, n.Y)
|
||||
return nil
|
||||
|
||||
default:
|
||||
// The AST package will try to resolve all basic literals for us.
|
||||
// If the typeinfo.Value is not nil we know that the expr is resolved
|
||||
// and needs no further action. e.g. x := 2 + 2 + 2 will be resolved to 6.
|
||||
// NOTE: Constants will also be automatically resolved be the AST parser.
|
||||
// example:
|
||||
// const x = 10
|
||||
// x + 2 will results into 12
|
||||
tinfo := c.typeInfo.Types[n]
|
||||
if tinfo.Value != nil {
|
||||
c.emitLoadConst(tinfo)
|
||||
return nil
|
||||
}
|
||||
|
||||
ast.Walk(c, n.X)
|
||||
ast.Walk(c, n.Y)
|
||||
|
||||
switch {
|
||||
case n.Op == token.ADD:
|
||||
// VM has separate opcodes for number and string concatenation
|
||||
if isStringType(tinfo.Type) {
|
||||
emitOpcode(c.prog.BinWriter, opcode.CAT)
|
||||
} else {
|
||||
emitOpcode(c.prog.BinWriter, opcode.ADD)
|
||||
}
|
||||
case n.Op == token.EQL:
|
||||
// VM has separate opcodes for number and string equality
|
||||
if isStringType(c.typeInfo.Types[n.X].Type) {
|
||||
emitOpcode(c.prog.BinWriter, opcode.EQUAL)
|
||||
} else {
|
||||
emitOpcode(c.prog.BinWriter, opcode.NUMEQUAL)
|
||||
}
|
||||
case n.Op == token.NEQ:
|
||||
// VM has separate opcodes for number and string equality
|
||||
if isStringType(c.typeInfo.Types[n.X].Type) {
|
||||
emitOpcode(c.prog.BinWriter, opcode.EQUAL)
|
||||
emitOpcode(c.prog.BinWriter, opcode.NOT)
|
||||
} else {
|
||||
emitOpcode(c.prog.BinWriter, opcode.NUMNOTEQUAL)
|
||||
}
|
||||
default:
|
||||
c.convertToken(n.Op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
case *ast.CallExpr:
|
||||
var (
|
||||
f *funcScope
|
||||
ok bool
|
||||
numArgs = len(n.Args)
|
||||
isBuiltin = isBuiltin(n.Fun)
|
||||
)
|
||||
|
||||
switch fun := n.Fun.(type) {
|
||||
case *ast.Ident:
|
||||
f, ok = c.funcs[fun.Name]
|
||||
if !ok && !isBuiltin {
|
||||
c.prog.Err = fmt.Errorf("could not resolve function %s", fun.Name)
|
||||
return nil
|
||||
}
|
||||
case *ast.SelectorExpr:
|
||||
// If this is a method call we need to walk the AST to load the struct locally.
|
||||
// Otherwise this is a function call from a imported package and we can call it
|
||||
// directly.
|
||||
if c.typeInfo.Selections[fun] != nil {
|
||||
ast.Walk(c, fun.X)
|
||||
// Dont forget to add 1 extra argument when its a method.
|
||||
numArgs++
|
||||
}
|
||||
|
||||
f, ok = c.funcs[fun.Sel.Name]
|
||||
// @FIXME this could cause runtime errors.
|
||||
f.selector = fun.X.(*ast.Ident)
|
||||
if !ok {
|
||||
c.prog.Err = fmt.Errorf("could not resolve function %s", fun.Sel.Name)
|
||||
return nil
|
||||
}
|
||||
case *ast.ArrayType:
|
||||
// For now we will assume that there is only 1 argument passed which
|
||||
// will be a basic literal (string kind). This only to handle string
|
||||
// to byte slice conversions. E.G. []byte("foobar")
|
||||
arg := n.Args[0].(*ast.BasicLit)
|
||||
c.emitLoadConst(c.typeInfo.Types[arg])
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle the arguments
|
||||
for _, arg := range n.Args {
|
||||
ast.Walk(c, arg)
|
||||
}
|
||||
// Do not swap for builtin functions.
|
||||
if !isBuiltin {
|
||||
if numArgs == 2 {
|
||||
emitOpcode(c.prog.BinWriter, opcode.SWAP)
|
||||
} else if numArgs == 3 {
|
||||
emitInt(c.prog.BinWriter, 2)
|
||||
emitOpcode(c.prog.BinWriter, opcode.XSWAP)
|
||||
} else {
|
||||
for i := 1; i < numArgs; i++ {
|
||||
emitInt(c.prog.BinWriter, int64(i))
|
||||
emitOpcode(c.prog.BinWriter, opcode.ROLL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check builtin first to avoid nil pointer on funcScope!
|
||||
switch {
|
||||
case isBuiltin:
|
||||
// Use the ident to check, builtins are not in func scopes.
|
||||
// We can be sure builtins are of type *ast.Ident.
|
||||
c.convertBuiltin(n)
|
||||
case isSyscall(f):
|
||||
c.convertSyscall(f.selector.Name, f.name)
|
||||
default:
|
||||
emitCall(c.prog.BinWriter, opcode.CALL, int16(f.label))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case *ast.SelectorExpr:
|
||||
switch t := n.X.(type) {
|
||||
case *ast.Ident:
|
||||
typ := c.typeInfo.ObjectOf(t).Type().Underlying()
|
||||
if strct, ok := typ.(*types.Struct); ok {
|
||||
c.emitLoadLocal(t.Name) // load the struct
|
||||
i := indexOfStruct(strct, n.Sel.Name)
|
||||
c.emitLoadField(i) // load the field
|
||||
}
|
||||
default:
|
||||
c.prog.Err = fmt.Errorf("nested selectors not supported yet")
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
case *ast.UnaryExpr:
|
||||
ast.Walk(c, n.X)
|
||||
// From https://golang.org/ref/spec#Operators
|
||||
// there can be only following unary operators
|
||||
// "+" | "-" | "!" | "^" | "*" | "&" | "<-" .
|
||||
// of which last three are not used in SC
|
||||
switch n.Op {
|
||||
case token.ADD:
|
||||
// +10 == 10, no need to do anything in this case
|
||||
case token.SUB:
|
||||
emitOpcode(c.prog.BinWriter, opcode.NEGATE)
|
||||
case token.NOT:
|
||||
emitOpcode(c.prog.BinWriter, opcode.NOT)
|
||||
case token.XOR:
|
||||
emitOpcode(c.prog.BinWriter, opcode.INVERT)
|
||||
default:
|
||||
c.prog.Err = fmt.Errorf("invalid unary operator: %s", n.Op)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
case *ast.IncDecStmt:
|
||||
ast.Walk(c, n.X)
|
||||
c.convertToken(n.Tok)
|
||||
|
||||
// For now only identifiers are supported for (post) for stmts.
|
||||
// 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)
|
||||
}
|
||||
return nil
|
||||
|
||||
case *ast.IndexExpr:
|
||||
// Walk the expression, this could be either an Ident or SelectorExpr.
|
||||
// This will load local whatever X is.
|
||||
ast.Walk(c, n.X)
|
||||
|
||||
switch n.Index.(type) {
|
||||
case *ast.BasicLit:
|
||||
t := c.typeInfo.Types[n.Index]
|
||||
val, _ := constant.Int64Val(t.Value)
|
||||
c.emitLoadField(int(val))
|
||||
default:
|
||||
ast.Walk(c, n.Index)
|
||||
emitOpcode(c.prog.BinWriter, opcode.PICKITEM) // just pickitem here
|
||||
}
|
||||
return nil
|
||||
|
||||
case *ast.ForStmt:
|
||||
var (
|
||||
fstart = c.newLabel()
|
||||
fend = c.newLabel()
|
||||
)
|
||||
|
||||
// Walk the initializer and condition.
|
||||
ast.Walk(c, n.Init)
|
||||
|
||||
// Set label and walk the condition.
|
||||
c.setLabel(fstart)
|
||||
ast.Walk(c, n.Cond)
|
||||
|
||||
// Jump if the condition is false
|
||||
emitJmp(c.prog.BinWriter, opcode.JMPIFNOT, int16(fend))
|
||||
|
||||
// Walk body followed by the iterator (post stmt).
|
||||
ast.Walk(c, n.Body)
|
||||
ast.Walk(c, n.Post)
|
||||
|
||||
// Jump back to condition.
|
||||
emitJmp(c.prog.BinWriter, opcode.JMP, int16(fstart))
|
||||
c.setLabel(fend)
|
||||
|
||||
return nil
|
||||
|
||||
// We dont really care about assertions for the core logic.
|
||||
// The only thing we need is to please the compiler type checking.
|
||||
// For this to work properly, we only need to walk the expression
|
||||
// not the assertion type.
|
||||
case *ast.TypeAssertExpr:
|
||||
ast.Walk(c, n.X)
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *codegen) convertSyscall(api, name string) {
|
||||
api, ok := syscalls[api][name]
|
||||
if !ok {
|
||||
c.prog.Err = fmt.Errorf("unknown VM syscall api: %s", name)
|
||||
return
|
||||
}
|
||||
emitSyscall(c.prog.BinWriter, api)
|
||||
|
||||
// This NOP instruction is basically not needed, but if we do, we have a
|
||||
// one to one matching avm file with neo-python which is very nice for debugging.
|
||||
emitOpcode(c.prog.BinWriter, opcode.NOP)
|
||||
}
|
||||
|
||||
func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
|
||||
var name string
|
||||
switch t := expr.Fun.(type) {
|
||||
case *ast.Ident:
|
||||
name = t.Name
|
||||
case *ast.SelectorExpr:
|
||||
name = t.Sel.Name
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "len":
|
||||
arg := expr.Args[0]
|
||||
typ := c.typeInfo.Types[arg].Type
|
||||
if isStringType(typ) {
|
||||
emitOpcode(c.prog.BinWriter, opcode.SIZE)
|
||||
} else {
|
||||
emitOpcode(c.prog.BinWriter, opcode.ARRAYSIZE)
|
||||
}
|
||||
case "append":
|
||||
arg := expr.Args[0]
|
||||
typ := c.typeInfo.Types[arg].Type
|
||||
if isByteArrayType(typ) {
|
||||
emitOpcode(c.prog.BinWriter, opcode.CAT)
|
||||
} else {
|
||||
emitOpcode(c.prog.BinWriter, opcode.SWAP)
|
||||
emitOpcode(c.prog.BinWriter, opcode.DUP)
|
||||
emitOpcode(c.prog.BinWriter, opcode.PUSH2)
|
||||
emitOpcode(c.prog.BinWriter, opcode.XSWAP)
|
||||
emitOpcode(c.prog.BinWriter, opcode.APPEND)
|
||||
}
|
||||
case "SHA256":
|
||||
emitOpcode(c.prog.BinWriter, opcode.SHA256)
|
||||
case "SHA1":
|
||||
emitOpcode(c.prog.BinWriter, opcode.SHA1)
|
||||
case "Hash256":
|
||||
emitOpcode(c.prog.BinWriter, opcode.HASH256)
|
||||
case "Hash160":
|
||||
emitOpcode(c.prog.BinWriter, opcode.HASH160)
|
||||
case "Equals":
|
||||
emitOpcode(c.prog.BinWriter, opcode.EQUAL)
|
||||
case "FromAddress":
|
||||
// We can be sure that this is a ast.BasicLit just containing a simple
|
||||
// address string. Note that the string returned from calling Value will
|
||||
// contain double quotes that need to be stripped.
|
||||
addressStr := expr.Args[0].(*ast.BasicLit).Value
|
||||
addressStr = strings.Replace(addressStr, "\"", "", 2)
|
||||
uint160, err := crypto.Uint160DecodeAddress(addressStr)
|
||||
if err != nil {
|
||||
c.prog.Err = err
|
||||
return
|
||||
}
|
||||
bytes := uint160.Bytes()
|
||||
emitBytes(c.prog.BinWriter, bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *codegen) convertByteArray(lit *ast.CompositeLit) {
|
||||
buf := make([]byte, len(lit.Elts))
|
||||
for i := 0; i < len(lit.Elts); i++ {
|
||||
t := c.typeInfo.Types[lit.Elts[i]]
|
||||
val, _ := constant.Int64Val(t.Value)
|
||||
buf[i] = byte(val)
|
||||
}
|
||||
emitBytes(c.prog.BinWriter, buf)
|
||||
}
|
||||
|
||||
func (c *codegen) convertStruct(lit *ast.CompositeLit) {
|
||||
// Create a new structScope to initialize and store
|
||||
// the positions of its variables.
|
||||
strct, ok := c.typeInfo.TypeOf(lit).Underlying().(*types.Struct)
|
||||
if !ok {
|
||||
c.prog.Err = fmt.Errorf("the given literal is not of type struct: %v", lit)
|
||||
return
|
||||
}
|
||||
|
||||
emitOpcode(c.prog.BinWriter, opcode.NOP)
|
||||
emitInt(c.prog.BinWriter, int64(strct.NumFields()))
|
||||
emitOpcode(c.prog.BinWriter, opcode.NEWSTRUCT)
|
||||
emitOpcode(c.prog.BinWriter, opcode.TOALTSTACK)
|
||||
|
||||
// We need to locally store all the fields, even if they are not initialized.
|
||||
// We will initialize all fields to their "zero" value.
|
||||
for i := 0; i < strct.NumFields(); i++ {
|
||||
sField := strct.Field(i)
|
||||
fieldAdded := false
|
||||
|
||||
// Fields initialized by the program.
|
||||
for _, field := range lit.Elts {
|
||||
f := field.(*ast.KeyValueExpr)
|
||||
fieldName := f.Key.(*ast.Ident).Name
|
||||
|
||||
if sField.Name() == fieldName {
|
||||
ast.Walk(c, f.Value)
|
||||
pos := indexOfStruct(strct, fieldName)
|
||||
c.emitStoreLocal(pos)
|
||||
fieldAdded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if fieldAdded {
|
||||
continue
|
||||
}
|
||||
|
||||
typeAndVal, err := typeAndValueForField(sField)
|
||||
if err != nil {
|
||||
c.prog.Err = err
|
||||
return
|
||||
}
|
||||
c.emitLoadConst(typeAndVal)
|
||||
c.emitStoreLocal(i)
|
||||
}
|
||||
emitOpcode(c.prog.BinWriter, opcode.FROMALTSTACK)
|
||||
}
|
||||
|
||||
func (c *codegen) convertToken(tok token.Token) {
|
||||
switch tok {
|
||||
case token.ADD_ASSIGN:
|
||||
emitOpcode(c.prog.BinWriter, opcode.ADD)
|
||||
case token.SUB_ASSIGN:
|
||||
emitOpcode(c.prog.BinWriter, opcode.SUB)
|
||||
case token.MUL_ASSIGN:
|
||||
emitOpcode(c.prog.BinWriter, opcode.MUL)
|
||||
case token.QUO_ASSIGN:
|
||||
emitOpcode(c.prog.BinWriter, opcode.DIV)
|
||||
case token.ADD:
|
||||
emitOpcode(c.prog.BinWriter, opcode.ADD)
|
||||
case token.SUB:
|
||||
emitOpcode(c.prog.BinWriter, opcode.SUB)
|
||||
case token.MUL:
|
||||
emitOpcode(c.prog.BinWriter, opcode.MUL)
|
||||
case token.QUO:
|
||||
emitOpcode(c.prog.BinWriter, opcode.DIV)
|
||||
case token.LSS:
|
||||
emitOpcode(c.prog.BinWriter, opcode.LT)
|
||||
case token.LEQ:
|
||||
emitOpcode(c.prog.BinWriter, opcode.LTE)
|
||||
case token.GTR:
|
||||
emitOpcode(c.prog.BinWriter, opcode.GT)
|
||||
case token.GEQ:
|
||||
emitOpcode(c.prog.BinWriter, opcode.GTE)
|
||||
case token.EQL:
|
||||
emitOpcode(c.prog.BinWriter, opcode.NUMEQUAL)
|
||||
case token.NEQ:
|
||||
emitOpcode(c.prog.BinWriter, opcode.NUMNOTEQUAL)
|
||||
case token.DEC:
|
||||
emitOpcode(c.prog.BinWriter, opcode.DEC)
|
||||
case token.INC:
|
||||
emitOpcode(c.prog.BinWriter, opcode.INC)
|
||||
case token.NOT:
|
||||
emitOpcode(c.prog.BinWriter, opcode.NOT)
|
||||
case token.AND:
|
||||
emitOpcode(c.prog.BinWriter, opcode.AND)
|
||||
case token.OR:
|
||||
emitOpcode(c.prog.BinWriter, opcode.OR)
|
||||
case token.SHL:
|
||||
emitOpcode(c.prog.BinWriter, opcode.SHL)
|
||||
case token.SHR:
|
||||
emitOpcode(c.prog.BinWriter, opcode.SHR)
|
||||
case token.XOR:
|
||||
emitOpcode(c.prog.BinWriter, opcode.XOR)
|
||||
default:
|
||||
c.prog.Err = fmt.Errorf("compiler could not convert token: %s", tok)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *codegen) newFunc(decl *ast.FuncDecl) *funcScope {
|
||||
f := newFuncScope(decl, c.newLabel())
|
||||
c.funcs[f.name] = f
|
||||
return f
|
||||
}
|
||||
|
||||
// CodeGen compiles the program to bytecode.
|
||||
func CodeGen(info *buildInfo) ([]byte, error) {
|
||||
pkg := info.program.Package(info.initialPackage)
|
||||
c := &codegen{
|
||||
buildInfo: info,
|
||||
prog: io.NewBufBinWriter(),
|
||||
l: []int{},
|
||||
funcs: map[string]*funcScope{},
|
||||
typeInfo: &pkg.Info,
|
||||
}
|
||||
|
||||
// Resolve the entrypoint of the program.
|
||||
main, mainFile := resolveEntryPoint(mainIdent, pkg)
|
||||
if main == nil {
|
||||
c.prog.Err = fmt.Errorf("could not find func main. Did you forget to declare it? ")
|
||||
return []byte{}, c.prog.Err
|
||||
}
|
||||
|
||||
funUsage := analyzeFuncUsage(info.program.AllPackages)
|
||||
|
||||
// Bring all imported functions into scope.
|
||||
for _, pkg := range info.program.AllPackages {
|
||||
for _, f := range pkg.Files {
|
||||
c.resolveFuncDecls(f)
|
||||
}
|
||||
}
|
||||
|
||||
// convert the entry point first.
|
||||
c.convertFuncDecl(mainFile, main)
|
||||
|
||||
// sort map keys to generate code deterministically.
|
||||
keys := make([]*types.Package, 0, len(info.program.AllPackages))
|
||||
for p := range info.program.AllPackages {
|
||||
keys = append(keys, p)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i].Path() < keys[j].Path() })
|
||||
|
||||
// Generate the code for the program.
|
||||
for _, k := range keys {
|
||||
pkg := info.program.AllPackages[k]
|
||||
c.typeInfo = &pkg.Info
|
||||
|
||||
for _, f := range pkg.Files {
|
||||
for _, decl := range f.Decls {
|
||||
switch n := decl.(type) {
|
||||
case *ast.FuncDecl:
|
||||
// Don't convert the function if it's not used. This will save a lot
|
||||
// of bytecode space.
|
||||
if n.Name.Name != mainIdent && funUsage.funcUsed(n.Name.Name) {
|
||||
c.convertFuncDecl(f, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.prog.Err != nil {
|
||||
return nil, c.prog.Err
|
||||
}
|
||||
buf := c.prog.Bytes()
|
||||
c.writeJumps(buf)
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (c *codegen) resolveFuncDecls(f *ast.File) {
|
||||
for _, decl := range f.Decls {
|
||||
switch n := decl.(type) {
|
||||
case *ast.FuncDecl:
|
||||
if n.Name.Name != mainIdent {
|
||||
c.newFunc(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *codegen) writeJumps(b []byte) {
|
||||
for i, op := range b {
|
||||
j := i + 1
|
||||
switch opcode.Opcode(op) {
|
||||
case opcode.JMP, opcode.JMPIFNOT, opcode.JMPIF, opcode.CALL:
|
||||
index := int16(binary.LittleEndian.Uint16(b[j : j+2]))
|
||||
if int(index) > len(c.l) || int(index) < 0 {
|
||||
continue
|
||||
}
|
||||
offset := uint16(c.l[index] - i)
|
||||
binary.LittleEndian.PutUint16(b[j:j+2], offset)
|
||||
}
|
||||
}
|
||||
}
|
100
pkg/compiler/compiler.go
Normal file
100
pkg/compiler/compiler.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/types"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/loader"
|
||||
)
|
||||
|
||||
const fileExt = "avm"
|
||||
|
||||
// Options contains all the parameters that affect the behaviour of the compiler.
|
||||
type Options struct {
|
||||
// The extension of the output file default set to .avm
|
||||
Ext string
|
||||
|
||||
// The name of the output file.
|
||||
Outfile string
|
||||
|
||||
// Debug outputs a hex encoded string of the generated bytecode.
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type buildInfo struct {
|
||||
initialPackage string
|
||||
program *loader.Program
|
||||
}
|
||||
|
||||
// Compile compiles a Go program into bytecode that can run on the NEO virtual machine.
|
||||
func Compile(r io.Reader) ([]byte, error) {
|
||||
conf := loader.Config{ParserMode: parser.ParseComments}
|
||||
f, err := conf.ParseFile("", r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.CreateFromFiles("", f)
|
||||
|
||||
prog, err := conf.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := &buildInfo{
|
||||
initialPackage: f.Name.Name,
|
||||
program: prog,
|
||||
}
|
||||
|
||||
buf, err := CodeGen(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
type archive struct {
|
||||
f *ast.File
|
||||
typeInfo *types.Info
|
||||
}
|
||||
|
||||
// CompileAndSave will compile and save the file to disk.
|
||||
func CompileAndSave(src string, o *Options) ([]byte, error) {
|
||||
if !strings.HasSuffix(src, ".go") {
|
||||
return nil, fmt.Errorf("%s is not a Go file", src)
|
||||
}
|
||||
o.Outfile = strings.TrimSuffix(o.Outfile, fmt.Sprintf(".%s", fileExt))
|
||||
if len(o.Outfile) == 0 {
|
||||
o.Outfile = strings.TrimSuffix(src, ".go")
|
||||
}
|
||||
if len(o.Ext) == 0 {
|
||||
o.Ext = fileExt
|
||||
}
|
||||
b, err := ioutil.ReadFile(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err = Compile(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while trying to compile smart contract file: %v", err)
|
||||
}
|
||||
|
||||
out := fmt.Sprintf("%s.%s", o.Outfile, o.Ext)
|
||||
return b, ioutil.WriteFile(out, b, os.ModePerm)
|
||||
}
|
||||
|
||||
func gopath() string {
|
||||
gopath := os.Getenv("GOPATH")
|
||||
if len(gopath) == 0 {
|
||||
gopath = build.Default.GOPATH
|
||||
}
|
||||
return gopath
|
||||
}
|
53
pkg/compiler/compiler_test.go
Normal file
53
pkg/compiler/compiler_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package compiler_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/CityOfZion/neo-go/pkg/compiler"
|
||||
)
|
||||
|
||||
const examplePath = "../../examples"
|
||||
|
||||
func TestExamplesFolder(t *testing.T) {
|
||||
infos, err := ioutil.ReadDir(examplePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
infos, err := ioutil.ReadDir(path.Join(examplePath, info.Name()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(infos) == 0 {
|
||||
t.Fatal("detected smart contract folder with no contract in it")
|
||||
}
|
||||
|
||||
filename := filterFilename(infos)
|
||||
targetPath := path.Join(examplePath, info.Name(), filename)
|
||||
if err := compileFile(targetPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterFilename(infos []os.FileInfo) string {
|
||||
for _, info := range infos {
|
||||
if !info.IsDir() {
|
||||
return info.Name()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func compileFile(src string) error {
|
||||
file, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = compiler.Compile(file)
|
||||
return err
|
||||
}
|
119
pkg/compiler/emit.go
Normal file
119
pkg/compiler/emit.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/CityOfZion/neo-go/pkg/io"
|
||||
"github.com/CityOfZion/neo-go/pkg/util"
|
||||
"github.com/CityOfZion/neo-go/pkg/vm/opcode"
|
||||
)
|
||||
|
||||
// emit a VM Instruction with data to the given buffer.
|
||||
func emit(w *io.BinWriter, instr opcode.Opcode, b []byte) {
|
||||
emitOpcode(w, instr)
|
||||
w.WriteBytes(b)
|
||||
}
|
||||
|
||||
// emitOpcode emits a single VM Instruction the given buffer.
|
||||
func emitOpcode(w *io.BinWriter, instr opcode.Opcode) {
|
||||
w.WriteLE(byte(instr))
|
||||
}
|
||||
|
||||
// emitBool emits a bool type the given buffer.
|
||||
func emitBool(w *io.BinWriter, ok bool) {
|
||||
if ok {
|
||||
emitOpcode(w, opcode.PUSHT)
|
||||
return
|
||||
}
|
||||
emitOpcode(w, opcode.PUSHF)
|
||||
}
|
||||
|
||||
// emitInt emits a int type to the given buffer.
|
||||
func emitInt(w *io.BinWriter, i int64) {
|
||||
switch {
|
||||
case i == -1:
|
||||
emitOpcode(w, opcode.PUSHM1)
|
||||
return
|
||||
case i == 0:
|
||||
emitOpcode(w, opcode.PUSHF)
|
||||
return
|
||||
case i > 0 && i < 16:
|
||||
val := opcode.Opcode(int(opcode.PUSH1) - 1 + int(i))
|
||||
emitOpcode(w, val)
|
||||
return
|
||||
}
|
||||
|
||||
bInt := big.NewInt(i)
|
||||
val := util.ArrayReverse(bInt.Bytes())
|
||||
emitBytes(w, val)
|
||||
}
|
||||
|
||||
// emitString emits a string to the given buffer.
|
||||
func emitString(w *io.BinWriter, s string) {
|
||||
emitBytes(w, []byte(s))
|
||||
}
|
||||
|
||||
// emitBytes emits a byte array to the given buffer.
|
||||
func emitBytes(w *io.BinWriter, b []byte) {
|
||||
n := len(b)
|
||||
|
||||
switch {
|
||||
case n <= int(opcode.PUSHBYTES75):
|
||||
emit(w, opcode.Opcode(n), b)
|
||||
return
|
||||
case n < 0x100:
|
||||
emit(w, opcode.PUSHDATA1, []byte{byte(n)})
|
||||
case n < 0x10000:
|
||||
buf := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(buf, uint16(n))
|
||||
emit(w, opcode.PUSHDATA2, buf)
|
||||
default:
|
||||
buf := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(buf, uint32(n))
|
||||
emit(w, opcode.PUSHDATA4, buf)
|
||||
if w.Err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteBytes(b)
|
||||
}
|
||||
|
||||
// emitSyscall emits the syscall API to the given buffer.
|
||||
// Syscall API string cannot be 0.
|
||||
func emitSyscall(w *io.BinWriter, api string) {
|
||||
if len(api) == 0 {
|
||||
w.Err = errors.New("syscall api cannot be of length 0")
|
||||
return
|
||||
}
|
||||
buf := make([]byte, len(api)+1)
|
||||
buf[0] = byte(len(api))
|
||||
copy(buf[1:], api)
|
||||
emit(w, opcode.SYSCALL, buf)
|
||||
}
|
||||
|
||||
// emitCall emits a call Instruction with label to the given buffer.
|
||||
func emitCall(w *io.BinWriter, instr opcode.Opcode, label int16) {
|
||||
emitJmp(w, instr, label)
|
||||
}
|
||||
|
||||
// emitJmp emits a jump Instruction along with label to the given buffer.
|
||||
func emitJmp(w *io.BinWriter, instr opcode.Opcode, label int16) {
|
||||
if !isInstrJmp(instr) {
|
||||
w.Err = fmt.Errorf("opcode %s is not a jump or call type", instr)
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(buf, uint16(label))
|
||||
emit(w, instr, buf)
|
||||
}
|
||||
|
||||
func isInstrJmp(instr opcode.Opcode) bool {
|
||||
if instr == opcode.JMP || instr == opcode.JMPIFNOT || instr == opcode.JMPIF || instr == opcode.CALL {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
117
pkg/compiler/func_scope.go
Normal file
117
pkg/compiler/func_scope.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package compiler
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
)
|
||||
|
||||
// A funcScope represents the scope within the function context.
|
||||
// It holds al the local variables along with the initialized struct positions.
|
||||
type funcScope struct {
|
||||
// Identifier of the function.
|
||||
name string
|
||||
|
||||
// Selector of the function if there is any. Only functions imported
|
||||
// from other packages should have a selector.
|
||||
selector *ast.Ident
|
||||
|
||||
// The declaration of the function in the AST. Nil if this scope is not a function.
|
||||
decl *ast.FuncDecl
|
||||
|
||||
// Program label of the scope
|
||||
label int
|
||||
|
||||
// Local variables
|
||||
locals map[string]int
|
||||
|
||||
// voidCalls are basically functions that return their value
|
||||
// into nothing. The stack has their return value but there
|
||||
// is nothing that consumes it. We need to keep track of
|
||||
// these functions so we can cleanup (drop) the returned
|
||||
// value from the stack. We also need to add every voidCall
|
||||
// return value to the stack size.
|
||||
voidCalls map[*ast.CallExpr]bool
|
||||
|
||||
// Local variable counter.
|
||||
i int
|
||||
}
|
||||
|
||||
func newFuncScope(decl *ast.FuncDecl, label int) *funcScope {
|
||||
return &funcScope{
|
||||
name: decl.Name.Name,
|
||||
decl: decl,
|
||||
label: label,
|
||||
locals: map[string]int{},
|
||||
voidCalls: map[*ast.CallExpr]bool{},
|
||||
i: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// analyzeVoidCalls checks for functions that are not assigned
|
||||
// and therefore we need to cleanup the return value from the stack.
|
||||
func (c *funcScope) analyzeVoidCalls(node ast.Node) bool {
|
||||
switch n := node.(type) {
|
||||
case *ast.AssignStmt:
|
||||
for i := 0; i < len(n.Rhs); i++ {
|
||||
switch n.Rhs[i].(type) {
|
||||
case *ast.CallExpr:
|
||||
return false
|
||||
}
|
||||
}
|
||||
case *ast.ReturnStmt:
|
||||
switch n.Results[0].(type) {
|
||||
case *ast.CallExpr:
|
||||
return false
|
||||
}
|
||||
case *ast.BinaryExpr:
|
||||
return false
|
||||
case *ast.CallExpr:
|
||||
c.voidCalls[n] = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *funcScope) stackSize() int64 {
|
||||
size := 0
|
||||
ast.Inspect(c.decl, func(n ast.Node) bool {
|
||||
switch n := n.(type) {
|
||||
case *ast.AssignStmt:
|
||||
size += len(n.Rhs)
|
||||
case *ast.ReturnStmt, *ast.IfStmt:
|
||||
size++
|
||||
// This handles the inline GenDecl like "var x = 2"
|
||||
case *ast.GenDecl:
|
||||
switch t := n.Specs[0].(type) {
|
||||
case *ast.ValueSpec:
|
||||
if len(t.Values) > 0 {
|
||||
size++
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
numArgs := len(c.decl.Type.Params.List)
|
||||
// Also take care of struct methods recv: e.g. (t Token).Foo().
|
||||
if c.decl.Recv != nil {
|
||||
numArgs += len(c.decl.Recv.List)
|
||||
}
|
||||
return int64(size + numArgs + len(c.voidCalls))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
91
pkg/compiler/syscall.go
Normal file
91
pkg/compiler/syscall.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package compiler
|
||||
|
||||
var syscalls = map[string]map[string]string{
|
||||
"storage": {
|
||||
"GetContext": "Neo.Storage.GetContext",
|
||||
"Put": "Neo.Storage.Put",
|
||||
"Get": "Neo.Storage.Get",
|
||||
"Delete": "Neo.Storage.Delete",
|
||||
"Find": "Neo.Storage.Find",
|
||||
},
|
||||
"runtime": {
|
||||
"GetTrigger": "Neo.Runtime.GetTrigger",
|
||||
"CheckWitness": "Neo.Runtime.CheckWitness",
|
||||
"Notify": "Neo.Runtime.Notify",
|
||||
"Log": "Neo.Runtime.Log",
|
||||
"GetTime": "Neo.Runtime.GetTime",
|
||||
"Serialize": "Neo.Runtime.Serialize",
|
||||
"Deserialize": "Neo.Runtime.Deserialize",
|
||||
},
|
||||
"blockchain": {
|
||||
"GetHeight": "Neo.Blockchain.GetHeight",
|
||||
"GetHeader": "Neo.Blockchain.GetHeader",
|
||||
"GetBlock": "Neo.Blockchain.GetBlock",
|
||||
"GetTransaction": "Neo.Blockchain.GetTransaction",
|
||||
"GetContract": "Neo.Blockchain.GetContract",
|
||||
"GetAccount": "Neo.Blockchain.GetAccount",
|
||||
"GetValidators": "Neo.Blockchain.GetValidators",
|
||||
"GetAsset": "Neo.Blockchain.GetAsset",
|
||||
},
|
||||
"header": {
|
||||
"GetIndex": "Neo.Header.GetIndex",
|
||||
"GetHash": "Neo.Header.GetHash",
|
||||
"GetPrevHash": "Neo.Header.GetPrevHash",
|
||||
"GetTimestamp": "Neo.Header.GetTimestamp",
|
||||
"GetVersion": "Neo.Header.GetVersion",
|
||||
"GetMerkleRoot": "Neo.Header.GetMerkleRoot",
|
||||
"GetConsensusData": "Neo.Header.GetConsensusData",
|
||||
"GetNextConsensus": "Neo.Header.GetNextConsensus",
|
||||
},
|
||||
"block": {
|
||||
"GetTransactionCount": "Neo.Block.GetTransactionCount",
|
||||
"GetTransactions": "Neo.Block.GetTransactions",
|
||||
"GetTransaction": "Neo.Block.GetTransaction",
|
||||
},
|
||||
"transaction": {
|
||||
"GetHash": "Neo.Transaction.GetHash",
|
||||
"GetType": "Neo.Transaction.GetType",
|
||||
"GetAttributes": "Neo.Transaction.GetAttributes",
|
||||
"GetInputs": "Neo.Transaction.GetInputs",
|
||||
"GetOutputs": "Neo.Transaction.GetOutputs",
|
||||
"GetReferences": "Neo.Transaction.GetReferences",
|
||||
"GetUnspentCoins": "Neo.Transaction.GetUnspentCoins",
|
||||
"GetScript": "Neo.Transaction.GetScript",
|
||||
},
|
||||
"asset": {
|
||||
"GetAssetID": "Neo.Asset.GetAssetID",
|
||||
"GetAssetType": "Neo.Asset.GetAssetType",
|
||||
"GetAmount": "Neo.Asset.GetAmount",
|
||||
"Create": "Neo.Asset.Create",
|
||||
"Renew": "Neo.Asset.Renew",
|
||||
},
|
||||
"contract": {
|
||||
"GetScript": "Neo.Contract.GetScript",
|
||||
"IsPayable": "Neo.Contract.IsPayable",
|
||||
"Create": "Neo.Contract.Create",
|
||||
"Destroy": "Neo.Contract.Destroy",
|
||||
"Migrate": "Neo.Contract.Migrate",
|
||||
"GetStorageContext": "Neo.Contract.GetStorageContext",
|
||||
},
|
||||
"input": {
|
||||
"GetHash": "Neo.Input.GetHash",
|
||||
"GetIndex": "Neo.Input.GetIndex",
|
||||
},
|
||||
"output": {
|
||||
"GetAssetID": "Neo.Output.GetAssetID",
|
||||
"GetValue": "Neo.Output.GetValue",
|
||||
"GetScriptHash": "Neo.Output.GetScriptHash",
|
||||
},
|
||||
"engine": {
|
||||
"GetScriptContainer": "System.ExecutionEngine.GetScriptContainer",
|
||||
"GetCallingScriptHash": "System.ExecutionEngine.GetCallingScriptHash",
|
||||
"GetEntryScriptHash": "System.ExecutionEngine.GetEntryScriptHash",
|
||||
"GetExecutingScriptHash": "System.ExecutionEngine.GetExecutingScriptHash",
|
||||
},
|
||||
"iterator": {
|
||||
"Create": "Neo.Iterator.Create",
|
||||
"Key": "Neo.Iterator.Key",
|
||||
"Keys": "Neo.Iterator.Keys",
|
||||
"Values": "Neo.Iterator.Values",
|
||||
},
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue