mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-09 05:23:40 +00:00
compiler: reimplement interface type asserts
This is a big reimplementation that simplifies the compiler a lot. Instead of storing the method set in metadata and lowering the type asserts as a whole program pass, this change just puts the list of methods in the type code (and a separate global for the interface type).
This commit is contained in:
@@ -272,13 +272,6 @@ func (p *lowerInterfacesPass) run() error {
|
||||
p.defineInterfaceMethodFunc(fn, itf, signature)
|
||||
}
|
||||
|
||||
// Define all interface type assert functions.
|
||||
for _, fn := range interfaceAssertFunctions {
|
||||
methodsAttr := fn.GetStringAttributeAtIndex(-1, "tinygo-methods")
|
||||
itf := p.interfaces[methodsAttr.GetStringValue()]
|
||||
p.defineInterfaceImplementsFunc(fn, itf)
|
||||
}
|
||||
|
||||
// Replace each type assert with an actual type comparison or (if the type
|
||||
// assert is impossible) the constant false.
|
||||
llvmFalse := llvm.ConstInt(p.ctx.Int1Type(), 0, false)
|
||||
@@ -428,66 +421,6 @@ func (p *lowerInterfacesPass) getSignature(name string) *signatureInfo {
|
||||
return p.signatures[name]
|
||||
}
|
||||
|
||||
// defineInterfaceImplementsFunc defines the interface type assert function. It
|
||||
// checks whether the given interface type (passed as an argument) is one of the
|
||||
// types it implements.
|
||||
//
|
||||
// The type match is implemented using an if/else chain over all possible types.
|
||||
// This if/else chain is easily converted to a big switch over all possible
|
||||
// types by the LLVM simplifycfg pass.
|
||||
func (p *lowerInterfacesPass) defineInterfaceImplementsFunc(fn llvm.Value, itf *interfaceInfo) {
|
||||
// Create the function and function signature.
|
||||
fn.Param(0).SetName("actualType")
|
||||
fn.SetLinkage(llvm.InternalLinkage)
|
||||
fn.SetUnnamedAddr(true)
|
||||
AddStandardAttributes(fn, p.config)
|
||||
|
||||
// Start the if/else chain at the entry block.
|
||||
entry := p.ctx.AddBasicBlock(fn, "entry")
|
||||
thenBlock := p.ctx.AddBasicBlock(fn, "then")
|
||||
p.builder.SetInsertPointAtEnd(entry)
|
||||
|
||||
if p.dibuilder != nil {
|
||||
difile := p.getDIFile("<Go interface assert>")
|
||||
diFuncType := p.dibuilder.CreateSubroutineType(llvm.DISubroutineType{
|
||||
File: difile,
|
||||
})
|
||||
difunc := p.dibuilder.CreateFunction(difile, llvm.DIFunction{
|
||||
Name: "(Go interface assert)",
|
||||
File: difile,
|
||||
Line: 0,
|
||||
Type: diFuncType,
|
||||
LocalToUnit: true,
|
||||
IsDefinition: true,
|
||||
ScopeLine: 0,
|
||||
Flags: llvm.FlagPrototyped,
|
||||
Optimized: true,
|
||||
})
|
||||
fn.SetSubprogram(difunc)
|
||||
p.builder.SetCurrentDebugLocation(0, 0, difunc, llvm.Metadata{})
|
||||
}
|
||||
|
||||
// Iterate over all possible types. Each iteration creates a new branch
|
||||
// either to the 'then' block (success) or the .next block, for the next
|
||||
// check.
|
||||
actualType := fn.Param(0)
|
||||
for _, typ := range itf.types {
|
||||
nextBlock := p.ctx.AddBasicBlock(fn, typ.name+".next")
|
||||
cmp := p.builder.CreateICmp(llvm.IntEQ, actualType, typ.typecodeGEP, typ.name+".icmp")
|
||||
p.builder.CreateCondBr(cmp, thenBlock, nextBlock)
|
||||
p.builder.SetInsertPointAtEnd(nextBlock)
|
||||
}
|
||||
|
||||
// The builder is now inserting at the last *.next block. Once we reach
|
||||
// this point, all types have been checked so the type assert will have
|
||||
// failed.
|
||||
p.builder.CreateRet(llvm.ConstInt(p.ctx.Int1Type(), 0, false))
|
||||
|
||||
// Fill 'then' block (type assert was successful).
|
||||
p.builder.SetInsertPointAtEnd(thenBlock)
|
||||
p.builder.CreateRet(llvm.ConstInt(p.ctx.Int1Type(), 1, false))
|
||||
}
|
||||
|
||||
// defineInterfaceMethodFunc defines this thunk by calling the concrete method
|
||||
// of the type that implements this interface.
|
||||
//
|
||||
|
||||
@@ -65,7 +65,6 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error {
|
||||
|
||||
// Run TinyGo-specific optimization passes.
|
||||
OptimizeStringToBytes(mod)
|
||||
OptimizeReflectImplements(mod)
|
||||
maxStackSize := config.MaxStackAlloc()
|
||||
OptimizeAllocs(mod, nil, maxStackSize, nil)
|
||||
err = LowerInterfaces(mod, config)
|
||||
|
||||
@@ -4,8 +4,6 @@ package transform
|
||||
// calls.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"tinygo.org/x/go-llvm"
|
||||
)
|
||||
|
||||
@@ -100,79 +98,3 @@ func OptimizeStringEqual(mod llvm.Module) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OptimizeReflectImplements optimizes the following code:
|
||||
//
|
||||
// implements := someType.Implements(someInterfaceType)
|
||||
//
|
||||
// where someType is an arbitrary reflect.Type and someInterfaceType is a
|
||||
// reflect.Type of interface kind, to the following code:
|
||||
//
|
||||
// _, implements := someType.(interfaceType)
|
||||
//
|
||||
// if the interface type is known at compile time (that is, someInterfaceType is
|
||||
// a LLVM constant aggregate). This optimization is especially important for the
|
||||
// encoding/json package, which uses this method.
|
||||
//
|
||||
// As of this writing, the (reflect.Type).Interface method has not yet been
|
||||
// implemented so this optimization is critical for the encoding/json package.
|
||||
func OptimizeReflectImplements(mod llvm.Module) {
|
||||
implementsSignature := mod.NamedGlobal("reflect/methods.Implements(reflect.Type) bool")
|
||||
if implementsSignature.IsNil() {
|
||||
return
|
||||
}
|
||||
|
||||
builder := mod.Context().NewBuilder()
|
||||
defer builder.Dispose()
|
||||
|
||||
// Look up the (reflect.Value).Implements() method.
|
||||
var implementsFunc llvm.Value
|
||||
for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) {
|
||||
attr := fn.GetStringAttributeAtIndex(-1, "tinygo-invoke")
|
||||
if attr.IsNil() {
|
||||
continue
|
||||
}
|
||||
if attr.GetStringValue() == "reflect/methods.Implements(reflect.Type) bool" {
|
||||
implementsFunc = fn
|
||||
break
|
||||
}
|
||||
}
|
||||
if implementsFunc.IsNil() {
|
||||
// Doesn't exist in the program, so nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
for _, call := range getUses(implementsFunc) {
|
||||
if call.IsACallInst().IsNil() {
|
||||
continue
|
||||
}
|
||||
interfaceType := stripPointerCasts(call.Operand(2))
|
||||
if interfaceType.IsAGlobalVariable().IsNil() {
|
||||
// Interface is unknown at compile time. This can't be optimized.
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(interfaceType.Name(), "reflect/types.type:named:") {
|
||||
// Get the underlying type.
|
||||
interfaceType = stripPointerCasts(builder.CreateExtractValue(interfaceType.Initializer(), 3, ""))
|
||||
}
|
||||
if !strings.HasPrefix(interfaceType.Name(), "reflect/types.type:interface:") {
|
||||
// This is an error. The Type passed to Implements should be of
|
||||
// interface type. Ignore it here (don't report it), it will be
|
||||
// reported at runtime.
|
||||
continue
|
||||
}
|
||||
typeAssertFunction := mod.NamedFunction(strings.TrimPrefix(interfaceType.Name(), "reflect/types.type:") + ".$typeassert")
|
||||
if typeAssertFunction.IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Replace Implements call with the type assert call.
|
||||
builder.SetInsertPointBefore(call)
|
||||
implements := builder.CreateCall(typeAssertFunction.GlobalValueType(), typeAssertFunction, []llvm.Value{
|
||||
call.Operand(0), // typecode to check
|
||||
}, "")
|
||||
call.ReplaceAllUsesWith(implements)
|
||||
call.EraseFromParentAsInstruction()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,3 @@ func TestOptimizeStringEqual(t *testing.T) {
|
||||
transform.OptimizeStringEqual(mod)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOptimizeReflectImplements(t *testing.T) {
|
||||
t.Parallel()
|
||||
testTransform(t, "testdata/reflect-implements", func(mod llvm.Module) {
|
||||
// Run optimization pass.
|
||||
transform.OptimizeReflectImplements(mod)
|
||||
})
|
||||
}
|
||||
|
||||
Vendored
-28
@@ -16,10 +16,7 @@ target triple = "armv7m-none-eabi"
|
||||
declare i1 @runtime.typeAssert(ptr, ptr)
|
||||
declare void @runtime.printuint8(i8)
|
||||
declare void @runtime.printint16(i16)
|
||||
declare void @runtime.printint32(i32)
|
||||
declare void @runtime.printptr(i32)
|
||||
declare void @runtime.printnl()
|
||||
declare void @runtime.nilPanic(ptr)
|
||||
|
||||
define void @printInterfaces() {
|
||||
call void @printInterface(ptr @"reflect/types.type:basic:int", ptr inttoptr (i32 5 to ptr))
|
||||
@@ -30,25 +27,6 @@ define void @printInterfaces() {
|
||||
}
|
||||
|
||||
define void @printInterface(ptr %typecode, ptr %value) {
|
||||
%isUnmatched = call i1 @Unmatched$typeassert(ptr %typecode)
|
||||
br i1 %isUnmatched, label %typeswitch.Unmatched, label %typeswitch.notUnmatched
|
||||
|
||||
typeswitch.Unmatched:
|
||||
%unmatched = ptrtoint ptr %value to i32
|
||||
call void @runtime.printptr(i32 %unmatched)
|
||||
call void @runtime.printnl()
|
||||
ret void
|
||||
|
||||
typeswitch.notUnmatched:
|
||||
%isDoubler = call i1 @Doubler$typeassert(ptr %typecode)
|
||||
br i1 %isDoubler, label %typeswitch.Doubler, label %typeswitch.notDoubler
|
||||
|
||||
typeswitch.Doubler:
|
||||
%doubler.result = call i32 @"Doubler.Double$invoke"(ptr %value, ptr %typecode, ptr undef)
|
||||
call void @runtime.printint32(i32 %doubler.result)
|
||||
ret void
|
||||
|
||||
typeswitch.notDoubler:
|
||||
%isByte = call i1 @runtime.typeAssert(ptr %typecode, ptr nonnull @"reflect/types.typeid:basic:uint8")
|
||||
br i1 %isByte, label %typeswitch.byte, label %typeswitch.notByte
|
||||
|
||||
@@ -86,10 +64,4 @@ define i32 @"(Number).Double$invoke"(ptr %receiverPtr, ptr %context) {
|
||||
|
||||
declare i32 @"Doubler.Double$invoke"(ptr %receiver, ptr %typecode, ptr %context) #0
|
||||
|
||||
declare i1 @Doubler$typeassert(ptr %typecode) #1
|
||||
|
||||
declare i1 @Unmatched$typeassert(ptr %typecode) #2
|
||||
|
||||
attributes #0 = { "tinygo-invoke"="reflect/methods.Double() int" "tinygo-methods"="reflect/methods.Double() int" }
|
||||
attributes #1 = { "tinygo-methods"="reflect/methods.Double() int" }
|
||||
attributes #2 = { "tinygo-methods"="reflect/methods.NeverImplementedMethod()" }
|
||||
|
||||
Vendored
+2
-65
@@ -12,14 +12,8 @@ declare void @runtime.printuint8(i8)
|
||||
|
||||
declare void @runtime.printint16(i16)
|
||||
|
||||
declare void @runtime.printint32(i32)
|
||||
|
||||
declare void @runtime.printptr(i32)
|
||||
|
||||
declare void @runtime.printnl()
|
||||
|
||||
declare void @runtime.nilPanic(ptr)
|
||||
|
||||
define void @printInterfaces() {
|
||||
call void @printInterface(ptr @"reflect/types.type:basic:int", ptr inttoptr (i32 5 to ptr))
|
||||
call void @printInterface(ptr @"reflect/types.type:basic:uint8", ptr inttoptr (i8 120 to ptr))
|
||||
@@ -28,35 +22,16 @@ define void @printInterfaces() {
|
||||
}
|
||||
|
||||
define void @printInterface(ptr %typecode, ptr %value) {
|
||||
%isUnmatched = call i1 @"Unmatched$typeassert"(ptr %typecode)
|
||||
br i1 %isUnmatched, label %typeswitch.Unmatched, label %typeswitch.notUnmatched
|
||||
|
||||
typeswitch.Unmatched: ; preds = %0
|
||||
%unmatched = ptrtoint ptr %value to i32
|
||||
call void @runtime.printptr(i32 %unmatched)
|
||||
call void @runtime.printnl()
|
||||
ret void
|
||||
|
||||
typeswitch.notUnmatched: ; preds = %0
|
||||
%isDoubler = call i1 @"Doubler$typeassert"(ptr %typecode)
|
||||
br i1 %isDoubler, label %typeswitch.Doubler, label %typeswitch.notDoubler
|
||||
|
||||
typeswitch.Doubler: ; preds = %typeswitch.notUnmatched
|
||||
%doubler.result = call i32 @"Doubler.Double$invoke"(ptr %value, ptr %typecode, ptr undef)
|
||||
call void @runtime.printint32(i32 %doubler.result)
|
||||
ret void
|
||||
|
||||
typeswitch.notDoubler: ; preds = %typeswitch.notUnmatched
|
||||
%typeassert.ok = icmp eq ptr @"reflect/types.type:basic:uint8", %typecode
|
||||
br i1 %typeassert.ok, label %typeswitch.byte, label %typeswitch.notByte
|
||||
|
||||
typeswitch.byte: ; preds = %typeswitch.notDoubler
|
||||
typeswitch.byte: ; preds = %0
|
||||
%byte = ptrtoint ptr %value to i8
|
||||
call void @runtime.printuint8(i8 %byte)
|
||||
call void @runtime.printnl()
|
||||
ret void
|
||||
|
||||
typeswitch.notByte: ; preds = %typeswitch.notDoubler
|
||||
typeswitch.notByte: ; preds = %0
|
||||
br i1 false, label %typeswitch.int16, label %typeswitch.notInt16
|
||||
|
||||
typeswitch.int16: ; preds = %typeswitch.notByte
|
||||
@@ -79,41 +54,3 @@ define i32 @"(Number).Double$invoke"(ptr %receiverPtr, ptr %context) {
|
||||
%ret = call i32 @"(Number).Double"(i32 %receiver, ptr undef)
|
||||
ret i32 %ret
|
||||
}
|
||||
|
||||
define internal i32 @"Doubler.Double$invoke"(ptr %receiver, ptr %actualType, ptr %context) unnamed_addr #0 {
|
||||
entry:
|
||||
%"named:Number.icmp" = icmp eq ptr %actualType, @"reflect/types.type:named:Number"
|
||||
br i1 %"named:Number.icmp", label %"named:Number", label %"named:Number.next"
|
||||
|
||||
"named:Number": ; preds = %entry
|
||||
%0 = call i32 @"(Number).Double$invoke"(ptr %receiver, ptr undef)
|
||||
ret i32 %0
|
||||
|
||||
"named:Number.next": ; preds = %entry
|
||||
call void @runtime.nilPanic(ptr undef)
|
||||
unreachable
|
||||
}
|
||||
|
||||
define internal i1 @"Doubler$typeassert"(ptr %actualType) unnamed_addr #1 {
|
||||
entry:
|
||||
%"named:Number.icmp" = icmp eq ptr %actualType, @"reflect/types.type:named:Number"
|
||||
br i1 %"named:Number.icmp", label %then, label %"named:Number.next"
|
||||
|
||||
then: ; preds = %entry
|
||||
ret i1 true
|
||||
|
||||
"named:Number.next": ; preds = %entry
|
||||
ret i1 false
|
||||
}
|
||||
|
||||
define internal i1 @"Unmatched$typeassert"(ptr %actualType) unnamed_addr #2 {
|
||||
entry:
|
||||
ret i1 false
|
||||
|
||||
then: ; No predecessors!
|
||||
ret i1 true
|
||||
}
|
||||
|
||||
attributes #0 = { "tinygo-invoke"="reflect/methods.Double() int" "tinygo-methods"="reflect/methods.Double() int" }
|
||||
attributes #1 = { "tinygo-methods"="reflect/methods.Double() int" }
|
||||
attributes #2 = { "tinygo-methods"="reflect/methods.NeverImplementedMethod()" }
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
target datalayout = "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-f64:32:64-f80:32-n8:16:32-S128"
|
||||
target triple = "i686--linux"
|
||||
|
||||
%runtime._interface = type { ptr, ptr }
|
||||
|
||||
@"reflect/types.type:named:error" = internal constant { i8, i16, ptr, ptr } { i8 52, i16 0, ptr @"reflect/types.type:pointer:named:error", ptr @"reflect/types.type:interface:{Error:func:{}{basic:string}}" }, align 4
|
||||
@"reflect/types.type:interface:{Error:func:{}{basic:string}}" = internal constant { i8, ptr } { i8 20, ptr @"reflect/types.type:pointer:interface:{Error:func:{}{basic:string}}" }, align 4
|
||||
@"reflect/types.type:pointer:interface:{Error:func:{}{basic:string}}" = internal constant { i8, ptr } { i8 21, ptr @"reflect/types.type:interface:{Error:func:{}{basic:string}}" }, align 4
|
||||
@"reflect/types.type:pointer:named:error" = internal constant { i8, i16, ptr } { i8 21, i16 0, ptr @"reflect/types.type:named:error" }, align 4
|
||||
@"reflect/types.type:pointer:named:reflect.rawType" = internal constant { ptr, i8, i16, ptr } { ptr null, i8 21, i16 0, ptr null }, align 4
|
||||
@"reflect/methods.Implements(reflect.Type) bool" = internal constant i8 0, align 1
|
||||
|
||||
; var errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
; func isError(typ reflect.Type) bool {
|
||||
; return typ.Implements(errorType)
|
||||
; }
|
||||
; The type itself is stored in %typ.value, %typ.typecode just refers to the
|
||||
; type of reflect.Type. This function can be optimized because errorType is
|
||||
; known at compile time (after the interp pass has run).
|
||||
define i1 @main.isError(ptr %typ.typecode, ptr %typ.value, ptr %context) {
|
||||
entry:
|
||||
%result = call i1 @"reflect.Type.Implements$invoke"(ptr %typ.value, ptr getelementptr inbounds ({ ptr, i8, ptr }, ptr @"reflect/types.type:pointer:named:reflect.rawType", i32 0, i32 1), ptr @"reflect/types.type:named:error", ptr %typ.typecode, ptr undef)
|
||||
ret i1 %result
|
||||
}
|
||||
|
||||
; This Implements method call can not be optimized because itf is not known at
|
||||
; compile time.
|
||||
; func isUnknown(typ, itf reflect.Type) bool {
|
||||
; return typ.Implements(itf)
|
||||
; }
|
||||
define i1 @main.isUnknown(ptr %typ.typecode, ptr %typ.value, ptr %itf.typecode, ptr %itf.value, ptr %context) {
|
||||
entry:
|
||||
%result = call i1 @"reflect.Type.Implements$invoke"(ptr %typ.value, ptr %itf.typecode, ptr %itf.value, ptr %typ.typecode, ptr undef)
|
||||
ret i1 %result
|
||||
}
|
||||
|
||||
declare i1 @"reflect.Type.Implements$invoke"(ptr, ptr, ptr, ptr, ptr) #0
|
||||
declare i1 @"interface:{Error:func:{}{basic:string}}.$typeassert"(ptr %0) #1
|
||||
|
||||
attributes #0 = { "tinygo-invoke"="reflect/methods.Implements(reflect.Type) bool" "tinygo-methods"="reflect/methods.Align() int; reflect/methods.Implements(reflect.Type) bool" }
|
||||
attributes #1 = { "tinygo-methods"="reflect/methods.Error() string" }
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
target datalayout = "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-f64:32:64-f80:32-n8:16:32-S128"
|
||||
target triple = "i686--linux"
|
||||
|
||||
@"reflect/types.type:named:error" = internal constant { i8, i16, ptr, ptr } { i8 52, i16 0, ptr @"reflect/types.type:pointer:named:error", ptr @"reflect/types.type:interface:{Error:func:{}{basic:string}}" }, align 4
|
||||
@"reflect/types.type:interface:{Error:func:{}{basic:string}}" = internal constant { i8, ptr } { i8 20, ptr @"reflect/types.type:pointer:interface:{Error:func:{}{basic:string}}" }, align 4
|
||||
@"reflect/types.type:pointer:interface:{Error:func:{}{basic:string}}" = internal constant { i8, ptr } { i8 21, ptr @"reflect/types.type:interface:{Error:func:{}{basic:string}}" }, align 4
|
||||
@"reflect/types.type:pointer:named:error" = internal constant { i8, i16, ptr } { i8 21, i16 0, ptr @"reflect/types.type:named:error" }, align 4
|
||||
@"reflect/types.type:pointer:named:reflect.rawType" = internal constant { ptr, i8, i16, ptr } { ptr null, i8 21, i16 0, ptr null }, align 4
|
||||
@"reflect/methods.Implements(reflect.Type) bool" = internal constant i8 0, align 1
|
||||
|
||||
define i1 @main.isError(ptr %typ.typecode, ptr %typ.value, ptr %context) {
|
||||
entry:
|
||||
%0 = call i1 @"interface:{Error:func:{}{basic:string}}.$typeassert"(ptr %typ.value)
|
||||
ret i1 %0
|
||||
}
|
||||
|
||||
define i1 @main.isUnknown(ptr %typ.typecode, ptr %typ.value, ptr %itf.typecode, ptr %itf.value, ptr %context) {
|
||||
entry:
|
||||
%result = call i1 @"reflect.Type.Implements$invoke"(ptr %typ.value, ptr %itf.typecode, ptr %itf.value, ptr %typ.typecode, ptr undef)
|
||||
ret i1 %result
|
||||
}
|
||||
|
||||
declare i1 @"reflect.Type.Implements$invoke"(ptr, ptr, ptr, ptr, ptr) #0
|
||||
|
||||
declare i1 @"interface:{Error:func:{}{basic:string}}.$typeassert"(ptr) #1
|
||||
|
||||
attributes #0 = { "tinygo-invoke"="reflect/methods.Implements(reflect.Type) bool" "tinygo-methods"="reflect/methods.Align() int; reflect/methods.Implements(reflect.Type) bool" }
|
||||
attributes #1 = { "tinygo-methods"="reflect/methods.Error() string" }
|
||||
Reference in New Issue
Block a user