mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-27 23:31:35 +00:00
b257a06f3e
* 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.
503 lines
12 KiB
Go
503 lines
12 KiB
Go
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"
|
|
}
|