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