diff --git a/compiler/compiler.go b/compiler/compiler.go index 19410d49ea..5f1a51d784 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -1535,6 +1535,15 @@ func (b *builder) createInstruction(instr ssa.Instruction) { b.setDebugLocation(getPos(instr)) } + switch instr := instr.(type) { + case *ssa.Call: + b.markReflectMethodUse(&instr.Call) + case *ssa.Defer: + b.markReflectMethodUse(&instr.Call) + case *ssa.Go: + b.markReflectMethodUse(&instr.Call) + } + switch instr := instr.(type) { case ssa.Value: if value, err := b.createExpr(instr); err != nil { @@ -1603,6 +1612,36 @@ func (b *builder) createInstruction(instr ssa.Instruction) { } } +func (b *builder) markReflectMethodUse(call *ssa.CallCommon) { + pkg := b.fn.Pkg + if pkg == nil && b.fn.Origin() != nil { + pkg = b.fn.Origin().Pkg + } + if pkg != nil { + switch pkg.Pkg.Path() { + case "reflect", "internal/reflectlite": + return + } + } + + var method *types.Func + if call.IsInvoke() { + method = call.Method + } else if callee := call.StaticCallee(); callee != nil { + if object := callee.Object(); object != nil { + method, _ = object.(*types.Func) + } + } + if method == nil || method.Pkg() == nil || method.Pkg().Path() != "reflect" { + return + } + switch method.Name() { + case "Method", "MethodByName", "Methods": + attr := b.ctx.CreateStringAttribute("tinygo-reflect-method", "") + b.llvmFn.AddFunctionAttr(attr) + } +} + func (b *builder) setValue(value ssa.Value, llvmValue llvm.Value) { if b.isAggregateValue(value.Type()) && !llvmValue.IsNil() && llvmValue.Type().TypeKind() == llvm.PointerTypeKind { b.indirectValues[value] = llvmValue diff --git a/compiler/interface.go b/compiler/interface.go index 84f91cc448..59e062539d 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -225,9 +225,13 @@ func (c *compilerContext) getTypeCode(typ types.Type) llvm.Value { } methods = append(methods, fn) } + methodEntryType := types.NewStruct([]*types.Var{ + types.NewVar(token.NoPos, nil, "signature", types.Typ[types.UnsafePointer]), + types.NewVar(token.NoPos, nil, "name", types.Typ[types.UnsafePointer]), + }, nil) 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)))), + types.NewVar(token.NoPos, nil, "methods", types.NewArray(methodEntryType, int64(len(methods)))), }, nil) methodSetValue := c.getMethodSetValue(methods) switch typ := typ.(type) { @@ -1183,12 +1187,15 @@ func (c *compilerContext) getMethodsString(itf *types.Interface) string { } // getMethodSetValue creates the method set struct value for a list of methods. -// The struct contains a length and a sorted array of method signature pointers. +// The struct contains a length and a sorted array of {signature, name} entries. +// Each entry pairs a method signature pointer (for Implements comparison) with +// a pointer to the method's null-terminated name string. func (c *compilerContext) getMethodSetValue(methods []*types.Func) llvm.Value { // Create a sorted list of method signature global names. type methodRef struct { - name string - value llvm.Value + sigGlobalName string + methodName string + sigValue llvm.Value } var refs []methodRef for _, method := range methods { @@ -1219,23 +1226,44 @@ func (c *compilerContext) getMethodSetValue(methods []*types.Func) llvm.Value { value.AddMetadata(0, diglobal) } } - refs = append(refs, methodRef{globalName, value}) + refs = append(refs, methodRef{globalName, name, value}) } sort.Slice(refs, func(i, j int) bool { - return refs[i].name < refs[j].name + return refs[i].sigGlobalName < refs[j].sigGlobalName }) - var values []llvm.Value + pairType := c.ctx.StructType([]llvm.Type{c.dataPtrType, c.dataPtrType}, false) + var pairs []llvm.Value for _, ref := range refs { - values = append(values, ref.value) + nameGlobal := c.getMethodNameGlobal(ref.methodName) + pair := c.ctx.ConstStruct([]llvm.Value{ref.sigValue, nameGlobal}, false) + pairs = append(pairs, pair) } return c.ctx.ConstStruct([]llvm.Value{ - llvm.ConstInt(c.uintptrType, uint64(len(values)), false), - llvm.ConstArray(c.dataPtrType, values), + llvm.ConstInt(c.uintptrType, uint64(len(pairs)), false), + llvm.ConstArray(pairType, pairs), }, false) } +// getMethodNameGlobal returns a global containing the null-terminated method +// name string, creating it if needed. +func (c *compilerContext) getMethodNameGlobal(name string) llvm.Value { + globalName := "reflect/types.methodname:" + name + g := c.mod.NamedGlobal(globalName) + if !g.IsNil() { + return g + } + nameBytes := c.ctx.ConstString(name+"\x00", false) + g = llvm.AddGlobal(c.mod, nameBytes.Type(), globalName) + g.SetInitializer(nameBytes) + g.SetGlobalConstant(true) + g.SetLinkage(llvm.LinkOnceODRLinkage) + g.SetAlignment(1) + g.SetUnnamedAddr(true) + return g +} + // getInvokeFunction returns the thunk to call the given interface method. The // thunk is declared, not defined: it will be defined by the interface lowering // pass. diff --git a/compiler/testdata/go1.27.ll b/compiler/testdata/go1.27.ll index d52ba19a38..2f6f3ea46c 100644 --- a/compiler/testdata/go1.27.ll +++ b/compiler/testdata/go1.27.ll @@ -7,9 +7,10 @@ target triple = "wasm32-unknown-wasi" %runtime._interface = type { ptr, ptr } @"reflect/types.signature:Regular:func:{basic:int}{basic:int}" = linkonce_odr constant i8 0, align 1 -@"reflect/types.type:named:main.genericMethod" = linkonce_odr constant { ptr, i8, i16, ptr, ptr, ptr, { i32, [1 x ptr] }, [19 x i8] } { ptr @"named:main.genericMethod$methodset", i8 122, i16 -32767, ptr getelementptr ({ ptr, i8, i16, ptr, { i32, [1 x ptr] } }, ptr @"reflect/types.type:pointer:named:main.genericMethod", i32 0, i32 1), ptr @"reflect/types.type:struct:{}", ptr @"reflect/types.type.pkgpath:main", { i32, [1 x ptr] } { i32 1, [1 x ptr] [ptr @"reflect/types.signature:Regular:func:{basic:int}{basic:int}"] }, [19 x i8] c"main.genericMethod\00" }, align 4 +@"reflect/types.methodname:Regular" = linkonce_odr unnamed_addr constant [8 x i8] c"Regular\00", align 1 +@"reflect/types.type:named:main.genericMethod" = linkonce_odr constant { ptr, i8, i16, ptr, ptr, ptr, { i32, [1 x { ptr, ptr }] }, [19 x i8] } { ptr @"named:main.genericMethod$methodset", i8 122, i16 -32767, ptr getelementptr ({ ptr, i8, i16, ptr, { i32, [1 x { ptr, ptr }] } }, ptr @"reflect/types.type:pointer:named:main.genericMethod", i32 0, i32 1), ptr @"reflect/types.type:struct:{}", ptr @"reflect/types.type.pkgpath:main", { i32, [1 x { ptr, ptr }] } { i32 1, [1 x { ptr, ptr }] [{ ptr, ptr } { ptr @"reflect/types.signature:Regular:func:{basic:int}{basic:int}", ptr @"reflect/types.methodname:Regular" }] }, [19 x i8] c"main.genericMethod\00" }, align 4 @"reflect/types.type.pkgpath:main" = linkonce_odr unnamed_addr constant [5 x i8] c"main\00", align 1 -@"reflect/types.type:pointer:named:main.genericMethod" = linkonce_odr constant { ptr, i8, i16, ptr, { i32, [1 x ptr] } } { ptr @"pointer:named:main.genericMethod$methodset", i8 -43, i16 -32767, ptr getelementptr ({ ptr, i8, i16, ptr, ptr, ptr, { i32, [1 x ptr] }, [19 x i8] }, ptr @"reflect/types.type:named:main.genericMethod", i32 0, i32 1), { i32, [1 x ptr] } { i32 1, [1 x ptr] [ptr @"reflect/types.signature:Regular:func:{basic:int}{basic:int}"] } }, align 4 +@"reflect/types.type:pointer:named:main.genericMethod" = linkonce_odr constant { ptr, i8, i16, ptr, { i32, [1 x { ptr, ptr }] } } { ptr @"pointer:named:main.genericMethod$methodset", i8 -43, i16 -32767, ptr getelementptr ({ ptr, i8, i16, ptr, ptr, ptr, { i32, [1 x { ptr, ptr }] }, [19 x i8] }, ptr @"reflect/types.type:named:main.genericMethod", i32 0, i32 1), { i32, [1 x { ptr, ptr }] } { i32 1, [1 x { ptr, ptr }] [{ ptr, ptr } { ptr @"reflect/types.signature:Regular:func:{basic:int}{basic:int}", ptr @"reflect/types.methodname:Regular" }] } }, align 4 @"reflect/methods.Regular:func:{basic:int}{basic:int}" = linkonce_odr constant i8 0, align 1 @"main$string" = internal unnamed_addr constant [18 x i8] c"main.genericMethod", align 1 @"main$string.1" = internal unnamed_addr constant [7 x i8] c"Regular", align 1 @@ -21,7 +22,7 @@ target triple = "wasm32-unknown-wasi" @"reflect/types.type:named:main.onlyGenericMethod" = linkonce_odr constant { i8, i16, ptr, ptr, ptr, [23 x i8] } { i8 122, i16 0, ptr @"reflect/types.type:pointer:named:main.onlyGenericMethod", ptr @"reflect/types.type:struct:{}", ptr @"reflect/types.type.pkgpath:main", [23 x i8] c"main.onlyGenericMethod\00" }, align 4 @"reflect/types.type:pointer:named:main.onlyGenericMethod" = linkonce_odr constant { i8, i16, ptr } { i8 -43, i16 0, ptr @"reflect/types.type:named:main.onlyGenericMethod" }, align 4 -declare void @runtime.trackPointer(ptr nocapture readonly, ptr, ptr) #0 +declare void @runtime.trackPointer(ptr readonly captures(none), ptr, ptr) #0 ; Function Attrs: nounwind define hidden void @main.init(ptr %context) unnamed_addr #1 { @@ -43,7 +44,7 @@ entry: call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #2 call void @runtime.trackPointer(ptr nonnull @"reflect/types.type:named:main.onlyGenericMethod", ptr nonnull %stackalloc, ptr undef) #2 call void @runtime.trackPointer(ptr null, ptr nonnull %stackalloc, ptr undef) #2 - ret { %runtime._interface, %runtime._interface } { %runtime._interface { ptr getelementptr ({ ptr, i8, i16, ptr, ptr, ptr, { i32, [1 x ptr] }, [19 x i8] }, ptr @"reflect/types.type:named:main.genericMethod", i32 0, i32 1), ptr null }, %runtime._interface { ptr @"reflect/types.type:named:main.onlyGenericMethod", ptr null } } + ret { %runtime._interface, %runtime._interface } { %runtime._interface { ptr getelementptr ({ ptr, i8, i16, ptr, ptr, ptr, { i32, [1 x { ptr, ptr }] }, [19 x i8] }, ptr @"reflect/types.type:named:main.genericMethod", i32 0, i32 1), ptr null }, %runtime._interface { ptr @"reflect/types.type:named:main.onlyGenericMethod", ptr null } } } ; Function Attrs: nounwind diff --git a/compiler/testdata/interface.ll b/compiler/testdata/interface.ll index 016a461a4c..143ec4bb02 100644 --- a/compiler/testdata/interface.ll +++ b/compiler/testdata/interface.ll @@ -10,13 +10,15 @@ target triple = "wasm32-unknown-wasi" @"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.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 -32767, 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.methodname:Error" = linkonce_odr unnamed_addr constant [6 x i8] c"Error\00", align 1 +@"reflect/types.type:named:error" = linkonce_odr constant { i8, i16, ptr, ptr, ptr, { i32, [1 x { ptr, ptr }] }, [7 x i8] } { i8 116, i16 -32767, 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, ptr }] } { i32 1, [1 x { ptr, ptr }] [{ ptr, ptr } { ptr @"reflect/types.signature:Error:func:{}{basic:string}", ptr @"reflect/types.methodname:Error" }] }, [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, { 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:interface:{Error:func:{}{basic:string}}" = linkonce_odr constant { i8, ptr, { i32, [1 x { ptr, ptr }] } } { i8 84, ptr @"reflect/types.type:pointer:interface:{Error:func:{}{basic:string}}", { i32, [1 x { ptr, ptr }] } { i32 1, [1 x { ptr, ptr }] [{ ptr, ptr } { ptr @"reflect/types.signature:Error:func:{}{basic:string}", ptr @"reflect/types.methodname:Error" }] } }, 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.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.methodname:String" = linkonce_odr unnamed_addr constant [7 x i8] c"String\00", align 1 +@"reflect/types.type:interface:{String:func:{}{basic:string}}" = linkonce_odr constant { i8, ptr, { i32, [1 x { ptr, ptr }] } } { i8 84, ptr @"reflect/types.type:pointer:interface:{String:func:{}{basic:string}}", { i32, [1 x { ptr, ptr }] } { i32 1, [1 x { ptr, ptr }] [{ ptr, ptr } { ptr @"reflect/types.signature:String:func:{}{basic:string}", ptr @"reflect/types.methodname:String" }] } }, align 4 @"reflect/types.typeid:basic:int" = external constant i8 declare void @runtime.trackPointer(ptr nocapture readonly, ptr, ptr) #0 diff --git a/src/internal/reflectlite/type.go b/src/internal/reflectlite/type.go index 5ced5d3573..67323bd838 100644 --- a/src/internal/reflectlite/type.go +++ b/src/internal/reflectlite/type.go @@ -252,10 +252,18 @@ type structField struct { data unsafe.Pointer // various bits of information, packed in a byte array } +// Method set entry, as emitted by the compiler. Each entry pairs a signature +// identity pointer (for Implements/AssignableTo comparison) with a pointer to +// the method's null-terminated name string. +type methodEntry struct { + signature unsafe.Pointer + name *byte +} + // Method set, as emitted by the compiler. type methodSet struct { length uintptr - methods [0]unsafe.Pointer // variable number of method signature pointers + methods [0]methodEntry } // Equivalent to (go/types.Type).Underlying(): if this is a named type return @@ -894,8 +902,8 @@ func (t *RawType) Implements(u Type) bool { // typeImplementsMethodSet checks whether the concrete type (identified by its // typecode pointer) implements the given method set. Both the concrete type's -// method set and the asserted method set are sorted arrays of method signature -// pointers, so comparison is O(n+m). +// method set and the asserted method set are sorted arrays of {signature, name} +// entries, so comparison is O(n+m). Only the signature field is compared. // //go:linkname typeImplementsMethodSet runtime.typeImplementsMethodSet func typeImplementsMethodSet(concreteType, assertedMethodSet unsafe.Pointer) bool { @@ -903,7 +911,7 @@ func typeImplementsMethodSet(concreteType, assertedMethodSet unsafe.Pointer) boo return false } - const ptrSize = unsafe.Sizeof((*byte)(nil)) + const entrySize = unsafe.Sizeof(methodEntry{}) itfNumMethod := *(*uintptr)(assertedMethodSet) if itfNumMethod == 0 { return true @@ -942,13 +950,14 @@ func typeImplementsMethodSet(concreteType, assertedMethodSet unsafe.Pointer) boo } concreteTypePtr := unsafe.Pointer(&methods.methods) - concreteTypeEnd := unsafe.Add(concreteTypePtr, uintptr(methods.length)*ptrSize) + concreteTypeEnd := unsafe.Add(concreteTypePtr, uintptr(methods.length)*entrySize) // 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, so we can use a linear scan. - assertedTypePtr := unsafe.Add(assertedMethodSet, ptrSize) - assertedTypeEnd := unsafe.Add(assertedTypePtr, itfNumMethod*ptrSize) + // Each entry is a {signature, name} pair; we compare only the signature. + assertedTypePtr := unsafe.Add(assertedMethodSet, unsafe.Sizeof(uintptr(0))) + assertedTypeEnd := unsafe.Add(assertedTypePtr, itfNumMethod*entrySize) for assertedTypePtr != assertedTypeEnd { assertedMethod := *(*unsafe.Pointer)(assertedTypePtr) @@ -957,13 +966,13 @@ func typeImplementsMethodSet(concreteType, assertedMethodSet unsafe.Pointer) boo return false } concreteMethod := *(*unsafe.Pointer)(concreteTypePtr) - concreteTypePtr = unsafe.Add(concreteTypePtr, ptrSize) + concreteTypePtr = unsafe.Add(concreteTypePtr, entrySize) if concreteMethod == assertedMethod { break } } - assertedTypePtr = unsafe.Add(assertedTypePtr, ptrSize) + assertedTypePtr = unsafe.Add(assertedTypePtr, entrySize) } return true @@ -1002,14 +1011,157 @@ func (t *RawType) NumMethod() int { case Struct: return int((*structType)(unsafe.Pointer(t)).numMethod & ^uint16(numMethodHasMethodSet)) case Interface: - //FIXME: Use len(methods) - return (*interfaceType)(unsafe.Pointer(t)).ptrTo.NumMethod() + ct := (*interfaceType)(unsafe.Pointer(t.underlying())) + return int(ct.methods.length) } // Other types have no methods attached. Note we don't panic here. return 0 } +// getMethodSet returns the method set for a type, or nil if the type has no +// inline method set. +func (t *RawType) getMethodSet() *methodSet { + if t.isNamed() { + ct := (*namedType)(unsafe.Pointer(t)) + if ct.numMethod&numMethodHasMethodSet == 0 { + return nil + } + return (*methodSet)(unsafe.Add(unsafe.Pointer(ct), unsafe.Sizeof(*ct))) + } + switch t.Kind() { + case Interface: + ct := (*interfaceType)(unsafe.Pointer(t.underlying())) + return &ct.methods + case Pointer: + ct := (*ptrType)(unsafe.Pointer(t)) + if ct.numMethod&numMethodHasMethodSet == 0 { + return nil + } + return &ct.methods + case Struct: + ct := (*structType)(unsafe.Pointer(t)) + if ct.numMethod&numMethodHasMethodSet == 0 { + return nil + } + fieldSize := unsafe.Sizeof(structField{}) + methodsPtr := unsafe.Add(unsafe.Pointer(&ct.fields[0]), uintptr(ct.numField)*fieldSize) + return (*methodSet)(methodsPtr) + } + return nil +} + +// methodSetEntry returns the i-th entry in the method set. +func methodSetEntry(ms *methodSet, i int) *methodEntry { + return (*methodEntry)(unsafe.Add(unsafe.Pointer(&ms.methods), uintptr(i)*unsafe.Sizeof(methodEntry{}))) +} + +// isExportedMethod reports whether the method entry has an exported name. +// Unexported method names are stored as "pkg/path.name" by the compiler. +func isExportedMethod(entry *methodEntry) bool { + if entry.name == nil { + return false // name was stripped by DCE + } + for name := entry.name; *name != 0; name = (*byte)(unsafe.Add(unsafe.Pointer(name), 1)) { + if *name == '.' { + return false + } + } + return true +} + +// methodName returns the name and pkgPath of a method entry. +// For exported methods, name is the method name and pkgPath is empty. +// For unexported methods, name is just the method name and pkgPath is +// the package path (stored as "pkg/path.name" by the compiler). +func methodName(entry *methodEntry) (name, pkgPath string) { + if entry.name == nil { + return "", "" + } + full := readStringZ(unsafe.Pointer(entry.name)) + // Unexported methods are stored as "pkg/path.name". + for i := len(full) - 1; i >= 0; i-- { + if full[i] == '.' { + return full[i+1:], full[:i] + } + } + return full, "" +} + +// Method returns the i-th method in the type's method set. +// For non-interface types, this indexes only exported methods. +// For interface types, all methods are included. +// +//go:linkname reflectTypeMethodByIndex reflect.(*rawType).Method +func (t *RawType) Method(i int) MethodInfo { + n := t.NumMethod() + if i < 0 || i >= n { + panic("reflect: Method index out of range") + } + ms := t.getMethodSet() + if ms == nil { + // Method set was pruned or stripped; name unavailable. + return MethodInfo{Index: i} + } + isIface := t.Kind() == Interface + exportedIdx := 0 + for j := 0; j < int(ms.length); j++ { + entry := methodSetEntry(ms, j) + if !isIface && !isExportedMethod(entry) { + continue + } + if exportedIdx == i { + name, pkgPath := methodName(entry) + return MethodInfo{ + Name: name, + PkgPath: pkgPath, + Index: i, + } + } + exportedIdx++ + } + // Method set was pruned; name unavailable. + return MethodInfo{Index: i} +} + +// MethodByName returns the method with the given name in the type's method +// set, and a boolean indicating if the method was found. +// For non-interface types, only exported methods are searched. +// +//go:linkname reflectTypeMethodByName reflect.(*rawType).MethodByName +func (t *RawType) MethodByName(name string) (MethodInfo, bool) { + ms := t.getMethodSet() + if ms == nil { + return MethodInfo{}, false + } + isIface := t.Kind() == Interface + exportedIdx := 0 + for j := 0; j < int(ms.length); j++ { + entry := methodSetEntry(ms, j) + if !isIface && !isExportedMethod(entry) { + continue + } + ename, pkgPath := methodName(entry) + if ename == name { + return MethodInfo{ + Name: name, + PkgPath: pkgPath, + Index: exportedIdx, + }, true + } + exportedIdx++ + } + return MethodInfo{}, false +} + +// MethodInfo describes a single method. This is the internal reflectlite +// representation; the reflect package wraps this in reflect.Method. +type MethodInfo struct { + Name string + PkgPath string + Index int +} + // Read and return a null terminated string starting from data. func readStringZ(data unsafe.Pointer) string { start := data @@ -1028,8 +1180,8 @@ func (t *RawType) name() string { ptr := unsafe.Add(unsafe.Pointer(ntype), unsafe.Sizeof(*ntype)) if ntype.numMethod&numMethodHasMethodSet != 0 { ms := (*methodSet)(ptr) - // Skip past the length field and the method pointer entries. - ptr = unsafe.Add(ptr, unsafe.Sizeof(uintptr(0))+uintptr(ms.length)*unsafe.Sizeof(unsafe.Pointer(nil))) + // Skip past the length field and the method entries. + ptr = unsafe.Add(ptr, unsafe.Sizeof(uintptr(0))+uintptr(ms.length)*unsafe.Sizeof(methodEntry{})) } return readStringZ(ptr) } diff --git a/src/internal/reflectlite/value.go b/src/internal/reflectlite/value.go index b353b1a92a..68e469775b 100644 --- a/src/internal/reflectlite/value.go +++ b/src/internal/reflectlite/value.go @@ -16,6 +16,7 @@ const ( valueFlagExported valueFlagEmbedRO valueFlagStickyRO + valueFlagMethod // set when the Value represents a bound method valueFlagRO = valueFlagEmbedRO | valueFlagStickyRO ) @@ -244,6 +245,9 @@ func (v Value) RawType() *RawType { } func (v Value) Kind() Kind { + if v.flags&valueFlagMethod != 0 { + return Func + } return v.typecode.Kind() } @@ -254,6 +258,9 @@ func (v Value) IsNil() bool { case Chan, Map, Ptr, UnsafePointer: return v.pointer() == nil case Func: + if v.flags&valueFlagMethod != 0 { + return false // bound methods are never nil + } if v.value == nil { return true } @@ -289,6 +296,10 @@ func (v Value) UnsafePointer() unsafe.Pointer { slice := (*sliceHeader)(v.value) return slice.data case Func: + if v.flags&valueFlagMethod != 0 { + // Bound methods do not have a meaningful function pointer. + return v.value + } fn := (*funcHeader)(v.value) if fn.Context != nil { return fn.Context @@ -2263,11 +2274,35 @@ func (v Value) CallSlice(in []Value) []Value { } func (v Value) Method(i int) Value { - panic("unimplemented: (reflect.Value).Method()") + if v.Kind() == Invalid { + panic(&ValueError{Method: "reflect.Value.Method", Kind: Invalid}) + } + n := v.typecode.NumMethod() + if i < 0 || i >= n { + panic("reflect: Method index out of range") + } + // Return a valid Value representing the bound method. Without Call() + // support, this value cannot be invoked but satisfies IsValid() and + // Kind() == Func checks. + return Value{ + typecode: v.typecode, + value: v.value, + flags: (v.flags & valueFlagExported) | valueFlagMethod, + } } func (v Value) MethodByName(name string) Value { - panic("unimplemented: (reflect.Value).MethodByName()") + if v.Kind() == Invalid { + panic(&ValueError{Method: "reflect.Value.MethodByName", Kind: Invalid}) + } + if _, ok := v.typecode.MethodByName(name); !ok { + return Value{} + } + return Value{ + typecode: v.typecode, + value: v.value, + flags: (v.flags & valueFlagExported) | valueFlagMethod, + } } func (v Value) Recv() (x Value, ok bool) { diff --git a/src/reflect/type.go b/src/reflect/type.go index f40863ee89..608df63af6 100644 --- a/src/reflect/type.go +++ b/src/reflect/type.go @@ -482,11 +482,24 @@ func (t *rawType) Key() Type { } func (t *rawType) Method(i int) Method { - panic("unimplemented: (reflect.Type).Method()") + m := t.RawType.Method(i) + return Method{ + Name: m.Name, + PkgPath: m.PkgPath, + Index: m.Index, + } } func (t *rawType) MethodByName(name string) (Method, bool) { - panic("unimplemented: (reflect.Type).MethodByName()") + m, ok := t.RawType.MethodByName(name) + if !ok { + return Method{}, false + } + return Method{ + Name: m.Name, + PkgPath: m.PkgPath, + Index: m.Index, + }, true } func (t *rawType) Methods() iter.Seq[Method] { diff --git a/src/reflect/value.go b/src/reflect/value.go index cf6952a770..a078229213 100644 --- a/src/reflect/value.go +++ b/src/reflect/value.go @@ -214,11 +214,11 @@ func (v Value) Equal(u Value) bool { } func (v Value) Method(i int) Value { - panic("unimplemented: (reflect.Value).Method()") + return Value{v.Value.Method(i)} } func (v Value) MethodByName(name string) Value { - panic("unimplemented: (reflect.Value).MethodByName()") + return Value{v.Value.MethodByName(name)} } func (v Value) Recv() (x Value, ok bool) { diff --git a/testdata/reflect.go b/testdata/reflect.go index 476ccd1148..30861ee536 100644 --- a/testdata/reflect.go +++ b/testdata/reflect.go @@ -360,6 +360,9 @@ func main() { println("\nv.Interface() method") testInterfaceMethod() + println("\ntype method sets") + testMethodSets() + // Test reflect.DeepEqual. var selfref1, selfref2 selfref selfref1.x = &selfref1 @@ -947,3 +950,129 @@ func testMakeMapPaddedKey() { println("padded lookup: not found") } } + +type HasMethods struct { + Name string +} + +func (h HasMethods) Len() int { + return len(h.Name) +} + +func (h HasMethods) String() string { + return h.Name +} + +// Unexported methods should not appear in the method set. +func (h HasMethods) hidden() {} + +// PtrMethod is only on the pointer receiver. +func (h *HasMethods) PtrMethod() {} + +// Embedded type tests. +type Inner struct{ X int } + +func (i Inner) InnerMethod() int { return i.X } + +type Outer struct { + Y int + Inner +} + +func (o Outer) OuterMethod() int { return o.Y } + +// Interface type method set test. +type Iface interface { + Alpha() + Beta() +} + +func testMethodSets() { + // --- Struct value type: only value-receiver methods --- + t := reflect.TypeOf(HasMethods{}) + println("struct NumMethod:", t.NumMethod()) + for i := 0; i < t.NumMethod(); i++ { + m := t.Method(i) + println("struct Method:", m.Name) + } + + // --- Pointer type: includes both value and pointer receiver methods --- + pt := reflect.TypeOf(new(HasMethods)) + println("pointer NumMethod:", pt.NumMethod()) + for i := 0; i < pt.NumMethod(); i++ { + m := pt.Method(i) + println("pointer Method:", m.Name) + } + + // --- MethodByName: found and not found --- + m, ok := t.MethodByName("String") + println("MethodByName(String):", m.Name, ok) + + m, ok = t.MethodByName("Len") + println("MethodByName(Len):", m.Name, ok) + + _, ok = t.MethodByName("Nonexistent") + println("MethodByName(Nonexistent):", ok) + + // MethodByName for pointer-only method on pointer type + m, ok = pt.MethodByName("PtrMethod") + println("pointer MethodByName(PtrMethod):", m.Name, ok) + + // MethodByName for pointer-only method on value type → not found + _, ok = t.MethodByName("PtrMethod") + println("struct MethodByName(PtrMethod):", ok) + + // --- Embedded types --- + ot := reflect.TypeOf(Outer{}) + println("embedded NumMethod:", ot.NumMethod()) + for i := 0; i < ot.NumMethod(); i++ { + m := ot.Method(i) + println("embedded Method:", m.Name) + } + + // --- Interface type --- + ifaceT := reflect.TypeOf((*Iface)(nil)).Elem() + println("interface NumMethod:", ifaceT.NumMethod()) + for i := 0; i < ifaceT.NumMethod(); i++ { + m := ifaceT.Method(i) + println("interface Method:", m.Name) + } + + // --- Value.Method / Value.MethodByName --- + v := reflect.ValueOf(HasMethods{Name: "hello"}) + mv := v.MethodByName("String") + println("Value.MethodByName(String).IsValid():", mv.IsValid()) + println("Value.MethodByName(String).Kind():", mv.Kind().String()) + + mv = v.MethodByName("Nonexistent") + println("Value.MethodByName(Nonexistent).IsValid():", mv.IsValid()) + + // Value.Method by index + mv = v.Method(0) + println("Value.Method(0).IsValid():", mv.IsValid()) + println("Value.Method(0).Kind():", mv.Kind().String()) + + // Bound method values are never nil. + println("Value.Method(0).IsNil():", mv.IsNil()) + + // Pointer value includes pointer receiver methods. + pv := reflect.ValueOf(&HasMethods{Name: "hello"}) + mv = pv.MethodByName("PtrMethod") + println("ptrValue.MethodByName(PtrMethod).IsValid():", mv.IsValid()) + println("ptrValue.MethodByName(PtrMethod).Kind():", mv.Kind().String()) + + // MethodByName should NOT find unexported methods. + _, ok = t.MethodByName("hidden") + println("MethodByName(hidden):", ok) + + // Method.IsExported should be true for exported methods. + em, _ := t.MethodByName("String") + println("Method(String).IsExported():", em.IsExported()) + + // --- Types with no methods --- + noMethodT := reflect.TypeOf(42) + println("int NumMethod:", noMethodT.NumMethod()) + + _, ok = noMethodT.MethodByName("Foo") + println("int MethodByName(Foo):", ok) +} diff --git a/testdata/reflect.txt b/testdata/reflect.txt index dbd992e870..b243d01ba8 100644 --- a/testdata/reflect.txt +++ b/testdata/reflect.txt @@ -530,4 +530,36 @@ blue gopher v.Interface() method kind: interface int 5 + +type method sets +struct NumMethod: 2 +struct Method: Len +struct Method: String +pointer NumMethod: 3 +pointer Method: Len +pointer Method: PtrMethod +pointer Method: String +MethodByName(String): String true +MethodByName(Len): Len true +MethodByName(Nonexistent): false +pointer MethodByName(PtrMethod): PtrMethod true +struct MethodByName(PtrMethod): false +embedded NumMethod: 2 +embedded Method: InnerMethod +embedded Method: OuterMethod +interface NumMethod: 2 +interface Method: Alpha +interface Method: Beta +Value.MethodByName(String).IsValid(): true +Value.MethodByName(String).Kind(): func +Value.MethodByName(Nonexistent).IsValid(): false +Value.Method(0).IsValid(): true +Value.Method(0).Kind(): func +Value.Method(0).IsNil(): false +ptrValue.MethodByName(PtrMethod).IsValid(): true +ptrValue.MethodByName(PtrMethod).Kind(): func +MethodByName(hidden): false +Method(String).IsExported(): true +int NumMethod: 0 +int MethodByName(Foo): false reflect map interface key ok diff --git a/transform/interface-lowering.go b/transform/interface-lowering.go index 9c1adf7247..33a4810e55 100644 --- a/transform/interface-lowering.go +++ b/transform/interface-lowering.go @@ -90,18 +90,19 @@ type interfaceInfo struct { // pass has been implemented as an object type because of its complexity, but // should be seen as a regular function call (see LowerInterfaces). type lowerInterfacesPass struct { - mod llvm.Module - config *compileopts.Config - builder llvm.Builder - dibuilder *llvm.DIBuilder - difiles map[string]llvm.Metadata - ctx llvm.Context - uintptrType llvm.Type - targetData llvm.TargetData - ptrType llvm.Type - types map[string]*typeInfo - signatures map[string]*signatureInfo - interfaces map[string]*interfaceInfo + mod llvm.Module + config *compileopts.Config + builder llvm.Builder + dibuilder *llvm.DIBuilder + difiles map[string]llvm.Metadata + ctx llvm.Context + uintptrType llvm.Type + targetData llvm.TargetData + ptrType llvm.Type + types map[string]*typeInfo + signatures map[string]*signatureInfo + interfaces map[string]*interfaceInfo + keepMethodNames bool // true when reflection methods can inspect method names } // LowerInterfaces lowers all intermediate interface calls and globals that are @@ -342,17 +343,27 @@ func (p *lowerInterfacesPass) run() error { stripMethodSets = true } + keepAllMethods := p.usesReflectMethods() + if keepAllMethods { + stripMethodSets = false // Method sets are needed. + } + + p.keepMethodNames = keepAllMethods + // Collect all method signatures that appear in any interface type // descriptor. When reflect is imported and method sets are kept, // concrete type method sets are pruned: individual methods not in any // interface are removed, and types that can't fully satisfy at least // one interface have their method sets emptied entirely. // + // When keepAllMethods is true, pruning is disabled and all methods are + // kept. + // // When method sets are stripped entirely (reflect not imported), // methodFilter is nil and filterMethodSet replaces with empty. var methodFilter map[string]struct{} var ifaceMethodSets []map[string]struct{} - if !stripMethodSets { + if !stripMethodSets && !keepAllMethods { methodFilter = make(map[string]struct{}) for _, name := range typeNames { if !strings.HasPrefix(name, "interface:") { @@ -398,9 +409,16 @@ func (p *lowerInterfacesPass) run() error { } var newInitializerFields []llvm.Value + numMethodFieldIdx := -1 // index into newInitializerFields for i := 1; i < numFields; i++ { field := p.builder.CreateExtractValue(initializer, i, "") - field = p.filterMethodSet(field, methodFilter, ifaceMethodSets) + if !keepAllMethods { + field = p.filterMethodSet(field, methodFilter, ifaceMethodSets) + } + // Track where the numMethod field lands in the new slice. + if i == 2 && numMethodsIsI16 { + numMethodFieldIdx = len(newInitializerFields) + } // Strip empty inline method sets for Named, Pointer, and // Struct types. When the method set is pruned to empty, we // remove it and clear the numMethodHasMethodSet flag (bit 15 @@ -408,8 +426,10 @@ func (p *lowerInterfacesPass) run() error { if numMethodsIsI16 && numMethodsConst&numMethodHasMethodSet != 0 && p.isMethodSetType(field.Type()) { elems := field.Type().StructElementTypes() if elems[1].ArrayLength() == 0 { - clearedNumMethods := numMethodsConst & ^uint64(numMethodHasMethodSet) - newInitializerFields[1] = llvm.ConstInt(p.ctx.Int16Type(), clearedNumMethods, false) + if numMethodFieldIdx >= 0 && numMethodFieldIdx < len(newInitializerFields) { + clearedNumMethods := numMethodsConst & ^uint64(numMethodHasMethodSet) + newInitializerFields[numMethodFieldIdx] = llvm.ConstInt(p.ctx.Int16Type(), clearedNumMethods, false) + } continue } } @@ -442,12 +462,45 @@ func (p *lowerInterfacesPass) run() error { t.typecode.EraseFromParentAsGlobal() newGlobal.SetName(typecodeName) t.typecode = newGlobal + } else if !keepAllMethods { + // Types without an external method set (e.g., interface types) + // may still have inline method sets with name pointers that + // should be nulled out when reflection cannot inspect methods. + initializer := t.typecode.Initializer() + if initializer.Type().TypeKind() != llvm.StructTypeKind { + continue + } + numFields := initializer.Type().StructElementTypesCount() + changed := false + var fields []llvm.Value + for i := 0; i < numFields; i++ { + field := p.builder.CreateExtractValue(initializer, i, "") + filtered := p.filterMethodSet(field, methodFilter, ifaceMethodSets) + if filtered.C != field.C { + changed = true + } + fields = append(fields, filtered) + } + if changed { + newInitializer := p.ctx.ConstStruct(fields, false) + t.typecode.SetInitializer(newInitializer) + } } } return nil } +func (p *lowerInterfacesPass) usesReflectMethods() bool { + for fn := p.mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + attr := fn.GetStringAttributeAtIndex(-1, "tinygo-reflect-method") + if !attr.IsNil() && (fn.Linkage() != llvm.InternalLinkage || hasUses(fn)) { + return true + } + } + return false +} + // addTypeMethods reads the method set of the given type info struct. It // retrieves the signatures and the references to the method functions // themselves for later type<->interface matching. @@ -668,7 +721,7 @@ func (p *lowerInterfacesPass) defineInterfaceAssertFunc(fn llvm.Value, itf *inte } // isMethodSetType reports whether ty has the shape of a method-set struct: -// { uintptr, [N x ptr] }. +// { uintptr, [N x { ptr, ptr }] } where each entry is a {signature, name} pair. func (p *lowerInterfacesPass) isMethodSetType(ty llvm.Type) bool { if ty.TypeKind() != llvm.StructTypeKind { return false @@ -680,12 +733,25 @@ func (p *lowerInterfacesPass) isMethodSetType(ty llvm.Type) bool { if elems[0] != p.uintptrType { return false } - return elems[1].TypeKind() == llvm.ArrayTypeKind && elems[1].ElementType() == p.ptrType + if elems[1].TypeKind() != llvm.ArrayTypeKind { + return false + } + entryType := elems[1].ElementType() + if entryType.TypeKind() != llvm.StructTypeKind { + return false + } + entryElems := entryType.StructElementTypes() + return len(entryElems) == 2 && entryElems[0] == p.ptrType && entryElems[1] == p.ptrType +} + +// methodEntryType returns the LLVM type for a method set entry: { ptr, ptr }. +func (p *lowerInterfacesPass) methodEntryType() llvm.Type { + return p.ctx.StructType([]llvm.Type{p.ptrType, p.ptrType}, false) } // extractMethodSigs returns the names of method signature globals inside a -// method-set field ({ uintptr, [N x ptr] }). Returns nil if field is not a -// method set. +// method-set field ({ uintptr, [N x {ptr, ptr}] }). Returns nil if field is +// not a method set. The signature is the first element of each pair. func (p *lowerInterfacesPass) extractMethodSigs(field llvm.Value) []string { if !p.isMethodSetType(field.Type()) { return nil @@ -694,7 +760,8 @@ func (p *lowerInterfacesPass) extractMethodSigs(field llvm.Value) []string { n := methodArray.Type().ArrayLength() sigs := make([]string, 0, n) for j := range n { - sig := p.builder.CreateExtractValue(methodArray, j, "") + pair := p.builder.CreateExtractValue(methodArray, j, "") + sig := p.builder.CreateExtractValue(pair, 0, "") sig = stripPointerCasts(sig) sigs = append(sigs, sig.Name()) } @@ -717,12 +784,13 @@ func (p *lowerInterfacesPass) filterMethodSet(field llvm.Value, keepSigs map[str methodArray := p.builder.CreateExtractValue(field, 1, "") numMethods := methodArray.Type().ArrayLength() + entryType := p.methodEntryType() // Strip mode: replace with empty method set. if keepSigs == nil { return p.ctx.ConstStruct([]llvm.Value{ llvm.ConstInt(p.uintptrType, 0, false), - llvm.ConstArray(p.ptrType, nil), + llvm.ConstArray(entryType, nil), }, false) } @@ -730,18 +798,19 @@ func (p *lowerInterfacesPass) filterMethodSet(field llvm.Value, keepSigs map[str return field } - // Extract all methods and their signature names. - type methodEntry struct { - value llvm.Value - name string + // Extract all method entries and their signature names. + type methodInfo struct { + pair llvm.Value // the full {sig, name} pair + name string // signature global name (for matching) } - entries := make([]methodEntry, numMethods) + entries := make([]methodInfo, numMethods) nameSet := make(map[string]struct{}, numMethods) for j := range numMethods { - sig := p.builder.CreateExtractValue(methodArray, j, "") + pair := p.builder.CreateExtractValue(methodArray, j, "") + sig := p.builder.CreateExtractValue(pair, 0, "") stripped := stripPointerCasts(sig) name := stripped.Name() - entries[j] = methodEntry{sig, name} + entries[j] = methodInfo{pair, name} nameSet[name] = struct{}{} } @@ -758,25 +827,35 @@ func (p *lowerInterfacesPass) filterMethodSet(field llvm.Value, keepSigs map[str if !implementsAny { return p.ctx.ConstStruct([]llvm.Value{ llvm.ConstInt(p.uintptrType, 0, false), - llvm.ConstArray(p.ptrType, nil), + llvm.ConstArray(entryType, nil), }, false) } - // Prune: keep only methods whose signature appears in keepSigs. + // Prune: keep only method entries whose signature appears in keepSigs. + // When reflection cannot inspect methods, null out name pointers so LLVM + // can eliminate the name string globals. var kept []llvm.Value for _, e := range entries { if _, ok := keepSigs[e.name]; ok { - kept = append(kept, e.value) + if p.keepMethodNames { + kept = append(kept, e.pair) + } else { + sig := p.builder.CreateExtractValue(e.pair, 0, "") + kept = append(kept, p.ctx.ConstStruct([]llvm.Value{ + sig, + llvm.ConstNull(p.ptrType), + }, false)) + } } } - if len(kept) == numMethods { + if len(kept) == numMethods && p.keepMethodNames { return field } return p.ctx.ConstStruct([]llvm.Value{ llvm.ConstInt(p.uintptrType, uint64(len(kept)), false), - llvm.ConstArray(p.ptrType, kept), + llvm.ConstArray(entryType, kept), }, false) } diff --git a/transform/interface-lowering_internal_test.go b/transform/interface-lowering_internal_test.go new file mode 100644 index 0000000000..cbdb2827b8 --- /dev/null +++ b/transform/interface-lowering_internal_test.go @@ -0,0 +1,48 @@ +package transform + +import ( + "testing" + + "tinygo.org/x/go-llvm" +) + +func TestUsesReflectMethods(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + want bool + }{ + { + name: "unused marker", + path: "testdata/reflect-method-unused.ll", + }, + { + name: "used marker", + path: "testdata/reflect-method-used.ll", + want: true, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := llvm.NewContext() + defer ctx.Dispose() + buf, err := llvm.NewMemoryBufferFromFile(tc.path) + if err != nil { + t.Fatal(err) + } + mod, err := ctx.ParseIR(buf) + if err != nil { + t.Fatal(err) + } + defer mod.Dispose() + + p := lowerInterfacesPass{mod: mod} + if got := p.usesReflectMethods(); got != tc.want { + t.Errorf("usesReflectMethods() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/transform/testdata/reflect-method-unused.ll b/transform/testdata/reflect-method-unused.ll new file mode 100644 index 0000000000..e0a59964aa --- /dev/null +++ b/transform/testdata/reflect-method-unused.ll @@ -0,0 +1,6 @@ +define internal void @unused() #0 { +entry: + ret void +} + +attributes #0 = { "tinygo-reflect-method"="" } diff --git a/transform/testdata/reflect-method-used.ll b/transform/testdata/reflect-method-used.ll new file mode 100644 index 0000000000..cd643dfce5 --- /dev/null +++ b/transform/testdata/reflect-method-used.ll @@ -0,0 +1,8 @@ +@used.function = internal constant ptr @used + +define internal void @used() #0 { +entry: + ret void +} + +attributes #0 = { "tinygo-reflect-method"="" }