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

View file

@ -74,23 +74,23 @@ Currently, there is a minimal subset of the NEO protocol implemented.
To start experimenting make sure you a have a private net running on your machine.
If you dont, take a look at [docker-privnet-with-gas](https://hub.docker.com/r/metachris/neo-privnet-with-gas/).
Start the server:
Start a NEO node:
```
./bin/neo-go -seed 127.0.0.1:20333
./bin/neo-go node -seed 127.0.0.1:20333
```
You can add multiple seeds if you want:
```
./bin/neo-go -seed 127.0.0.1:20333,127.0.01:20334
./bin/neo-go node -seed 127.0.0.1:20333,127.0.01:20334
```
By default the server will currently run on port 3000, for testing purposes.
You can change that by setting the tcp flag:
```
./bin/neo-go -seed 127.0.0.1:20333 -tcp 1337
./bin/neo-go node -seed 127.0.0.1:20333 -tcp 1337
```
## RPC
@ -98,10 +98,10 @@ You can change that by setting the tcp flag:
If you want your node to also serve JSON-RPC, you can do that by setting the following flag:
```
./bin/neo-go -rpc 4000
./bin/neo-go node -rpc 4000
```
In this case server will accept and respond JSON-RPC on port 4000.
In this case the server will accept and respond JSON-RPC on port 4000.
Keep in mind that currently there is only a small subset of the JSON-RPC implemented.
Feel free to make a PR with more functionality.
@ -112,9 +112,42 @@ TODO
```
## Smart Contracts
> NOTE: At this moment there is only a small subset of the Go language implemented.
### Compile a smart contract
```
TODO
./bin/neo-go contract compile mycontract.go
```
By default the filename will be the name of your .go file with the .avm extension, the file will be located in the same directory where you called the command from. If you want another location for your compiled contract:
```
./bin/neo-go contract compile mycontract.go --out /Users/foo/bar/contract.avm
```
### Debugging your smart contract
You can dump the opcodes generated by the compiler with the following command:
```
./bin/neo-go contract opdump mycontract.go
```
This will result in something like this:
```
INDEX OPCODE DESC
0 0x52 OpPush2
1 0xc5 OpNewArray
2 0x6b OpToAltStack
3 0x 0 OpPush0
4 0x6c OpFromAltStack
5 0x76 OpDup
6 0x6b OpToAltStack
7 0x 0 OpPush0
8 0x52 OpPush2
9 0x7a OpRoll
10 0xc4 OpSetItem
```
# Contributing

View file

@ -1 +1 @@
0.9.0
0.10.0

View file

@ -1,9 +1,14 @@
package smartcontract
import (
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/CityOfZion/neo-go/pkg/vm"
"github.com/CityOfZion/neo-go/pkg/vm/compiler"
"github.com/urfave/cli"
)
@ -17,6 +22,12 @@ func NewCommand() cli.Command {
Name: "compile",
Usage: "compile a smart contract to a .avm file",
Action: contractCompile,
Flags: []cli.Flag{
cli.StringFlag{
Name: "out, o",
Usage: "Output of the compiled contract",
},
},
},
{
Name: "opdump",
@ -28,14 +39,40 @@ func NewCommand() cli.Command {
}
func contractCompile(ctx *cli.Context) error {
fmt.Println("compile")
return nil
if len(ctx.Args()) == 0 {
return errors.New("not enough arguments")
}
src := ctx.Args()[0]
c := compiler.New()
if err := c.CompileSource(src); err != nil {
return err
}
filename := strings.Split(src, ".")[0]
filename = filename + ".avm"
out := ctx.String("out")
if len(out) > 0 {
filename = out
}
f, err := os.Create(out)
if err != nil {
return err
}
hx := hex.EncodeToString(c.Buffer().Bytes())
fmt.Println(hx)
_, err = io.Copy(f, c.Buffer())
return err
}
func contractDumpOpcode(ctx *cli.Context) error {
src := ctx.Args()[0]
c := vm.NewCompiler()
c := compiler.New()
if err := c.CompileSource(src); err != nil {
return err
}

View file

@ -45,8 +45,12 @@ func Uint256DecodeFromBytes(b []byte) (Uint256, error) {
// ToArrayReverse return a reversed version of the given byte slice.
func ToArrayReverse(b []byte) []byte {
dest := make([]byte, len(b))
// Protect from big.Ints that have 1 len bytes.
if len(b) < 2 {
return b
}
dest := make([]byte, len(b))
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
dest[i], dest[j] = b[j], b[i]
}

View file

@ -1 +1,25 @@
package util
import (
"bytes"
"testing"
)
func TestToArrayReverse(t *testing.T) {
arr := []byte{0x01, 0x02, 0x03, 0x04}
have := ToArrayReverse(arr)
want := []byte{0x04, 0x03, 0x02, 0x01}
if bytes.Compare(have, want) != 0 {
t.Fatalf("expected %v got %v", want, have)
}
}
// This tests a bug that occured with arrays of size 1
func TestToArrayReverseLen2(t *testing.T) {
arr := []byte{0x01}
have := ToArrayReverse(arr)
want := []byte{0x01}
if bytes.Compare(have, want) != 0 {
t.Fatalf("expected %v got %v", want, have)
}
}

View file

@ -1,194 +0,0 @@
package vm
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"os"
"reflect"
"strconv"
"strings"
)
const (
outputExt = ".avm"
)
// Syscalls that have no return value.
var nonReturnSysCalls = []string{
"Notify", "print", "Log", "Put", "Register",
"append", "Delete", "SetVotes", "ContractDestroy",
"MerkleRoot", "Hash", "PrevHash", "GetHeader"}
// Compiler holds the output buffer of the compiled source.
type Compiler struct {
// Output extension of the file. Default .avm.
OutputExt string
sb *ScriptBuilder
curLineNum int
i int
varList []Variable
vars map[string]Variable
}
// NewCompiler returns a new compiler ready to compile smartcontracts.
func NewCompiler() *Compiler {
return &Compiler{
OutputExt: outputExt,
sb: &ScriptBuilder{new(bytes.Buffer)},
vars: map[string]Variable{},
varList: []Variable{},
}
}
// 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)
}
// Visit implements the ast.Visitor interface.
func (c *Compiler) Visit(node ast.Node) ast.Visitor {
switch t := node.(type) {
case *ast.AssignStmt:
c.processAssignStmt(t)
case *ast.FuncDecl:
case *ast.ReturnStmt:
}
return c
}
func (c *Compiler) newVariable(k token.Token, i, val string) Variable {
v := Variable{
kind: k,
ident: i,
value: val,
pos: c.i,
}
c.vars[v.ident] = v
c.i++
return v
}
func (c *Compiler) initialize(n OpCode) {
// Get the (n) localVars which is basicly the number of args passed in Main
// and the number of local Vars in the function body.
c.sb.emitPush(n)
c.sb.emitPush(OpNewArray)
c.sb.emitPush(OpToAltStack)
}
func (c *Compiler) teardown() {
c.sb.emitPush(OpNOP)
c.sb.emitPush(OpFromAltStack)
c.sb.emitPush(OpDrop)
c.sb.emitPush(OpRET)
}
// Push a variable on to the stack.
func (c *Compiler) storeLocal(v Variable) {
if v.kind == token.INT {
val, _ := strconv.Atoi(v.value)
c.sb.emitPushInt(int64(val))
}
if v.kind == token.STRING {
val := strings.Replace(v.value, `"`, "", 2)
c.sb.emitPushString(val)
}
c.sb.emitPush(OpFromAltStack)
c.sb.emitPush(OpDup)
c.sb.emitPush(OpToAltStack)
pos := int64(v.pos)
c.sb.emitPushInt(pos)
c.sb.emitPushInt(2)
c.sb.emitPush(OpRoll)
c.sb.emitPush(OpSetItem)
}
func (c *Compiler) loadLocal(ident string) {
val, ok := c.vars[ident]
if !ok {
c.reportError(fmt.Sprintf("local variable %s not found", ident))
}
pos := int64(val.pos)
c.sb.emitPush(OpFromAltStack)
c.sb.emitPush(OpDup)
c.sb.emitPush(OpToAltStack)
// push it's index on the stack
c.sb.emitPushInt(pos)
c.sb.emitPush(OpPickItem)
}
// TODO: instead of passing the stmt in to this, put the lhs and rhs in.
// so we can reuse this.
func (c *Compiler) processAssignStmt(stmt *ast.AssignStmt) {
lhs := stmt.Lhs[0].(*ast.Ident)
switch t := stmt.Rhs[0].(type) {
case *ast.BasicLit:
c.storeLocal(c.newVariable(t.Kind, lhs.Name, t.Value))
case *ast.CompositeLit:
switch t.Type.(type) {
case *ast.StructType:
c.reportError("assigning struct literals not yet implemented")
case *ast.ArrayType:
// for _, expr := range t.Elts {
// v := expr.(*ast.BasicLit)
// c.storeLocal(c.newVariable(v.Kind, lhs.Name, v.Value))
// }
}
case *ast.Ident:
c.loadLocal(t.Name)
case *ast.FuncLit:
c.reportError("assigning function literals not yet implemented")
default:
fmt.Println(reflect.TypeOf(t))
}
}
// 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
}
c.initialize(OpPush2) // initialize the compiler with n local stack vars.
ast.Walk(c, f) // walk through and process the AST
c.teardown() // done compiling
return nil
}
// TODO: More detailed report (lineno, ...)
func (c *Compiler) reportError(msg string) {
fmt.Printf("COMPILER ERROR :: %s\n", msg)
os.Exit(1)
}
// DumpOpcode dumps the current buffer, formatted with index, hex and opcode.
// Usefull for debugging smartcontracts.
func (c *Compiler) DumpOpcode() {
c.sb.dumpOpcode()
}
// A Variable can represent any variable in the program.
type Variable struct {
ident string
kind token.Token
value string
pos int
}

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

