Skip to content

Commit dbd761a

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 dbd761a

2 files changed

Lines changed: 126 additions & 11 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.

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)