## Status - Status: Proposed - Scope: define LLGo's link-time semantic method pruning scheme for executable builds - Current rollout scope: prioritize the `exe` build path - Background: the design is inspired by Go's link-time deadcode approach, but this document defines only LLGo's own semantic model, input contract, and implementation direction ## Abstract LLGo needs a link-time method pruning mechanism that is specifically designed for Go semantics. Ordinary `call/ref` reachability can only see direct symbol references, but in Go, whether a method must be kept often also depends on extra semantics such as interface conversions, interface method calls, name-based method lookup, and reflection. Because of that, ordinary DCE alone cannot correctly express which types have entered the dynamic dispatch semantic domain and which method-table slots are actually demanded. The proposed scheme is to model those Go semantics explicitly under a whole-program view and analyze them together with ordinary reachability in one unified multi-round propagation process, ultimately producing a stable result of `type symbol -> live method indexes`. For interface method demands, the matching rule is no longer just "method name + method type"; it additionally requires that the concrete type fully implement the target interface, preventing methods on concrete types that do not actually satisfy the interface from being kept by mistake. This is not a problem that can be solved by relying on the ordinary reference graph alone; it requires explicit modeling of Go semantics at link time and a dedicated method-liveness analysis. ## Problem Definition ### Why ordinary `gc-sections`, ordinary linker DCE, and ordinary IR DCE are not enough Ordinary linkers and ordinary IR DCE rely on explicit reference relationships: - one function calls another function - one global constant references another global symbol - one initializer contains an ordinary reference to a symbol That model is not enough for Go, because method invocation in Go is not a purely direct-reference model. In Go, whether a method must be kept often depends on semantic events such as: 1. whether a concrete type may enter interface dispatch 2. whether a specific interface method slot is actually called 3. whether a method name is requested by `MethodByName` 4. whether reflection forces the analysis into a conservative mode If the compiler does not encode those events explicitly, the linker only sees a pile of "type constants reference method symbols," and the result degenerates into: - as long as the type descriptor is live, methods are likely to stay live with it - it cannot distinguish "listed in a method table" from "actually dynamically callable" - it cannot precisely delete some methods while keeping others at link time ### A Minimal Example Consider the following simplified Go program: ```go package main type Reader interface { Read([]byte) (int, error) } type Writer interface { Write([]byte) (int, error) } type stringReader struct{} func (*stringReader) Len() int { return 0 } func (*stringReader) Read([]byte) (int, error) { return 0, nil } func (*stringReader) Close() error { return nil } func (*stringReader) WriteTo(Writer) (int64, error) { return 0, nil } func ReadAll(r Reader) { _, _ = r.Read(nil) } func main() { ReadAll(&stringReader{}) } ``` The program is semantically simple: 1. the `Reader` interface requires only one method: `Read([]byte) (int, error)` 2. the concrete type `stringReader` still has other methods such as `Len`, `Close`, and `WriteTo` 3. the program boxes `stringReader` into `Reader` and calls `Read` through the interface One point needs to be stated up front: these are not a bunch of irrelevant references artificially inserted by the compiler. They are capabilities that the Go runtime actually needs. Once a concrete type may participate in interface-based dynamic dispatch, the runtime must be able to answer: 1. what methods this concrete type has 2. what each method is called 3. what the signature of each method is 4. which function entry point to jump to for interface calls 5. which function entry point to jump to for direct method calls As a result, types with method sets naturally carry an `abi.Method` table inside `abi.Type` / `abi.UncommonType`. Each entry in that table carries the method name, the method type, and the corresponding `IFn` / `TFn`. In IR, `IFn` / `TFn` are real function addresses. In current LLGo `v0.13.0`, this method table is still emitted at compile time as the complete method set in one shot. The current implementation lives in `ssa/abitype.go`: 1. `abiType` builds the overall layout `abi.Type + abi.UncommonType + [N]abi.Method` for types with methods 2. `abiUncommonMethods` walks the full method set via `mset.Len()`, constructs `[N]abi.Method` entry by entry, and writes every `Name / MType / IFn / TFn` So if the program above is compiled to current LLGo LLVM IR, you will see that although the only semantically demanded slot is `Reader.Read`, the pointer type descriptor of `stringReader` still carries the full method table: ```llvm @"*_llgo_github.com/goplus/llgo/cl/_testgo/reader.stringReader" = weak_odr constant { %"github.com/goplus/llgo/runtime/abi.PtrType", %"github.com/goplus/llgo/runtime/abi.UncommonType", [10 x %"github.com/goplus/llgo/runtime/abi.Method"] } { ..., [10 x %"github.com/goplus/llgo/runtime/abi.Method"] [ { ..., ptr @"github.com/goplus/llgo/cl/_testgo/reader.(*stringReader).Len", ptr @"github.com/goplus/llgo/cl/_testgo/reader.(*stringReader).Len" }, { ..., ptr @"github.com/goplus/llgo/cl/_testgo/reader.(*stringReader).Read", ptr @"github.com/goplus/llgo/cl/_testgo/reader.(*stringReader).Read" }, { ..., ptr @"github.com/goplus/llgo/cl/_testgo/reader.(*stringReader).ReadAt", ptr @"github.com/goplus/llgo/cl/_testgo/reader.(*stringReader).ReadAt" }, ..., { ..., ptr @"github.com/goplus/llgo/cl/_testgo/reader.(*stringReader).WriteTo", ptr @"github.com/goplus/llgo/cl/_testgo/reader.(*stringReader).WriteTo" } ] } ``` At the same time, in an ordinary function body you can also see that the interface boxing directly references that type descriptor: ```llvm %2 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.NewItab"( ptr @"_llgo_iface$uycIKA3bbxRhudEjW1hHKWKdLqHQsCVy8NdW1bkQmNw", ptr @"*_llgo_github.com/goplus/llgo/cl/_testgo/reader.stringReader") ``` From the perspective of ordinary `gc-sections` or ordinary `lld`, this is a very direct strong reference chain: ```text main / ReadAll -> stringReader's abi.Type -> abi.UncommonType / [10 x abi.Method] -> Name / MType / IFn / TFn inside each abi.Method -> Len / Read / ReadAt / ... / WriteTo ``` That is the problem: 1. the linker only sees "a live global constant containing real function pointers and type pointers" 2. it does not know that semantically the only demanded slot is `Reader.Read` 3. therefore, once `@"*_llgo_github.com/goplus/llgo/cl/_testgo/reader.stringReader"` becomes reachable, all methods in the table are kept together 4. this is not an `lld` bug; ordinary reference graphs simply cannot express "only some slots in the same method table actually need to be kept" That is exactly why LLGo needs extra semantic analysis: 1. first determine which types actually enter the interface semantic domain under a whole-program view 2. then determine which interface slots are actually demanded 3. finally retain only the corresponding method indexes instead of keeping the whole `abi.Method` table alive ### The Core Problem There is only one question we really want to answer: **For a given type's method table, which methods are actually required by runtime semantics?** The example above already shows the problem: 1. once a concrete type descriptor becomes reachable, the ordinary reference graph sees the entire `abi.Method` table as reachable 2. but what we really need is not "the type is live, therefore all its methods are live" 3. what we really need is: the type itself may stay live, while the method table must be partially prunable according to runtime semantics In other words, link time must support "keep the type, prune the method table partially," and the ordinary reference graph cannot express that capability. Go's existing link-time deadcode design already demonstrates that this problem cannot be solved by pushing harder on an ordinary reference graph. Go solves it by making the linker understand extra semantics related to types, interfaces, and reflection, and then running a dedicated method-liveness analysis under a whole-program view. LLGo will follow the same direction: it will not try to keep forcing this problem back into the ordinary reference graph. Instead, it will introduce a separate link-time semantic analysis model. ## Relation to Go's Design This proposal is explicitly inspired by Go's link-time deadcode approach, but it does not aim to reproduce Go's internal implementation details. What LLGo wants to absorb falls into two categories: 1. the direction of the solution: bring interface conversions, interface method calls, name-based method lookup, and conservative reflection mode into link-time analysis 2. the analysis method itself: perform semantic reachability analysis under a whole-program view with repeated propagation until convergence, rather than depending only on the ordinary reference graph What LLGo does not want to copy is: 1. Go's object format and loader structure 2. Go's internal relocation encoding 3. the exact layering of Go's linker implementation The biggest engineering difference between the two schemes is where the semantic input is carried: 1. Go stores related semantic information in its `goobj` / relocation / type data / symbol attribute system, such as "type enters interface semantics," interface method demands, name-based method lookup, and conservative reflection markers 2. LLGo emits that semantic information explicitly into named metadata on `llvm.Module`, then reads it back uniformly during the link build stage At a higher semantic level, however, the two are aligned: 1. both collect ordinary reachability edges and semantic events under a global view 2. both perform repeated propagation until convergence at whole-program scope 3. both attempt to answer the same question: which slots in a given concrete type's method table must ultimately be retained LLGo's current design also explicitly adopts and improves one conservative point in Go: 1. interface demands must not be matched by "method name + method type" alone 2. the concrete type must also fully implement the target interface This avoids keeping methods on partially implementing types just because their signatures happen to match. Based on this common direction and these differences, LLGo will use an architecture of "per-package semantic emission, global unified analysis, and result rewrite." ## Analysis Architecture The core characteristics of this design are: 1. when compiling a single package, emit only the semantic information that is already statically knowable inside that package 2. do not decide at per-package compile time whether a method is ultimately retained 3. wait until the whole-program `[]llvm.Module` set is available, then analyze method reachability from a global view Exactly which semantics must be marked during per-package compilation, and how the global stage consumes those semantics to decide method liveness, are described later. Here we first present the overall architecture. The pipeline can be summarized as: ```mermaid flowchart TD A["Package A<br/>ssa/cl compile<br/>module A + metadata"] B["Package B<br/>ssa/cl compile<br/>module B + metadata"] C["Package C<br/>ssa/cl compile<br/>module C + metadata"] A --> D["Build-stage aggregation<br/>whole-program LLVM modules"] B --> D C --> D D --> E["graph<br/>extract ordinary edges<br/>and semantic input"] E --> F["analyze<br/>global method reachability analysis"] F --> G["result<br/>live method indexes by type symbol"] G --> H["rewrite<br/>copy and prune strong abi.Type"] ``` More concretely: 1. `ssa/cl` still compiles packages independently, but additionally writes semantics such as interface conversions, interface method demands, name-based method lookup, conservative reflection markers, and method-slot information into the current package's `llvm.Module` 2. once the build stage has collected all package `[]llvm.Module` values, `graph` scans ordinary reference edges and those metadata rows across all packages and aggregates them into a single whole-program input 3. `analyze` runs repeated propagation under a whole-program view and produces only one stable result: ```text type symbol -> live method indexes ``` 4. once that result is computed, the analysis itself is finished; `rewrite` merely consumes the result, copies weak `abi.Type` definitions, prunes method tables, emits strong definitions, and lets final linking override the originals This separation has two important layers: 1. ordinary reference edges still come from ordinary IR content such as function bodies, global initializers, and constant expressions 2. extra semantics related to interfaces, reflection, and method tables are emitted explicitly during per-package compilation and consumed uniformly during the global stage ### Goals and Boundaries The goals of this proposal are: 1. define an official link-time semantic method-pruning scheme for LLGo 2. stably produce `type symbol -> live method indexes` during analysis 3. rewrite pruned strong `abi.Type` definitions from that result This proposal does not currently include: 1. reproducing Go's internal object / relocation / loader structure 2. covering every build mode in one go 3. discussing generic IR optimizations such as SCCP, CFG DCE, or constant propagation at the same time ## Whole-Program Information Model This section defines the normalized whole-program information required by the algorithm itself, denoted as `WholeProgramInfo`. Here, whole-program does not mean "the information model of one package." It means the final aggregate input after combining all packages that participate in the link. In other words, every field in `WholeProgramInfo` should be understood as the result of merging all per-package contributions of ordinary edges, type propagation edges, interface information, method-slot information, and semantic demands. The boundary of `analyze` is not LLVM IR, nor metadata themselves, but this normalized `WholeProgramInfo`. As long as the outside world can provide equivalent information, the algorithm does not care which object format, scanning process, or intermediate carrier produced it. In the current LLGo implementation, this `WholeProgramInfo` is aggregated by the build stage from all package LLVM modules. That also means the algorithm itself would not need to change if, in the future, pre-extracted per-package semantic information were cached and then assembled directly into `WholeProgramInfo` during the whole-program stage. That kind of caching is not part of the current implementation. The current baseline still extracts uniformly from `ssa -> llvm.Module` outputs. Therefore, the keys and values in the definitions below describe whole-program aggregated semantics, not the local view of any single package. Its shape is: ```go type Symbol = string type MethodSig struct { Name string MType Symbol } type MethodSlot struct { Index int Sig MethodSig IFn Symbol TFn Symbol } type IfaceMethodDemand struct { Target Symbol Sig MethodSig } type WholeProgramInfo struct { OrdinaryEdges map[Symbol]map[Symbol]struct{} TypeChildren map[Symbol]map[Symbol]struct{} InterfaceInfo map[Symbol][]MethodSig UseIface map[Symbol][]Symbol UseIfaceMethod map[Symbol][]IfaceMethodDemand MethodInfo map[Symbol][]MethodSlot UseNamedMethod map[Symbol][]string ReflectMethod map[Symbol]struct{} } ``` Here, `MethodSig` is not a top-level input item. It is the shared method-signature representation used by both `MethodInfo` and `InterfaceInfo`. Under the current baseline: 1. `Name` must be the normalized method name 2. exported methods use the plain method name directly, for example `Read` 3. unexported methods use a normalized name that includes the full package path 4. `MType` is the symbol of the method type descriptor ### `OrdinaryEdges` `OrdinaryEdges` is the ordinary symbol reachability graph. What the outside world needs to provide here is the ordinary reference graph formed by ordinary `call/ref`. For example: ```go func helper() {} func main() { helper() } ``` In this example: 1. `main -> helper` goes into `OrdinaryEdges` 2. this is a direct ordinary call 3. it is not interface semantics and needs no extra metadata That means: 1. the key is the source symbol 2. the value is the set of target symbols directly reachable from that symbol through ordinary `call/ref` Likewise, direct struct-method calls also belong to `OrdinaryEdges`. For example: ```go type S struct{} func (*S) M() {} func (S) N() {} func main() { var s S s.M() s.N() } ``` In this example: 1. `main -> (*S).M` and `main -> S.N` are still ordinary edges 2. because IR can already see the concrete method symbols directly 3. although the source says `s.M()`, `s` is addressable, so it eventually lowers to `(*S).M` Using the symbol shapes that current LLGo actually emits, the structured form is: ```go OrdinaryEdges = map[Symbol]map[Symbol]struct{}{ "github.com/goplus/llgo/doc/tt.main": { "github.com/goplus/llgo/doc/tt.(*S).M": {}, "github.com/goplus/llgo/doc/tt.S.N": {}, }, } ``` Interface method calls are different. For example: ```go type I interface{ M() } func call(i I) { i.M() } ``` Here we do not record `call -> some concrete implementation of M` directly in `OrdinaryEdges`, because the IR does not contain a unique concrete target symbol. This kind of call is expressed separately later through extra semantic input. ### `TypeChildren` `TypeChildren` records direct propagation edges between types. Its meaning is: 1. if a source type has entered the interface/reflection-related analysis domain 2. then child types observable through runtime type data must enter the same analysis domain as well Note: 1. the recorded edges are direct propagation edges, not a pre-expanded transitive closure 2. the recursive closure is expanded during analysis by following these edges This is needed because: 1. once a type may continue to be observed through interface values or reflection 2. the program may later obtain child types that appear inside it 3. the method tables of those child types may also affect which dynamic methods must ultimately be kept 4. therefore the analyzer must know about this child-type propagation relationship For example: ```go type Inner struct{} func (Inner) M() {} type Outer struct { F Inner } func main() { var x any = Outer{} _ = x } ``` In structured form: ```go TypeChildren = map[Symbol]map[Symbol]struct{}{ "_llgo_github.com/goplus/llgo/demo.Outer": { "_llgo_github.com/goplus/llgo/demo.Inner": {}, }, } ``` ### `MethodInfo` `MethodInfo` describes the method-slot table of a concrete type. It is not "the set of already demanded methods." It is "the set of candidate slots that the analyzer may mark live." When a slot is matched for the first time, the analyzer continues by pulling in its `MType/IFn/TFn`. If a type has no methods, it may simply be absent from `MethodInfo`. For example, consider `demo/file.go`: ```go package demo type File struct{} func (*File) Read([]byte) (int, error) { return 0, nil } func (*File) Close() error { return nil } ``` Assuming the compiled package path is `github.com/goplus/llgo/demo`, its corresponding `MethodInfo` can be understood as: ```go MethodInfo["_llgo_github.com/goplus/llgo/demo.File"] = []MethodSlot{ { Index: 0, Sig: MethodSig{ Name: "Read", MType: "_llgo_func$A1...", }, IFn: "github.com/goplus/llgo/demo.(*File).Read", TFn: "github.com/goplus/llgo/demo.File.Read", }, { Index: 1, Sig: MethodSig{ Name: "Close", MType: "_llgo_func$B2...", }, IFn: "github.com/goplus/llgo/demo.(*File).Close", TFn: "github.com/goplus/llgo/demo.File.Close", }, } ``` Where: 1. the key is the concrete type symbol 2. the value is the `[]MethodSlot` array ordered exactly like the type's `abi.Method` table 3. each entry is one `MethodSlot`, and its `Sig` is a `MethodSig` 4. `Index` must exactly match the final slot index in the `abi.Method` table 5. under the current baseline, `IFn` and `TFn` always exist, though they may be equal for some methods ### `InterfaceInfo` `InterfaceInfo` describes the method set of an interface. For example, consider `demo/reader.go`: ```go package demo type Reader interface { Read([]byte) (int, error) Close() error } ``` Assuming the compiled package path is `github.com/goplus/llgo/demo`, its corresponding `InterfaceInfo` can be understood as: ```go InterfaceInfo["_llgo_github.com/goplus/llgo/demo.Reader"] = []MethodSig{ { Name: "Read", MType: "_llgo_func$A1...", }, { Name: "Close", MType: "_llgo_func$B2...", }, } ``` Where: 1. the key is the interface type symbol 2. the value is the set of method signatures required by that interface, expressed here as `[]MethodSig` 3. semantically, it means "which methods this interface requires in full"; the adaptation layer is responsible for deduplication and normalization ### `UseIface` `UseIface` is grouped by `Owner`. The value is the set of concrete types that must enter `UsedInIface` once that `Owner` becomes reachable. For example, consider `demo/reader.go`: ```go package demo type Reader interface { Read([]byte) (int, error) } type File struct{} func (*File) Read([]byte) (int, error) { return 0, nil } func main() { var r Reader = &File{} _ = r } ``` Assuming the compiled package path is `github.com/goplus/llgo/demo`, the corresponding `UseIface` can be understood as: ```go UseIface = map[Symbol][]Symbol{ "github.com/goplus/llgo/demo.main": []Symbol{ "*_llgo_github.com/goplus/llgo/demo.File", }, } ``` ### `UseIfaceMethod` `UseIfaceMethod` is grouped by `Owner`. The value is the set of interface method demands produced once that `Owner` becomes reachable. For example, consider `demo/reader.go`: ```go package demo type Reader interface { Read([]byte) (int, error) } func consume(r Reader, p []byte) { _, _ = r.Read(p) } ``` Assuming the compiled package path is `github.com/goplus/llgo/demo`, the corresponding `UseIfaceMethod` can be understood as: ```go UseIfaceMethod = map[Symbol][]IfaceMethodDemand{ "github.com/goplus/llgo/demo.consume": []IfaceMethodDemand{ { Target: "_llgo_github.com/goplus/llgo/demo.Reader", Sig: MethodSig{ Name: "Read", MType: "_llgo_func$A1...", }, }, }, } ``` ### `UseNamedMethod` `UseNamedMethod` is grouped by `Owner`. The value is the set of method-name demands produced once that `Owner` becomes reachable. Only when the method name is statically known at compile time does a `MethodByName`-like operation go here. If the name is not a compile-time constant, the case degrades to `ReflectMethod` described later. For example, consider `demo/lookup.go`: ```go package demo func lookup(v any) { _, _ = reflect.TypeOf(v).MethodByName("ServeHTTP") } ``` Assuming the compiled package path is `github.com/goplus/llgo/demo`, the corresponding `UseNamedMethod` can be understood as: ```go UseNamedMethod = map[Symbol][]string{ "github.com/goplus/llgo/demo.lookup": []string{ "ServeHTTP", }, } ``` ### `ReflectMethod` `ReflectMethod` is a set of owners. Once any `Owner` in that set becomes reachable, the analysis must enter conservative reflection mode. Typical sources include `reflect.Type.Method`, `reflect.Value.Method`, and `MethodByName` when the method name cannot be determined statically at compile time. For example, consider `demo/value.go`: ```go package demo func valueByIndex(v any) { _ = reflect.ValueOf(v).Method(0) } ``` Assuming the compiled package path is `github.com/goplus/llgo/demo`, the corresponding `ReflectMethod` can be understood as: ```go ReflectMethod = map[Symbol]struct{}{ "github.com/goplus/llgo/demo.valueByIndex": {}, } ``` Once this input is hit, the current baseline semantics are: for all concrete types in `UsedInIface`, retain the slots corresponding to exported methods. ## Core Algorithm ### Input The algorithm takes two inputs: 1. a whole-program `WholeProgramInfo` 2. a set of link roots Where: 1. `WholeProgramInfo` is the aggregate result of merging semantic information from all packages participating in the current link 2. roots are the starting points of ordinary reachability, such as `main`, `_start`, and `__main_argc_argv` ### Output Shape The analyzer outputs: ```go type Result map[string]map[int]struct{} ``` Semantically: 1. the key is a concrete type symbol 2. the value is the set of method-slot indexes that must be retained for that type This is the final result of the analysis stage. Once this result is available, the algorithm stops. It does not continue into ABI type rewriting or strong-symbol generation; those belong to the later rewrite stage. ### Liveness Loop The core logic can be summarized as a loop of "ordinary flood + semantic-demand propagation + method-slot marking": 1. start from roots and propagate ordinary reachability along `OrdinaryEdges` 2. whenever an `owner` becomes reachable, read its semantic demands from `UseIface`, `UseIfaceMethod`, `UseNamedMethod`, and `ReflectMethod` 3. `UseIface` adds concrete types into `UsedInIface` and continues propagation along `TypeChildren` 4. `UseIfaceMethod` accumulates interface method demands for interfaces; `UseNamedMethod` accumulates exact method-name demands; `ReflectMethod` turns on conservative reflection mode 5. then, for all concrete types already in `UsedInIface`, inspect their `MethodInfo` slots and decide which ones should be kept 6. the most important rule is: an interface method demand is allowed to match a slot on a concrete type only when that concrete type fully implements the target interface; identical method name and method type are not enough on their own 7. once a slot becomes live for the first time, its `MType`, `IFn`, and `TFn` are reinserted into the ordinary reachability graph 8. repeat until there are no new ordinary reachable symbols, no new semantic demands, and no new live method slots The final result is: ```text type symbol -> live method indexes ``` ## Examples ### Example 1: Interface Calls Keep Only Truly Demanded Methods Consider: ```go type Reader interface { Read([]byte) (int, error) } type File struct{} func (File) Read([]byte) (int, error) { return 0, nil } func (File) Close() error { return nil } func consume(r Reader, p []byte) { _, _ = r.Read(p) } func main() { consume(File{}, nil) } ``` Before analysis starts, the whole-program input for this example can be approximated as: ```go roots := []Symbol{ "github.com/goplus/llgo/demo.main", } OrdinaryEdges = map[Symbol]map[Symbol]struct{}{ "github.com/goplus/llgo/demo.main": { "github.com/goplus/llgo/demo.consume": {}, }, } UseIface = map[Symbol][]Symbol{ "github.com/goplus/llgo/demo.main": { "_llgo_github.com/goplus/llgo/demo.File", }, } UseIfaceMethod = map[Symbol][]IfaceMethodDemand{ "github.com/goplus/llgo/demo.consume": { { Target: "_llgo_github.com/goplus/llgo/demo.Reader", Sig: MethodSig{ Name: "Read", MType: "_llgo_func$readsig", }, }, }, } InterfaceInfo = map[Symbol][]MethodSig{ "_llgo_github.com/goplus/llgo/demo.Reader": { { Name: "Read", MType: "_llgo_func$readsig", }, }, } MethodInfo = map[Symbol][]MethodSlot{ "_llgo_github.com/goplus/llgo/demo.File": { { Index: 0, Sig: MethodSig{ Name: "Read", MType: "_llgo_func$readsig", }, IFn: "github.com/goplus/llgo/demo.(*File).Read", TFn: "github.com/goplus/llgo/demo.File.Read", }, { Index: 1, Sig: MethodSig{ Name: "Close", MType: "_llgo_func$closesig", }, IFn: "github.com/goplus/llgo/demo.(*File).Close", TFn: "github.com/goplus/llgo/demo.File.Close", }, }, } ``` Here `TypeChildren`, `UseNamedMethod`, and `ReflectMethod` are empty. The global analysis proceeds as follows: 1. the initial root is `main`, so the analyzer first places `github.com/goplus/llgo/demo.main` into the reachable set 2. the first `flood` propagates along `OrdinaryEdges`, making `github.com/goplus/llgo/demo.consume` reachable as well 3. during semantic activation, `UseIface[main]` matches, so `_llgo_github.com/goplus/llgo/demo.File` enters `UsedInIface` 4. in the same round, `UseIfaceMethod[consume]` also matches, producing one interface method demand: `Reader.Read` 5. the analysis then enters the method-marking phase; it checks only concrete types already in `UsedInIface`, which here is `File` 6. `File` has two slots in `MethodInfo`: `Read` and `Close` 7. for the `Read` slot, its `MethodSig` matches the current interface demand `Reader.Read`, and `File` fully implements `Reader`, so the slot becomes live 8. once the `Read` slot becomes live for the first time, the result records `File -> [0]`, and the slot's `MType/IFn/TFn` are put back into ordinary reachability propagation 9. the `Close` slot matches no interface demand, no name demand, and no conservative-reflection condition, so it is not kept 10. the next round runs `flood` again to process the newly added `MType/IFn/TFn`; if those newly reachable symbols trigger more semantic events, the loop continues 11. when ordinary flood, semantic activation, and method marking all stop producing new information, the analysis converges The final result is: ```text _llgo_github.com/goplus/llgo/demo.File: [0] ``` Where `0` is the slot index of `Read`. ### Example 2: Partially Implementing Types Must Not Be Kept by Mistake Consider: ```go type A struct{} func (*A) Foo() {} func (*A) Bar() {} type B struct{} func (*B) Foo() {} func (*B) Bar() {} func (*B) Car() {} type BI interface { Foo() Bar() Car() } func callBI(x BI) { x.Foo() x.Bar() } func main() { var _ any = &A{} callBI(&B{}) } ``` In this example: 1. `A` does enter the interface semantic domain, because `main` contains a conversion `&A{} -> any` 2. `B` also enters the interface semantic domain, because `main` passes `&B{}` to `BI` 3. however, the actual interface calls happen only inside `callBI`, and they only call `Foo` and `Bar` From the algorithm's point of view, the whole-program input can be approximated as: ```go roots := []Symbol{ "github.com/goplus/llgo/demo.main", } OrdinaryEdges = map[Symbol]map[Symbol]struct{}{ "github.com/goplus/llgo/demo.main": { "github.com/goplus/llgo/demo.callBI": {}, }, } UseIface = map[Symbol][]Symbol{ "github.com/goplus/llgo/demo.main": { "*_llgo_github.com/goplus/llgo/demo.A", "*_llgo_github.com/goplus/llgo/demo.B", }, } UseIfaceMethod = map[Symbol][]IfaceMethodDemand{ "github.com/goplus/llgo/demo.callBI": { { Target: "_llgo_github.com/goplus/llgo/demo.BI", Sig: MethodSig{ Name: "Foo", MType: "_llgo_func$foosig", }, }, { Target: "_llgo_github.com/goplus/llgo/demo.BI", Sig: MethodSig{ Name: "Bar", MType: "_llgo_func$barsig", }, }, }, } InterfaceInfo = map[Symbol][]MethodSig{ "_llgo_github.com/goplus/llgo/demo.BI": { {Name: "Foo", MType: "_llgo_func$foosig"}, {Name: "Bar", MType: "_llgo_func$barsig"}, {Name: "Car", MType: "_llgo_func$carsig"}, }, } MethodInfo = map[Symbol][]MethodSlot{ "*_llgo_github.com/goplus/llgo/demo.A": { {Index: 0, Sig: MethodSig{Name: "Foo", MType: "_llgo_func$foosig"}, IFn: "github.com/goplus/llgo/demo.(*A).Foo", TFn: "github.com/goplus/llgo/demo.(*A).Foo"}, {Index: 1, Sig: MethodSig{Name: "Bar", MType: "_llgo_func$barsig"}, IFn: "github.com/goplus/llgo/demo.(*A).Bar", TFn: "github.com/goplus/llgo/demo.(*A).Bar"}, }, "*_llgo_github.com/goplus/llgo/demo.B": { {Index: 0, Sig: MethodSig{Name: "Foo", MType: "_llgo_func$foosig"}, IFn: "github.com/goplus/llgo/demo.(*B).Foo", TFn: "github.com/goplus/llgo/demo.(*B).Foo"}, {Index: 1, Sig: MethodSig{Name: "Bar", MType: "_llgo_func$barsig"}, IFn: "github.com/goplus/llgo/demo.(*B).Bar", TFn: "github.com/goplus/llgo/demo.(*B).Bar"}, {Index: 2, Sig: MethodSig{Name: "Car", MType: "_llgo_func$carsig"}, IFn: "github.com/goplus/llgo/demo.(*B).Car", TFn: "github.com/goplus/llgo/demo.(*B).Car"}, }, } ``` The global analysis proceeds as follows: 1. the initial root is `main`, so the analyzer first places `github.com/goplus/llgo/demo.main` into the reachable set 2. the first `flood` propagates along `OrdinaryEdges`, making `github.com/goplus/llgo/demo.callBI` reachable as well 3. during semantic activation, `UseIface[main]` matches, so both `*_llgo_github.com/goplus/llgo/demo.A` and `*_llgo_github.com/goplus/llgo/demo.B` enter `UsedInIface` 4. in the same round, `UseIfaceMethod[callBI]` also matches, producing two interface method demands: `BI.Foo` and `BI.Bar` 5. the analysis then enters the method-marking phase; it inspects all concrete types already in `UsedInIface`, namely `A` and `B` 6. if we matched only by `Name + MType`, then `A.Foo`, `A.Bar`, `B.Foo`, and `B.Bar` would all satisfy the demand; this is exactly the false positive we want to avoid 7. the current design does not do that directly; it first uses `InterfaceInfo[BI]` to decide whether a concrete type fully implements `BI` 8. for `A`, its own method set contains only `Foo` and `Bar`, while `InterfaceInfo[BI]` requires `Foo`, `Bar`, and `Car`; therefore `A` does not fully implement `BI` 9. therefore, even though `A.Foo` and `A.Bar` both have matching `MethodSig`, they cannot satisfy the interface demands `BI.Foo` or `BI.Bar`, and they are not kept 10. for `B`, its method set completely covers `InterfaceInfo[BI]`, so `B` is judged to fully implement `BI` 11. under that condition, `B.Foo` and `B.Bar` are now allowed to satisfy the current demands, so those two slots become live and their `MType/IFn/TFn` are pushed back into ordinary reachability propagation 12. `B.Car` is one of the methods needed for `B` to fully implement `BI`, but the program never issues a `BI.Car` demand, so it is not kept merely because it participated in interface-implementation checking 13. later rounds continue running `flood -> semantic activation -> method marking` until all three phases stop producing new information The final result contains only the `Foo` and `Bar` slots on `B`. It does not contain partial methods on `A`, and it does not keep `B.Car` merely because `B` fully implements `BI`. In other words, the matching rule here is: 1. first check whether the concrete type fully covers `InterfaceInfo[target]` 2. only if it does, a slot on that type is eligible to satisfy the interface method demand 3. then check whether that slot's own `MethodSig` actually matches the current demand So the real keep condition is not "method name and method type happen to match." It is: ```text the concrete type fully implements the target interface AND the slot corresponds exactly to the currently demanded interface method ``` ### Example 3: `MethodByName` Produces Exact Name Demands Consider: ```go func lookup(v any) { _, _ = reflect.TypeOf(v).MethodByName("ServeHTTP") } ``` If `"ServeHTTP"` is a compile-time constant, then: 1. the compiler emits `UseNamedMethod(owner, "ServeHTTP")` 2. once `owner` becomes reachable 3. every slot named `ServeHTTP` on types in `UsedInIface` must be kept This is more precise than conservative reflection mode, because: 1. the demand is triggered by an exact name 2. rather than "all exported methods must be kept" ### Example 4: Conservative Reflection Mode Expands the Live Set Consider: ```go func valueByIndex(v any) { _ = reflect.ValueOf(v).Method(0) } ``` Here the target method name cannot be known statically at compile time, so: 1. the compiler emits `ReflectMethod(owner)` 2. once `owner` becomes reachable, the analysis enters conservative reflection mode 3. for all types in `UsedInIface`, slots corresponding to exported methods must be kept This is a conservative but necessary degradation. ### Example 5: `UsedInIface` Propagates Along Child Types Consider a type `Outer` whose runtime type data can continue to reach a child type `Inner`. If: 1. `Outer` enters `UsedInIface` Then: 1. `Inner` must also enter `UsedInIface` 2. otherwise methods on `Inner` that may later be reached via reflection or further interface conversion would be missed Therefore, `TypeChildren` exists not for ordinary graph traversal, but to express this semantic propagation explicitly. ## Current Realization In the current branch, the realization is split into two stages: 1. analysis stage 2. rewrite stage ### Analysis Stage The goal of the analysis stage is to aggregate whole-program input and compute: ```text type symbol -> live method indexes ``` In the current branch, the analyzer input is not produced by one standalone file. It is aggregated during the build stage from two sources. The first source is the `llgo.xxx` named metadata explicitly emitted by `ssa/cl` during per-package compilation. Among them, `llgo.interfaceinfo` and `llgo.methodinfo` are currently encoded as "one method / one slot per row" rather than packing a whole group into one row. This makes parsing in `graph` more direct. 1. `llgo.useiface` row shape: `{ owner, concrete type }` 2. `llgo.useifacemethod` row shape: `{ owner, interface type, normalized method name, mtype }` 3. `llgo.interfaceinfo` row shape: `{ interface type, method name, mtype }` 4. `llgo.methodinfo` row shape: `{ concrete type, index, method name, mtype, ifn, tfn }` if a type has no methods, it emits no `llgo.methodinfo` rows 5. `llgo.usenamedmethod` row shape: `{ owner, normalized method name }` 6. `llgo.reflectmethod` row shape: `{ owner }` These metadata express interface conversions, interface method demands, complete interface method sets, concrete-type method slots, name-based method lookup, and conservative reflection mode. The second source is not emitted through `llgo.xxx`. It is extracted directly by the build stage from ordinary contents of each package's `llvm.Module`: 1. `OrdinaryEdges` scanned from ordinary LLVM IR contents such as function bodies, global initializers, and constant operands 2. `TypeChildren` scanned recursively from ABI type globals in each package After collecting all package `llvm.Module` values, the build stage merges those two parts into one whole-program `WholeProgramInfo` and then feeds it into the liveness algorithm defined above. ### Rewrite Stage Once the analysis stage finishes, the answer to "which methods must be kept" is already stable. The rewrite stage no longer performs any semantic reasoning. It only turns that answer into final link artifacts. Concretely, the rewrite stage needs to: 1. find all ABI types that need pruning 2. copy each original weak definition into a new strong definition 3. prune its method-table array according to `Result[type]` 4. place the new definitions into a separate module 5. override the original weak definitions with the strong ones during final linking This split has three direct benefits: 1. the algorithm definition stays clean, and the analysis result does not get mixed with rewrite engineering 2. the analyzer can be tested independently, without depending on strong-symbol generation 3. the rewrite stage can be validated independently as long as `Result` is correct --- package semantic metadata emit https://github.com/goplus/llgo/pull/1728