View file

@ -1,86 +0,0 @@
package vm
import (
"strings"
"testing"
)
// opener
//
// 0x53 push len arguments
// 0xc5 open new array
// 0x6B to alt stack
//
// 0x5A the number 10 is pushed on the stack
// 0x6C put the input onto the main stack remove from alt
// 0x76 dup the item on top of the stack
// 0x6B put the item on the alt stack
// 0x00 put empty array on the stack
// 0x52 put the number 2 on the stack
// 0x7A put the item n back on top of the stack
// 0xC4 set item
// 0x59 put the number 9 on the stack
// 0x6C put the input onto the main stack remove from alt stack
// 0x76 dup the item on top of the stackj
// 0x6B put the item on the alt stack
// 0x51 push the number 1 on the stack
// 0x52 push the number 2 on the stack
// 0x7A put the item n back on top of the stack
// 0xC4 set the item
// 0x62 JMP
// 0x03 the next 3 bytes is dat pushed on the stack
// 0x6C put the input ont the main stack remove from alt stack
// 0x00 put empty array onto the stack
// 0x02 the next 2 bytes is data pushed on the stack
// 0xE8 1000 uint16
// 0x03 1000 uint16
// 0x6C put the input onto the main stack remove from alt
// 0x76 dup the item on top of the stack
// 0x6B put the item on the alt stack
// 0x52 push the number 2 on the stack
// 0x52 push the number 2 on the stack
// 0x7A put the item n back on top of the stack
// 0xC4 set the item
// 0x00 empty array is pushed on the stack
// 0x61 nop
// 0x6C put the input onto the main stack remove from alt
// 0x75 removes the top stack item
// 0x66 return
//
func TestSimpleAssign(t *testing.T) {
src := `
package NEP5
func Main() {
x := 10
y := 8
}
`
c := NewCompiler()
if err := c.Compile(strings.NewReader(src)); err != nil {
t.Fatal(err)
}
// c.DumpOpcode()
}
func TestAssignLoadLocal(t *testing.T) {
src := `
package NEP5
func Main() {
x := 1
y := x
}
`
c := NewCompiler()
if err := c.Compile(strings.NewReader(src)); err != nil {
t.Fatal(err)
}
// c.DumpOpcode()
}

