Skip to content

[WIP] feat(size opt): Go linktime like DCE#1618

Draft
luoliwoshang wants to merge 172 commits intoxgo-dev:mainfrom
luoliwoshang:codex/reloc-context-dce
Draft

[WIP] feat(size opt): Go linktime like DCE#1618
luoliwoshang wants to merge 172 commits intoxgo-dev:mainfrom
luoliwoshang:codex/reloc-context-dce

Conversation

@luoliwoshang
Copy link
Copy Markdown
Contributor

@luoliwoshang luoliwoshang commented Feb 6, 2026

Implement #1587

Background and Motivation

The current llgo build pipeline compiles each package/file into .o (or .bc) and then performs a final link.
This works well for explicit symbol references (functions/globals), but it is weak for implicit Go semantic dependencies, which leads to larger final binaries.

We want to follow Go linker's model: reachability analysis + method-table sentinel/backfill.
The first priority is to mark the right symbols; pruning/backfilling strategy can evolve iteratively.

Goals

  • Reduce final binary size while preserving behavior compatibility.
  • Handle Go semantic dependencies that default ELF/COFF linkers cannot see.
  • Build a solid foundation for future whole-program bitcode passes (marking correctness first, then iterative pruning/backfilling).

Current State and Pain Points

  • Build flow: each .go/.c produces an independent object, then everything is linked together.
  • Normal public symbols can be pruned via ordinary relocations.
  • Go features (interfaces/generics/reflect/type metadata) introduce implicit references.
  • In abi.Type -> abi.Method, Ifn/Tfn are real function pointers; once a type is reachable, method entries are treated as strong dependencies.

Result: many actually-unused methods and type metadata are retained, increasing binary size.

Reference Relationship between abi.Type and abi.Method (Simplified)

Example source:

type Game struct{}
func (g *Game) Load() {}
func (g *Game) initGame() {} // assume unexported

Simplified generated metadata:

@_llgo_github.com/.../Game = {
  StructType { ... },
  UncommonType {
    PkgPath, Mcount=2, Xcount=1, Moff=... -> [2]Method
  },
  [2]Method [
    { Name="Load",     Mtyp=&func(...), Ifn=@Game.Load,     Tfn=@Game.Load },
    { Name="initGame", Mtyp=&func(...), Ifn=@Game.initGame, Tfn=@Game.initGame }
  ]
}

Key points:

  • abi.Type.UncommonType.Moff points to the [n]Method array.
  • Each abi.Method contains:
    • Name (method-name symbol)
    • Mtyp (method function-type symbol)
    • Ifn (interface-call wrapper symbol)
    • Tfn (direct method-call symbol)

Under the default layout, Ifn/Tfn are real addresses, so the linker cannot treat these relationships as optional.

Core Bottleneck

From LLD's perspective, Ifn/Tfn in method tables are hard references.
Even if initGame is never called, once the Game type descriptor is reachable, the method-table Ifn/Tfn are considered reachable and cannot be precisely pruned.

This is the main reason for the current size optimization ceiling.

How Go Does It (Reference Model)

LLGo is a Go compiler, so the right reference point is Go's own pipeline.
At a high level, Go uses two phases:

  • Phase 1 (compile time, per package): record semantic relocation markers and emit package .o.
  • Phase 2 (link time, global): perform deadcode flood on a global view, then backfill method tables by reachability (real offsets for reachable entries, sentinel for unreachable entries), and emit the final binary.

Phase 1: Per-package semantic marking

