Skip to content

Commit 1cb67a2

Browse files
committed
Make interface checks similar to invoke, allowing typeImplementsMethodSet and method info to be dropped when reflect is not present
1 parent a9d5c29 commit 1cb67a2

3 files changed

Lines changed: 150 additions & 33 deletions

File tree

compiler/interface.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -715,16 +715,11 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value {
715715
// This type assertion always succeeds, so we can just set commaOk to true.
716716
commaOk = llvm.ConstInt(b.ctx.Int1Type(), 1, true)
717717
} else {
718-
// Type assert using interface type with methods.
719-
// This is implemented using a runtime call, which checks that the
720-
// type implements each method of the interface.
721-
// For comparison, here is how the Go compiler does this (which is
722-
// very similar):
723-
// https://research.swtch.com/interfaces
724-
commaOk = b.createRuntimeCall("typeImplementsMethodSet", []llvm.Value{
725-
actualTypeNum,
726-
b.getInterfaceMethodSet(intf),
727-
}, "")
718+
// Type assert on an interface type with methods.
719+
// Create a call to a declared-but-not-defined function that will
720+
// be lowered by the interface lowering pass into a type-ID
721+
// comparison chain.
722+
commaOk = b.createInterfaceTypeAssert(intf, actualTypeNum)
728723
}
729724
} else {
730725
name, _ := getTypeCodeName(expr.AssertedType)
@@ -895,6 +890,24 @@ func (c *compilerContext) getInvokeFunction(instr *ssa.CallCommon) llvm.Value {
895890
return llvmFn
896891
}
897892

893+
// createInterfaceTypeAssert creates a call to a declared-but-not-defined
894+
// $typeassert function for the given interface. This function will be defined
895+
// by the interface lowering pass as a type-ID comparison chain, avoiding the
896+
// need for runtime.typeImplementsMethodSet at compile time.
897+
func (b *builder) createInterfaceTypeAssert(intf *types.Interface, actualType llvm.Value) llvm.Value {
898+
s, _ := getTypeCodeName(intf)
899+
fnName := s + ".$typeassert"
900+
llvmFn := b.mod.NamedFunction(fnName)
901+
if llvmFn.IsNil() {
902+
llvmFnType := llvm.FunctionType(b.ctx.Int1Type(), []llvm.Type{b.dataPtrType}, false)
903+
llvmFn = llvm.AddFunction(b.mod, fnName, llvmFnType)
904+
b.addStandardDeclaredAttributes(llvmFn)
905+
methods := b.getMethodsString(intf)
906+
llvmFn.AddFunctionAttr(b.ctx.CreateStringAttribute("tinygo-methods", methods))
907+
}
908+
return b.CreateCall(llvmFn.GlobalValueType(), llvmFn, []llvm.Value{actualType}, "")
909+
}
910+
898911
// getInterfaceInvokeWrapper returns a wrapper for the given method so it can be
899912
// invoked from an interface. The wrapper takes in a pointer to the underlying
900913
// value, dereferences or unpacks it if necessary, and calls the real method.

compiler/testdata/interface.ll

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ target triple = "wasm32-unknown-wasi"
1818
@"reflect/types.signature:String:func:{}{basic:string}" = linkonce_odr constant i8 0, align 1
1919
@"reflect/types.type:interface:{String:func:{}{basic:string}}" = linkonce_odr constant { i8, ptr, { i32, [1 x ptr] } } { i8 84, ptr @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}", { i32, [1 x ptr] } { i32 1, [1 x ptr] [ptr @"reflect/types.signature:String:func:{}{basic:string}"] } }, align 4
2020
@"reflect/types.typeid:basic:int" = external constant i8
21-
@"interface:{Error:func:{}{basic:string}}$itfmethods" = linkonce_odr unnamed_addr constant { i32, [1 x ptr] } { i32 1, [1 x ptr] [ptr @"reflect/types.signature:Error:func:{}{basic:string}"] }, align 4
22-
@"interface:{String:func:{}{basic:string}}$itfmethods" = linkonce_odr unnamed_addr constant { i32, [1 x ptr] } { i32 1, [1 x ptr] [ptr @"reflect/types.signature:String:func:{}{basic:string}"] }, align 4
2321

2422
; Function Attrs: allockind("alloc,zeroed") allocsize(0)
2523
declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #0
@@ -36,42 +34,42 @@ entry:
3634
define hidden %runtime._interface @main.simpleType(ptr %context) unnamed_addr #2 {
3735
entry:
3836
%stackalloc = alloca i8, align 1
39-
call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:basic:int", ptr nonnull %stackalloc, ptr undef) #5
40-
call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #5
37+
call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:basic:int", ptr nonnull %stackalloc, ptr undef) #7
38+
call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #7
4139
ret %runtime._interface { ptr @"reflect/types.type:basic:int", ptr null }
4240
}
4341

