compiler: implement method-set based AssignableTo and Implements (#5304)

* reflect: implement method-set based AssignableTo and Implements

Based on the design from #4376 by aykevl.
Fixes #4277, fixes #3580.

Co-authored-by: Ayke van Laethem <aykevanlaethem@gmail.com>

* builder: update expected binary sizes for reflect changes

* Make interface checks similar to invoke, allowing typeImplementsMethodSet and method info to be dropped when reflect is not present

* Add more tests that BigGo reflect tests

* Even more pruning

* Add go/token and net/url to passing tests

* Prune even further, I am less happy with this, though

* Update size test now that we are smaller

* Skip some tests

* elide method lists

* format, oops

* fix tests

* Add a panic, pull out constant to keep in sync

* Add debug info

* Remove code that was leftover from a previous refactor

---------

Co-authored-by: Ayke van Laethem <aykevanlaethem@gmail.com>
This commit is contained in:
Jake Bailey
2026-04-17 12:57:03 -07:00
committed by GitHub
parent b792c10680
commit 5ba8766cbc
16 changed files with 853 additions and 438 deletions
-29
View File
@@ -7,7 +7,6 @@ target triple = "armv7m-none-eabi"
@"reflect/types.typeid:basic:int16" = external constant i8
@"reflect/types.type:basic:int" = linkonce_odr constant { i8, ptr } { i8 2, ptr @"reflect/types.type:pointer:basic:int" }, align 4
@"reflect/types.type:pointer:basic:int" = linkonce_odr constant { i8, ptr } { i8 21, ptr @"reflect/types.type:basic:int" }, align 4
@"reflect/methods.NeverImplementedMethod()" = linkonce_odr constant i8 0
@"reflect/methods.Double() int" = linkonce_odr constant i8 0
@"Number$methodset" = linkonce_odr unnamed_addr constant { i32, [1 x ptr], { ptr } } { i32 1, [1 x ptr] [ptr @"reflect/methods.Double() int"], { ptr } { ptr @"(Number).Double$invoke" } }
@"reflect/types.type:named:Number" = linkonce_odr constant { ptr, i8, ptr, ptr } { ptr @"Number$methodset", i8 34, ptr @"reflect/types.type:pointer:named:Number", ptr @"reflect/types.type:basic:int" }, align 4
@@ -16,10 +15,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 +26,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 +63,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()" }
+2 -65
View File
@@ -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
View File
@@ -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
View File
@@ -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" }