Skip to content

Commit e73cf8e

Browse files
committed
transform: preserve methods only for reflective calls
1 parent d2a90c6 commit e73cf8e

5 files changed

Lines changed: 205 additions & 19 deletions

File tree

compiler/compiler.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,6 +1535,15 @@ func (b *builder) createInstruction(instr ssa.Instruction) {
15351535
b.setDebugLocation(getPos(instr))
15361536
}
15371537

1538+
switch instr := instr.(type) {
1539+
case *ssa.Call:
1540+
b.markReflectMethodUse(&instr.Call)
1541+
case *ssa.Defer:
1542+
b.markReflectMethodUse(&instr.Call)
1543+
case *ssa.Go:
1544+
b.markReflectMethodUse(&instr.Call)
1545+
}
1546+
15381547
switch instr := instr.(type) {
15391548
case ssa.Value:
15401549
if value, err := b.createExpr(instr); err != nil {
@@ -1603,6 +1612,36 @@ func (b *builder) createInstruction(instr ssa.Instruction) {
16031612
}
16041613
}
16051614

1615+
func (b *builder) markReflectMethodUse(call *ssa.CallCommon) {
1616+
pkg := b.fn.Pkg
1617+
if pkg == nil && b.fn.Origin() != nil {
1618+
pkg = b.fn.Origin().Pkg
1619+
}
1620+
if pkg != nil {
1621+
switch pkg.Pkg.Path() {
1622+
case "reflect", "internal/reflectlite":
1623+
return
1624+
}
1625+
}
1626+
1627+
var method *types.Func
1628+
if call.IsInvoke() {
1629+
method = call.Method
1630+
} else if callee := call.StaticCallee(); callee != nil {
1631+
if object := callee.Object(); object != nil {
1632+
method, _ = object.(*types.Func)
1633+
}
1634+
}
1635+
if method == nil || method.Pkg() == nil || method.Pkg().Path() != "reflect" {
1636+
return
1637+
}
1638+
switch method.Name() {
1639+
case "Method", "MethodByName", "Methods":
1640+
attr := b.ctx.CreateStringAttribute("tinygo-reflect-method", "")
1641+
b.llvmFn.AddFunctionAttr(attr)
1642+
}
1643+
}
1644+
16061645
func (b *builder) setValue(value ssa.Value, llvmValue llvm.Value) {
16071646
if b.isAggregateValue(value.Type()) && !llvmValue.IsNil() && llvmValue.Type().TypeKind() == llvm.PointerTypeKind {
16081647
b.indirectValues[value] = llvmValue

transform/interface-lowering.go

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,19 @@ type interfaceInfo struct {
9090
// pass has been implemented as an object type because of its complexity, but
9191
// should be seen as a regular function call (see LowerInterfaces).
9292
type lowerInterfacesPass struct {
93-
mod llvm.Module
94-
config *compileopts.Config
95-
builder llvm.Builder
96-
dibuilder *llvm.DIBuilder
97-
difiles map[string]llvm.Metadata
98-
ctx llvm.Context
99-
uintptrType llvm.Type
100-
targetData llvm.TargetData
101-
ptrType llvm.Type
102-
types map[string]*typeInfo
103-
signatures map[string]*signatureInfo
104-
interfaces map[string]*interfaceInfo
93+
mod llvm.Module
94+
config *compileopts.Config
95+
builder llvm.Builder
96+
dibuilder *llvm.DIBuilder
97+
difiles map[string]llvm.Metadata
98+
ctx llvm.Context
99+
uintptrType llvm.Type
100+
targetData llvm.TargetData
101+
ptrType llvm.Type
102+
types map[string]*typeInfo
103+
signatures map[string]*signatureInfo
104+
interfaces map[string]*interfaceInfo
105+
keepMethodNames bool // true when reflection methods can inspect method names
105106
}
106107

107108
// LowerInterfaces lowers all intermediate interface calls and globals that are
@@ -342,9 +343,50 @@ func (p *lowerInterfacesPass) run() error {
342343
stripMethodSets = true
343344
}
344345

345-
// TODO: Restore method-set pruning once reflective method calls can be
346-
// distinguished from calls made inside the reflect implementation.
347-
// For now, preserve complete method sets whenever reflection needs them.
346+
keepAllMethods := p.usesReflectMethods()
347+
if keepAllMethods {
348+
stripMethodSets = false // Method sets are needed.
349+
}
350+
351+
p.keepMethodNames = keepAllMethods
352+
353+
// Collect all method signatures that appear in any interface type
354+
// descriptor. When reflect is imported and method sets are kept,
355+
// concrete type method sets are pruned: individual methods not in any
356+
// interface are removed, and types that can't fully satisfy at least
357+
// one interface have their method sets emptied entirely.
358+
//
359+
// When keepAllMethods is true, pruning is disabled and all methods are
360+
// kept.
361+
//
362+
// When method sets are stripped entirely (reflect not imported),
363+
// methodFilter is nil and filterMethodSet replaces with empty.
364+
var methodFilter map[string]struct{}
365+
var ifaceMethodSets []map[string]struct{}
366+
if !stripMethodSets && !keepAllMethods {
367+
methodFilter = make(map[string]struct{})
368+
for _, name := range typeNames {
369+
if !strings.HasPrefix(name, "interface:") {
370+
continue
371+
}
372+
t := p.types[name]
373+
initializer := t.typecode.Initializer()
374+
ifaceSet := make(map[string]struct{})
375+
for i := 0; i < initializer.Type().StructElementTypesCount(); i++ {
376+
field := p.builder.CreateExtractValue(initializer, i, "")
377+
for _, sig := range p.extractMethodSigs(field) {
378+
methodFilter[sig] = struct{}{}
379+
ifaceSet[sig] = struct{}{}
380+
}
381+
}
382+
if len(ifaceSet) > 0 {
383+
ifaceMethodSets = append(ifaceMethodSets, ifaceSet)
384+
}
385+
}
386+
}
387+
388+
// Remove all method sets, which are now unnecessary and inhibit later
389+
// optimizations if they are left in place.
348390
zero := llvm.ConstInt(p.ctx.Int32Type(), 0, false)
349391
for _, name := range typeNames {
350392
t := p.types[name]
@@ -370,8 +412,8 @@ func (p *lowerInterfacesPass) run() error {
370412
numMethodFieldIdx := -1 // index into newInitializerFields
371413
for i := 1; i < numFields; i++ {
372414
field := p.builder.CreateExtractValue(initializer, i, "")
373-
if stripMethodSets {
374-
field = p.filterMethodSet(field, nil, nil)
415+
if !keepAllMethods {
416+
field = p.filterMethodSet(field, methodFilter, ifaceMethodSets)
375417
}
376418
// Track where the numMethod field lands in the new slice.
377419
if i == 2 && numMethodsIsI16 {
@@ -420,12 +462,45 @@ func (p *lowerInterfacesPass) run() error {
420462
t.typecode.EraseFromParentAsGlobal()
421463
newGlobal.SetName(typecodeName)
422464
t.typecode = newGlobal
465+
} else if !keepAllMethods {
466+
// Types without an external method set (e.g., interface types)
467+
// may still have inline method sets with name pointers that
468+
// should be nulled out when reflection cannot inspect methods.
469+
initializer := t.typecode.Initializer()
470+
if initializer.Type().TypeKind() != llvm.StructTypeKind {
471+
continue
472+
}
473+
numFields := initializer.Type().StructElementTypesCount()
474+
changed := false
475+
var fields []llvm.Value
476+
for i := 0; i < numFields; i++ {
477+
field := p.builder.CreateExtractValue(initializer, i, "")
478+
filtered := p.filterMethodSet(field, methodFilter, ifaceMethodSets)
479+
if filtered.C != field.C {
480+
changed = true
481+
}
482+
fields = append(fields, filtered)
483+
}
484+
if changed {
485+
newInitializer := p.ctx.ConstStruct(fields, false)
486+
t.typecode.SetInitializer(newInitializer)
487+
}
423488
}
424489
}
425490

426491
return nil
427492
}
428493

494+
func (p *lowerInterfacesPass) usesReflectMethods() bool {
495+
for fn := p.mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) {
496+
attr := fn.GetStringAttributeAtIndex(-1, "tinygo-reflect-method")
497+
if !attr.IsNil() && (fn.Linkage() != llvm.InternalLinkage || hasUses(fn)) {
498+
return true
499+
}
500+
}
501+
return false
502+
}
503+
429504
// addTypeMethods reads the method set of the given type info struct. It
430505
// retrieves the signatures and the references to the method functions
431506
// themselves for later type<->interface matching.
@@ -757,14 +832,24 @@ func (p *lowerInterfacesPass) filterMethodSet(field llvm.Value, keepSigs map[str
757832
}
758833

759834
// Prune: keep only method entries whose signature appears in keepSigs.
835+
// When reflection cannot inspect methods, null out name pointers so LLVM
836+
// can eliminate the name string globals.
760837
var kept []llvm.Value
761838
for _, e := range entries {
762839
if _, ok := keepSigs[e.name]; ok {
763-
kept = append(kept, e.pair)
840+
if p.keepMethodNames {
841+
kept = append(kept, e.pair)
842+
} else {
843+
sig := p.builder.CreateExtractValue(e.pair, 0, "")
844+
kept = append(kept, p.ctx.ConstStruct([]llvm.Value{
845+
sig,
846+
llvm.ConstNull(p.ptrType),
847+
}, false))
848+
}
764849
}
765850
}
766851

767-
if len(kept) == numMethods {
852+
if len(kept) == numMethods && p.keepMethodNames {
768853
return field
769854
}
770855

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package transform
2+
3+
import (
4+
"testing"
5+
6+
"tinygo.org/x/go-llvm"
7+
)
8+
9+
func TestUsesReflectMethods(t *testing.T) {
10+
t.Parallel()
11+
tests := []struct {
12+
name string
13+
path string
14+
want bool
15+
}{
16+
{
17+
name: "unused marker",
18+
path: "testdata/reflect-method-unused.ll",
19+
},
20+
{
21+
name: "used marker",
22+
path: "testdata/reflect-method-used.ll",
23+
want: true,
24+
},
25+
}
26+
for _, tc := range tests {
27+
tc := tc
28+
t.Run(tc.name, func(t *testing.T) {
29+
t.Parallel()
30+
ctx := llvm.NewContext()
31+
defer ctx.Dispose()
32+
buf, err := llvm.NewMemoryBufferFromFile(tc.path)
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
mod, err := ctx.ParseIR(buf)
37+
if err != nil {
38+
t.Fatal(err)
39+
}
40+
defer mod.Dispose()
41+
42+
p := lowerInterfacesPass{mod: mod}
43+
if got := p.usesReflectMethods(); got != tc.want {
44+
t.Errorf("usesReflectMethods() = %v, want %v", got, tc.want)
45+
}
46+
})
47+
}
48+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
define internal void @unused() #0 {
2+
entry:
3+
ret void
4+
}
5+
6+
attributes #0 = { "tinygo-reflect-method"="" }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@used.function = internal constant ptr @used
2+
3+
define internal void @used() #0 {
4+
entry:
5+
ret void
6+
}
7+
8+
attributes #0 = { "tinygo-reflect-method"="" }

0 commit comments

Comments
 (0)