View file

@ -1,127 +0,0 @@
package vm
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math/big"
"github.com/CityOfZion/neo-go/pkg/util"
)
// ScriptBuilder generates bytecode and will write all
// generated bytecode into its internal buffer.
type ScriptBuilder struct {
buf *bytes.Buffer
}
func (sb *ScriptBuilder) emit(op 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 OpCode) error {
return sb.buf.WriteByte(byte(op))
}
func (sb *ScriptBuilder) emitPushBool(b bool) error {
if b {
return sb.emitPush(OpPushT)
}
return sb.emitPush(OpPushF)
}
func (sb *ScriptBuilder) emitPushInt(i int64) error {
if i == -1 {
return sb.emitPush(OpPushM1)
}
if i == 0 {
return sb.emitPush(OpPushF)
}
if i > 0 && i < 16 {
val := OpCode((int(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(OpPushBytes75) {
return sb.emit(OpCode(n), b)
} else if n < 0x100 {
err = sb.emit(OpPushData1, []byte{byte(n)})
} else if n < 0x10000 {
buf := make([]byte, 2)
binary.LittleEndian.PutUint16(buf, uint16(n))
err = sb.emit(OpPushData2, buf)
} else {
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, uint32(n))
err = sb.emit(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(OpSysCall, args)
}
func (sb *ScriptBuilder) emitPushCall(scriptHash []byte, tailCall bool) error {
if len(scriptHash) != 20 {
return errors.New("expected a 20 byte long scriptHash (uint160) for pushCall")
}
op := OpAppCall
if tailCall {
op = OpTailCall
}
return sb.emit(op, scriptHash)
}
func (sb *ScriptBuilder) emitJump(op OpCode, offset int16) error {
if op != OpJMP && op != OpJMPIF && op != OpJMPIFNOT && op != OpCall {
return fmt.Errorf("invalid jump opcode: %v", op)
}
return sb.emit(op, []byte{}) // convert to bits?
}
func (sb *ScriptBuilder) dumpOpcode() {
buf := sb.buf.Bytes()
for i := 0; i < len(buf); i++ {
fmt.Printf("OPCODE AT INDEX \t %d \t 0x%2x \t %s \n", i, buf[i], OpCode(buf[i]))
}
}

View file

@ -1,74 +0,0 @@
package vm
import (
"bytes"
"math/big"
"testing"
"github.com/CityOfZion/neo-go/pkg/util"
)
func TestEmitPush(t *testing.T) {
sb := &ScriptBuilder{new(bytes.Buffer)}
if err := sb.emitPush(OpPush1); err != nil {
t.Fatal(err)
}
if sb.buf.Len() != 1 {
t.Fatalf("expect buffer len of 1 got %d", sb.buf.Len())
}
}
func TestEmitPushInt(t *testing.T) {
sb := &ScriptBuilder{new(bytes.Buffer)}
val := -1
if err := sb.emitPushInt(int64(val)); err != nil {
t.Fatal(err)
}
if want, have := OpPushM1, OpCode(sb.buf.Bytes()[0]); want != have {
t.Fatalf("expected %v got %v", want, have)
}
val = 0
if err := sb.emitPushInt(int64(val)); err != nil {
t.Fatal(err)
}
if want, have := OpPushF, OpCode(sb.buf.Bytes()[1]); want != have {
t.Fatalf("expected %v got %v", want, have)
}
val = 1
if err := sb.emitPushInt(int64(val)); err != nil {
t.Fatal(err)
}
if want, have := OpPush1, OpCode(sb.buf.Bytes()[2]); want != have {
t.Fatalf("expected %v got %v", want, have)
}
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()[3]); want != have {
t.Fatalf("expected %v got %v", want, have)
}
want := util.ToArrayReverse(bInt.Bytes()) // reverse
have := sb.buf.Bytes()[4:]
if bytes.Compare(want, have) != 0 {
t.Fatalf("expected %v got %v", want, have)
}
}
func TestEmitPushString(t *testing.T) {
sb := &ScriptBuilder{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)
}
}