diff --git a/docs/compiler.md b/docs/compiler.md index d3166c73b..382aa0d93 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -36,6 +36,10 @@ functionality as Neo .net Framework library. Compiler provides some helpful builtins in `util` and `convert` packages. Refer to them for detailed documentation. +`_deploy()` function has a special meaning and is executed when contract is deployed. +It should return no value and accept single bool argument which will be true on contract update. +`_deploy()` functions are called for every imported package in the same order as `init()`. + ## Quick start ### Compiling diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 66bbfcda0..b1015d3cd 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -40,13 +40,15 @@ func (c *codegen) getIdentName(pkg string, name string) string { // and returns number of variables initialized. // Second return value is -1 if no `init()` functions were encountered // and number of maximum amount of locals in any of init functions otherwise. -func (c *codegen) traverseGlobals() (int, int) { +// Same for `_deploy()` functions (see docs/compiler.md). +func (c *codegen) traverseGlobals() (int, int, int) { var hasDefer bool var n int initLocals := -1 + deployLocals := -1 c.ForEachFile(func(f *ast.File, _ *types.Package) { n += countGlobals(f) - if initLocals == -1 || !hasDefer { + if initLocals == -1 || deployLocals == -1 || !hasDefer { ast.Inspect(f, func(node ast.Node) bool { switch n := node.(type) { case *ast.FuncDecl: @@ -55,6 +57,11 @@ func (c *codegen) traverseGlobals() (int, int) { if c > initLocals { initLocals = c } + } else if isDeployFunc(n) { + c, _ := countLocals(n) + if c > deployLocals { + deployLocals = c + } } return !hasDefer case *ast.DeferStmt: @@ -71,7 +78,7 @@ func (c *codegen) traverseGlobals() (int, int) { if n != 0 || initLocals > -1 { if n > 255 { c.prog.BinWriter.Err = errors.New("too many global variables") - return 0, initLocals + return 0, initLocals, deployLocals } if n != 0 { emit.Instruction(c.prog.BinWriter, opcode.INITSSLOT, []byte{byte(n)}) @@ -104,7 +111,7 @@ func (c *codegen) traverseGlobals() (int, int) { c.globals[""] = c.exceptionIndex } } - return n, initLocals + return n, initLocals, deployLocals } // countGlobals counts the global variables in the program to add diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index da2f971b1..5a9bbebf9 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -61,6 +61,8 @@ type codegen struct { // initEndOffset specifies the end of the initialization method. initEndOffset int + // deployEndOffset specifies the end of the deployment method. + deployEndOffset int // importMap contains mapping from package aliases to full package names for the current file. importMap map[string]string @@ -328,13 +330,46 @@ func (c *codegen) convertInitFuncs(f *ast.File, pkg *types.Package, seenBefore b return seenBefore } +func isDeployFunc(decl *ast.FuncDecl) bool { + if decl.Name.Name != "_deploy" || decl.Recv != nil || + decl.Type.Params.NumFields() != 1 || + decl.Type.Results.NumFields() != 0 { + return false + } + typ, ok := decl.Type.Params.List[0].Type.(*ast.Ident) + return ok && typ.Name == "bool" +} + +func (c *codegen) convertDeployFuncs() { + seenBefore := false + c.ForEachFile(func(f *ast.File, pkg *types.Package) { + ast.Inspect(f, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.FuncDecl: + if isDeployFunc(n) { + if seenBefore { + cnt, _ := countLocals(n) + c.clearSlots(cnt) + } + c.convertFuncDecl(f, n, pkg) + seenBefore = true + } + case *ast.GenDecl: + return false + } + return true + }) + }) +} + func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl, pkg *types.Package) { var ( f *funcScope ok, isLambda bool ) isInit := isInitFunc(decl) - if isInit { + isDeploy := isDeployFunc(decl) + if isInit || isDeploy { f = c.newFuncScope(decl, c.newLabel()) } else { f, ok = c.funcs[c.getFuncNameFromDecl("", decl)] @@ -358,7 +393,7 @@ func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl, pkg *types. // All globals copied into the scope of the function need to be added // to the stack size of the function. - if !isInit { + if !isInit && !isDeploy { sizeLoc := f.countLocals() if sizeLoc > 255 { c.prog.Err = errors.New("maximum of 255 local variables is allowed") @@ -401,7 +436,7 @@ func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl, pkg *types. // If we have reached the end of the function without encountering `return` statement, // we should clean alt.stack manually. // This can be the case with void and named-return functions. - if !isInit && !lastStmtIsReturn(decl) { + if !isInit && !isDeploy && !lastStmtIsReturn(decl) { c.saveSequencePoint(decl.Body) emit.Opcodes(c.prog.BinWriter, opcode.RET) } @@ -1792,11 +1827,19 @@ func (c *codegen) compile(info *buildInfo, pkg *loader.PackageInfo) error { // Bring all imported functions into scope. c.ForEachFile(c.resolveFuncDecls) - n, initLocals := c.traverseGlobals() + n, initLocals, deployLocals := c.traverseGlobals() hasInit := initLocals > -1 if n > 0 || hasInit { - emit.Opcodes(c.prog.BinWriter, opcode.RET) c.initEndOffset = c.prog.Len() + emit.Opcodes(c.prog.BinWriter, opcode.RET) + } + + hasDeploy := deployLocals > -1 + if hasDeploy { + emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{byte(deployLocals), 1}) + c.convertDeployFuncs() + c.deployEndOffset = c.prog.Len() + emit.Opcodes(c.prog.BinWriter, opcode.RET) } // sort map keys to generate code deterministically. @@ -1814,7 +1857,7 @@ func (c *codegen) compile(info *buildInfo, pkg *loader.PackageInfo) error { // Don't convert the function if it's not used. This will save a lot // of bytecode space. name := c.getFuncNameFromDecl(pkg.Path(), n) - if !isInitFunc(n) && funUsage.funcUsed(name) && !isInteropPath(pkg.Path()) { + if !isInitFunc(n) && !isDeployFunc(n) && funUsage.funcUsed(name) && !isInteropPath(pkg.Path()) { c.convertFuncDecl(f, n, pkg) } } @@ -1837,6 +1880,9 @@ func newCodegen(info *buildInfo, pkg *loader.PackageInfo) *codegen { constMap: map[string]types.TypeAndValue{}, docIndex: map[string]int{}, + initEndOffset: -1, + deployEndOffset: -1, + sequencePoints: make(map[string][]DebugSeqPoint), } } diff --git a/pkg/compiler/debug.go b/pkg/compiler/debug.go index 4fd9a2b59..bd51eb8ee 100644 --- a/pkg/compiler/debug.go +++ b/pkg/compiler/debug.go @@ -134,6 +134,27 @@ func (c *codegen) emitDebugInfo(contract []byte) *DebugInfo { SeqPoints: c.sequencePoints["init"], }) } + if c.deployEndOffset >= 0 { + d.Methods = append(d.Methods, MethodDebugInfo{ + ID: manifest.MethodDeploy, + Name: DebugMethodName{ + Name: manifest.MethodDeploy, + Namespace: c.mainPkg.Pkg.Name(), + }, + IsExported: true, + IsFunction: true, + Range: DebugRange{ + Start: uint16(c.initEndOffset + 1), + End: uint16(c.deployEndOffset), + }, + Parameters: []DebugParam{{ + Name: "isUpdate", + Type: "Boolean", + }}, + ReturnType: "Void", + SeqPoints: c.sequencePoints[manifest.MethodDeploy], + }) + } for name, scope := range c.funcs { m := c.methodInfoFromScope(name, scope) if m.Range.Start == m.Range.End { diff --git a/pkg/compiler/debug_test.go b/pkg/compiler/debug_test.go index d795a05e8..6eabaf703 100644 --- a/pkg/compiler/debug_test.go +++ b/pkg/compiler/debug_test.go @@ -55,6 +55,7 @@ func MethodParams(addr interop.Hash160, h interop.Hash256, type MyStruct struct {} func (ms MyStruct) MethodOnStruct() { } func (ms *MyStruct) MethodOnPointerToStruct() { } +func _deploy(isUpdate bool) {} ` info, err := getBuildInfo("foo.go", src) @@ -83,6 +84,7 @@ func (ms *MyStruct) MethodOnPointerToStruct() { } "MethodOnStruct": "Void", "MethodOnPointerToStruct": "Void", "MethodParams": "Boolean", + "_deploy": "Void", } for i := range d.Methods { name := d.Methods[i].ID @@ -104,6 +106,10 @@ func (ms *MyStruct) MethodOnPointerToStruct() { } t.Run("param types", func(t *testing.T) { paramTypes := map[string][]DebugParam{ + "_deploy": {{ + Name: "isUpdate", + Type: "Boolean", + }}, "MethodInt": {{ Name: "a", Type: "String", @@ -151,8 +157,16 @@ func (ms *MyStruct) MethodOnPointerToStruct() { } Hash: hash.Hash160(buf), Methods: []manifest.Method{ { - Name: "main", + Name: "_deploy", Offset: 0, + Parameters: []manifest.Parameter{ + manifest.NewParameter("isUpdate", smartcontract.BoolType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "main", + Offset: 4, Parameters: []manifest.Parameter{ manifest.NewParameter("op", smartcontract.StringType), }, @@ -160,7 +174,7 @@ func (ms *MyStruct) MethodOnPointerToStruct() { } }, { Name: "methodInt", - Offset: 66, + Offset: 70, Parameters: []manifest.Parameter{ { Name: "a", @@ -171,31 +185,31 @@ func (ms *MyStruct) MethodOnPointerToStruct() { } }, { Name: "methodString", - Offset: 97, + Offset: 101, Parameters: []manifest.Parameter{}, ReturnType: smartcontract.StringType, }, { Name: "methodByteArray", - Offset: 103, + Offset: 107, Parameters: []manifest.Parameter{}, ReturnType: smartcontract.ByteArrayType, }, { Name: "methodArray", - Offset: 108, + Offset: 112, Parameters: []manifest.Parameter{}, ReturnType: smartcontract.ArrayType, }, { Name: "methodStruct", - Offset: 113, + Offset: 117, Parameters: []manifest.Parameter{}, ReturnType: smartcontract.ArrayType, }, { Name: "methodConcat", - Offset: 88, + Offset: 92, Parameters: []manifest.Parameter{ { Name: "a", @@ -214,7 +228,7 @@ func (ms *MyStruct) MethodOnPointerToStruct() { } }, { Name: "methodParams", - Offset: 125, + Offset: 129, Parameters: []manifest.Parameter{ manifest.NewParameter("addr", smartcontract.Hash160Type), manifest.NewParameter("h", smartcontract.Hash256Type),