The compiler records semantic reachability markers on functions/symbols. Main marker kinds:

  • R_USEIFACE: recorded on concrete-type-to-interface conversion.
    Written on the current function symbol, targeting concrete type type.*.
    Example:
    package main
    
    type A interface{ Load() }
    type B struct{}
    func (B) Load() {}
    
    func main() {
        var _ A = B{} // marker written: R_USEIFACE -> Sym=type.B
    }
  • R_USEIFACEMETHOD: recorded on interface method calls.
    Written on the current function symbol, targeting interface type type.* with method offset.
    Example:
    package main
    
    type A interface{ Load() }
    type B struct{}
    func (B) Load() {}
    
    func main() {
        var i A = B{}
        i.Load() // marker written: R_USEIFACEMETHOD -> Sym=type.A, Add=offset of Load
    }
  • R_USENAMEDMETHOD: conservative marker when only method name is known.
    Typical for generic interface calls or constant-name MethodByName("Foo").
    Example:
    package main
    
    import "reflect"
    
    type B struct{}
    func (B) Load() {}
    
    func main() {
        _, _ = reflect.TypeOf(B{}).MethodByName("Load")
        // constant-name case writes marker R_USENAMEDMETHOD("Load")
    }
  • R_METHODOFF: relocations in method-table triples (Mtyp/Ifn/Tfn).
    Example:
    package main
    
    type A interface{ Load() }
    type B struct{}
    func (B) Load() {}
    func (B) hidden() {}
    
    func main() {
        var _ A = B{}
    }
    In type.B method table, both Load and hidden each have three R_METHODOFF entries (Mtyp/Ifn/Tfn), and link time decides real offset vs sentinel.

Conceptual relation:

  • R_USEIFACE(type.B) on main.main makes type.B enter the reachable path.
  • R_USENAMEDMETHOD("Foo") on some function conservatively retains methods named Foo.
  • Every method in type.B contributes three R_METHODOFF, and reachability decides real offset vs sentinel.

Combined source-to-markers example:

// src/main.go
package main

import "reflect"

type B struct{}
func (B) Load() {}
func (B) hidden() {}

type A interface { Load() }

func main() {
    var i A = B{}                                // R_USEIFACE(type.B) on main.main
    _ = i.Load()                                 // R_USEIFACEMETHOD(type.A + method offset) on main.main
    _ = reflect.TypeOf(i).MethodByName("Load")   // R_USENAMEDMETHOD("Load") in constant-name case
}

Markers are conceptually placed as:

  • main.main relocation table:
    • R_USEIFACE -> Sym=type.B
    • R_USEIFACEMETHOD -> Sym=type.A, Add=offset of Load
    • R_USENAMEDMETHOD("Load") (constant-name case)
  • type.B method table (uncommon.Methods):
    • three R_METHODOFF entries per method (Mtyp/Ifn/Tfn)

Phase 2: Global link-time reachability (deadcode flood)

The linker performs a global flood using compile-time markers:

  • Root set includes main/main..inittask, runtime base symbols, plugin/export entries, runtime.unreachableMethod, etc.
  • In shared-library mode, symbols in the library are conservatively retained.
  • Traversing relocations:
    • normal references propagate function/global reachability.
    • interface/reflect markers drive type/method retention.
    • R_METHODOFF is backfilled by reachability: real offset if reachable, -1 sentinel if unreachable.
  • Reflect/generic markers (AttrReflectMethod, R_USENAMEDMETHOD, etc.) trigger conservative method retention.
  • Dynamic export symbols (dynexp) are treated as roots at deadcode initialization to avoid over-pruning.

Minimal BFS flood example:

package main

func foo() {}
func bar() {}

func main() {
    foo()
}

func foo() {
    bar()
}

func bar() {}

Traversal:

  • roots start with main.
  • pop main, see reference to foo, mark+enqueue foo.
  • pop foo, see reference to bar, mark+enqueue bar.
  • pop bar, no new refs, queue ends.
    Reachable: main, foo, bar.

Another example: precise interface-method retention by reloc matching (abiType + Name):

package main

type I interface {
    Load()
    Hidden()
}

type T struct{}
func (T) Load() {}
func (T) Hidden() {}

func use(i I) {
    i.Load() // writes marker R_USEIFACEMETHOD (I + Load offset/signature)
}

