Skip to content

Proposal: incrementally reuse GOROOT runtime sources through source patches #2165

Description

@cpunion

Summary

Incrementally replace LLGo-maintained standard-library runtime substitutes with the pinned Go toolchain's GOROOT/src/runtime implementation, using minimal source patches first and Plan 9 assembly only when LLGo's ABI and stack semantics are compatible.

This is a staged compiler/runtime migration. It must not be treated as a file move or folded into the pthread G/M/P cleanup: directly enabling the upstream runtime would also require compatible compiler intrinsics, function ABI, getg, stack switching, GC metadata, write barriers, generated offsets, OS startup, and runtime assembly.

Current architecture

LLGo currently has two runtime layers with different responsibilities:

  • runtime/internal/runtime is the compiler-facing low-level ABI. LLGo-generated code calls it for allocation, maps, channels, panic/defer/recover, goroutine startup, locality, and other language operations.
  • runtime/internal/lib/runtime is the replacement implementation compiled as the standard-library package runtime. It provides public and linknamed runtime APIs and already depends on the low-level layer.

The standard runtime package is currently an altPkgReplace package. That replacement also prevents the normal GOROOT runtime assembly from being selected. The two directories therefore overlap in concepts, but they are not interchangeable copies today.

Motivation

Maintaining large copied or simplified runtime implementations has several costs:

  • behavior drifts from the pinned Go version;
  • new standard-library releases add hooks manually;
  • copied assembly and platform code are difficult to keep correct;
  • package replacement gives a poorer patch and upgrade experience than compiling the original source with small, reviewable source patches;
  • scheduler, syscall, timer, GC, traceback, and profiling behavior can be implemented twice.

Where semantics and ABI match, the Go implementation should be the source of truth. LLGo-specific code should describe only the incompatibility.

Principles

  1. Pin the supported Go version and upgrade it explicitly. Runtime source reuse is version-sensitive.
  2. Prefer source patches over package replacement for reusable Go source. Keep every patch as small and local as possible.
  3. Reuse GOROOT .s files through Plan 9 assembly only after their ABI, TLS, stack, GC, and generated-offset assumptions are satisfied.
  4. Keep a stable compiler-facing runtime ABI so moving an implementation does not repeatedly change SSA lowering.
  5. Migrate one coherent subsystem at a time and remove the replaced LLGo implementation in the same change.
  6. Preserve native, C library, WebAssembly, and bare-metal build selection. Unsupported targets must select an explicit fallback, not accidentally compile a host implementation.
  7. Do not claim compatibility from successful compilation alone; exercise the reused behavior at runtime.

Goals

  • Define which runtime symbols are owned by the compiler-facing layer, the standard runtime package, or a platform backend.
  • Make runtime eligible for incremental source patching rather than requiring an all-at-once replacement switch.
  • Reuse upstream leaf Go implementations and compatible assembly with minimal deltas.
  • Shrink runtime/internal/lib/runtime as ownership moves back to GOROOT sources.
  • Prevent duplicate definitions and detect missing runtime hooks during Go upgrades.
  • Establish gates for the later scheduler, stack, and GC migration.

Non-goals

  • Do not enable the complete upstream runtime/proc.go in the first change.
  • Do not reuse mstart, mcall, systemstack, morestack, or related assembly before LLGo implements their required g0 stack, register ABI, TLS, stack-map, and preemption semantics.
  • Do not change LLGo to Go's full moving/growable stack model as part of source-patch plumbing.
  • Do not redesign the WebAssembly scheduler proposed in Proposal: complete LLGo WebAssembly runtime with Asyncify and resumable backends #2152.
  • Do not introduce a large new source-patch framework if the existing overlay mechanism can express the migration.

Proposed design

1. Runtime ownership manifest

Add a checked manifest or generated report for runtime symbols that records:

  • compiler intrinsic / low-level ABI ownership;
  • upstream source ownership;
  • LLGo source-patch replacement;
  • platform backend ownership;
  • required linkname aliases and assembly definitions.

The check must reject duplicate definitions and report upstream symbols that become newly required after a pinned Go upgrade.

2. Stable compiler ABI boundary

Keep compiler lowering pointed at a small, target-independent low-level ABI. For example, a go statement should submit an entry function and startup record to one runtime newproc-like boundary; it should not emit pthread operations.

Initially that boundary may still live in runtime/internal/runtime. The standard runtime package can call or link to it. Ownership should move only when the upstream ABI is actually implementable; source directory layout alone must not dictate the compiler ABI.

3. Incremental source patching of runtime

Extend the existing source-patch/package-patch merge so runtime can be migrated declaration by declaration:

  1. compile the pinned GOROOT source;
  2. skip only incompatible declarations or files;
  3. inject a small LLGo replacement for those declarations;
  4. remove the corresponding implementation from runtime/internal/lib/runtime;
  5. retain the replacement package only for the remaining unsupported surface.

Source patches should explain the semantic or ABI incompatibility and should be guarded by the relevant Go version and target tags.

4. Assembly reuse

Build on #2128 for selective GOROOT assembly reuse. Each reused file requires validation of:

  • Go ABI variant and argument/result layout;
  • TLS and getg representation;
  • stack-growth and g0 assumptions;
  • GC visibility and write-barrier requirements;
  • generated go_asm.h offsets;
  • unwind, DWARF, and C transition behavior;
  • architecture and OS build constraints.

