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:
Anthony De Meulemeester 2018-02-15 16:35:49 +01:00 committed by GitHub
parent f7d57e4e49
commit b257a06f3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1253 additions and 494 deletions

503
pkg/vm/compiler/compiler.go Normal file
View 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"
}

View 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
}

View 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()
}

View 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)
}
}

View 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",
},
}

View 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()
}

View 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",
},
}

View 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",
},
}

View file

@ -0,0 +1,15 @@
package compiler_test
var stringTestCases = []testCase{
{
"simple string",
`
package testcase
func Main() string {
x := "NEO"
return x
}
`,
"52c56b034e454f6c766b00527ac46203006c766b00c3616c7566",
},
}