func main() {
    var i I = T{} // writes marker R_USEIFACE(type.T) on main.main
    use(i)
}

Simplified reachability/backfill flow:

  • roots start from main; flood reaches use, and R_USEIFACE(type.T) puts type.T into interface-related reachable domain.
  • read R_USEIFACEMETHOD on use to obtain interface method demand (Name=Load + compatible signature).
  • scan type.T method-table R_METHODOFF triples; match by abiType + Name; mark only Load as reachable method.
  • after flood, backfill: keep real Ifn/Tfn for Load; write sentinel/null for unmatched methods (Hidden), enabling later pruning.

Relevant Go sources:

  • Compile-time marker writing:
    • cmd/compile/internal/reflectdata/reflect.go (MarkTypeUsedInInterface, MarkUsedIfaceMethod)
    • cmd/compile/internal/walk/expr.go (usemethod, etc.)
  • Link-time reachability and backfill:
    • cmd/link/internal/ld/deadcode.go (roots + R_USEIFACE/R_USEIFACEMETHOD/R_USENAMEDMETHOD/R_METHODOFF)
    • cmd/link/internal/ld/data.go (R_METHODOFF real offset for reachable, -1 sentinel for unreachable)

LLGo Go-Style LinkTime Adaptation Strategy (This PR)

LLGo strategy in this PR:

  • For normal symbol references (A referencing B), we do not invent new formats; we directly scan call/ref from each package's llvm.Module.
  • Go-semantic relationships (USEIFACE/USEIFACEMETHOD/USENAMEDMETHOD/METHODOFF/...) are produced by llssa during SSA lowering.
  • Both sources are merged into a unified per-package relocgraph.Graph, then globally merged and analyzed with one flood algorithm.
  • After obtaining reachable method info (type -> method indices), we emit strong symbol overrides in mainPkg and remove unnecessary IFn/TFn.

Core data structures (simplified):

// Unified dependency edge for both module call/ref and SSA reloc.
type Edge struct {
    Owner  SymID
    Target SymID
    Kind   EdgeKind // EdgeCall/EdgeRef/EdgeReloc*
    Addend int64
    Name   string   // optional metadata (e.g., method name)
    FnType SymID    // optional method signature symbol
}
// Deadcode analysis result; ReachableMethods is the key output for backfill.
type Result struct {
    Reachable        map[SymID]bool
    UsedInIface      map[SymID]bool
    IfaceMethods     map[MethodSig]bool
    ReachableMethods map[SymID]map[int]bool // type -> method index set
}

ReachableMethods is the direct input to the later rewrite stage:
type symbol -> reachable method index set.

Data Flow (This PR)

flowchart TD
  A["Per-package LLVM Module"] -->|scan call/ref| B["ModuleEdges: EdgeCall/EdgeRef"]
  C["llssa SSA lowering"] -->|emit semantic reloc| D["SSAEdges: EdgeReloc*"]
  B --> E["BuildPackageGraph(mod, ssaEdges)"]
  D --> E
  E --> F["aPkg.IRGraph (full per-package graph)"]
  F --> G["MergeAll (global graph)"]
  G --> H["deadcode.Analyze(roots)"]
  H --> I["ReachableMethods: map[type]set[index]"]
  I --> J["dcepass.EmitStrongTypeOverrides(mainPkg, srcMods, ReachableMethods)"]
  J --> K["Strong-symbol type metadata in mainPkg"]
  K --> L["Clear unreachable Ifn/Tfn (null or sentinel semantics)"]
  L --> M["Final linked binary"]
Loading

Key properties of this design:

  • Normal symbol reachability (call/ref) and Go semantic reachability (reloc) are modeled in one graph.
  • deadcode only computes reachable method sets; it stays decoupled from output-format specifics.
  • dcepass performs strong-symbol backfill from reachable method sets; this works on the normal .o pipeline (not .ll-only).

Why This Design