4442
; Function Attrs: nounwind
4543
define hidden %runtime._interface @main.pointerType(ptr %context) unnamed_addr #2 {
4644
entry:
4745
%stackalloc = alloca i8, align 1
48-
call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:basic:int", ptr nonnull %stackalloc, ptr undef) #5
49-
call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #5
46+
call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:basic:int", ptr nonnull %stackalloc, ptr undef) #7
47+
call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #7
5048
ret %runtime._interface { ptr @"reflect/types.type:pointer:basic:int", ptr null }
5149
}
5250

5351
; Function Attrs: nounwind
5452
define hidden %runtime._interface @main.interfaceType(ptr %context) unnamed_addr #2 {
5553
entry:
5654
%stackalloc = alloca i8, align 1
57-
call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:named:error", ptr nonnull %stackalloc, ptr undef) #5
58-
call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #5
55+
call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:named:error", ptr nonnull %stackalloc, ptr undef) #7
56+
call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #7
5957
ret %runtime._interface { ptr @"reflect/types.type:pointer:named:error", ptr null }
6058
}
6159

6260
; Function Attrs: nounwind
6361
define hidden %runtime._interface @main.anonymousInterfaceType(ptr %context) unnamed_addr #2 {
6462
entry:
6563
%stackalloc = alloca i8, align 1
66-
call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}", ptr nonnull %stackalloc, ptr undef) #5
67-
call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #5
64+
call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}", ptr nonnull %stackalloc, ptr undef) #7
65+
call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #7
6866
ret %runtime._interface { ptr @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}", ptr null }
6967
}
7068