Leaf assembly such as compatible syscall/time helpers can migrate before scheduler context-switch assembly. Runtime trampolines depend on #2129.

5. Scheduler, stack, and GC gate

Upstream runtime.newproc is coupled to systemstack, per-P run queues, growable Go stacks, stack maps, write barriers, and runtime assembly. Reusing it is a later gated phase and requires at least:

  • a defined LLGo G/M/P model and replaceable execution backend;
  • compatible function-value and compiler runtime-call ABI;
  • a getg representation usable from Go and assembly;
  • safe-point, root, and stack metadata contracts;
  • g0/system-stack or an explicitly adapted equivalent;
  • target-specific startup and cgo/C-library transition rules.

Until those gates pass, LLGo should reuse upstream naming, state meanings, and isolated algorithms where useful without pretending that the full upstream scheduler is active.

Phased work

Phase 0: audit and compile harness

  • Produce the runtime ownership/dependency report.
  • Add a diagnostic build mode that attempts selected GOROOT runtime files and reports unsupported directives, intrinsics, symbols, and assembly assumptions.
  • Record code-size and build-time baselines.

Phase 1: leaf Go source

  • Migrate self-contained helpers with matching LLGo semantics.
  • Convert broad package replacements into narrow source patches where possible.
  • Add upgrade checks against the pinned Go version.

Phase 2: platform and assembly helpers

Phase 3: standard runtime integration

  • Move public/linknamed runtime APIs back to upstream implementations subsystem by subsystem.
  • Reduce runtime/internal/lib/runtime and keep the compiler-facing layer focused on language ABI and backend primitives.

Phase 4: scheduler and stack feasibility

  • Prototype the upstream newproc/getg ABI against LLGo G/M/P.
  • Validate panic/defer/recover, C entry, C libraries, GC roots, traceback, and preemption before adopting scheduler assembly.
  • Split stack/GC/compiler work into separate proposals if the feasibility gate confirms the expected large scope.

Validation

Every migration PR should include the relevant subset of:

  • source-patch unit and merge tests;
  • duplicate/missing runtime symbol checks;
  • real execution tests, not symbol references only;
  • goroutine, defer/panic/recover, channel, timer, syscall, and GC-root tests;
  • executable, c-archive, and c-shared coverage where the subsystem is reachable there;
  • macOS arm64 and Linux amd64 execution;
  • supported architecture compile checks for reused assembly;
  • pinned Go compatibility lanes;
  • patch coverage and before/after code-size and build-time measurements.

Acceptance criteria

  • A documented ownership boundary exists for all runtime symbols used by compiler lowering and the standard library.
  • At least one non-trivial runtime subsystem is compiled from pinned GOROOT source through a minimal source patch, with the LLGo duplicate removed.
  • Selective assembly reuse has explicit ABI validation and target fallbacks.
  • Go version upgrades detect newly missing or conflicting runtime symbols.
  • Compiler lowering remains platform-independent across the migration.
  • The proposal does not regress supported native, library, WebAssembly, or bare-metal configurations.

Related work

internal/abi reuse gate

LLGo already reuses part of the pinned GOROOT internal/abi package: its alternate package is merged without skipall, so unoverridden declarations continue to come from GOROOT. The LLGo file currently overrides only the compiler/runtime boundary such as Type, InterfaceType, FuncPCABI0, FuncPCABIInternal, NoEscape, TypeOf, and escape helpers. This is the preferred incremental shape and should be preserved.

Full package reuse has two independent compatibility gates:

  • Runtime metadata layout. LLGo runtime/abi.Type, map metadata, interface metadata, and LLGo-specific type flags are not layout-compatible with Go 1.26 internal/abi. This affects reflection, maps, interfaces, linker metadata, and GC visibility; closure ABI changes alone do not solve it.
  • Calling convention. Go RegArgs, reflect-call helpers, function values, the closure-context register, stack maps, and runtime assembly form one compiler/runtime ABI. LLGo currently represents a function value as code plus context and passes the context as an explicit hidden argument. That is intentionally friendly to the C ABI and the existing libffi bridge, but it is not the Go internal ABI.

Declarations should therefore be classified and checked as:

  1. directly reusable constants and helpers that are independent of metadata layout and calling convention;
  2. reusable GOROOT algorithms compiled through small source patches against LLGo-owned boundary types;
  3. blocked declarations tied to type metadata, RegArgs, reflect-call, stack maps, or the closure-context ABI.

The closure-context ABI must be treated as a feasibility gate rather than silently changed:

  • Keeping the explicit hidden argument is the compatibility baseline and permits incremental source reuse, but requires adapters for Go-ABI assembly that expects a context register.
  • A Go-like dedicated context register could enable more direct assembly and ABI reuse, but requires LLVM register reservation or a defined calling convention on every target, C-call and callback wrappers, GC visibility rules, and architecture-specific libffi call/closure support. Rebuilding libffi with a fixed-register option alone does not marshal the context register.
  • A dual-ABI design can retain the current C-friendly function-value ABI and add narrow adapters for selected upstream Go-ABI entry points. This lowers migration risk at the cost of maintaining explicit transition points.

Phase 0 should prototype the latter two options on amd64 and arm64 and validate direct closures, C round trips, callbacks, libffi calls and closures, register-clobber stress, LTO, GC roots, panic/recover, and code-size/build-time impact. A later proposal or decision record should select the closure ABI only from those results; metadata-layout migration remains a separate prerequisite for full internal/abi reuse.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions