From 1f238ce6fd20d3e0d1708f0833b47bb67c08b859 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 5 Feb 2021 14:31:45 +0300 Subject: [PATCH 1/8] compiler: do not emit RET twice for nested BlockStmt When function finishes with `*ast.BlockStmt`, last return in that block should be looked for. --- pkg/compiler/analysis.go | 14 ++++++++++---- pkg/compiler/codegen.go | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 0004db63b..880ce84fa 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -175,10 +175,16 @@ func (f funcUsage) funcUsed(name string) bool { } // lastStmtIsReturn checks if last statement of the declaration was return statement.. -func lastStmtIsReturn(decl *ast.FuncDecl) (b bool) { - if l := len(decl.Body.List); l != 0 { - _, ok := decl.Body.List[l-1].(*ast.ReturnStmt) - return ok +func lastStmtIsReturn(body *ast.BlockStmt) (b bool) { + if l := len(body.List); l != 0 { + switch inner := body.List[l-1].(type) { + case *ast.BlockStmt: + return lastStmtIsReturn(inner) + case *ast.ReturnStmt: + return true + default: + return false + } } return false } diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 4dceef1d1..ae98df43b 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -440,7 +440,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 && !isDeploy && !lastStmtIsReturn(decl) { + if !isInit && !isDeploy && !lastStmtIsReturn(decl.Body) { c.saveSequencePoint(decl.Body) emit.Opcodes(c.prog.BinWriter, opcode.RET) } From 1ae0d022dde36120cc05ffb190ee70e89a4bd76c Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Thu, 4 Feb 2021 15:41:00 +0300 Subject: [PATCH 2/8] compiler: support basic inlining --- pkg/compiler/analysis.go | 8 ++ pkg/compiler/codegen.go | 27 +++++- pkg/compiler/func_scope.go | 2 + pkg/compiler/inline.go | 49 ++++++++++ pkg/compiler/inline_test.go | 125 +++++++++++++++++++++++++ pkg/compiler/testdata/inline/a/a.go | 7 ++ pkg/compiler/testdata/inline/b/b.go | 7 ++ pkg/compiler/testdata/inline/inline.go | 32 +++++++ pkg/compiler/types.go | 5 + 9 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 pkg/compiler/inline.go create mode 100644 pkg/compiler/inline_test.go create mode 100644 pkg/compiler/testdata/inline/a/a.go create mode 100644 pkg/compiler/testdata/inline/b/b.go create mode 100644 pkg/compiler/testdata/inline/inline.go diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 880ce84fa..396b7d4c0 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -304,3 +304,11 @@ func canConvert(s string) bool { } return true } + +// canInline returns true if function is to be inlined. +// Currently there is a static list of function which are inlined, +// this may change in future. +func canInline(s string) bool { + return isNativeHelpersPath(s) || + strings.HasPrefix(s, "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline") +} diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index ae98df43b..992b9c49b 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -31,6 +31,8 @@ type codegen struct { // Type information. typeInfo *types.Info + // pkgInfoInline is stack of type information for packages containing inline functions. + pkgInfoInline []*loader.PackageInfo // A mapping of func identifiers with their scope. funcs map[string]*funcScope @@ -406,6 +408,7 @@ func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl, pkg *types. if sizeArg > 255 { c.prog.Err = errors.New("maximum of 255 local variables is allowed") } + sizeLoc = 255 // FIXME count locals including inline variables if sizeLoc != 0 || sizeArg != 0 { emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{byte(sizeLoc), byte(sizeArg)}) } @@ -623,7 +626,9 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { c.processDefers() c.saveSequencePoint(n) - emit.Opcodes(c.prog.BinWriter, opcode.RET) + if len(c.pkgInfoInline) == 0 { + emit.Opcodes(c.prog.BinWriter, opcode.RET) + } return nil case *ast.IfStmt: @@ -800,7 +805,12 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { switch fun := n.Fun.(type) { case *ast.Ident: - f, ok = c.funcs[c.getIdentName("", fun.Name)] + var pkgName string + if len(c.pkgInfoInline) != 0 { + pkgName = c.pkgInfoInline[len(c.pkgInfoInline)-1].Pkg.Path() + } + f, ok = c.funcs[c.getIdentName(pkgName, fun.Name)] + isBuiltin = isGoBuiltin(fun.Name) if !ok && !isBuiltin { name = fun.Name @@ -809,6 +819,10 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { if fun.Obj != nil && fun.Obj.Kind == ast.Var { isFunc = true } + if ok && canInline(f.pkg.Path()) { + c.inlineCall(f, n) + return nil + } case *ast.SelectorExpr: // If this is a method call we need to walk the AST to load the struct locally. // Otherwise this is a function call from a imported package and we can call it @@ -824,6 +838,10 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { if ok { f.selector = fun.X.(*ast.Ident) isBuiltin = isCustomBuiltin(f) + if canInline(f.pkg.Path()) { + c.inlineCall(f, n) + return nil + } } else { typ := c.typeOf(fun) if _, ok := typ.(*types.Signature); ok { @@ -1919,7 +1937,7 @@ func (c *codegen) compile(info *buildInfo, pkg *loader.PackageInfo) error { // of bytecode space. name := c.getFuncNameFromDecl(pkg.Path(), n) if !isInitFunc(n) && !isDeployFunc(n) && funUsage.funcUsed(name) && - (!isInteropPath(pkg.Path()) || isNativeHelpersPath(pkg.Path())) { + (!isInteropPath(pkg.Path()) && !canInline(pkg.Path())) { c.convertFuncDecl(f, n, pkg) } } @@ -1970,7 +1988,8 @@ func (c *codegen) resolveFuncDecls(f *ast.File, pkg *types.Package) { for _, decl := range f.Decls { switch n := decl.(type) { case *ast.FuncDecl: - c.newFunc(n) + fs := c.newFunc(n) + fs.file = f } } } diff --git a/pkg/compiler/func_scope.go b/pkg/compiler/func_scope.go index f7923fe95..2734712bc 100644 --- a/pkg/compiler/func_scope.go +++ b/pkg/compiler/func_scope.go @@ -22,6 +22,8 @@ type funcScope struct { // Package where the function is defined. pkg *types.Package + file *ast.File + // Program label of the scope label uint16 diff --git a/pkg/compiler/inline.go b/pkg/compiler/inline.go new file mode 100644 index 000000000..9facbe792 --- /dev/null +++ b/pkg/compiler/inline.go @@ -0,0 +1,49 @@ +package compiler + +import ( + "go/ast" + "go/types" + + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" +) + +// inlineCall inlines call of n for function represented by f. +// Call `f(a,b)` for definition `func f(x,y int)` is translated to block: +// { +// x := a +// y := b +// +// } +func (c *codegen) inlineCall(f *funcScope, n *ast.CallExpr) { + pkg := c.buildInfo.program.Package(f.pkg.Path()) + sig := c.typeOf(n.Fun).(*types.Signature) + + // Arguments need to be walked with the current scope, + // while stored in the new. + oldScope := c.scope.vars.locals + c.scope.vars.newScope() + newScope := c.scope.vars.locals + defer c.scope.vars.dropScope() + for i := range n.Args { + c.scope.vars.locals = oldScope + ast.Walk(c, n.Args[i]) + c.scope.vars.locals = newScope + name := sig.Params().At(i).Name() + c.scope.newLocal(name) + c.emitStoreVar("", name) + } + + c.pkgInfoInline = append(c.pkgInfoInline, pkg) + oldMap := c.importMap + c.fillImportMap(f.file, pkg.Pkg) + ast.Inspect(f.decl, c.scope.analyzeVoidCalls) + ast.Walk(c, f.decl.Body) + if c.scope.voidCalls[n] { + for i := 0; i < f.decl.Type.Results.NumFields(); i++ { + emit.Opcodes(c.prog.BinWriter, opcode.DROP) + } + } + c.importMap = oldMap + c.pkgInfoInline = c.pkgInfoInline[:len(c.pkgInfoInline)-1] +} diff --git a/pkg/compiler/inline_test.go b/pkg/compiler/inline_test.go new file mode 100644 index 000000000..05c6db8b3 --- /dev/null +++ b/pkg/compiler/inline_test.go @@ -0,0 +1,125 @@ +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/opcode" + "github.com/stretchr/testify/require" +) + +func checkCallCount(t *testing.T, src string, expectedCall, expectedInitSlot int) { + v := vmAndCompile(t, src) + ctx := v.Context() + actualCall := 0 + actualInitSlot := 0 + + for op, _, err := ctx.Next(); ; op, _, err = ctx.Next() { + require.NoError(t, err) + switch op { + case opcode.CALL, opcode.CALLL: + actualCall++ + case opcode.INITSLOT: + actualInitSlot++ + } + if ctx.IP() == ctx.LenInstr() { + break + } + } + require.Equal(t, expectedCall, actualCall) + require.Equal(t, expectedInitSlot, actualInitSlot) +} + +func TestInline(t *testing.T) { + srcTmpl := `package foo + import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline" + // local alias + func sum(a, b int) int { + return 42 + } + func Main() int { + %s + }` + t.Run("no return", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `inline.NoArgsNoReturn() + return 1`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(1)) + }) + t.Run("has return, dropped", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `inline.NoArgsReturn1() + return 2`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(2)) + }) + t.Run("drop twice", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `inline.DropInsideInline() + return 42`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(42)) + }) + t.Run("no args return 1", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `return inline.NoArgsReturn1()`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(1)) + }) + t.Run("sum", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `return inline.Sum(1, 2)`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(3)) + }) + t.Run("sum squared (nested inline)", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `return inline.SumSquared(1, 2)`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(9)) + }) + t.Run("inline function in inline function parameter", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `return inline.Sum(inline.SumSquared(1, 2), inline.Sum(3, 4))`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(9+3+4)) + }) + t.Run("global name clash", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `return inline.GetSumSameName()`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(42)) + }) + t.Run("local name clash", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `return inline.Sum(inline.SumSquared(1, 2), sum(3, 4))`) + checkCallCount(t, src, 1, 2) + eval(t, src, big.NewInt(51)) + }) +} + +func TestInlineConversion(t *testing.T) { + src1 := `package foo + import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline" + var _ = inline.A + func Main() int { + a := 2 + return inline.SumSquared(1, a) + }` + b1, err := compiler.Compile("foo.go", strings.NewReader(src1)) + require.NoError(t, err) + + src2 := `package foo + import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline" + var _ = inline.A + func Main() int { + a := 2 + { + b := 1 + c := a + { + bb := b + cc := c + return (bb + cc) * (b + c) + } + } + }` + b2, err := compiler.Compile("foo.go", strings.NewReader(src2)) + require.NoError(t, err) + require.Equal(t, b2, b1) +} diff --git a/pkg/compiler/testdata/inline/a/a.go b/pkg/compiler/testdata/inline/a/a.go new file mode 100644 index 000000000..aa61e8142 --- /dev/null +++ b/pkg/compiler/testdata/inline/a/a.go @@ -0,0 +1,7 @@ +package a + +var A = 29 + +func GetA() int { + return A +} diff --git a/pkg/compiler/testdata/inline/b/b.go b/pkg/compiler/testdata/inline/b/b.go new file mode 100644 index 000000000..197ffb124 --- /dev/null +++ b/pkg/compiler/testdata/inline/b/b.go @@ -0,0 +1,7 @@ +package b + +var A = 12 + +func GetA() int { + return A +} diff --git a/pkg/compiler/testdata/inline/inline.go b/pkg/compiler/testdata/inline/inline.go new file mode 100644 index 000000000..d44ec461a --- /dev/null +++ b/pkg/compiler/testdata/inline/inline.go @@ -0,0 +1,32 @@ +package inline + +import ( + "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline/a" + "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline/b" +) + +func NoArgsNoReturn() {} +func NoArgsReturn1() int { + return 1 +} +func Sum(a, b int) int { + return a + b +} +func sum(x, y int) int { + return x + y +} +func SumSquared(a, b int) int { + return sum(a, b) * (a + b) +} + +var A = 1 + +func GetSumSameName() int { + return a.GetA() + b.GetA() + A +} + +func DropInsideInline() int { + sum(1, 2) + sum(3, 4) + return 7 +} diff --git a/pkg/compiler/types.go b/pkg/compiler/types.go index 649c1f146..f331b7eaf 100644 --- a/pkg/compiler/types.go +++ b/pkg/compiler/types.go @@ -8,6 +8,11 @@ import ( ) func (c *codegen) typeAndValueOf(e ast.Expr) types.TypeAndValue { + for i := len(c.pkgInfoInline) - 1; i >= 0; i-- { + if tv, ok := c.pkgInfoInline[i].Types[e]; ok { + return tv + } + } return c.typeInfo.Types[e] } From 27e60455c789de9af8993e05f3ec43b500e459f4 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Thu, 4 Feb 2021 16:26:33 +0300 Subject: [PATCH 3/8] compiler: do not introduce excessive locals on inline When function call-site parameter is an identifier, we may load it directly. Currently it can be also modified, this will be fixed in a separate commit. --- pkg/compiler/inline.go | 12 +++++++++++- pkg/compiler/inline_test.go | 7 +------ pkg/compiler/vars.go | 25 ++++++++++++++++++++----- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/pkg/compiler/inline.go b/pkg/compiler/inline.go index 9facbe792..39ba8ef7e 100644 --- a/pkg/compiler/inline.go +++ b/pkg/compiler/inline.go @@ -27,9 +27,19 @@ func (c *codegen) inlineCall(f *funcScope, n *ast.CallExpr) { defer c.scope.vars.dropScope() for i := range n.Args { c.scope.vars.locals = oldScope + name := sig.Params().At(i).Name() + if arg, ok := n.Args[i].(*ast.Ident); ok { + // When function argument is variable or const, we may avoid + // introducing additional variables for parameters. + // This is done by providing additional alias to variable. + if vt, index := c.scope.vars.getVarIndex(arg.Name); index != -1 { + c.scope.vars.locals = newScope + c.scope.vars.addAlias(name, vt, index) + continue + } + } ast.Walk(c, n.Args[i]) c.scope.vars.locals = newScope - name := sig.Params().At(i).Name() c.scope.newLocal(name) c.emitStoreVar("", name) } diff --git a/pkg/compiler/inline_test.go b/pkg/compiler/inline_test.go index 05c6db8b3..fa14c31bf 100644 --- a/pkg/compiler/inline_test.go +++ b/pkg/compiler/inline_test.go @@ -111,12 +111,7 @@ func TestInlineConversion(t *testing.T) { a := 2 { b := 1 - c := a - { - bb := b - cc := c - return (bb + cc) * (b + c) - } + return (b + a) * (b + a) } }` b2, err := compiler.Compile("foo.go", strings.NewReader(src2)) diff --git a/pkg/compiler/vars.go b/pkg/compiler/vars.go index 006a7556f..f8a3fc481 100644 --- a/pkg/compiler/vars.go +++ b/pkg/compiler/vars.go @@ -4,7 +4,12 @@ type varScope struct { localsCnt int argCnt int arguments map[string]int - locals []map[string]int + locals []map[string]varInfo +} + +type varInfo struct { + refType varType + index int } func newVarScope() varScope { @@ -14,17 +19,24 @@ func newVarScope() varScope { } func (c *varScope) newScope() { - c.locals = append(c.locals, map[string]int{}) + c.locals = append(c.locals, map[string]varInfo{}) } func (c *varScope) dropScope() { c.locals = c.locals[:len(c.locals)-1] } +func (c *varScope) addAlias(name string, vt varType, index int) { + c.locals[len(c.locals)-1][name] = varInfo{ + refType: vt, + index: index, + } +} + func (c *varScope) getVarIndex(name string) (varType, int) { for i := len(c.locals) - 1; i >= 0; i-- { - if i, ok := c.locals[i][name]; ok { - return varLocal, i + if vi, ok := c.locals[i][name]; ok { + return vi.refType, vi.index } } if i, ok := c.arguments[name]; ok { @@ -56,7 +68,10 @@ func (c *varScope) newVariable(t varType, name string) int { func (c *varScope) newLocal(name string) int { idx := len(c.locals) - 1 m := c.locals[idx] - m[name] = c.localsCnt + m[name] = varInfo{ + refType: varLocal, + index: c.localsCnt, + } c.localsCnt++ c.locals[idx] = m return c.localsCnt - 1 From 34840250650ddc3a4008380504e14593dbffbfcf Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 5 Feb 2021 16:15:26 +0300 Subject: [PATCH 4/8] compiler: load constants directly on inline --- pkg/compiler/codegen.go | 25 +++++++++++++++---------- pkg/compiler/inline.go | 9 +++++++-- pkg/compiler/inline_test.go | 3 +-- pkg/compiler/vars.go | 21 ++++++++++++++++----- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 992b9c49b..ea96ef834 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -194,20 +194,21 @@ func (c *codegen) emitStoreStructField(i int) { // getVarIndex returns variable type and position in corresponding slot, // according to current scope. -func (c *codegen) getVarIndex(pkg string, name string) (varType, int) { +func (c *codegen) getVarIndex(pkg string, name string) *varInfo { if pkg == "" { if c.scope != nil { - vt, val := c.scope.vars.getVarIndex(name) - if val >= 0 { - return vt, val + vi := c.scope.vars.getVarInfo(name) + if vi != nil { + return vi } } } if i, ok := c.globals[c.getIdentName(pkg, name)]; ok { - return varGlobal, i + return &varInfo{refType: varGlobal, index: i} } - return varLocal, c.scope.newVariable(varLocal, name) + c.scope.newVariable(varLocal, name) + return c.scope.vars.getVarInfo(name) } func getBaseOpcode(t varType) (opcode.Opcode, opcode.Opcode) { @@ -225,8 +226,12 @@ func getBaseOpcode(t varType) (opcode.Opcode, opcode.Opcode) { // emitLoadVar loads specified variable to the evaluation stack. func (c *codegen) emitLoadVar(pkg string, name string) { - t, i := c.getVarIndex(pkg, name) - c.emitLoadByIndex(t, i) + vi := c.getVarIndex(pkg, name) + if vi.tv.Value != nil { + c.emitLoadConst(vi.tv) + return + } + c.emitLoadByIndex(vi.refType, vi.index) } // emitLoadByIndex loads specified variable type with index i. @@ -245,8 +250,8 @@ func (c *codegen) emitStoreVar(pkg string, name string) { emit.Opcodes(c.prog.BinWriter, opcode.DROP) return } - t, i := c.getVarIndex(pkg, name) - c.emitStoreByIndex(t, i) + vi := c.getVarIndex(pkg, name) + c.emitStoreByIndex(vi.refType, vi.index) } // emitLoadByIndex stores top value in the specified variable type with index i. diff --git a/pkg/compiler/inline.go b/pkg/compiler/inline.go index 39ba8ef7e..417e5b857 100644 --- a/pkg/compiler/inline.go +++ b/pkg/compiler/inline.go @@ -28,13 +28,18 @@ func (c *codegen) inlineCall(f *funcScope, n *ast.CallExpr) { for i := range n.Args { c.scope.vars.locals = oldScope name := sig.Params().At(i).Name() + if tv := c.typeAndValueOf(n.Args[i]); tv.Value != nil { + c.scope.vars.locals = newScope + c.scope.vars.addAlias(name, varLocal, unspecifiedVarIndex, tv) + continue + } if arg, ok := n.Args[i].(*ast.Ident); ok { // When function argument is variable or const, we may avoid // introducing additional variables for parameters. // This is done by providing additional alias to variable. - if vt, index := c.scope.vars.getVarIndex(arg.Name); index != -1 { + if vi := c.scope.vars.getVarInfo(arg.Name); vi != nil { c.scope.vars.locals = newScope - c.scope.vars.addAlias(name, vt, index) + c.scope.vars.addAlias(name, vi.refType, vi.index, vi.tv) continue } } diff --git a/pkg/compiler/inline_test.go b/pkg/compiler/inline_test.go index fa14c31bf..8870f1a47 100644 --- a/pkg/compiler/inline_test.go +++ b/pkg/compiler/inline_test.go @@ -110,8 +110,7 @@ func TestInlineConversion(t *testing.T) { func Main() int { a := 2 { - b := 1 - return (b + a) * (b + a) + return (1 + a) * (1 + a) } }` b2, err := compiler.Compile("foo.go", strings.NewReader(src2)) diff --git a/pkg/compiler/vars.go b/pkg/compiler/vars.go index f8a3fc481..4753d56f8 100644 --- a/pkg/compiler/vars.go +++ b/pkg/compiler/vars.go @@ -1,5 +1,9 @@ package compiler +import ( + "go/types" +) + type varScope struct { localsCnt int argCnt int @@ -10,8 +14,11 @@ type varScope struct { type varInfo struct { refType varType index int + tv types.TypeAndValue } +const unspecifiedVarIndex = -1 + func newVarScope() varScope { return varScope{ arguments: make(map[string]int), @@ -26,23 +33,27 @@ func (c *varScope) dropScope() { c.locals = c.locals[:len(c.locals)-1] } -func (c *varScope) addAlias(name string, vt varType, index int) { +func (c *varScope) addAlias(name string, vt varType, index int, tv types.TypeAndValue) { c.locals[len(c.locals)-1][name] = varInfo{ refType: vt, index: index, + tv: tv, } } -func (c *varScope) getVarIndex(name string) (varType, int) { +func (c *varScope) getVarInfo(name string) *varInfo { for i := len(c.locals) - 1; i >= 0; i-- { if vi, ok := c.locals[i][name]; ok { - return vi.refType, vi.index + return &vi } } if i, ok := c.arguments[name]; ok { - return varArgument, i + return &varInfo{ + refType: varArgument, + index: i, + } } - return 0, -1 + return nil } // newVariable creates a new local variable or argument in the scope of the function. From 57a0377c8119b90609045aa6bcb881e2b4f2fc8e Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 5 Feb 2021 17:26:26 +0300 Subject: [PATCH 5/8] compiler: load `nil` directly on inline --- pkg/compiler/codegen.go | 3 +++ pkg/compiler/inline.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index ea96ef834..a9b62d8cd 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -230,6 +230,9 @@ func (c *codegen) emitLoadVar(pkg string, name string) { if vi.tv.Value != nil { c.emitLoadConst(vi.tv) return + } else if vi.index == unspecifiedVarIndex { + emit.Opcodes(c.prog.BinWriter, opcode.PUSHNULL) + return } c.emitLoadByIndex(vi.refType, vi.index) } diff --git a/pkg/compiler/inline.go b/pkg/compiler/inline.go index 417e5b857..55e0c8364 100644 --- a/pkg/compiler/inline.go +++ b/pkg/compiler/inline.go @@ -41,6 +41,10 @@ func (c *codegen) inlineCall(f *funcScope, n *ast.CallExpr) { c.scope.vars.locals = newScope c.scope.vars.addAlias(name, vi.refType, vi.index, vi.tv) continue + } else if arg.Name == "nil" { + c.scope.vars.locals = newScope + c.scope.vars.addAlias(name, varLocal, unspecifiedVarIndex, types.TypeAndValue{}) + continue } } ast.Walk(c, n.Args[i]) From 339187a56dba16686f6f6d113f1c69590e1d9012 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 5 Feb 2021 17:26:48 +0300 Subject: [PATCH 6/8] compiler: count locals number properly --- pkg/compiler/analysis.go | 12 ++++---- pkg/compiler/codegen.go | 7 ++--- pkg/compiler/func_scope.go | 59 +++++++++++++++++++++++++++++++------- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/pkg/compiler/analysis.go b/pkg/compiler/analysis.go index 396b7d4c0..d6553bfe4 100644 --- a/pkg/compiler/analysis.go +++ b/pkg/compiler/analysis.go @@ -55,14 +55,14 @@ func (c *codegen) traverseGlobals() (int, int, int) { switch n := node.(type) { case *ast.FuncDecl: if isInitFunc(n) { - c, _ := countLocals(n) - if c > initLocals { - initLocals = c + num, _ := c.countLocals(n) + if num > initLocals { + initLocals = num } } else if isDeployFunc(n) { - c, _ := countLocals(n) - if c > deployLocals { - deployLocals = c + num, _ := c.countLocals(n) + if num > deployLocals { + deployLocals = num } } return !hasDefer diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index a9b62d8cd..5203e9af6 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -330,7 +330,7 @@ func (c *codegen) convertInitFuncs(f *ast.File, pkg *types.Package, seenBefore b case *ast.FuncDecl: if isInitFunc(n) { if seenBefore { - cnt, _ := countLocals(n) + cnt, _ := c.countLocals(n) c.clearSlots(cnt) seenBefore = true } @@ -362,7 +362,7 @@ func (c *codegen) convertDeployFuncs() { case *ast.FuncDecl: if isDeployFunc(n) { if seenBefore { - cnt, _ := countLocals(n) + cnt, _ := c.countLocals(n) c.clearSlots(cnt) } c.convertFuncDecl(f, n, pkg) @@ -408,7 +408,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 && !isDeploy { - sizeLoc := f.countLocals() + sizeLoc := c.countLocalsWithDefer(f) if sizeLoc > 255 { c.prog.Err = errors.New("maximum of 255 local variables is allowed") } @@ -416,7 +416,6 @@ func (c *codegen) convertFuncDecl(file ast.Node, decl *ast.FuncDecl, pkg *types. if sizeArg > 255 { c.prog.Err = errors.New("maximum of 255 local variables is allowed") } - sizeLoc = 255 // FIXME count locals including inline variables if sizeLoc != 0 || sizeArg != 0 { emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{byte(sizeLoc), byte(sizeArg)}) } diff --git a/pkg/compiler/func_scope.go b/pkg/compiler/func_scope.go index 2734712bc..3f5891b8c 100644 --- a/pkg/compiler/func_scope.go +++ b/pkg/compiler/func_scope.go @@ -102,11 +102,47 @@ func (c *funcScope) analyzeVoidCalls(node ast.Node) bool { return true } -func countLocals(decl *ast.FuncDecl) (int, bool) { +func (c *codegen) countLocals(decl *ast.FuncDecl) (int, bool) { + return c.countLocalsInline(decl, nil, nil) +} + +func (c *codegen) countLocalsInline(decl *ast.FuncDecl, pkg *types.Package, f *funcScope) (int, bool) { + oldMap := c.importMap + if pkg != nil { + c.fillImportMap(f.file, pkg) + } + size := 0 hasDefer := false ast.Inspect(decl, func(n ast.Node) bool { switch n := n.(type) { + case *ast.CallExpr: + var name string + switch fun := n.Fun.(type) { + case *ast.Ident: + var pkgName string + if pkg != nil { + pkgName = pkg.Path() + } + name = c.getIdentName(pkgName, fun.Name) + case *ast.SelectorExpr: + name, _ = c.getFuncNameFromSelector(fun) + default: + return false + } + if inner, ok := c.funcs[name]; ok && canInline(name) { + for i := range n.Args { + switch n.Args[i].(type) { + case *ast.Ident: + case *ast.BasicLit: + default: + size++ + } + } + innerSz, _ := c.countLocalsInline(inner.decl, inner.pkg, inner) + size += innerSz + } + return false case *ast.FuncType: num := n.Results.NumFields() if num != 0 && len(n.Results.List[0].Names) != 0 { @@ -119,7 +155,11 @@ func countLocals(decl *ast.FuncDecl) (int, bool) { case *ast.DeferStmt: hasDefer = true return false - case *ast.ReturnStmt, *ast.IfStmt: + case *ast.ReturnStmt: + if pkg == nil { + size++ + } + case *ast.IfStmt: size++ // This handles the inline GenDecl like "var x = 2" case *ast.ValueSpec: @@ -136,13 +176,16 @@ func countLocals(decl *ast.FuncDecl) (int, bool) { } return true }) + if pkg != nil { + c.importMap = oldMap + } return size, hasDefer } -func (c *funcScope) countLocals() int { - size, hasDefer := countLocals(c.decl) +func (c *codegen) countLocalsWithDefer(f *funcScope) int { + size, hasDefer := c.countLocals(f.decl) if hasDefer { - c.finallyProcessedIndex = size + f.finallyProcessedIndex = size size++ } return size @@ -156,12 +199,6 @@ func (c *funcScope) countArgs() int { return n } -func (c *funcScope) stackSize() int64 { - size := c.countLocals() - numArgs := c.countArgs() - return int64(size + numArgs) -} - // newVariable creates a new local variable or argument in the scope of the function. func (c *funcScope) newVariable(t varType, name string) int { return c.vars.newVariable(t, name) From 6e560c6c9f06e90486c4713f5c1627facba17711 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Mon, 8 Feb 2021 13:51:25 +0300 Subject: [PATCH 7/8] compiler: allow to inline var arg functions --- pkg/compiler/codegen.go | 15 +++++++++++---- pkg/compiler/inline.go | 22 ++++++++++++++++++++++ pkg/compiler/inline_test.go | 16 ++++++++++++++++ pkg/compiler/testdata/inline/inline.go | 8 ++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 5203e9af6..41b6aa963 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -892,10 +892,7 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { typ, ok := c.typeOf(n.Fun).(*types.Signature) if ok && typ.Variadic() && !n.Ellipsis.IsValid() { // pack variadic args into an array only if last argument is not of form `...` - varSize := len(n.Args) - typ.Params().Len() + 1 - c.emitReverse(varSize) - emit.Int(c.prog.BinWriter, int64(varSize)) - emit.Opcodes(c.prog.BinWriter, opcode.PACK) + varSize := c.packVarArgs(n, typ) numArgs -= varSize - 1 } c.emitReverse(numArgs) @@ -1232,6 +1229,16 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { return c } +// packVarArgs packs variadic arguments into an array +// and returns amount of arguments packed. +func (c *codegen) packVarArgs(n *ast.CallExpr, typ *types.Signature) int { + varSize := len(n.Args) - typ.Params().Len() + 1 + c.emitReverse(varSize) + emit.Int(c.prog.BinWriter, int64(varSize)) + emit.Opcodes(c.prog.BinWriter, opcode.PACK) + return varSize +} + // processDefers emits code for `defer` statements. // TRY-related opcodes handle exception as follows: // 1. CATCH block is executed only if exception has occurred. diff --git a/pkg/compiler/inline.go b/pkg/compiler/inline.go index 55e0c8364..6d2ba8cf1 100644 --- a/pkg/compiler/inline.go +++ b/pkg/compiler/inline.go @@ -25,8 +25,16 @@ func (c *codegen) inlineCall(f *funcScope, n *ast.CallExpr) { c.scope.vars.newScope() newScope := c.scope.vars.locals defer c.scope.vars.dropScope() + + hasVarArgs := !n.Ellipsis.IsValid() + needPack := sig.Variadic() && hasVarArgs for i := range n.Args { c.scope.vars.locals = oldScope + // true if normal arg or var arg is `slice...` + needStore := i < sig.Params().Len()-1 || !sig.Variadic() || !hasVarArgs + if !needStore { + break + } name := sig.Params().At(i).Name() if tv := c.typeAndValueOf(n.Args[i]); tv.Value != nil { c.scope.vars.locals = newScope @@ -53,6 +61,20 @@ func (c *codegen) inlineCall(f *funcScope, n *ast.CallExpr) { c.emitStoreVar("", name) } + if needPack { + // traverse variadic args and pack them + // if they are provided directly i.e. without `...` + c.scope.vars.locals = oldScope + for i := sig.Params().Len() - 1; i < len(n.Args); i++ { + ast.Walk(c, n.Args[i]) + } + c.scope.vars.locals = newScope + c.packVarArgs(n, sig) + name := sig.Params().At(sig.Params().Len() - 1).Name() + c.scope.newLocal(name) + c.emitStoreVar("", name) + } + c.pkgInfoInline = append(c.pkgInfoInline, pkg) oldMap := c.importMap c.fillImportMap(f.file, pkg.Pkg) diff --git a/pkg/compiler/inline_test.go b/pkg/compiler/inline_test.go index 8870f1a47..ed9c16b3e 100644 --- a/pkg/compiler/inline_test.go +++ b/pkg/compiler/inline_test.go @@ -91,6 +91,22 @@ func TestInline(t *testing.T) { checkCallCount(t, src, 1, 2) eval(t, src, big.NewInt(51)) }) + t.Run("var args, empty", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `return inline.VarSum(11)`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(11)) + }) + t.Run("var args, direct", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `return inline.VarSum(11, 14, 17)`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(42)) + }) + t.Run("var args, array", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `arr := []int{14, 17} + return inline.VarSum(11, arr...)`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(42)) + }) } func TestInlineConversion(t *testing.T) { diff --git a/pkg/compiler/testdata/inline/inline.go b/pkg/compiler/testdata/inline/inline.go index d44ec461a..13201acaf 100644 --- a/pkg/compiler/testdata/inline/inline.go +++ b/pkg/compiler/testdata/inline/inline.go @@ -30,3 +30,11 @@ func DropInsideInline() int { sum(3, 4) return 7 } + +func VarSum(a int, b ...int) int { + sum := a + for i := range b { + sum += b[i] + } + return sum +} From cf459002f77c84f2986962d7b17d59e4733b37b6 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Mon, 8 Feb 2021 17:25:42 +0300 Subject: [PATCH 8/8] compiler: allow to inline global variables --- pkg/compiler/inline.go | 4 ++++ pkg/compiler/inline_test.go | 28 ++++++++++++++++++++++++++ pkg/compiler/testdata/inline/inline.go | 4 ++++ 3 files changed, 36 insertions(+) diff --git a/pkg/compiler/inline.go b/pkg/compiler/inline.go index 6d2ba8cf1..92d89652b 100644 --- a/pkg/compiler/inline.go +++ b/pkg/compiler/inline.go @@ -53,6 +53,10 @@ func (c *codegen) inlineCall(f *funcScope, n *ast.CallExpr) { c.scope.vars.locals = newScope c.scope.vars.addAlias(name, varLocal, unspecifiedVarIndex, types.TypeAndValue{}) continue + } else if index, ok := c.globals[c.getIdentName("", arg.Name)]; ok { + c.scope.vars.locals = newScope + c.scope.vars.addAlias(name, varGlobal, index, types.TypeAndValue{}) + continue } } ast.Walk(c, n.Args[i]) diff --git a/pkg/compiler/inline_test.go b/pkg/compiler/inline_test.go index ed9c16b3e..9035a8d2a 100644 --- a/pkg/compiler/inline_test.go +++ b/pkg/compiler/inline_test.go @@ -40,6 +40,7 @@ func TestInline(t *testing.T) { func sum(a, b int) int { return 42 } + var Num = 1 func Main() int { %s }` @@ -107,6 +108,11 @@ func TestInline(t *testing.T) { checkCallCount(t, src, 0, 1) eval(t, src, big.NewInt(42)) }) + t.Run("globals", func(t *testing.T) { + src := fmt.Sprintf(srcTmpl, `return inline.Concat(Num)`) + checkCallCount(t, src, 0, 1) + eval(t, src, big.NewInt(221)) + }) } func TestInlineConversion(t *testing.T) { @@ -133,3 +139,25 @@ func TestInlineConversion(t *testing.T) { require.NoError(t, err) require.Equal(t, b2, b1) } + +func TestInlineConversionQualified(t *testing.T) { + src1 := `package foo + import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline" + var A = 1 + func Main() int { + return inline.Concat(A) + }` + b1, err := compiler.Compile("foo.go", strings.NewReader(src1)) + require.NoError(t, err) + + src2 := `package foo + import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline" + import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline/b" + var A = 1 + func Main() int { + return A * 100 + b.A * 10 + inline.A + }` + b2, err := compiler.Compile("foo.go", strings.NewReader(src2)) + require.NoError(t, err) + require.Equal(t, b2, b1) +} diff --git a/pkg/compiler/testdata/inline/inline.go b/pkg/compiler/testdata/inline/inline.go index 13201acaf..c319c6b1d 100644 --- a/pkg/compiler/testdata/inline/inline.go +++ b/pkg/compiler/testdata/inline/inline.go @@ -38,3 +38,7 @@ func VarSum(a int, b ...int) int { } return sum } + +func Concat(n int) int { + return n*100 + b.A*10 + A +}