First, this path is structurally aligned with Go.
Per-package semantic markers + global link-time reachability + method-table backfill is the fastest, most stable way to approach Go-equivalent pruning behavior.

Second, reachability is computed directly from symbol references.
For many ordinary call paths (especially runtime calls), llvm.Module use/ref already gives stable graph edges directly, without requiring extra SSA-level interpretation of how Go semantics get lowered into llgo calls.

These relationships are already close to binary-level expression: stable and composable.

Three minimal examples:

Example 1: Channel ops map directly to runtime symbols

internal/relocgraph/_testdata/chanops/in.go

package chanops

func A() {
	ch := make(chan int, 1)
	ch <- 1
	<-ch
}

internal/relocgraph/_testdata/chanops/out.txt

reloc(directcall) chanops.A -> github.com/goplus/llgo/runtime/internal/runtime.ChanRecv
reloc(directcall) chanops.A -> github.com/goplus/llgo/runtime/internal/runtime.ChanSend
reloc(directcall) chanops.A -> github.com/goplus/llgo/runtime/internal/runtime.NewChan
reloc(directref) chanops.A -> github.com/goplus/llgo/runtime/internal/runtime.NewChan
reloc(directref) chanops.init -> chanops.init$guard

Example 2: Closure calls are explicit at symbol level

internal/relocgraph/_testdata/closure/in.go

package closure

func B() {}

func A() {
	x := 1
	_ = x
	func() {
		B()
	}()
}

internal/relocgraph/_testdata/closure/out.txt

reloc(directcall) closure.A -> closure.A$1
reloc(directcall) closure.A$1 -> closure.B
reloc(directref) closure.init -> closure.init$guard

Example 3: Local function pointers become stable symbol edges

internal/relocgraph/_testdata/funcptrlocal/in.go

package funcptrlocal

func B() {}

func A() func() {
	f := B
	return f
}

internal/relocgraph/_testdata/funcptrlocal/out.txt

reloc(directcall) __llgo_stub.funcptrlocal.B -> funcptrlocal.B
reloc(directref) funcptrlocal.A -> __llgo_stub.funcptrlocal.B
reloc(directref) funcptrlocal.init -> funcptrlocal.init$guard

Key Points of This PR

  • Make reachability marking correct first (especially interface/reflect/type-metadata paths).
  • Drive method-level retention from reachability results rather than relying only on explicit linker references.
  • Preserve behavior compatibility while iteratively improving size reduction.

Current Limitation and Follow-up (cache-hit and ForceRebuild)

In the current implementation, DCE temporarily enables ForceRebuild because:

  • The per-package reloc graph is built from two inputs:
    • semantic reloc edges emitted by llssa during SSA lowering, and
    • direct call/ref edges collected from the package LLVM module.
  • On cache-hit, we currently restore .a artifacts and link metadata, but we do not yet restore the LLVM-side call/ref edge set.
  • Therefore, in cache-hit scenarios we cannot guarantee a complete per-package relocgraph, which can impact global reachability correctness.

So for now, DCE forces rebuild to guarantee correctness.

Planned follow-up:

  • Persist per-package reloc metadata together with .a (for example as an archive member or a sidecar file).
  • On cache-hit, restore this metadata directly instead of rebuilding.
  • After this is in place, DCE can remove the hard dependency on ForceRebuild while keeping correctness.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @luoliwoshang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a refined approach to dead code elimination by integrating Go-specific semantic information directly into the IR graph construction. By capturing implicit dependencies like interface conversions and reflection usage at the SSA lowering stage, the system can build a more accurate and comprehensive dependency graph. This enables a more effective DCE pass that can prune unused methods and metadata, leading to smaller binaries while maintaining Go's runtime semantics.

