From 19e2d125179e0b4797ea0592ce346d6619e0406d Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 29 Jul 2024 14:16:10 +0200 Subject: [PATCH 1/2] 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). --- compiler/interface.go | 117 +++++++--- compiler/testdata/gc.ll | 2 +- compiler/testdata/interface.ll | 62 ++--- interp/interpreter.go | 58 ----- src/reflect/runtime.go | 83 +++++++ src/reflect/set_test.go | 229 +++++++++++++++++++ src/reflect/type.go | 27 ++- src/runtime/interface.go | 3 + testdata/interface.go | 3 + testdata/interface.txt | 1 + transform/interface-lowering.go | 67 ------ transform/optimizer.go | 1 - transform/rtcalls.go | 78 ------- transform/rtcalls_test.go | 8 - transform/testdata/interface.ll | 28 --- transform/testdata/interface.out.ll | 67 +----- transform/testdata/reflect-implements.ll | 41 ---- transform/testdata/reflect-implements.out.ll | 28 --- 18 files changed, 467 insertions(+), 436 deletions(-) create mode 100644 src/reflect/runtime.go create mode 100644 src/reflect/set_test.go delete mode 100644 transform/testdata/reflect-implements.ll delete mode 100644 transform/testdata/reflect-implements.out.ll diff --git a/compiler/interface.go b/compiler/interface.go index fc698c7a97..603510ce44 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -10,6 +10,7 @@ import ( "fmt" "go/token" "go/types" + "sort" "strconv" "strings" @@ -180,6 +181,15 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { typeFieldTypes := []*types.Var{ types.NewVar(token.NoPos, nil, "kind", types.Typ[types.Int8]), } + var methods []*types.Func + for i := 0; i < ms.Len(); i++ { + methods = append(methods, ms.At(i).Obj().(*types.Func)) + } + methodSetType := types.NewStruct([]*types.Var{ + types.NewVar(token.NoPos, nil, "length", types.Typ[types.Uintptr]), + types.NewVar(token.NoPos, nil, "methods", types.NewArray(types.Typ[types.UnsafePointer], int64(len(methods)))), + }, nil) + methodSetValue := c.getMethodSetValue(methods) switch typ := typ.(type) { case *types.Basic: typeFieldTypes = append(typeFieldTypes, @@ -196,6 +206,7 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { types.NewVar(token.NoPos, nil, "ptrTo", types.Typ[types.UnsafePointer]), types.NewVar(token.NoPos, nil, "underlying", types.Typ[types.UnsafePointer]), types.NewVar(token.NoPos, nil, "pkgpath", types.Typ[types.UnsafePointer]), + types.NewVar(token.NoPos, nil, "methods", methodSetType), types.NewVar(token.NoPos, nil, "name", types.NewArray(types.Typ[types.Int8], int64(len(pkgname)+1+len(name)+1))), ) case *types.Chan: @@ -214,6 +225,7 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { typeFieldTypes = append(typeFieldTypes, types.NewVar(token.NoPos, nil, "numMethods", types.Typ[types.Uint16]), types.NewVar(token.NoPos, nil, "elementType", types.Typ[types.UnsafePointer]), + types.NewVar(token.NoPos, nil, "methods", methodSetType), ) case *types.Array: typeFieldTypes = append(typeFieldTypes, @@ -238,12 +250,13 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { types.NewVar(token.NoPos, nil, "size", types.Typ[types.Uint32]), types.NewVar(token.NoPos, nil, "numFields", types.Typ[types.Uint16]), types.NewVar(token.NoPos, nil, "fields", types.NewArray(c.getRuntimeType("structField"), int64(typ.NumFields()))), + types.NewVar(token.NoPos, nil, "methods", methodSetType), ) case *types.Interface: typeFieldTypes = append(typeFieldTypes, types.NewVar(token.NoPos, nil, "ptrTo", types.Typ[types.UnsafePointer]), + types.NewVar(token.NoPos, nil, "methods", methodSetType), ) - // TODO: methods case *types.Signature: typeFieldTypes = append(typeFieldTypes, types.NewVar(token.NoPos, nil, "ptrTo", types.Typ[types.UnsafePointer]), @@ -294,6 +307,7 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { c.getTypeCode(types.NewPointer(typ)), // ptrTo c.getTypeCode(typ.Underlying()), // underlying pkgPathPtr, // pkgpath pointer + methodSetValue, // methods c.ctx.ConstString(pkgname+"."+name+"\x00", false), // name } metabyte |= 1 << 5 // "named" flag @@ -323,6 +337,7 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { typeFields = []llvm.Value{ llvm.ConstInt(c.ctx.Int16Type(), uint64(numMethods), false), // numMethods c.getTypeCode(typ.Elem()), + methodSetValue, // methods } case *types.Array: typeFields = []llvm.Value{ @@ -404,9 +419,12 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { })) } typeFields = append(typeFields, llvm.ConstArray(structFieldType, fields)) + typeFields = append(typeFields, methodSetValue) case *types.Interface: - typeFields = []llvm.Value{c.getTypeCode(types.NewPointer(typ))} - // TODO: methods + typeFields = []llvm.Value{ + c.getTypeCode(types.NewPointer(typ)), + methodSetValue, + } case *types.Signature: typeFields = []llvm.Value{c.getTypeCode(types.NewPointer(typ))} // TODO: params, return values, etc @@ -685,7 +703,7 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value { assertedType := b.getLLVMType(expr.AssertedType) actualTypeNum := b.CreateExtractValue(itf, 0, "interface.type") - commaOk := llvm.Value{} + var commaOk llvm.Value if intf, ok := expr.AssertedType.Underlying().(*types.Interface); ok { if intf.Empty() { @@ -693,17 +711,16 @@ func (b *builder) createTypeAssert(expr *ssa.TypeAssert) llvm.Value { // This type assertion always succeeds, so we can just set commaOk to true. commaOk = llvm.ConstInt(b.ctx.Int1Type(), 1, true) } else { - // Type assert on interface type with methods. - // This is a call to an interface type assert function. - // The interface lowering pass will define this function by filling it - // with a type switch over all concrete types that implement this - // interface, and returning whether it's one of the matched types. - // This is very different from how interface asserts are implemented in - // the main Go compiler, where the runtime checks whether the type - // implements each method of the interface. See: + // Type assert using interface type with methods. + // This is implemented using a runtime call, which checks that the + // type implements each method of the interface. + // For comparison, here is how the Go compiler does this (which is + // very similar): // https://research.swtch.com/interfaces - fn := b.getInterfaceImplementsFunc(expr.AssertedType) - commaOk = b.CreateCall(fn.GlobalValueType(), fn, []llvm.Value{actualTypeNum}, "") + commaOk = b.createRuntimeCall("typeImplementsMethodSet", []llvm.Value{ + actualTypeNum, + b.getInterfaceMethodSet(intf), + }, "") } } else { name, _ := getTypeCodeName(expr.AssertedType) @@ -780,20 +797,66 @@ func (c *compilerContext) getMethodsString(itf *types.Interface) string { return strings.Join(methods, "; ") } -// getInterfaceImplementsFunc returns a declared function that works as a type -// switch. The interface lowering pass will define this function. -func (c *compilerContext) getInterfaceImplementsFunc(assertedType types.Type) llvm.Value { - s, _ := getTypeCodeName(assertedType.Underlying()) - fnName := s + ".$typeassert" - llvmFn := c.mod.NamedFunction(fnName) - if llvmFn.IsNil() { - llvmFnType := llvm.FunctionType(c.ctx.Int1Type(), []llvm.Type{c.dataPtrType}, false) - llvmFn = llvm.AddFunction(c.mod, fnName, llvmFnType) - c.addStandardDeclaredAttributes(llvmFn) - methods := c.getMethodsString(assertedType.Underlying().(*types.Interface)) - llvmFn.AddFunctionAttr(c.ctx.CreateStringAttribute("tinygo-methods", methods)) +// Get the global that contains an interface method set, creating it if needed. +func (c *compilerContext) getInterfaceMethodSet(t *types.Interface) llvm.Value { + s, _ := getTypeCodeName(t) + methodSetName := s + "$itfmethods" + methodSet := c.mod.NamedFunction(methodSetName) + if methodSet.IsNil() { + var methods []*types.Func + for i := 0; i < t.NumMethods(); i++ { + methods = append(methods, t.Method(i)) + } + if len(methods) == 0 { + // This *should* be unreachable: the caller checks whether the + // interface is empty before creating a method set. + panic("unreachable") + } + + methodSetValue := c.getMethodSetValue(methods) + methodSet = llvm.AddGlobal(c.mod, methodSetValue.Type(), methodSetName) + methodSet.SetInitializer(methodSetValue) + methodSet.SetGlobalConstant(true) + methodSet.SetLinkage(llvm.LinkOnceODRLinkage) + methodSet.SetAlignment(c.targetData.ABITypeAlignment(methodSetValue.Type())) + methodSet.SetUnnamedAddr(true) } - return llvmFn + + return methodSet +} + +// Get the method set value that is used in a number of type structs. +func (c *compilerContext) getMethodSetValue(methods []*types.Func) llvm.Value { + // Create a sorted list of method names. + var methodRefNames []string + for _, method := range methods { + name := method.Name() + if !token.IsExported(name) { + name = method.Pkg().Path() + "." + name + } + s, _ := getTypeCodeName(method.Type()) + methodRefNames = append(methodRefNames, "reflect/types.signature:"+name+":"+s) + } + sort.Strings(methodRefNames) + + // Turn this slice of strings in a slice of global variables. + var methodRefValues []llvm.Value + for _, name := range methodRefNames { + value := c.mod.NamedGlobal(name) + if value.IsNil() { + value = llvm.AddGlobal(c.mod, c.ctx.Int8Type(), name) + value.SetInitializer(llvm.ConstNull(c.ctx.Int8Type())) + value.SetGlobalConstant(true) + value.SetLinkage(llvm.LinkOnceODRLinkage) + value.SetAlignment(1) + } + methodRefValues = append(methodRefValues, value) + } + + return c.ctx.ConstStruct([]llvm.Value{ + llvm.ConstInt(c.uintptrType, uint64(len(methodRefValues)), false), + llvm.ConstArray(c.dataPtrType, methodRefValues), + }, false) } // getInvokeFunction returns the thunk to call the given interface method. The diff --git a/compiler/testdata/gc.ll b/compiler/testdata/gc.ll index 82260fbf45..fb615f35e0 100644 --- a/compiler/testdata/gc.ll +++ b/compiler/testdata/gc.ll @@ -22,7 +22,7 @@ target triple = "wasm32-unknown-wasi" @"runtime/gc.layout:62-2000000000000001" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00 " } @"runtime/gc.layout:62-0001" = linkonce_odr unnamed_addr constant { i32, [8 x i8] } { i32 62, [8 x i8] c"\01\00\00\00\00\00\00\00" } @"reflect/types.type:basic:complex128" = linkonce_odr constant { i8, ptr } { i8 80, ptr @"reflect/types.type:pointer:basic:complex128" }, align 4 -@"reflect/types.type:pointer:basic:complex128" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:basic:complex128" }, align 4 +@"reflect/types.type:pointer:basic:complex128" = linkonce_odr constant { i8, i16, ptr, { i32, [0 x ptr] } } { i8 -43, i16 0, ptr @"reflect/types.type:basic:complex128", { i32, [0 x ptr] } zeroinitializer }, align 4 ; Function Attrs: allockind("alloc,zeroed") allocsize(0) declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #0 diff --git a/compiler/testdata/interface.ll b/compiler/testdata/interface.ll index 801f370d58..9bc07d88c8 100644 --- a/compiler/testdata/interface.ll +++ b/compiler/testdata/interface.ll @@ -7,15 +7,19 @@ target triple = "wasm32-unknown-wasi" %runtime._string = type { ptr, i32 } @"reflect/types.type:basic:int" = linkonce_odr constant { i8, ptr } { i8 -62, ptr @"reflect/types.type:pointer:basic:int" }, align 4 -@"reflect/types.type:pointer:basic:int" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:basic:int" }, align 4 -@"reflect/types.type:pointer:named:error" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:named:error" }, align 4 -@"reflect/types.type:named:error" = linkonce_odr constant { i8, i16, ptr, ptr, ptr, [7 x i8] } { i8 116, i16 1, ptr @"reflect/types.type:pointer:named:error", ptr @"reflect/types.type:interface:{Error:func:{}{basic:string}}", ptr @"reflect/types.type.pkgpath.empty", [7 x i8] c".error\00" }, align 4 +@"reflect/types.type:pointer:basic:int" = linkonce_odr constant { i8, i16, ptr, { i32, [0 x ptr] } } { i8 -43, i16 0, ptr @"reflect/types.type:basic:int", { i32, [0 x ptr] } zeroinitializer }, align 4 +@"reflect/types.type:pointer:named:error" = linkonce_odr constant { i8, i16, ptr, { i32, [0 x ptr] } } { i8 -43, i16 0, ptr @"reflect/types.type:named:error", { i32, [0 x ptr] } zeroinitializer }, align 4 +@"reflect/types.signature:Error:func:{}{basic:string}" = linkonce_odr constant i8 0, align 1 +@"reflect/types.type:named:error" = linkonce_odr constant { i8, i16, ptr, ptr, ptr, { i32, [1 x ptr] }, [7 x i8] } { i8 116, i16 1, ptr @"reflect/types.type:pointer:named:error", ptr @"reflect/types.type:interface:{Error:func:{}{basic:string}}", ptr @"reflect/types.type.pkgpath.empty", { i32, [1 x ptr] } { i32 1, [1 x ptr] [ptr @"reflect/types.signature:Error:func:{}{basic:string}"] }, [7 x i8] c".error\00" }, align 4 @"reflect/types.type.pkgpath.empty" = linkonce_odr unnamed_addr constant [1 x i8] zeroinitializer, align 1 -@"reflect/types.type:interface:{Error:func:{}{basic:string}}" = linkonce_odr constant { i8, ptr } { i8 84, ptr @"reflect/types.type:pointer:interface:{Error:func:{}{basic:string}}" }, align 4 -@"reflect/types.type:pointer:interface:{Error:func:{}{basic:string}}" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:interface:{Error:func:{}{basic:string}}" }, align 4 -@"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:interface:{String:func:{}{basic:string}}" }, align 4 -@"reflect/types.type:interface:{String:func:{}{basic:string}}" = linkonce_odr constant { i8, ptr } { i8 84, ptr @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}" }, align 4 +@"reflect/types.type:interface:{Error:func:{}{basic:string}}" = linkonce_odr constant { i8, ptr, { i32, [1 x ptr] } } { i8 84, ptr @"reflect/types.type:pointer:interface:{Error:func:{}{basic:string}}", { i32, [1 x ptr] } { i32 1, [1 x ptr] [ptr @"reflect/types.signature:Error:func:{}{basic:string}"] } }, align 4 +@"reflect/types.type:pointer:interface:{Error:func:{}{basic:string}}" = linkonce_odr constant { i8, i16, ptr, { i32, [0 x ptr] } } { i8 -43, i16 0, ptr @"reflect/types.type:interface:{Error:func:{}{basic:string}}", { i32, [0 x ptr] } zeroinitializer }, align 4 +@"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}" = linkonce_odr constant { i8, i16, ptr, { i32, [0 x ptr] } } { i8 -43, i16 0, ptr @"reflect/types.type:interface:{String:func:{}{basic:string}}", { i32, [0 x ptr] } zeroinitializer }, align 4 +@"reflect/types.signature:String:func:{}{basic:string}" = linkonce_odr constant i8 0, align 1 +@"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 @"reflect/types.typeid:basic:int" = external constant i8 +@"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 +@"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 ; Function Attrs: allockind("alloc,zeroed") allocsize(0) declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #0 @@ -32,8 +36,8 @@ entry: define hidden %runtime._interface @main.simpleType(ptr %context) unnamed_addr #2 { entry: %stackalloc = alloca i8, align 1 - call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:basic:int", ptr nonnull %stackalloc, ptr undef) #7 - call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:basic:int", ptr nonnull %stackalloc, ptr undef) #5 + call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #5 ret %runtime._interface { ptr @"reflect/types.type:basic:int", ptr null } } @@ -41,8 +45,8 @@ entry: define hidden %runtime._interface @main.pointerType(ptr %context) unnamed_addr #2 { entry: %stackalloc = alloca i8, align 1 - call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:basic:int", ptr nonnull %stackalloc, ptr undef) #7 - call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:basic:int", ptr nonnull %stackalloc, ptr undef) #5 + call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #5 ret %runtime._interface { ptr @"reflect/types.type:pointer:basic:int", ptr null } } @@ -50,8 +54,8 @@ entry: define hidden %runtime._interface @main.interfaceType(ptr %context) unnamed_addr #2 { entry: %stackalloc = alloca i8, align 1 - call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:named:error", ptr nonnull %stackalloc, ptr undef) #7 - call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:named:error", ptr nonnull %stackalloc, ptr undef) #5 + call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #5 ret %runtime._interface { ptr @"reflect/types.type:pointer:named:error", ptr null } } @@ -59,15 +63,15 @@ entry: define hidden %runtime._interface @main.anonymousInterfaceType(ptr %context) unnamed_addr #2 { entry: %stackalloc = alloca i8, align 1 - call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}", ptr nonnull %stackalloc, ptr undef) #7 - call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}", ptr nonnull %stackalloc, ptr undef) #5 + call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #5 ret %runtime._interface { ptr @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}", ptr null } } ; Function Attrs: nounwind define hidden i1 @main.isInt(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 { entry: - %typecode = call i1 @runtime.typeAssert(ptr %itf.typecode, ptr nonnull @"reflect/types.typeid:basic:int", ptr undef) #7 + %typecode = call i1 @runtime.typeAssert(ptr %itf.typecode, ptr nonnull @"reflect/types.typeid:basic:int", ptr undef) #5 br i1 %typecode, label %typeassert.ok, label %typeassert.next typeassert.next: ; preds = %typeassert.ok, %entry @@ -82,7 +86,7 @@ declare i1 @runtime.typeAssert(ptr, ptr dereferenceable_or_null(1), ptr) #1 ; Function Attrs: nounwind define hidden i1 @main.isError(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 { entry: - %0 = call i1 @"interface:{Error:func:{}{basic:string}}.$typeassert"(ptr %itf.typecode) #7 + %0 = call i1 @runtime.typeImplementsMethodSet(ptr %itf.typecode, ptr nonnull @"interface:{Error:func:{}{basic:string}}$itfmethods", ptr undef) #5 br i1 %0, label %typeassert.ok, label %typeassert.next typeassert.next: ; preds = %typeassert.ok, %entry @@ -92,12 +96,12 @@ typeassert.ok: ; preds = %entry br label %typeassert.next } -declare i1 @"interface:{Error:func:{}{basic:string}}.$typeassert"(ptr) #3 +declare i1 @runtime.typeImplementsMethodSet(ptr, ptr, ptr) #1 ; Function Attrs: nounwind define hidden i1 @main.isStringer(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 { entry: - %0 = call i1 @"interface:{String:func:{}{basic:string}}.$typeassert"(ptr %itf.typecode) #7 + %0 = call i1 @runtime.typeImplementsMethodSet(ptr %itf.typecode, ptr nonnull @"interface:{String:func:{}{basic:string}}$itfmethods", ptr undef) #5 br i1 %0, label %typeassert.ok, label %typeassert.next typeassert.next: ; preds = %typeassert.ok, %entry @@ -107,34 +111,30 @@ typeassert.ok: ; preds = %entry br label %typeassert.next } -declare i1 @"interface:{String:func:{}{basic:string}}.$typeassert"(ptr) #4 - ; Function Attrs: nounwind define hidden i8 @main.callFooMethod(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 { entry: - %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 + %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 ret i8 %0 } -declare i8 @"interface:{String:func:{}{basic:string},main.foo:func:{basic:int}{basic:uint8}}.foo$invoke"(ptr, i32, ptr, ptr) #5 +declare i8 @"interface:{String:func:{}{basic:string},main.foo:func:{basic:int}{basic:uint8}}.foo$invoke"(ptr, i32, ptr, ptr) #3 ; Function Attrs: nounwind define hidden %runtime._string @main.callErrorMethod(ptr %itf.typecode, ptr %itf.value, ptr %context) unnamed_addr #2 { entry: %stackalloc = alloca i8, align 1 - %0 = call %runtime._string @"interface:{Error:func:{}{basic:string}}.Error$invoke"(ptr %itf.value, ptr %itf.typecode, ptr undef) #7 + %0 = call %runtime._string @"interface:{Error:func:{}{basic:string}}.Error$invoke"(ptr %itf.value, ptr %itf.typecode, ptr undef) #5 %1 = extractvalue %runtime._string %0, 0 - call void @runtime.trackPointer(ptr %1, ptr nonnull %stackalloc, ptr undef) #7 + call void @runtime.trackPointer(ptr %1, ptr nonnull %stackalloc, ptr undef) #5 ret %runtime._string %0 } -declare %runtime._string @"interface:{Error:func:{}{basic:string}}.Error$invoke"(ptr, ptr, ptr) #6 +declare %runtime._string @"interface:{Error:func:{}{basic:string}}.Error$invoke"(ptr, ptr, ptr) #4 attributes #0 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" } attributes #1 = { "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" } attributes #2 = { nounwind "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" } -attributes #3 = { "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" "tinygo-methods"="reflect/methods.Error() string" } -attributes #4 = { "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" "tinygo-methods"="reflect/methods.String() string" } -attributes #5 = { "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" "tinygo-invoke"="main.$methods.foo(int) uint8" "tinygo-methods"="reflect/methods.String() string; main.$methods.foo(int) uint8" } -attributes #6 = { "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" "tinygo-invoke"="reflect/methods.Error() string" "tinygo-methods"="reflect/methods.Error() string" } -attributes #7 = { nounwind } +attributes #3 = { "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" "tinygo-invoke"="main.$methods.foo(int) uint8" "tinygo-methods"="reflect/methods.String() string; main.$methods.foo(int) uint8" } +attributes #4 = { "target-features"="+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext" "tinygo-invoke"="reflect/methods.Error() string" "tinygo-methods"="reflect/methods.Error() string" } +attributes #5 = { nounwind } diff --git a/interp/interpreter.go b/interp/interpreter.go index b35129b814..e5f85ae73c 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -421,64 +421,6 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // Special function that will trigger an error. // This is used to test error reporting. return nil, mem, r.errorAt(inst, errors.New("test error")) - case strings.HasSuffix(callFn.name, ".$typeassert"): - if r.debug { - fmt.Fprintln(os.Stderr, indent+"interface assert:", operands[1:]) - } - - // Load various values for the interface implements check below. - typecodePtr, err := operands[1].asPointer(r) - if err != nil { - return nil, mem, r.errorAt(inst, err) - } - // typecodePtr always point to the numMethod field in the type - // description struct. The methodSet, when present, comes right - // before the numMethod field (the compiler doesn't generate - // method sets for concrete types without methods). - // Considering that the compiler doesn't emit interface type - // asserts for interfaces with no methods (as the always succeed) - // then if the offset is zero, this assert must always fail. - if typecodePtr.offset() == 0 { - locals[inst.localIndex] = literalValue{uint8(0)} - break - } - typecodePtrOffset, err := typecodePtr.addOffset(-int64(r.pointerSize)) - if err != nil { - return nil, mem, r.errorAt(inst, err) - } - methodSetPtr, err := mem.load(typecodePtrOffset, r.pointerSize).asPointer(r) - if err != nil { - return nil, mem, r.errorAt(inst, err) - } - methodSet := mem.get(methodSetPtr.index()).llvmGlobal.Initializer() - numMethods := int(r.builder.CreateExtractValue(methodSet, 0, "").ZExtValue()) - llvmFn := inst.llvmInst.CalledValue() - methodSetAttr := llvmFn.GetStringAttributeAtIndex(-1, "tinygo-methods") - methodSetString := methodSetAttr.GetStringValue() - - // Make a set of all the methods on the concrete type, for - // easier checking in the next step. - concreteTypeMethods := map[string]struct{}{} - for i := 0; i < numMethods; i++ { - methodInfo := r.builder.CreateExtractValue(methodSet, 1, "") - name := r.builder.CreateExtractValue(methodInfo, i, "").Name() - concreteTypeMethods[name] = struct{}{} - } - - // Check whether all interface methods are also in the list - // of defined methods calculated above. This is the interface - // assert itself. - assertOk := uint8(1) // i1 true - for _, name := range strings.Split(methodSetString, "; ") { - if _, ok := concreteTypeMethods[name]; !ok { - // There is a method on the interface that is not - // implemented by the type. The assertion will fail. - assertOk = 0 // i1 false - break - } - } - // If assertOk is still 1, the assertion succeeded. - locals[inst.localIndex] = literalValue{assertOk} case strings.HasSuffix(callFn.name, "$invoke"): // This thunk is the interface method dispatcher: it is called // with all regular parameters and a type code. It will then diff --git a/src/reflect/runtime.go b/src/reflect/runtime.go new file mode 100644 index 0000000000..3835025ae6 --- /dev/null +++ b/src/reflect/runtime.go @@ -0,0 +1,83 @@ +package reflect + +// This file defines functions that logically belong in the runtime but that +// depend on the internals of the reflect package and are therefore better +// implemented directly in the reflect package. + +import ( + "unsafe" +) + +// Return whether the concrete type implements the asserted method set. +// This function is called both from the compiler and from the reflect package. +// +//go:linkname typeImplementsMethodSet runtime.typeImplementsMethodSet +func typeImplementsMethodSet(concreteType, assertedMethodSet unsafe.Pointer) bool { + if concreteType == nil { + // This can happen when doing a type assert on a nil interface value. + // According to the Go language specification, a type assert on a nil + // interface always fails. + return false + } + + const ptrSize = unsafe.Sizeof((*byte)(nil)) + itfNumMethod := *(*uintptr)(assertedMethodSet) + if itfNumMethod == 0 { + // While the compiler shouldn't emit such type asserts, the reflect + // package may do so. + // This is actually needed for correctness: for example a value of type + // int may be assigned to a variable of type interface{}. + return true + } + + // Pull the method set out of the concrete type. + var methods *methodSet + metaByte := *(*uint8)(concreteType) + if metaByte&flagNamed != 0 { + concreteType := (*namedType)(concreteType) + methods = &concreteType.methods + } else if metaByte&kindMask == uint8(Struct) { + concreteType := (*structType)(concreteType) + methods = &concreteType.methods + } else if metaByte&kindMask == uint8(Pointer) { + concreteType := (*ptrType)(concreteType) + methods = &concreteType.methods + } else { + // Other types don't have a method set. + return false + } + concreteTypePtr := unsafe.Pointer(&methods.methods) + concreteTypeEnd := unsafe.Add(concreteTypePtr, uintptr(methods.length)*ptrSize) + + // Iterate over each method in the interface method set, and check whether + // the method exists in the method set of the concrete type. + // Both method sets are sorted in the same way, so we can use a simple loop + // to check for a match. + assertedTypePtr := unsafe.Add(assertedMethodSet, ptrSize) + assertedTypeEnd := unsafe.Add(assertedTypePtr, itfNumMethod*ptrSize) + for assertedTypePtr != assertedTypeEnd { + assertedMethod := *(*unsafe.Pointer)(assertedTypePtr) + + // Search for the same method in the concrete type. + for { + if concreteTypePtr == concreteTypeEnd { + // Reached the end of the concrete type method set. + // The method wasn't found, so the type assert failed. + return false + } + concreteMethod := *(*unsafe.Pointer)(concreteTypePtr) + concreteTypePtr = unsafe.Add(concreteTypePtr, ptrSize) + if concreteMethod == assertedMethod { + // Found the method in the concrete type. Continue with the + // next in the interface method set. + break + } + } + + // The method was found. Continue with the next. + assertedTypePtr = unsafe.Add(assertedTypePtr, ptrSize) + } + + // All methods in the interface were found. + return true +} diff --git a/src/reflect/set_test.go b/src/reflect/set_test.go new file mode 100644 index 0000000000..9517ae8f68 --- /dev/null +++ b/src/reflect/set_test.go @@ -0,0 +1,229 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package reflect_test + +import ( + "bytes" + "go/ast" + "go/token" + "io" + . "reflect" + "testing" +) + +//func TestImplicitMapConversion(t *testing.T) { +// // Test implicit conversions in MapIndex and SetMapIndex. +// { +// // direct +// m := make(map[int]int) +// mv := ValueOf(m) +// mv.SetMapIndex(ValueOf(1), ValueOf(2)) +// x, ok := m[1] +// if x != 2 { +// t.Errorf("#1 after SetMapIndex(1,2): %d, %t (map=%v)", x, ok, m) +// } +// if n := mv.MapIndex(ValueOf(1)).Interface().(int); n != 2 { +// t.Errorf("#1 MapIndex(1) = %d", n) +// } +// } +// { +// // convert interface key +// m := make(map[any]int) +// mv := ValueOf(m) +// mv.SetMapIndex(ValueOf(1), ValueOf(2)) +// x, ok := m[1] +// if x != 2 { +// t.Errorf("#2 after SetMapIndex(1,2): %d, %t (map=%v)", x, ok, m) +// } +// if n := mv.MapIndex(ValueOf(1)).Interface().(int); n != 2 { +// t.Errorf("#2 MapIndex(1) = %d", n) +// } +// } +// { +// // convert interface value +// m := make(map[int]any) +// mv := ValueOf(m) +// mv.SetMapIndex(ValueOf(1), ValueOf(2)) +// x, ok := m[1] +// if x != 2 { +// t.Errorf("#3 after SetMapIndex(1,2): %d, %t (map=%v)", x, ok, m) +// } +// if n := mv.MapIndex(ValueOf(1)).Interface().(int); n != 2 { +// t.Errorf("#3 MapIndex(1) = %d", n) +// } +// } +// { +// // convert both interface key and interface value +// m := make(map[any]any) +// mv := ValueOf(m) +// mv.SetMapIndex(ValueOf(1), ValueOf(2)) +// x, ok := m[1] +// if x != 2 { +// t.Errorf("#4 after SetMapIndex(1,2): %d, %t (map=%v)", x, ok, m) +// } +// if n := mv.MapIndex(ValueOf(1)).Interface().(int); n != 2 { +// t.Errorf("#4 MapIndex(1) = %d", n) +// } +// } +// { +// // convert both, with non-empty interfaces +// m := make(map[io.Reader]io.Writer) +// mv := ValueOf(m) +// b1 := new(bytes.Buffer) +// b2 := new(bytes.Buffer) +// mv.SetMapIndex(ValueOf(b1), ValueOf(b2)) +// x, ok := m[b1] +// if x != b2 { +// t.Errorf("#5 after SetMapIndex(b1, b2): %p (!= %p), %t (map=%v)", x, b2, ok, m) +// } +// if p := mv.MapIndex(ValueOf(b1)).Elem().UnsafePointer(); p != unsafe.Pointer(b2) { +// t.Errorf("#5 MapIndex(b1) = %#x want %p", p, b2) +// } +// } +// { +// // convert channel direction +// m := make(map[<-chan int]chan int) +// mv := ValueOf(m) +// c1 := make(chan int) +// c2 := make(chan int) +// mv.SetMapIndex(ValueOf(c1), ValueOf(c2)) +// x, ok := m[c1] +// if x != c2 { +// t.Errorf("#6 after SetMapIndex(c1, c2): %p (!= %p), %t (map=%v)", x, c2, ok, m) +// } +// if p := mv.MapIndex(ValueOf(c1)).UnsafePointer(); p != ValueOf(c2).UnsafePointer() { +// t.Errorf("#6 MapIndex(c1) = %#x want %p", p, c2) +// } +// } +// { +// // convert identical underlying types +// type MyBuffer bytes.Buffer +// m := make(map[*MyBuffer]*bytes.Buffer) +// mv := ValueOf(m) +// b1 := new(MyBuffer) +// b2 := new(bytes.Buffer) +// mv.SetMapIndex(ValueOf(b1), ValueOf(b2)) +// x, ok := m[b1] +// if x != b2 { +// t.Errorf("#7 after SetMapIndex(b1, b2): %p (!= %p), %t (map=%v)", x, b2, ok, m) +// } +// if p := mv.MapIndex(ValueOf(b1)).UnsafePointer(); p != unsafe.Pointer(b2) { +// t.Errorf("#7 MapIndex(b1) = %#x want %p", p, b2) +// } +// } +// +//} +// +//func TestImplicitSetConversion(t *testing.T) { +// // Assume TestImplicitMapConversion covered the basics. +// // Just make sure conversions are being applied at all. +// var r io.Reader +// b := new(bytes.Buffer) +// rv := ValueOf(&r).Elem() +// rv.Set(ValueOf(b)) +// if r != b { +// t.Errorf("after Set: r=%T(%v)", r, r) +// } +//} +// +//func TestImplicitSendConversion(t *testing.T) { +// c := make(chan io.Reader, 10) +// b := new(bytes.Buffer) +// ValueOf(c).Send(ValueOf(b)) +// if bb := <-c; bb != b { +// t.Errorf("Received %p != %p", bb, b) +// } +//} +// +//func TestImplicitCallConversion(t *testing.T) { +// // Arguments must be assignable to parameter types. +// fv := ValueOf(io.WriteString) +// b := new(strings.Builder) +// fv.Call([]Value{ValueOf(b), ValueOf("hello world")}) +// if b.String() != "hello world" { +// t.Errorf("After call: string=%q want %q", b.String(), "hello world") +// } +//} +// +//func TestImplicitAppendConversion(t *testing.T) { +// // Arguments must be assignable to the slice's element type. +// s := []io.Reader{} +// sv := ValueOf(&s).Elem() +// b := new(bytes.Buffer) +// sv.Set(Append(sv, ValueOf(b))) +// if len(s) != 1 || s[0] != b { +// t.Errorf("after append: s=%v want [%p]", s, b) +// } +//} + +var implementsTests = []struct { + x any + t any + b bool +}{ + {new(*bytes.Buffer), new(io.Reader), true}, + {new(bytes.Buffer), new(io.Reader), false}, + {new(*bytes.Buffer), new(io.ReaderAt), false}, + {new(*ast.Ident), new(ast.Expr), true}, + {new(*notAnExpr), new(ast.Expr), false}, + {new(*ast.Ident), new(notASTExpr), false}, + {new(notASTExpr), new(ast.Expr), false}, + {new(ast.Expr), new(notASTExpr), false}, + {new(*notAnExpr), new(notASTExpr), true}, +} + +type notAnExpr struct{} + +func (notAnExpr) Pos() token.Pos { return token.NoPos } +func (notAnExpr) End() token.Pos { return token.NoPos } +func (notAnExpr) exprNode() {} + +type notASTExpr interface { + Pos() token.Pos + End() token.Pos + exprNode() +} + +func TestImplements(t *testing.T) { + for _, tt := range implementsTests { + xv := TypeOf(tt.x).Elem() + xt := TypeOf(tt.t).Elem() + if b := xv.Implements(xt); b != tt.b { + t.Errorf("(%s).Implements(%s) = %v, want %v", xv.String(), xt.String(), b, tt.b) + } + } +} + +// TODO: AssignableTo should be possible to implement now that Implements() +// mostly works. + +//var assignableTests = []struct { +// x any +// t any +// b bool +//}{ +// {new(chan int), new(<-chan int), true}, +// {new(<-chan int), new(chan int), false}, +// {new(*int), new(IntPtr), true}, +// {new(IntPtr), new(*int), true}, +// {new(IntPtr), new(IntPtr1), false}, +// {new(Ch), new(<-chan any), true}, +// // test runs implementsTests too +//} +// +//type IntPtr *int +//type IntPtr1 *int +//type Ch <-chan any +// +//func TestAssignableTo(t *testing.T) { +// for _, tt := range append(assignableTests, implementsTests...) { +// xv := TypeOf(tt.x).Elem() +// xt := TypeOf(tt.t).Elem() +// if b := xv.AssignableTo(xt); b != tt.b { +// t.Errorf("(%s).AssignableTo(%s) = %v, want %v", xv.String(), xt.String(), b, tt.b) +// } +// } +//} +// diff --git a/src/reflect/type.go b/src/reflect/type.go index 1356f67cdd..2ce030af6d 100644 --- a/src/reflect/type.go +++ b/src/reflect/type.go @@ -23,6 +23,7 @@ // meta uint8 // nmethods uint16 // elementType *typeStruct +// methods methodSet // - array types (see arrayType) // meta uint8 // nmethods uint16 (0) @@ -44,7 +45,8 @@ // pkgpath *byte // package path; null terminated // numField uint16 // fields [...]structField // the remaining fields are all of type structField -// - interface types (this is missing the interface methods): +// methods methodSet +// - interface types: // meta uint8 // ptrTo *typeStruct // - signature types (this is missing input and output parameters): @@ -56,6 +58,7 @@ // ptrTo *typeStruct // elem *typeStruct // underlying type // pkgpath *byte // pkgpath; null terminated +// methods methodSet // name [1]byte // actual name; null terminated // // The type struct is essentially a union of all the above types. Which it is, @@ -420,6 +423,7 @@ type ptrType struct { rawType numMethod uint16 elem *rawType + methods methodSet } type arrayType struct { @@ -445,6 +449,7 @@ type namedType struct { ptrTo *rawType elem *rawType pkg *byte + methods methodSet name [1]byte } @@ -463,6 +468,7 @@ type structType struct { size uint32 numField uint16 fields [1]structField // the remaining fields are all of type structField + methods methodSet } type structField struct { @@ -470,6 +476,18 @@ type structField struct { data unsafe.Pointer // various bits of information, packed in a byte array } +type interfaceType struct { + rawType + ptrTo *rawType + methods methodSet +} + +// Method set, as emitted by the compiler. +type methodSet struct { + length uintptr + methods [0]*byte // variable number of methods +} + // Equivalent to (go/types.Type).Underlying(): if this is a named type return // the underlying type, else just return the type itself. func (t *rawType) underlying() *rawType { @@ -972,7 +990,8 @@ func (t *rawType) Implements(u Type) bool { if u.Kind() != Interface { panic("reflect: non-interface type passed to Type.Implements") } - return t.AssignableTo(u) + u_itf := (*interfaceType)(unsafe.Pointer(u.(*rawType).underlying())) + return typeImplementsMethodSet(unsafe.Pointer(t), unsafe.Pointer(&u_itf.methods)) } // Comparable returns whether values of this type can be compared to each other. @@ -1046,7 +1065,9 @@ func readStringZ(data unsafe.Pointer) string { func (t *rawType) name() string { ntype := (*namedType)(unsafe.Pointer(t)) - return readStringZ(unsafe.Pointer(&ntype.name[0])) + ptr := unsafe.Pointer(&ntype.name[0]) + ptr = unsafe.Add(ptr, uintptr(ntype.methods.length)*unsafe.Sizeof(unsafe.Pointer(nil))) + return readStringZ(ptr) } func (t *rawType) Name() string { diff --git a/src/runtime/interface.go b/src/runtime/interface.go index b9813225f2..4fb405e1bc 100644 --- a/src/runtime/interface.go +++ b/src/runtime/interface.go @@ -82,6 +82,9 @@ func reflectValueEqual(x, y reflect.Value) bool { } } +// Implemented in the reflect package. +func typeImplementsMethodSet(actualTypeNum, assertedMethodSet unsafe.Pointer) bool + // interfaceTypeAssert is called when a type assert without comma-ok still // returns false. func interfaceTypeAssert(ok bool) { diff --git a/testdata/interface.go b/testdata/interface.go index 1a3a838288..ac304b4cc1 100644 --- a/testdata/interface.go +++ b/testdata/interface.go @@ -22,6 +22,7 @@ func main() { printItf(array) printItf(ArrayStruct{3, array}) printItf(SmallPair{3, 5}) + printItf(nil) s := Stringer(thing) println("Stringer.String():", s.String()) var itf interface{} = s @@ -150,6 +151,8 @@ func printItf(val interface{}) { println("is struct{n int `foo:\"bar\"`}") case Foo: println("is Foo:", val) + case nil: + println("is nil") default: println("is ?") } diff --git a/testdata/interface.txt b/testdata/interface.txt index 55f7ae1def..27b9a6745a 100644 --- a/testdata/interface.txt +++ b/testdata/interface.txt @@ -16,6 +16,7 @@ is Tuple: 1 7 11 13 ArrayStruct.Print: 4 3 is Tuple: 0 8 16 24 SmallPair.Print: 3 5 +is nil Stringer.String(): foo Stringer.(*Thing).String(): foo s has String() method: foo diff --git a/transform/interface-lowering.go b/transform/interface-lowering.go index 7f0b6fdc57..0dc9173bc0 100644 --- a/transform/interface-lowering.go +++ b/transform/interface-lowering.go @@ -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("") - 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. // diff --git a/transform/optimizer.go b/transform/optimizer.go index 05533b6a4a..91093fc721 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -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) diff --git a/transform/rtcalls.go b/transform/rtcalls.go index 8310fc9f19..49b138bcc9 100644 --- a/transform/rtcalls.go +++ b/transform/rtcalls.go @@ -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() - } -} diff --git a/transform/rtcalls_test.go b/transform/rtcalls_test.go index 9073b0ea5b..2ae1603155 100644 --- a/transform/rtcalls_test.go +++ b/transform/rtcalls_test.go @@ -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) - }) -} diff --git a/transform/testdata/interface.ll b/transform/testdata/interface.ll index 76ed029b47..17db19eacd 100644 --- a/transform/testdata/interface.ll +++ b/transform/testdata/interface.ll @@ -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()" } diff --git a/transform/testdata/interface.out.ll b/transform/testdata/interface.out.ll index cb041ab1db..bbbc5d9245 100644 --- a/transform/testdata/interface.out.ll +++ b/transform/testdata/interface.out.ll @@ -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()" } diff --git a/transform/testdata/reflect-implements.ll b/transform/testdata/reflect-implements.ll deleted file mode 100644 index 46536483b2..0000000000 --- a/transform/testdata/reflect-implements.ll +++ /dev/null @@ -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" } diff --git a/transform/testdata/reflect-implements.out.ll b/transform/testdata/reflect-implements.out.ll deleted file mode 100644 index b7b759c018..0000000000 --- a/transform/testdata/reflect-implements.out.ll +++ /dev/null @@ -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" } From 7ff626dcee6db11edff6f43c018b25d915f3982f Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 31 Jul 2024 19:53:37 +0200 Subject: [PATCH 2/2] reflect: fully implement Type.AssignableTo This fully implements AssignableTo, fixing a number of bugs in the previous implementation. In particular, it now supports AssignableTo for interface types. --- src/reflect/set_test.go | 56 ++++++++++++++++++--------------------- src/reflect/type.go | 43 +++++++++++++++++++++++++++--- src/reflect/value_test.go | 9 ------- 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/reflect/set_test.go b/src/reflect/set_test.go index 9517ae8f68..e31ba6c281 100644 --- a/src/reflect/set_test.go +++ b/src/reflect/set_test.go @@ -196,34 +196,30 @@ func TestImplements(t *testing.T) { } } -// TODO: AssignableTo should be possible to implement now that Implements() -// mostly works. +var assignableTests = []struct { + x any + t any + b bool +}{ + {new(chan int), new(<-chan int), true}, + {new(<-chan int), new(chan int), false}, + {new(*int), new(IntPtr), true}, + {new(IntPtr), new(*int), true}, + {new(IntPtr), new(IntPtr1), false}, + {new(Ch), new(<-chan any), true}, + // test runs implementsTests too +} -//var assignableTests = []struct { -// x any -// t any -// b bool -//}{ -// {new(chan int), new(<-chan int), true}, -// {new(<-chan int), new(chan int), false}, -// {new(*int), new(IntPtr), true}, -// {new(IntPtr), new(*int), true}, -// {new(IntPtr), new(IntPtr1), false}, -// {new(Ch), new(<-chan any), true}, -// // test runs implementsTests too -//} -// -//type IntPtr *int -//type IntPtr1 *int -//type Ch <-chan any -// -//func TestAssignableTo(t *testing.T) { -// for _, tt := range append(assignableTests, implementsTests...) { -// xv := TypeOf(tt.x).Elem() -// xt := TypeOf(tt.t).Elem() -// if b := xv.AssignableTo(xt); b != tt.b { -// t.Errorf("(%s).AssignableTo(%s) = %v, want %v", xv.String(), xt.String(), b, tt.b) -// } -// } -//} -// +type IntPtr *int +type IntPtr1 *int +type Ch <-chan any + +func TestAssignableTo(t *testing.T) { + for _, tt := range append(assignableTests, implementsTests...) { + xv := TypeOf(tt.x).Elem() + xt := TypeOf(tt.t).Elem() + if b := xv.AssignableTo(xt); b != tt.b { + t.Errorf("(%s).AssignableTo(%s) = %v, want %v", xv.String(), xt.String(), b, tt.b) + } + } +} diff --git a/src/reflect/type.go b/src/reflect/type.go index 2ce030af6d..3a5518caf6 100644 --- a/src/reflect/type.go +++ b/src/reflect/type.go @@ -972,17 +972,52 @@ func (t *rawType) FieldAlign() int { // AssignableTo returns whether a value of type t can be assigned to a variable // of type u. func (t *rawType) AssignableTo(u Type) bool { - if t == u.(*rawType) { + // Quotes come from the language spec: + // https://go.dev/ref/spec#Assignability + // > A value x of type V is assignable to a variable of type T ("x is + // > assignable to T") if one of the following conditions applies: + // [...] + // (Replace T with t, and x with u). + u_raw := u.(*rawType) + if t == u_raw { + // > V and T are identical. return true } - if u.Kind() == Interface && u.NumMethod() == 0 { + if u.Kind() == Interface { + // > T is an interface type, but not a type parameter, and x implements + // > T. + u_itf := (*interfaceType)(unsafe.Pointer(u_raw.underlying())) + res := typeImplementsMethodSet(unsafe.Pointer(t), unsafe.Pointer(&u_itf.methods)) + return res + } + + t_named := t.isNamed() + u_named := u_raw.isNamed() + if t_named && u_named { + return false + } + if t.underlying() == u_raw.underlying() { + // > V and T have identical underlying types but are not type parameters + // > and at least one of V or T is not a named type. return true } - if u.Kind() == Interface { - panic("reflect: unimplemented: AssignableTo with interface") + if t.Kind() == Chan && u_raw.Kind() == Chan { + // > V and T are channel types with identical element types, V is a + // > bidirectional channel, and at least one of V or T is not a named + // > type. + t_chan := (*elemType)(unsafe.Pointer(t.underlying())) + u_chan := (*elemType)(unsafe.Pointer(u_raw.underlying())) + if t_chan.elem != u_chan.elem { + return false + } + if t_chan.ChanDir() != BothDir { + return false + } + return true } + return false } diff --git a/src/reflect/value_test.go b/src/reflect/value_test.go index 40f0919bdc..eb28082999 100644 --- a/src/reflect/value_test.go +++ b/src/reflect/value_test.go @@ -592,15 +592,6 @@ func TestTinyNumMethods(t *testing.T) { } } -func TestAssignableTo(t *testing.T) { - var a any - refa := ValueOf(&a).Elem() - refa.Set(ValueOf(4)) - if got, want := refa.Interface().(int), 4; got != want { - t.Errorf("AssignableTo / Set failed, got %v, want %v", got, want) - } -} - func TestConvert(t *testing.T) { v := ValueOf(int64(3)) c := v.Convert(TypeOf(byte(0)))