7169
; Function Attrs: nounwind
7270
define hidden i1 @main.isInt(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 {
7371
entry:
74-
%typecode = call i1 @runtime.typeAssert(ptr %itf.typecode, ptr nonnull @"reflect/types.typeid:basic:int", ptr undef) #5
72+
%typecode = call i1 @runtime.typeAssert(ptr %itf.typecode, ptr nonnull @"reflect/types.typeid:basic:int", ptr undef) #7
7573
br i1 %typecode, label %typeassert.ok, label %typeassert.next
7674

7775
typeassert.next: ; preds = %typeassert.ok, %entry
@@ -86,7 +84,7 @@ declare i1 @runtime.typeAssert(ptr, ptr dereferenceable_or_null(1), ptr) #1
8684
; Function Attrs: nounwind
8785
define hidden i1 @main.isError(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 {
8886
entry:
89-
%0 = call i1 @runtime.typeImplementsMethodSet(ptr %itf.typecode, ptr nonnull @"interface:{Error:func:{}{basic:string}}$itfmethods", ptr undef) #5
87+
%0 = call i1 @"interface:{Error:func:{}{basic:string}}.$typeassert"(ptr %itf.typecode) #7
9088
br i1 %0, label %typeassert.ok, label %typeassert.next
9189

9290
typeassert.next: ; preds = %typeassert.ok, %entry
@@ -96,12 +94,12 @@ typeassert.ok: ; preds = %entry
9694
br label %typeassert.next
9795
}
9896

99-
declare i1 @runtime.typeImplementsMethodSet(ptr, ptr, ptr) #1
97+
declare i1 @"interface:{Error:func:{}{basic:string}}.$typeassert"(ptr) #3
10098

10199
; Function Attrs: nounwind
102100
define hidden i1 @main.isStringer(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 {
103101
entry:
104-
%0 = call i1 @runtime.typeImplementsMethodSet(ptr %itf.typecode, ptr nonnull @"interface:{String:func:{}{basic:string}}$itfmethods", ptr undef) #5
102+
%0 = call i1 @"interface:{String:func:{}{basic:string}}.$typeassert"(ptr %itf.typecode) #7
105103
br i1 %0, label %typeassert.ok, label %typeassert.next
106104

107105
typeassert.next: ; preds = %typeassert.ok, %entry
@@ -111,30 +109,34 @@ typeassert.ok: ; preds = %entry
111109
br label %typeassert.next
112110
}
113111

112+
declare i1 @"interface:{String:func:{}{basic:string}}.$typeassert"(ptr) #4
113+
114114
; Function Attrs: nounwind
115115
define hidden i8 @main.callFooMethod(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 {
116116
entry:
117-
%0 = call i8 @"interface:{String:func:{}{basic:string},main.foo:func:{basic:int}{basic:uint8}}.foo$invoke"(ptr %itf.value, i32 3, ptr %itf.typecode, ptr undef) #5
117+
%0 = call i8 @"interface:{String:func:{}{basic:string},main.foo:func:{basic:int}{basic:uint8}}.foo$invoke"(ptr %itf.value, i32 3, ptr %itf.typecode, ptr undef) #7
118118
ret i8 %0
119119
}
120120

121-
declare i8 @"interface:{String:func:{}{basic:string},main.foo:func:{basic:int}{basic:uint8}}.foo$invoke"(ptr, i32, ptr, ptr) #3
121+
declare i8 @"interface:{String:func:{}{basic:string},main.foo:func:{basic:int}{basic:uint8}}.foo$invoke"(ptr, i32, ptr, ptr) #5
122122

123123
; Function Attrs: nounwind
124124
define hidden %runtime._string @main.callErrorMethod(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 {
125125
entry:
126126
%stackalloc = alloca i8, align 1
127-
%0 = call %runtime._string @"interface:{Error:func:{}{basic:string}}.Error$invoke"(ptr %itf.value, ptr %itf.typecode, ptr undef) #5
127+
%0 = call %runtime._string @"interface:{Error:func:{}{basic:string}}.Error$invoke"(ptr %itf.value, ptr %itf.typecode, ptr undef) #7
128128
%1 = extractvalue %runtime._string %0, 0
129-
call void @runtime.trackPointer(ptr %1, ptr nonnull %stackalloc, ptr undef) #5
129+
call void @runtime.trackPointer(ptr %1, ptr nonnull %stackalloc, ptr undef) #7
130130
ret %runtime._string %0
131131
}
132132

133-
declare %runtime._string @"interface:{Error:func:{}{basic:string}}.Error$invoke"(ptr, ptr, ptr) #4
133+
declare %runtime._string @"interface:{Error:func:{}{basic:string}}.Error$invoke"(ptr, ptr, ptr) #6
134134

135135
attributes #0 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }
136136
attributes #1 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }
137137
attributes #2 = { nounwind "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" }
138-
attributes #3 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-invoke"="main.$methods.foo(int) uint8" "tinygo-methods"="reflect/methods.String() string; main.$methods.foo(int) uint8" }
139-
attributes #4 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-invoke"="reflect/methods.Error() string" "tinygo-methods"="reflect/methods.Error() string" }
140-
attributes #5 = { nounwind }
138+
attributes #3 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-methods"="reflect/methods.Error() string" }
139+
attributes #4 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-methods"="reflect/methods.String() string" }
140+
attributes #5 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-invoke"="main.$methods.foo(int) uint8" "tinygo-methods"="reflect/methods.String() string; main.$methods.foo(int) uint8" }
141+
attributes #6 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" "tinygo-invoke"="reflect/methods.Error() string" "tinygo-methods"="reflect/methods.Error() string" }
142+
attributes #7 = { nounwind }

transform/interface-lowering.go

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,13 @@ func (p *lowerInterfacesPass) run() error {
272272
p.defineInterfaceMethodFunc(fn, itf, signature)
273273
}
274274

275+
// Define all interface type assert functions.
276+
for _, fn := range interfaceAssertFunctions {
277+
methodsAttr := fn.GetStringAttributeAtIndex(-1, "tinygo-methods")
278+
itf := p.interfaces[methodsAttr.GetStringValue()]
279+
p.defineInterfaceAssertFunc(fn, itf)
280+
}
281+
275282
// Replace each type assert with an actual type comparison or (if the type
276283
// assert is impossible) the constant false.
277284
llvmFalse := llvm.ConstInt(p.ctx.Int1Type(), 0, false)
@@ -318,6 +325,17 @@ func (p *lowerInterfacesPass) run() error {
318325
}
319326
sort.Strings(typeNames)
320327

328+
// Check whether runtime.typeImplementsMethodSet still has uses. Now that
329+
// interface type assertions have been lowered to type-ID comparison
330+
// chains, the only remaining callers would be from reflect
331+
// (AssignableTo/Implements). If none remain, we can strip the inline
332+
// method-set data from type descriptors to save binary size.
333+
stripMethodSets := false
334+
typeImplementsFn := p.mod.NamedFunction("runtime.typeImplementsMethodSet")
335+
if !typeImplementsFn.IsNil() && !hasUses(typeImplementsFn) {
336+
stripMethodSets = true
337+
}
338+
321339
// Remove all method sets, which are now unnecessary and inhibit later
322340
// optimizations if they are left in place.
323341
zero := llvm.ConstInt(p.ctx.Int32Type(), 0, false)
@@ -327,7 +345,11 @@ func (p *lowerInterfacesPass) run() error {
327345
initializer := t.typecode.Initializer()
328346
var newInitializerFields []llvm.Value
329347
for i := 1; i < initializer.Type().StructElementTypesCount(); i++ {
330-
newInitializerFields = append(newInitializerFields, p.builder.CreateExtractValue(initializer, i, ""))
348+
field := p.builder.CreateExtractValue(initializer, i, "")
349+
if stripMethodSets {
350+
field = p.replaceMethodSetWithEmpty(name, i-1, field)
351+
}
352+
newInitializerFields = append(newInitializerFields, field)
331353
}
332354
newInitializer := p.ctx.ConstStruct(newInitializerFields, false)
333355
typecodeName := t.typecode.Name()
@@ -525,3 +547,83 @@ func (p *lowerInterfacesPass) getDIFile(file string) llvm.Metadata {
525547
}
526548
return difile
527549
}
550+
551+
// defineInterfaceAssertFunc defines a $typeassert function for the given
552+
// interface. The function returns true if the concrete type (passed as a
553+
// type-ID pointer) implements the interface, using a chain of type-ID
554+
// comparisons. This avoids pulling in runtime.typeImplementsMethodSet for
555+
// programs that don't use reflect.
556+
func (p *lowerInterfacesPass) defineInterfaceAssertFunc(fn llvm.Value, itf *interfaceInfo) {
557+
actualType := fn.FirstParam()
558+
actualType.SetName("actualType")
559+
fn.SetLinkage(llvm.InternalLinkage)
560+
fn.SetUnnamedAddr(true)
561+
AddStandardAttributes(fn, p.config)
562+
563+
entry := p.ctx.AddBasicBlock(fn, "entry")
564+
p.builder.SetInsertPointAtEnd(entry)
565+
566+
if p.dibuilder != nil {
567+
difile := p.getDIFile("<Go interface type assert>")
568+
diFuncType := p.dibuilder.CreateSubroutineType(llvm.DISubroutineType{
569+
File: difile,
570+
})
571+
difunc := p.dibuilder.CreateFunction(difile, llvm.DIFunction{
572+
Name: "(Go interface type assert)",
573+
File: difile,
574+
Line: 0,
575+
Type: diFuncType,
576+
LocalToUnit: true,
577+
IsDefinition: true,
578+
ScopeLine: 0,
579+
Flags: llvm.FlagPrototyped,
580+
Optimized: true,
581+
})
582+
fn.SetSubprogram(difunc)
583+
p.builder.SetCurrentDebugLocation(0, 0, difunc, llvm.Metadata{})
584+
}
585+
586+
// Build an OR chain: return (type == T1) || (type == T2) || ...
587+
llvmFalse := llvm.ConstInt(p.ctx.Int1Type(), 0, false)
588+
result := llvmFalse
589+
for _, typ := range itf.types {
590+
cmp := p.builder.CreateICmp(llvm.IntEQ, actualType, typ.typecodeGEP, typ.name+".icmp")
591+
result = p.builder.CreateOr(result, cmp, "")
592+
}
593+
p.builder.CreateRet(result)
594+
}
595+
596+
// replaceMethodSetWithEmpty replaces a method-set field in a type descriptor
597+
// with an empty method set ({0, [0]ptr}). This is used when reflect is not
598+
// needed and method-set data can be stripped to save binary size.
599+
//
600+
// The field index is relative to the type descriptor after $methodset removal
601+
// (i.e., field 0 is the kind/meta byte). Type descriptor layouts:
602+
//
603+
// named: [kind, numMethods, ptrTo, underlying, pkgpath, methods, name]
604+
// pointer: [kind, numMethods, elem, methods]
605+
// struct: [kind, numMethods, ptrTo, pkgpath, size, numFields, fields, methods]
606+
// interface: [kind, ptrTo, methods]
607+
func (p *lowerInterfacesPass) replaceMethodSetWithEmpty(typeName string, fieldIdx int, field llvm.Value) llvm.Value {
608+
isMethodSetField := false
609+
switch {
610+
case strings.HasPrefix(typeName, "named:"):
611+
isMethodSetField = (fieldIdx == 5)
612+
case strings.HasPrefix(typeName, "pointer:"):
613+
isMethodSetField = (fieldIdx == 3)
614+
case strings.HasPrefix(typeName, "struct:"):
615+
isMethodSetField = (fieldIdx == 7)
616+
case strings.HasPrefix(typeName, "interface:"):
617+
isMethodSetField = (fieldIdx == 2)
618+
}
619+
if !isMethodSetField {
620+
return field
621+
}
622+
623+
// Replace with empty method set: {length=0, methods=[0 x ptr]}
624+
emptyMethodSet := p.ctx.ConstStruct([]llvm.Value{
625+
llvm.ConstInt(p.uintptrType, 0, false),
626+
llvm.ConstArray(p.ptrType, nil),
627+
}, false)
628+
return emptyMethodSet
629+
}

0 commit comments

Comments
 (0)