Highlights

  • Dead Code Elimination (DCE) Refactoring: The core of this pull request is a significant refactoring of the dead code elimination process, moving the construction of the relocation graph from post-IR parsing to direct utilization of package-level context during SSA lowering.
  • SSA Relocation Records: The ssa package now generates and stores package-level relocation records, capturing implicit dependencies related to interfaces, reflection, and method tables directly within the package context.
  • IRGraph Enhancements: The irgraph package has been updated to focus its Build function on direct call and reference edges, with a new AddRelocRecords function to inject the SSA-generated relocation metadata, providing a comprehensive dependency graph.
  • Reflect Method Handling: New logic has been introduced in cl/instr.go to identify reflect.Method and reflect.MethodByName calls, marking them with specific relocation records. This allows for more precise DCE by distinguishing between constant and non-constant method names.
  • DCE Pass Implementation: A new cl/dcepass package is added to apply the DCE pass. This pass clears unreachable method pointers in type metadata and removes __llgo_relocs tables, enabling LLVM's global DCE to eliminate unused functions more effectively.
  • Build Process Integration: The internal/build package now constructs per-package IR graphs by combining module edges with the injected package relocation records, facilitating a whole-program view for DCE.
  • Comprehensive Test Coverage: Extensive new test cases and test data have been added across cl/deadcode, cl/irgraph, and ssa to validate the new graph construction and DCE logic, covering various scenarios including function calls, interface usage, and reflection.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • cl/dcepass/dcepass.go
    • Added new dcepass package with Apply function to clear unreachable methods and remove __llgo_relocs tables.
  • cl/dcepass/dcepass_test.go
    • Added new test file for dcepass to verify method clearing and pass application.
  • cl/deadcode/deadcode.go
    • Added new deadcode package with Analyze function to compute reachability from roots using call/ref edges and reloc metadata.
    • Introduced MethodSig and MethodRef types for method identification and tracking.
    • Implemented logic to process various irgraph.RelocEdge kinds, including EdgeRelocUseIface, EdgeRelocUseIfaceMethod, EdgeRelocUseNamedMethod, EdgeRelocReflectMethod, EdgeRelocTypeRef, and EdgeRelocMethodOff.
  • cl/deadcode/deadcode_flood_test.go
    • Added new test file for deadcode flood reachability, covering multiple roots, cycles, interface propagation, and reflection-based method retention.
  • cl/deadcode/deadcode_test.go
    • Added new test file for deadcode analysis using testdata, including rootSymbols and formatReachability helpers.
  • cl/instr.go
    • Added constStringArg helper to extract constant string arguments from SSA calls.
    • Added reflectMethodNameFromCall to identify reflect.Method and reflect.MethodByName calls.
    • Integrated MarkUseNamedMethod and MarkReflectMethod calls into the SSA context.call function to record reflection-related relocations.
  • cl/irgraph/graph.go
    • Added new irgraph package defining SymID, EdgeKind, NodeInfo, and Graph structures.
    • Implemented Build function to construct a graph from an LLVM module based on call and reference edges.
    • Added RelocEdge struct and AddRelocRecords function to inject SSA-collected relocation records into the graph.
  • cl/irgraph/irgraph_test.go
    • Added new test file for irgraph construction, including tests for direct call/ref edges and various relocation types from _testdata_reloc.
  • cmd/internal/flags/flags.go
    • Added DCE boolean flag to enable experimental Go-style link-time dead code elimination.
  • doc/deadcode-adapter.zh.md
    • Added new documentation explaining LLGo deadcode adaptation, current implementation, and future plans.
  • doc/go-link-dce.zh.md
    • Added new documentation detailing the design notes for LLGo link-time DCE, comparing with Go's approach.
  • doc/irgraph-cases.zh.md
    • Added new documentation explaining IRGraph test cases and expected outputs for direct call/ref edges and implicit dependencies.
  • doc/llgo-dce-pass.zh.md
    • Added new documentation describing the LLGo DCE two-stage pipeline and current deletion rules.
  • doc/ssa-reloc.zh.md
    • Added new documentation explaining SSA relocation metadata output, its structure, and test case interpretations.
  • internal/build/build.go
    • Integrated DCE process: enabled relocation table emission in llssa.NewProgram.
    • Modified Do function to enforce DCE constraints (build/run modes, buildmode=exe, non-WASM targets).
    • Updated compileExtraFiles to emit .bc files for DCE mode instead of .o.
    • Modified linkMainPkg to handle .bc inputs and merge them into a single LLVM module for DCE.
    • Implemented dceEntryRoots to identify entry points for DCE analysis.
    • Added mergePackageGraphs to combine IR graphs from all packages.
    • Applied deadcode.Analyze and dcepass.Apply to the merged module for DCE.
    • Added verbose logging for DCE process, including pre/post-pass LLVM IR dumps and detailed statistics.
    • Modified exportObject to produce .bc files when DCE is enabled.
  • internal/build/cgo.go
    • Updated clFile calls to pass verbose flag directly instead of printCmds.
  • internal/build/collect.go
    • Disabled cache usage (tryLoadFromCache, saveToCache) when DCE is enabled to ensure all packages are processed for graph construction.
  • ssa/abitype.go
    • Added recordTypeRef and recordTypeRefs functions to emit relocTypeRef for child type references within type descriptors.
    • Modified abiUncommonMethods to emit relocMethodOff for each method table entry (Mtyp, Ifn, Tfn) when relocation tables are enabled.
  • ssa/interface.go
    • Added markUseIface to record interface conversions (relocUseIface).
    • Added markUseIfaceMethod to record interface method calls (relocUseIfaceMethod).
    • Added MarkUseNamedMethod to record named method lookups (relocUseNamedMethod).
    • Added MarkReflectMethod to record generic reflection method lookups (relocReflectMethod).
    • Added canEmitReloc helper to check if relocation emission is enabled and valid.
  • ssa/package.go
    • Added emitReloc flag to aProgram to control relocation table generation.
    • Introduced RelocRecord struct and RelocRecords method to expose collected relocation data.
    • Implemented addReloc to store relocation metadata in relocRecords and relocs (LLVM values).
    • Added relocString to create global string constants for reloc targets.
    • Implemented ensureRelocGlobal to build the __llgo_relocs global constant in the LLVM module.
  • ssa/reloc_test.go
    • Added new test file for SSA relocation table generation, including extractRelocBlock helper.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/test_demo.sh
