Skip to content

Commit

Permalink
Merge pull request #1228 from nspcc-dev/fix/contractcall
Browse files Browse the repository at this point in the history
Adjust `System.Contract.Call.*` interops
  • Loading branch information
fyrchik authored Jul 28, 2020
2 parents 9adb3a0 + 84c1485 commit 5cf4481
Show file tree
Hide file tree
Showing 32 changed files with 539 additions and 285 deletions.
24 changes: 18 additions & 6 deletions pkg/compiler/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ var (
goBuiltins = []string{"len", "append", "panic"}
// Custom builtin utility functions.
customBuiltins = []string{
"AppCall",
"FromAddress", "Equals",
"ToBool", "ToByteArray", "ToInteger",
}
Expand All @@ -28,16 +27,23 @@ func (c *codegen) newGlobal(name string) {
}

// traverseGlobals visits and initializes global variables.
func (c *codegen) traverseGlobals(f ast.Node) {
n := countGlobals(f)
// and returns number of variables initialized.
func (c *codegen) traverseGlobals(fs ...*ast.File) int {
var n int
for _, f := range fs {
n += countGlobals(f)
}
if n != 0 {
if n > 255 {
c.prog.BinWriter.Err = errors.New("too many global variables")
return
return 0
}
emit.Instruction(c.prog.BinWriter, opcode.INITSSLOT, []byte{byte(n)})
for _, f := range fs {
c.convertGlobals(f)
}
}
c.convertGlobals(f)
return n
}

// countGlobals counts the global variables in the program to add
Expand Down Expand Up @@ -113,10 +119,11 @@ func lastStmtIsReturn(decl *ast.FuncDecl) (b bool) {
return false
}

func analyzeFuncUsage(pkgs map[*types.Package]*loader.PackageInfo) funcUsage {
func analyzeFuncUsage(mainPkg *loader.PackageInfo, pkgs map[*types.Package]*loader.PackageInfo) funcUsage {
usage := funcUsage{}

for _, pkg := range pkgs {
isMain := pkg == mainPkg
for _, f := range pkg.Files {
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
Expand All @@ -127,6 +134,11 @@ func analyzeFuncUsage(pkgs map[*types.Package]*loader.PackageInfo) funcUsage {
case *ast.SelectorExpr:
usage[t.Sel.Name] = true
}
case *ast.FuncDecl:
// exported functions are always assumed to be used
if isMain && n.Name.IsExported() {
usage[n.Name.Name] = true
}
}
return true
})
Expand Down
43 changes: 16 additions & 27 deletions pkg/compiler/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ import (
"golang.org/x/tools/go/loader"
)

// The identifier of the entry function. Default set to Main.
const mainIdent = "Main"

type codegen struct {
// Information about the program with all its dependencies.
buildInfo *buildInfo
Expand Down Expand Up @@ -62,6 +59,12 @@ type codegen struct {
// to a text span in the source file.
sequencePoints map[string][]DebugSeqPoint

// initEndOffset specifies the end of the initialization method.
initEndOffset int

// mainPkg is a main package metadata.
mainPkg *loader.PackageInfo

// Label table for recording jump destinations.
l []int
}
Expand Down Expand Up @@ -1220,13 +1223,6 @@ func (c *codegen) convertBuiltin(expr *ast.CallExpr) {
typ = stackitem.BooleanT
}
c.emitConvert(typ)
case "AppCall":
c.emitReverse(len(expr.Args))
buf := c.getByteArray(expr.Args[0])
if buf != nil && len(buf) != 20 {
c.prog.Err = errors.New("invalid script hash")
}
emit.Syscall(c.prog.BinWriter, "System.Contract.Call")
case "Equals":
emit.Opcode(c.prog.BinWriter, opcode.EQUAL)
case "FromAddress":
Expand Down Expand Up @@ -1419,14 +1415,7 @@ func (c *codegen) newLambda(u uint16, lit *ast.FuncLit) {
}

func (c *codegen) compile(info *buildInfo, pkg *loader.PackageInfo) error {
// Resolve the entrypoint of the program.
main, mainFile := resolveEntryPoint(mainIdent, pkg)
if main == nil {
c.prog.Err = fmt.Errorf("could not find func main. Did you forget to declare it? ")
return c.prog.Err
}

funUsage := analyzeFuncUsage(info.program.AllPackages)
funUsage := analyzeFuncUsage(pkg, info.program.AllPackages)

// Bring all imported functions into scope.
for _, pkg := range info.program.AllPackages {
Expand All @@ -1435,10 +1424,12 @@ func (c *codegen) compile(info *buildInfo, pkg *loader.PackageInfo) error {
}
}

c.traverseGlobals(mainFile)

// convert the entry point first.
c.convertFuncDecl(mainFile, main, pkg.Pkg)
c.mainPkg = pkg
n := c.traverseGlobals(pkg.Files...)
if n > 0 {
emit.Opcode(c.prog.BinWriter, opcode.RET)
c.initEndOffset = c.prog.Len()
}

// sort map keys to generate code deterministically.
keys := make([]*types.Package, 0, len(info.program.AllPackages))
Expand All @@ -1458,7 +1449,7 @@ func (c *codegen) compile(info *buildInfo, pkg *loader.PackageInfo) error {
case *ast.FuncDecl:
// Don't convert the function if it's not used. This will save a lot
// of bytecode space.
if n.Name.Name != mainIdent && funUsage.funcUsed(n.Name.Name) {
if funUsage.funcUsed(n.Name.Name) {
c.convertFuncDecl(f, n, k)
}
}
Expand Down Expand Up @@ -1504,10 +1495,8 @@ func (c *codegen) resolveFuncDecls(f *ast.File, pkg *types.Package) {
for _, decl := range f.Decls {
switch n := decl.(type) {
case *ast.FuncDecl:
if n.Name.Name != mainIdent {
c.newFunc(n)
c.funcs[n.Name.Name].pkg = pkg
}
c.newFunc(n)
c.funcs[n.Name.Name].pkg = pkg
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func CompileAndSave(src string, o *Options) ([]byte, error) {
}

if o.ManifestFile != "" {
m, err := di.convertToManifest(o.ContractFeatures)
m, err := di.ConvertToManifest(o.ContractFeatures)
if err != nil {
return b, errors.Wrap(err, "failed to convert debug info to manifest")
}
Expand Down
53 changes: 28 additions & 25 deletions pkg/compiler/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

// DebugInfo represents smart-contract debug information.
type DebugInfo struct {
MainPkg string `json:"-"`
Hash util.Uint160 `json:"hash"`
Documents []string `json:"documents"`
Methods []MethodDebugInfo `json:"methods"`
Expand Down Expand Up @@ -102,8 +103,24 @@ func (c *codegen) saveSequencePoint(n ast.Node) {

func (c *codegen) emitDebugInfo(contract []byte) *DebugInfo {
d := &DebugInfo{
Hash: hash.Hash160(contract),
Events: []EventDebugInfo{},
MainPkg: c.mainPkg.Pkg.Name(),
Hash: hash.Hash160(contract),
Events: []EventDebugInfo{},
}
if c.initEndOffset > 0 {
d.Methods = append(d.Methods, MethodDebugInfo{
ID: manifest.MethodInit,
Name: DebugMethodName{
Name: manifest.MethodInit,
Namespace: c.mainPkg.Pkg.Name(),
},
IsExported: true,
Range: DebugRange{
Start: 0,
End: uint16(c.initEndOffset),
},
ReturnType: "Void",
})
}
for name, scope := range c.funcs {
m := c.methodInfoFromScope(name, scope)
Expand Down Expand Up @@ -270,6 +287,7 @@ func (m *MethodDebugInfo) ToManifestMethod() (manifest.Method, error) {
return result, err
}
result.Name = strings.ToLower(string(m.Name.Name[0])) + m.Name.Name[1:]
result.Offset = int(m.Range.Start)
result.Parameters = parameters
result.ReturnType = returnType
return result, nil
Expand Down Expand Up @@ -337,30 +355,16 @@ func parsePairJSON(data []byte, sep string) (string, string, error) {
return ss[0], ss[1], nil
}

// convertToManifest converts contract to the manifest.Manifest struct for debugger.
// ConvertToManifest converts contract to the manifest.Manifest struct for debugger.
// Note: manifest is taken from the external source, however it can be generated ad-hoc. See #1038.
func (di *DebugInfo) convertToManifest(fs smartcontract.PropertyState) (*manifest.Manifest, error) {
var (
entryPoint manifest.Method
mainNamespace string
err error
)
for _, method := range di.Methods {
if method.Name.Name == mainIdent {
entryPoint, err = method.ToManifestMethod()
if err != nil {
return nil, err
}
mainNamespace = method.Name.Namespace
break
}
}
if entryPoint.Name == "" {
func (di *DebugInfo) ConvertToManifest(fs smartcontract.PropertyState) (*manifest.Manifest, error) {
var err error
if di.MainPkg == "" {
return nil, errors.New("no Main method was found")
}
methods := make([]manifest.Method, 0)
for _, method := range di.Methods {
if method.Name.Name != mainIdent && method.IsExported && method.Name.Namespace == mainNamespace {
if method.IsExported && method.Name.Namespace == di.MainPkg {
mMethod, err := method.ToManifestMethod()
if err != nil {
return nil, err
Expand All @@ -379,10 +383,9 @@ func (di *DebugInfo) convertToManifest(fs smartcontract.PropertyState) (*manifes
result := manifest.NewManifest(di.Hash)
result.Features = fs
result.ABI = manifest.ABI{
Hash: di.Hash,
EntryPoint: entryPoint,
Methods: methods,
Events: events,
Hash: di.Hash,
Methods: methods,
Events: events,
}
result.Permissions = []manifest.Permission{
{
Expand Down
30 changes: 17 additions & 13 deletions pkg/compiler/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,24 +127,24 @@ func unexportedMethod() int { return 1 }
}

t.Run("convert to Manifest", func(t *testing.T) {
actual, err := d.convertToManifest(smartcontract.HasStorage)
actual, err := d.ConvertToManifest(smartcontract.HasStorage)
require.NoError(t, err)
// note: offsets are hard to predict, so we just take them from the output
expected := &manifest.Manifest{
ABI: manifest.ABI{
Hash: hash.Hash160(buf),
EntryPoint: manifest.Method{
Name: "main",
Parameters: []manifest.Parameter{
{
Name: "op",
Type: smartcontract.StringType,
Methods: []manifest.Method{
{
Name: "main",
Offset: 0,
Parameters: []manifest.Parameter{
manifest.NewParameter("op", smartcontract.StringType),
},
ReturnType: smartcontract.BoolType,
},
ReturnType: smartcontract.BoolType,
},
Methods: []manifest.Method{
{
Name: "methodInt",
Name: "methodInt",
Offset: 66,
Parameters: []manifest.Parameter{
{
Name: "a",
Expand All @@ -155,26 +155,31 @@ func unexportedMethod() int { return 1 }
},
{
Name: "methodString",
Offset: 97,
Parameters: []manifest.Parameter{},
ReturnType: smartcontract.StringType,
},
{
Name: "methodByteArray",
Offset: 103,
Parameters: []manifest.Parameter{},
ReturnType: smartcontract.ByteArrayType,
},
{
Name: "methodArray",
Offset: 108,
Parameters: []manifest.Parameter{},
ReturnType: smartcontract.ArrayType,
},
{
Name: "methodStruct",
Offset: 113,
Parameters: []manifest.Parameter{},
ReturnType: smartcontract.ArrayType,
},
{
Name: "methodConcat",
Name: "methodConcat",
Offset: 88,
Parameters: []manifest.Parameter{
{
Name: "a",
Expand Down Expand Up @@ -214,7 +219,6 @@ func unexportedMethod() int { return 1 }
}
require.True(t, expected.ABI.Hash.Equals(actual.ABI.Hash))
require.ElementsMatch(t, expected.ABI.Methods, actual.ABI.Methods)
require.Equal(t, expected.ABI.EntryPoint, actual.ABI.EntryPoint)
require.Equal(t, expected.ABI.Events, actual.ABI.Events)
require.Equal(t, expected.Groups, actual.Groups)
require.Equal(t, expected.Features, actual.Features)
Expand Down
22 changes: 22 additions & 0 deletions pkg/compiler/global_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ package compiler_test
import (
"fmt"
"math/big"
"strings"
"testing"

"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/stretchr/testify/require"
)

func TestChangeGlobal(t *testing.T) {
Expand Down Expand Up @@ -105,3 +110,20 @@ func TestArgumentLocal(t *testing.T) {
eval(t, src, big.NewInt(40))
})
}

func TestContractWithNoMain(t *testing.T) {
src := `package foo
var someGlobal int = 1
func Add3(a int) int {
someLocal := 2
return someGlobal + someLocal + a
}`
b, di, err := compiler.CompileWithDebugInfo(strings.NewReader(src))
require.NoError(t, err)
v := vm.New()
invokeMethod(t, "Add3", b, v, di)
v.Estack().PushVal(39)
require.NoError(t, v.Run())
require.Equal(t, 1, v.Estack().Len())
require.Equal(t, big.NewInt(42), v.PopResult())
}
Loading

0 comments on commit 5cf4481

Please sign in to comment.