compiler: rename named unused global vars to "_"

So that (*codegen).Visit is able to omit code generation for these
unused global vars. The most tricky part is to detect unused global
variables, it is done in several steps:
1. Collect the set of named used/unused global vars.
2. Collect the set of globally declared expressions that contain
function calls.
3. Pick up global vars from the set made at step 2.
4. Traverse used functions and puck up those global vars that are used
from these functions.
5. Rename all globals that are presented in the set made at step 1
but are not presented in the set made on step 3 or step 4.
This commit is contained in:
Anna Shaleva 2022-08-04 17:47:32 +03:00
parent 1e6b70d570
commit 800321db06
10 changed files with 903 additions and 22 deletions

View file

@ -63,6 +63,7 @@ func TestUnusedGlobal(t *testing.T) {
return 5, 6
}`
eval(t, src, big.NewInt(6))
checkInstrCount(t, src, 1, 1, 0, 0) // sslot for A, single call to f
})
})
t.Run("unused without function call", func(t *testing.T) {
@ -80,6 +81,552 @@ func TestUnusedGlobal(t *testing.T) {
})
}
func TestUnusedOptimizedGlobalVar(t *testing.T) {
t.Run("unused, no initialization", func(t *testing.T) {
src := `package foo
var A int
var (
B int
C, D, E int
)
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("used, no initialization", func(t *testing.T) {
src := `package foo
var A int
func Main() int {
return A
}`
eval(t, src, big.NewInt(0))
checkInstrCount(t, src, 1, 0, 0, 0) // sslot for A
})
t.Run("used by unused var, no initialization", func(t *testing.T) {
src := `package foo
var Unused int
var Unused2 = Unused + 1
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("unused, with initialization", func(t *testing.T) {
src := `package foo
var Unused = 1
func Main() int {
return 2
}`
prog := eval(t, src, big.NewInt(2))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("unused, with initialization by used var", func(t *testing.T) {
src := `package foo
var (
A = 1
B, Unused, C = f(), A + 2, 3 // the code for Unused initialization won't be emitted as it's a pure expression without function calls
Unused2 = 4
)
var Unused3 = 5
func Main() int {
return A + C
}
func f() int {
return 4
}`
eval(t, src, big.NewInt(4), []interface{}{opcode.INITSSLOT, []byte{2}}, // sslot for A and C
opcode.PUSH1, opcode.STSFLD0, // store A
[]interface{}{opcode.CALL, []byte{10}}, opcode.DROP, // evaluate B and drop
opcode.PUSH3, opcode.STSFLD1, opcode.RET, // store C
opcode.LDSFLD0, opcode.LDSFLD1, opcode.ADD, opcode.RET, // Main
opcode.PUSH4, opcode.RET) // f
})
t.Run("used by unused var, with initialization", func(t *testing.T) {
src := `package foo
var (
Unused1 = 1
Unused2 = Unused1 + 1
)
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("used with combination of nested unused", func(t *testing.T) {
src := `package foo
var (
A = 1
Unused1 = 2
Unused2 = Unused1 + 1
)
func Main() int {
return A
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, opcode.RET, // store A
opcode.LDSFLD0, opcode.RET) // Main
})
t.Run("single var stmt with both used and unused vars", func(t *testing.T) {
src := `package foo
var A, Unused1, B, Unused2 = 1, 2, 3, 4
func Main() int {
return A + B
}`
eval(t, src, big.NewInt(4), []interface{}{opcode.INITSSLOT, []byte{2}}, // sslot for A and B
opcode.PUSH1, opcode.STSFLD0, // store A
opcode.PUSH3, opcode.STSFLD1, opcode.RET, // store B
opcode.LDSFLD0, opcode.LDSFLD1, opcode.ADD, opcode.RET) // Main
})
t.Run("single var decl token with multiple var specifications", func(t *testing.T) {
src := `package foo
var (
A, Unused1, B, Unused2 = 1, 2, 3, 4
C, Unused3 int
)
func Main() int {
return A + B + C
}`
eval(t, src, big.NewInt(4), []interface{}{opcode.INITSSLOT, []byte{3}}, // sslot for A, B, C
opcode.PUSH1, opcode.STSFLD0, // store A
opcode.PUSH3, opcode.STSFLD1, // store B
opcode.PUSH0, opcode.STSFLD2, opcode.RET, // store C
opcode.LDSFLD0, opcode.LDSFLD1, opcode.ADD, opcode.LDSFLD2, opcode.ADD, opcode.RET) // Main
})
t.Run("function as unused var value", func(t *testing.T) {
src := `package foo
var A, Unused1 = 1, f()
func Main() int {
return A
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, // store A
[]interface{}{opcode.CALL, []byte{6}}, opcode.DROP, opcode.RET, // evaluate Unused1 (call to f) and drop its value
opcode.LDSFLD0, opcode.RET, // Main
opcode.PUSH2, opcode.RET) // f
})
t.Run("function as unused struct field", func(t *testing.T) {
src := `package foo
type Str struct { Int int }
var _ = Str{Int: f()}
func Main() int {
return 1
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.CALL, []byte{8}}, opcode.PUSH1, opcode.PACKSTRUCT, opcode.DROP, opcode.RET, // evaluate struct val
opcode.PUSH1, opcode.RET, // Main
opcode.PUSH2, opcode.RET) // f
})
t.Run("used in unused function", func(t *testing.T) {
src := `package foo
var Unused1, Unused2, Unused3 = 1, 2, 3
func Main() int {
return 1
}
func unused1() int {
return Unused1
}
func unused2() int {
return Unused1 + unused1()
}
func unused3() int {
return Unused2 + unused2()
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("used in used function", func(t *testing.T) {
src := `package foo
var A = 1
func Main() int {
return f()
}
func f() int {
return A
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 1, 1, 0, 0)
})
t.Run("unused, initialized via init", func(t *testing.T) {
src := `package foo
var A int
func Main() int {
return 2
}
func init() {
A = 1 // Although A is unused from exported functions, it's used from init(), so it should be mark as "used" and stored.
}`
eval(t, src, big.NewInt(2))
checkInstrCount(t, src, 1, 0, 0, 0)
})
t.Run("used, initialized via init", func(t *testing.T) {
src := `package foo
var A int
func Main() int {
return A
}
func init() {
A = 1
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 1, 0, 0, 0)
})
t.Run("unused, initialized by function call", func(t *testing.T) {
t.Run("unnamed", func(t *testing.T) {
src := `package foo
var _ = f()
func Main() int {
return 1
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 0, 1, 0, 0)
})
t.Run("named", func(t *testing.T) {
src := `package foo
var A = f()
func Main() int {
return 1
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 0, 1, 0, 0)
})
t.Run("named, with dependency on unused var", func(t *testing.T) {
src := `package foo
var (
A = 1
B = A + 1 // To check nested ident values.
C = 3
D = B + f() + C // To check that both idents (before and after the call to f) will be marked as "used".
E = C + 1 // Unused, no code expected.
)
func Main() int {
return 1
}
func f() int {
return 2
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{3}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, // store A
opcode.LDSFLD0, opcode.PUSH1, opcode.ADD, opcode.STSFLD1, // store B
opcode.PUSH3, opcode.STSFLD2, // store C
opcode.LDSFLD1, []interface{}{opcode.CALL, []byte{9}}, opcode.ADD, opcode.LDSFLD2, opcode.ADD, opcode.DROP, opcode.RET, // evaluate D and drop
opcode.PUSH1, opcode.RET, // Main
opcode.PUSH2, opcode.RET) // f
})
t.Run("named, with dependency on unused var ident inside function call", func(t *testing.T) {
src := `package foo
var A = 1
var B = f(A)
func Main() int {
return 1
}
func f(a int) int {
return a
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, // store A
opcode.LDSFLD0, []interface{}{opcode.CALL, []byte{6}}, opcode.DROP, opcode.RET, // evaluate B and drop
opcode.PUSH1, opcode.RET, // Main
[]interface{}{opcode.INITSLOT, []byte{0, 1}}, opcode.LDARG0, opcode.RET) // f
})
t.Run("named, inside multi-specs and multi-vals var declaration", func(t *testing.T) {
src := `package foo
var (
Unused = 1
Unused1, A, Unused2 = 2, 3 + f(), 4
)
func Main() int {
return 1
}
func f() int {
return 5
}`
eval(t, src, big.NewInt(1), opcode.PUSH3, []interface{}{opcode.CALL, []byte{7}}, opcode.ADD, opcode.DROP, opcode.RET, // evaluate and drop A
opcode.PUSH1, opcode.RET, // Main
opcode.PUSH5, opcode.RET) // f
})
t.Run("unnamed + unused", func(t *testing.T) {
src := `package foo
var A = 1 // At least one global variable is used, thus, the whole set of package variables will be walked.
var B = 2
var _ = B + 1 // This variable is unnamed and doesn't contain call, thus its children won't be marked as "used".
func Main() int {
return A
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, opcode.RET, // store A
opcode.LDSFLD0, opcode.RET) // Main
})
t.Run("mixed value", func(t *testing.T) {
src := `package foo
var control int // At least one global variable is used, thus the whole set of package variables will be walked.
var B = 2
var _ = 1 + f() + B // This variable is unnamed but contains call, thus its children will be marked as "used".
func Main() int {
return control
}
func f() int {
control = 1
return 3
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 2 /* control + B */, 1, 0, 0)
})
t.Run("multiple function return values", func(t *testing.T) {
src := `package foo
var A, B = f()
func Main() int {
return A
}
func f() (int, int) {
return 3, 4
}`
eval(t, src, big.NewInt(3), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
[]interface{}{opcode.CALL, []byte{7}}, opcode.STSFLD0, opcode.DROP, opcode.RET, // evaluate and store A, drop B
opcode.LDSFLD0, opcode.RET, // Main
opcode.PUSH4, opcode.PUSH3, opcode.RET) // f
})
t.Run("constant in declaration", func(t *testing.T) {
src := `package foo
const A = 5
var Unused = 1 + A
func Main() int {
return 1
}`
prog := eval(t, src, big.NewInt(1))
require.Equal(t, 2, len(prog)) // Main
})
t.Run("mixed expression", func(t *testing.T) {
src := `package foo
type CustomInt struct {
Int int
}
var A = CustomInt{Int: 2}
var B = f(3) + A.f(1)
func Main() int {
return 1
}
func f(a int) int {
return a
}
func (i CustomInt) f(a int) int { // has the same name as f
return i.Int + a
}`
eval(t, src, big.NewInt(1))
checkInstrCount(t, src, 1 /* A */, 2, 2, 0)
})
})
t.Run("mixed nested expressions", func(t *testing.T) {
src := `package foo
type CustomInt struct { Int int} // has the same field name as Int variable, important for test
var A = CustomInt{Int: 2}
var B = f(A.Int)
var Unused = 4
var Int = 5 // unused and MUST NOT be treated as "used"
var C = CustomInt{Int: Unused}.Int + f(1) // uses Unused => Unused should be marked as "used"
func Main() int {
return 1
}
func f(a int) int {
return a
}
func (i CustomInt) f(a int) int { // has the same name as f
return i.Int + a
}`
eval(t, src, big.NewInt(1))
})
t.Run("composite literal", func(t *testing.T) {
src := `package foo
var A = 2
var B = []int{1, A, 3}[1]
var C = f(1) + B
func Main() int {
return 1
}
func f(a int) int {
return a
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{2}}, // sslot for A, B
opcode.PUSH2, opcode.STSFLD0, // store A
opcode.PUSH3, opcode.LDSFLD0, opcode.PUSH1, opcode.PUSH3, opcode.PACK, opcode.PUSH1, opcode.PICKITEM, opcode.STSFLD1, // evaluate B
opcode.PUSH1, []interface{}{opcode.CALL, []byte{8}}, opcode.LDSFLD1, opcode.ADD, opcode.DROP, opcode.RET, // evalute C and drop
opcode.PUSH1, opcode.RET, // Main
[]interface{}{opcode.INITSLOT, []byte{0, 1}}, opcode.LDARG0, opcode.RET) // f
})
t.Run("index expression", func(t *testing.T) {
src := `package foo
var Unused = 2
var A = f(1) + []int{1, 2, 3}[Unused] // index expression
func Main() int {
return 1
}
func f(a int) int {
return a
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for Unused
opcode.PUSH2, opcode.STSFLD0, // store Unused
opcode.PUSH1, []interface{}{opcode.CALL, []byte{14}}, // call f(1)
opcode.PUSH3, opcode.PUSH2, opcode.PUSH1, opcode.PUSH3, opcode.PACK, opcode.LDSFLD0, opcode.PICKITEM, // eval index expression
opcode.ADD, opcode.DROP, opcode.RET, // eval and drop A
opcode.PUSH1, opcode.RET, // Main
[]interface{}{opcode.INITSLOT, []byte{0, 1}}, opcode.LDARG0, opcode.RET) // f(a)
})
t.Run("used via nested function calls", func(t *testing.T) {
src := `package foo
var A = 1
func Main() int {
return f()
}
func f() int {
return g()
}
func g() int {
return A
}`
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for A
opcode.PUSH1, opcode.STSFLD0, opcode.RET, // store A
[]interface{}{opcode.CALL, []byte{3}}, opcode.RET, // Main
[]interface{}{opcode.CALL, []byte{3}}, opcode.RET, // f
opcode.LDSFLD0, opcode.RET) // g
})
t.Run("struct field name matches global var name", func(t *testing.T) {
src := `package foo
type CustomStr struct { Int int }
var str = CustomStr{Int: 2}
var Int = 5 // Unused and the code must not be emitted.
func Main() int {
return str.Int
}`
eval(t, src, big.NewInt(2), []interface{}{opcode.INITSSLOT, []byte{1}}, // sslot for str
opcode.PUSH2, opcode.PUSH1, opcode.PACKSTRUCT, opcode.STSFLD0, opcode.RET, // store str
opcode.LDSFLD0, opcode.PUSH0, opcode.PICKITEM, opcode.RET) // Main
})
t.Run("var as a struct field initializer", func(t *testing.T) {
src := `package foo
type CustomStr struct { Int int }
var A = 5
var Int = 6 // Unused
func Main() int {
return CustomStr{Int: A}.Int
}`
eval(t, src, big.NewInt(5))
})
t.Run("argument of globally called function", func(t *testing.T) {
src := `package foo
var Unused = 5
var control int
var _, A = f(Unused)
func Main() int {
return control
}
func f(int) (int, int) {
control = 5
return 1, 2
}`
eval(t, src, big.NewInt(5))
})
t.Run("argument of locally called function", func(t *testing.T) {
src := `package foo
var Unused = 5
func Main() int {
var _, a = f(Unused)
return a
}
func f(i int) (int, int) {
return i, i
}`
eval(t, src, big.NewInt(5))
})
t.Run("used in globally called defer", func(t *testing.T) {
src := `package foo
var control1, control2 int
var Unused = 5
var _ = f()
func Main() int {
return control1 + control2
}
func f() int {
control1 = 1
defer func(){
control2 = Unused
}()
return 2
}`
eval(t, src, big.NewInt(6))
})
t.Run("used in locally called defer", func(t *testing.T) {
src := `package foo
var control1, control2 int
var Unused = 5
func Main() int {
_ = f()
return control1 + control2
}
func f() int {
control1 = 1
defer func(){
control2 = Unused
}()
return 2
}`
eval(t, src, big.NewInt(6))
})
t.Run("imported", func(t *testing.T) {
t.Run("init by func call", func(t *testing.T) {
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar"
func Main() int {
return globalvar.Default
}`
eval(t, src, big.NewInt(0))
checkInstrCount(t, src, 1 /* Default */, 1 /* f */, 0, 0)
})
t.Run("nested var call", func(t *testing.T) {
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/nested1"
func Main() int {
return nested1.C
}`
eval(t, src, big.NewInt(81))
checkInstrCount(t, src, 6 /* dependant vars of nested1.C */, 3, 1, 1)
})
t.Run("nested func call", func(t *testing.T) {
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/funccall"
func Main() int {
return funccall.F()
}`
eval(t, src, big.NewInt(56))
checkInstrCount(t, src, 2 /* nested2.Argument + nested1.Argument */, -1, -1, -1)
})
t.Run("nested method call", func(t *testing.T) {
src := `package foo
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/globalvar/funccall"
func Main() int {
return funccall.GetAge()
}`
eval(t, src, big.NewInt(24))
checkInstrCount(t, src, 3, /* nested3.Anna + nested2.Argument + nested3.Argument */
5, /* funccall.GetAge() + Anna.GetAge() + nested1.f + nested1.f + nested2.f */
2 /* nested1.f + nested2.f */, 0)
})
})
}
func TestChangeGlobal(t *testing.T) {
src := `package foo
var a int
@ -360,19 +907,18 @@ func TestUnderscoreGlobalVarDontEmitCode(t *testing.T) {
_, B, _ = 4, 5, 6
_, C, _ = f(A, B)
)
var D = 7 // unused but named, so the code is expected
var D = 7 // named unused, after global codegen optimisation no code 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
eval(t, src, big.NewInt(1), []interface{}{opcode.INITSSLOT, []byte{2}}, // sslot for A, B
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.LDSFLD0, opcode.LDSFLD1, opcode.SWAP, []interface{}{opcode.CALL, []byte{8}}, // evaluate f(A,B)
opcode.DROP, opcode.DROP, opcode.DROP, opcode.RET, // drop result of f(A,B)
opcode.PUSH1, opcode.RET, // Main
[]interface{}{opcode.INITSLOT, []byte{0, 2}}, opcode.PUSH10, opcode.PUSH9, opcode.PUSH8, opcode.RET) // f
}