Activity
  • The pull request was created by luoliwoshang.
  • The author provided a detailed summary of the changes and verification steps in the pull request description.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@luoliwoshang luoliwoshang marked this pull request as draft February 6, 2026 06:06
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is a significant refactoring that introduces a Go-style dead code elimination (DCE) pass. The changes are extensive, touching SSA lowering, IR graph construction, and the build process. The core idea is to build a global dependency graph from per-package bitcode and relocation metadata, perform a reachability analysis, and then apply a DCE pass to remove unreachable code and data. The implementation appears solid and well-tested. My main feedback is a suggestion to simplify a complex conditional block in the build process to improve readability and remove dead code.

Comment thread internal/build/build.go Outdated

dceLLName := ""
if ctx.buildConf.DCE {
roots, err := dceEntryRoots(merged)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This if ctx.buildConf.DCE check is redundant, as this code block is already inside a check for ctx.buildConf.DCE at line 1221. You can remove this if statement to simplify the code.

Comment thread internal/build/build.go Outdated
Comment on lines +1361 to +1417
if ctx.buildConf.DCE {
if dceLLName == "" {
merged.Dispose()
return fmt.Errorf("missing DCE module .ll output")
}
// NOTE: after the DCE pass the merged module may contain mutated
// constants that occasionally trip LLVM type walkers. Skip the
// ABI metadata dump in DCE mode to avoid crashes during verbose runs.
if !ctx.buildConf.DCE {
dumpAbiTypeMetadata(merged, dceLLName, verbose)
}

combinedObj := strings.TrimSuffix(dceLLName, ".ll") + ".o"
args := []string{"-o", combinedObj, "-c", dceLLName, "-Wno-override-module"}
if verbose {
fmt.Fprintln(os.Stderr, "clang", args)
}
if err := ctx.compiler().Compile(args...); err != nil {
merged.Dispose()
return fmt.Errorf("failed to compile DCE ll: %v", err)
}
merged.Dispose()
objFiles = []string{combinedObj}
} else {
combinedBc, err := os.CreateTemp("", "llgo-link-*.bc")
if err != nil {
merged.Dispose()
return err
}
combinedBcName := combinedBc.Name()
if err := llvm.WriteBitcodeToFile(merged, combinedBc); err != nil {
combinedBc.Close()
merged.Dispose()
return fmt.Errorf("write combined bitcode: %w", err)
}
combinedBc.Close()
if verbose {
fmt.Fprintf(os.Stderr, "[bc-pass] merged bitcode written to %s\n", combinedBcName)
}

// Optional debug: dump abi.Type metadata from the merged module.
dumpAbiTypeMetadata(merged, combinedBcName, verbose)

// Compile the merged BC to a single object for final native link.
combinedObj := strings.TrimSuffix(combinedBcName, ".bc") + ".o"
args := []string{"-o", combinedObj, "-c", combinedBcName, "-Wno-override-module"}
if verbose {
fmt.Fprintln(os.Stderr, "clang", args)
}
if err := ctx.compiler().Compile(args...); err != nil {
merged.Dispose()
return fmt.Errorf("failed to compile combined bc: %v", err)
}

merged.Dispose()
objFiles = []string{combinedObj}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This if ctx.buildConf.DCE check is redundant for the same reason as the one on line 1275. Consequently, the else block (lines 1384-1417) is unreachable dead code. The entire if-else structure can be simplified by removing the conditional and the else block.

Comment on lines +267 to +281
relocs := d.relocsByOwner[m.Type]
for i := 0; i < len(relocs); i++ {
r := relocs[i]
if r.Kind != irgraph.EdgeRelocMethodOff || int(r.Addend) != m.Index {
continue
}
d.mark(r.Target)
if i+1 < len(relocs) && relocs[i+1].Kind == irgraph.EdgeRelocMethodOff {
d.mark(relocs[i+1].Target)
}
if i+2 < len(relocs) && relocs[i+2].Kind == irgraph.EdgeRelocMethodOff {
d.mark(relocs[i+2].Target)
}
break
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: This linear scan searches all relocs for the owner type to find the matching method index. For types with many methods and relocs, this is O(methods × relocs_per_type). Consider building an index (map from (owner, methodIndex) to reloc positions) during initialization to make this O(1).

Comment on lines +155 to +158
fnTyp := r.FnType
if fnTyp == "" {
panic("useifacemethod missing fnType")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Using panic() for malformed input makes the code less robust. Since this processes internal compiler data, crashes here could be confusing to users. Consider returning an error instead:

Suggested change
fnTyp := r.FnType
if fnTyp == "" {
panic("useifacemethod missing fnType")
}
return fmt.Errorf("useifacemethod missing fnType for target %s", r.Target)

If you prefer to keep the panic for internal consistency checks, consider wrapping with a more descriptive message that helps debugging.

Comment thread internal/deadcode/deadcode.go Outdated
Comment on lines +37 to +42
var verbose bool

// SetVerbose enables verbose debugging output during Analyze.
func SetVerbose(v bool) {
verbose = v
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Global mutable state with SetVerbose() is not thread-safe and makes the code harder to test in parallel. Consider passing the verbose flag through Analyze() via an options struct:

type AnalyzeOptions struct {
    Verbose bool
}

func Analyze(g *irgraph.Graph, roots []irgraph.SymID, opts AnalyzeOptions) Result {

This would also allow different callers to have different verbosity settings without interfering with each other.

Comment thread cl/dcepass/dcepass.go Outdated
Comment on lines +112 to +114
// clearUnreachableMethods zeros Mtyp/Ifn/Tfn for unreachable methods in type
// metadata constants. All method slots are cleared by default; the whitelist
// reachMethods marks which (type,index) to keep.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation/Code Quality: The comment mentions "zeros Mtyp/Ifn/Tfn" but the code actually preserves mtypField (field 1) and only zeros fields at indices 2 and 3 (Ifn and Tfn). The comment should say "zeros Ifn/Tfn" to match the actual behavior:

Suggested change
// clearUnreachableMethods zeros Mtyp/Ifn/Tfn for unreachable methods in type
// metadata constants. All method slots are cleared by default; the whitelist
// reachMethods marks which (type,index) to keep.
// clearUnreachableMethods zeros Ifn/Tfn for unreachable methods in type
// metadata constants. All method slots are cleared by default; the whitelist
// reachMethods marks which (type,index) to keep.

Comment thread internal/dcepass/dcepass.go Outdated
Comment on lines +27 to +34
// Stats reports basic DCE pass metrics.
type Stats struct {
Reachable int
DroppedFuncs int
DroppedGlobal int
DroppedMethod int
DroppedMethodDetail map[irgraph.SymID][]int
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: The Stats struct declares DroppedFuncs and DroppedGlobal fields that are never populated by the Apply function. These unused fields are misleading as they suggest the DCE pass tracks dropped functions and globals, but it currently does not. Consider removing them or adding a TODO comment if they are planned for future use.

Comment thread cl/dcepass/dcepass.go Outdated
newMethods[i] = zeroed
changed = true
dropped++
detail[irgraph.SymID(g.Name())] = append(detail[irgraph.SymID(g.Name())], i)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: Minor optimization: g.Name() is called twice, creating two SymID conversions. Consider caching the key:

key := irgraph.SymID(g.Name())
detail[key] = append(detail[key], i)

// Walk a value tree (constants + constant expressions) and record any
// function pointer it embeds. This captures func values stored in globals,
// composite literals, or casted constants.
visited := make(map[llvm.Value]bool)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: A new visited map is allocated on every call to addRefEdgesFromValue. This function is called for every global initializer and every instruction operand in the module. For large modules with thousands of functions and instructions, this creates significant GC pressure.

Consider either:

  1. Passing a reusable visited map from the caller and clearing it between uses
  2. Using sync.Pool for map allocation
  3. Moving the visited map to a struct field that persists across calls

@fennoai
Copy link
Copy Markdown
Contributor

fennoai Bot commented Feb 6, 2026

Code Review Summary

This PR refactors DCE to build the reloc graph from package context rather than parsing LLVM IR. The new deadcode, dcepass, and irgraph packages are well-structured with clear separation of concerns.

Strengths:

  • Clean architecture separating graph construction, reachability analysis, and LLVM transformations
  • Good test coverage with comprehensive testdata fixtures
  • Proper handling of interface method reachability and reflect-based lookups

Areas for improvement:

  • Consider making the verbose flag part of an options struct rather than global mutable state (thread-safety)
  • A few documentation comments don't match the actual code behavior
  • Some performance optimizations possible for large codebases (map allocations, linear searches)

Overall, this is solid compiler infrastructure code. The inline comments highlight specific actionable items.

@fennoai
Copy link
Copy Markdown
Contributor

fennoai Bot commented Feb 9, 2026

Code Review Summary

This PR implements a well-structured dead code elimination architecture with good separation between reachability analysis (deadcode) and LLVM IR transformation (dcepass). The unit tests in deadcode_flood_test.go are comprehensive.

Key observations:

  • No security vulnerabilities found
  • Shell script properly quotes variables
  • Tests only run on Linux - consider cross-platform support

Areas for improvement:

  • Replace panic calls with proper error handling for robustness
  • Address queue memory growth in flood-fill algorithm
  • Add documentation for public API structs
  • Define named constants for magic numbers in ABI detection

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant