compiler: do not emit code for unnamed unused variables
If variable is unnamed and does not contain function call then it's treated as unused and code generation may be omitted for it initialization/declaration.
This commit is contained in:
parent
1dcbdb011a
commit
91b36657d6
8 changed files with 151 additions and 18 deletions
|
@ -596,15 +596,32 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := range t.Names {
|
for i, id := range t.Names {
|
||||||
if len(t.Values) != 0 {
|
if id.Name != "_" {
|
||||||
if i == 0 || !multiRet {
|
if len(t.Values) != 0 {
|
||||||
ast.Walk(c, t.Values[i])
|
if i == 0 || !multiRet {
|
||||||
|
ast.Walk(c, t.Values[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.emitDefault(c.typeOf(t.Type))
|
||||||
}
|
}
|
||||||
} else {
|
c.emitStoreVar("", t.Names[i].Name)
|
||||||
c.emitDefault(c.typeOf(t.Type))
|
continue
|
||||||
|
}
|
||||||
|
// If var decl contains call then the code should be emitted for it, otherwise - do not evaluate.
|
||||||
|
if len(t.Values) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var hasCall bool
|
||||||
|
if i == 0 || !multiRet {
|
||||||
|
hasCall = containsCall(t.Values[i])
|
||||||
|
}
|
||||||
|
if hasCall {
|
||||||
|
ast.Walk(c, t.Values[i])
|
||||||
|
}
|
||||||
|
if hasCall || i != 0 && multiRet {
|
||||||
|
c.emitStoreVar("", "_") // drop unused after walk
|
||||||
}
|
}
|
||||||
c.emitStoreVar("", t.Names[i].Name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,6 +53,30 @@ func TestUnusedGlobal(t *testing.T) {
|
||||||
}`
|
}`
|
||||||
eval(t, src, big.NewInt(1))
|
eval(t, src, big.NewInt(1))
|
||||||
})
|
})
|
||||||
|
t.Run("used", func(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
var _, A = f()
|
||||||
|
func Main() int {
|
||||||
|
return A
|
||||||
|
}
|
||||||
|
func f() (int, int) {
|
||||||
|
return 5, 6
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(6))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("unused without function call", func(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
var _ = 1
|
||||||
|
var (
|
||||||
|
_ = 2 + 3
|
||||||
|
_, _ = 3 + 4, 5
|
||||||
|
)
|
||||||
|
func Main() int {
|
||||||
|
return 1
|
||||||
|
}`
|
||||||
|
prog := eval(t, src, big.NewInt(1))
|
||||||
|
require.Equal(t, 2, len(prog)) // PUSH1 + RET
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,3 +349,30 @@ func TestUnderscoreVarsDontUseSlots(t *testing.T) {
|
||||||
src := buf.String()
|
src := buf.String()
|
||||||
eval(t, src, big.NewInt(count))
|
eval(t, src, big.NewInt(count))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnderscoreGlobalVarDontEmitCode(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
var _ int
|
||||||
|
var _ = 1
|
||||||
|
var (
|
||||||
|
A = 2
|
||||||
|
_ = A + 3
|
||||||
|
_, B, _ = 4, 5, 6
|
||||||
|
_, C, _ = f(A, B)
|
||||||
|
)
|
||||||
|
var D = 7 // unused but named, so the code is expected
|
||||||
|
func Main() int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
func f(a, b int) (int, int, int) {
|
||||||
|
return 8, 9, 10
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{4}}, // sslot for A, B, C, D
|
||||||
|
opcode.PUSH2, opcode.STSFLD0, // store A
|
||||||
|
opcode.PUSH5, opcode.STSFLD1, // store B
|
||||||
|
opcode.LDSFLD0, opcode.LDSFLD1, opcode.SWAP, []interface{}{opcode.CALL, []byte{10}}, // evaluate f
|
||||||
|
opcode.DROP, opcode.STSFLD2, opcode.DROP, // store C
|
||||||
|
opcode.PUSH7, opcode.STSFLD3, opcode.RET, // store D
|
||||||
|
opcode.PUSH1, opcode.RET, // Main
|
||||||
|
[]interface{}{opcode.INITSLOT, []byte{0, 2}}, opcode.PUSH10, opcode.PUSH9, opcode.PUSH8, opcode.RET) // f
|
||||||
|
}
|
||||||
|
|
|
@ -100,7 +100,7 @@ func TestInitWithNoGlobals(t *testing.T) {
|
||||||
func Main() int {
|
func Main() int {
|
||||||
return 42
|
return 42
|
||||||
}`
|
}`
|
||||||
v, s := vmAndCompileInterop(t, src)
|
v, s, _ := vmAndCompileInterop(t, src)
|
||||||
require.NoError(t, v.Run())
|
require.NoError(t, v.Run())
|
||||||
assertResult(t, v, big.NewInt(42))
|
assertResult(t, v, big.NewInt(42))
|
||||||
require.True(t, len(s.events) == 1)
|
require.True(t, len(s.events) == 1)
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkCallCount(t *testing.T, src string, expectedCall, expectedInitSlot, expectedLocalsMain int) {
|
func checkCallCount(t *testing.T, src string, expectedCall, expectedInitSlot, expectedLocalsMain int) {
|
||||||
v, sp := vmAndCompileInterop(t, src)
|
v, sp, _ := vmAndCompileInterop(t, src)
|
||||||
|
|
||||||
mainStart := -1
|
mainStart := -1
|
||||||
for _, m := range sp.info.Methods {
|
for _, m := range sp.info.Methods {
|
||||||
|
|
|
@ -193,7 +193,7 @@ func TestNotify(t *testing.T) {
|
||||||
runtime.Notify("single")
|
runtime.Notify("single")
|
||||||
}`
|
}`
|
||||||
|
|
||||||
v, s := vmAndCompileInterop(t, src)
|
v, s, _ := vmAndCompileInterop(t, src)
|
||||||
v.Estack().PushVal(11)
|
v.Estack().PushVal(11)
|
||||||
|
|
||||||
require.NoError(t, v.Run())
|
require.NoError(t, v.Run())
|
||||||
|
@ -224,7 +224,7 @@ func TestSyscallInGlobalInit(t *testing.T) {
|
||||||
func Main() bool {
|
func Main() bool {
|
||||||
return a
|
return a
|
||||||
}`
|
}`
|
||||||
v, s := vmAndCompileInterop(t, src)
|
v, s, _ := vmAndCompileInterop(t, src)
|
||||||
s.interops[interopnames.ToID([]byte(interopnames.SystemRuntimeCheckWitness))] = func(v *vm.VM) error {
|
s.interops[interopnames.ToID([]byte(interopnames.SystemRuntimeCheckWitness))] = func(v *vm.VM) error {
|
||||||
s := v.Estack().Pop().Value().([]byte)
|
s := v.Estack().Pop().Value().([]byte)
|
||||||
require.Equal(t, "5T", string(s))
|
require.Equal(t, "5T", string(s))
|
||||||
|
@ -479,7 +479,7 @@ func TestInteropTypesComparison(t *testing.T) {
|
||||||
b := struct{}{}
|
b := struct{}{}
|
||||||
return a.Equals(b)
|
return a.Equals(b)
|
||||||
}`
|
}`
|
||||||
vm, _ := vmAndCompileInterop(t, src)
|
vm, _, _ := vmAndCompileInterop(t, src)
|
||||||
err := vm.Run()
|
err := vm.Run()
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.True(t, strings.Contains(err.Error(), "invalid conversion: Struct/ByteString"), err)
|
require.True(t, strings.Contains(err.Error(), "invalid conversion: Struct/ByteString"), err)
|
||||||
|
|
|
@ -3,6 +3,8 @@ package compiler_test
|
||||||
import (
|
import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGenDeclWithMultiRet(t *testing.T) {
|
func TestGenDeclWithMultiRet(t *testing.T) {
|
||||||
|
@ -29,3 +31,46 @@ func TestGenDeclWithMultiRet(t *testing.T) {
|
||||||
eval(t, src, big.NewInt(3))
|
eval(t, src, big.NewInt(3))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnderscoreLocalVarDontEmitCode(t *testing.T) {
|
||||||
|
src := `package foo
|
||||||
|
type Foo struct { Int int }
|
||||||
|
func Main() int {
|
||||||
|
var _ int
|
||||||
|
var _ = 1
|
||||||
|
var (
|
||||||
|
A = 2
|
||||||
|
_ = A + 3
|
||||||
|
_, B, _ = 4, 5, 6
|
||||||
|
_, _, _ = f(A, B) // unused, but has function call, so the code is expected
|
||||||
|
_, C, _ = f(A, B)
|
||||||
|
)
|
||||||
|
var D = 7 // unused but named, so the code is expected
|
||||||
|
_ = D
|
||||||
|
var _ = Foo{ Int: 5 }
|
||||||
|
var fo = Foo{ Int: 3 }
|
||||||
|
var _ = 1 + A + fo.Int
|
||||||
|
var _ = fo.GetInt() // unused, but has method call, so the code is expected
|
||||||
|
return C
|
||||||
|
}
|
||||||
|
func f(a, b int) (int, int, int) {
|
||||||
|
return 8, 9, 10
|
||||||
|
}
|
||||||
|
func (fo Foo) GetInt() int {
|
||||||
|
return fo.Int
|
||||||
|
}`
|
||||||
|
eval(t, src, big.NewInt(9), []interface{}{opcode.INITSLOT, []byte{5, 0}}, // local slot for A, B, C, D, fo
|
||||||
|
opcode.PUSH2, opcode.STLOC0, // store A
|
||||||
|
opcode.PUSH5, opcode.STLOC1, // store B
|
||||||
|
opcode.LDLOC0, opcode.LDLOC1, opcode.SWAP, []interface{}{opcode.CALL, []byte{27}}, // evaluate f() first time
|
||||||
|
opcode.DROP, opcode.DROP, opcode.DROP, // drop all values from f
|
||||||
|
opcode.LDLOC0, opcode.LDLOC1, opcode.SWAP, []interface{}{opcode.CALL, []byte{19}}, // evaluate f() second time
|
||||||
|
opcode.DROP, opcode.STLOC2, opcode.DROP, // store C
|
||||||
|
opcode.PUSH7, opcode.STLOC3, // store D
|
||||||
|
opcode.LDLOC3, opcode.DROP, // empty assignment
|
||||||
|
opcode.PUSH3, opcode.PUSH1, opcode.PACKSTRUCT, opcode.STLOC4, // fo decl
|
||||||
|
opcode.LDLOC4, []interface{}{opcode.CALL, []byte{12}}, opcode.DROP, // fo.GetInt()
|
||||||
|
opcode.LDLOC2, opcode.RET, // return C
|
||||||
|
[]interface{}{opcode.INITSLOT, []byte{0, 2}}, opcode.PUSH10, opcode.PUSH9, opcode.PUSH8, opcode.RET, // f
|
||||||
|
[]interface{}{opcode.INITSLOT, []byte{0, 1}}, opcode.LDARG0, opcode.PUSH0, opcode.PICKITEM, opcode.RET) // (fo Foo) GetInt() int
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ func TestVerifyGood(t *testing.T) {
|
||||||
pub, sig := signMessage(t, msg)
|
pub, sig := signMessage(t, msg)
|
||||||
src := getVerifyProg(pub, sig)
|
src := getVerifyProg(pub, sig)
|
||||||
|
|
||||||
v, p := vmAndCompileInterop(t, src)
|
v, p, _ := vmAndCompileInterop(t, src)
|
||||||
p.interops[interopnames.ToID([]byte(interopnames.SystemCryptoCheckSig))] = func(v *vm.VM) error {
|
p.interops[interopnames.ToID([]byte(interopnames.SystemCryptoCheckSig))] = func(v *vm.VM) error {
|
||||||
assert.Equal(t, pub, v.Estack().Pop().Bytes())
|
assert.Equal(t, pub, v.Estack().Pop().Bytes())
|
||||||
assert.Equal(t, sig, v.Estack().Pop().Bytes())
|
assert.Equal(t, sig, v.Estack().Pop().Bytes())
|
||||||
|
|
|
@ -9,9 +9,12 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -32,9 +35,25 @@ func runTestCases(t *testing.T, tcases []testCase) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func eval(t *testing.T, src string, result interface{}) {
|
func eval(t *testing.T, src string, result interface{}, expectedOps ...interface{}) []byte {
|
||||||
vm, _ := vmAndCompileInterop(t, src)
|
vm, _, script := vmAndCompileInterop(t, src)
|
||||||
|
if len(expectedOps) != 0 {
|
||||||
|
expected := io.NewBufBinWriter()
|
||||||
|
for _, op := range expectedOps {
|
||||||
|
switch typ := op.(type) {
|
||||||
|
case opcode.Opcode:
|
||||||
|
emit.Opcodes(expected.BinWriter, typ)
|
||||||
|
case []interface{}:
|
||||||
|
emit.Instruction(expected.BinWriter, typ[0].(opcode.Opcode), typ[1].([]byte))
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected evaluation operation: %v", typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, expected.Bytes(), script)
|
||||||
|
}
|
||||||
runAndCheck(t, vm, result)
|
runAndCheck(t, vm, result)
|
||||||
|
return script
|
||||||
}
|
}
|
||||||
|
|
||||||
func runAndCheck(t *testing.T, v *vm.VM, result interface{}) {
|
func runAndCheck(t *testing.T, v *vm.VM, result interface{}) {
|
||||||
|
@ -61,11 +80,11 @@ func assertResult(t *testing.T, vm *vm.VM, result interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func vmAndCompile(t *testing.T, src string) *vm.VM {
|
func vmAndCompile(t *testing.T, src string) *vm.VM {
|
||||||
v, _ := vmAndCompileInterop(t, src)
|
v, _, _ := vmAndCompileInterop(t, src)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func vmAndCompileInterop(t *testing.T, src string) (*vm.VM, *storagePlugin) {
|
func vmAndCompileInterop(t *testing.T, src string) (*vm.VM, *storagePlugin, []byte) {
|
||||||
vm := vm.New()
|
vm := vm.New()
|
||||||
|
|
||||||
storePlugin := newStoragePlugin()
|
storePlugin := newStoragePlugin()
|
||||||
|
@ -77,7 +96,7 @@ func vmAndCompileInterop(t *testing.T, src string) (*vm.VM, *storagePlugin) {
|
||||||
|
|
||||||
storePlugin.info = di
|
storePlugin.info = di
|
||||||
invokeMethod(t, testMainIdent, b.Script, vm, di)
|
invokeMethod(t, testMainIdent, b.Script, vm, di)
|
||||||
return vm, storePlugin
|
return vm, storePlugin, b.Script
|
||||||
}
|
}
|
||||||
|
|
||||||
func invokeMethod(t *testing.T, method string, script []byte, v *vm.VM, di *compiler.DebugInfo) {
|
func invokeMethod(t *testing.T, method string, script []byte, v *vm.VM, di *compiler.DebugInfo) {
|
||||||
|
|
Loading…
Reference in a new issue