Summary
llgo currently has two main mechanisms for Go stdlib compatibility work:
Both are useful, but they solve different problems and both have hard limits.
This proposal introduces a third mechanism, source patch, intended to sit between them:
- earlier than
alt patch
- much smaller-grained than
overlay
- preserves the original stdlib package identity
- keeps most compatibility work in normal Go source files with build tags
The goal is not to replace every existing mechanism. The goal is to use:
alt patch for symbol-level and runtime-coupled compatibility
overlay only as a last resort
source patch for packages that need source-level replacement while still keeping the original stdlib package identity
Background
Today we mainly rely on two strategies.
1. alt patch
This is the preferred mechanism when possible.
It works well for:
- adding missing symbols
- replacing a few functions or variables
- version-gated compatibility via normal Go build tags
- selective skipping via
//llgo:skip / //llgo:skipall
Its strengths are:
- symbol-level control
- easy version/platform gating
- implemented as normal Go code
- does not require replacing stdlib files directly
But it also has a limit:
- it is still fundamentally a patch applied to an already-loaded package shape
That becomes fragile when compatibility depends on:
- original package identity
- unexported stdlib types and methods
- method sets
- generic instantiation boundaries
- loaders/type checking seeing the patched source from the beginning
This is where alt patch starts to become too late in the pipeline.
2. overlay
overlay solves that by replacing files in the original stdlib package before load/typecheck.
Its main strength is:
- the package stays the original stdlib package
This is sometimes exactly what we need.
But overlay is also expensive:
- file-level replacement is coarse
- it is harder to keep compatible across Go versions
- it tends to depend on exact upstream file layout
- it is easy to over-copy stdlib code
- it increases build/load complexity
So overlay is powerful, but not a good default.
Problem
There is a real gap between alt patch and overlay.
Some packages need more than symbol-level patching, but less than full file replacement.
Typical examples are packages where we need to:
- replace a few declarations inside the original stdlib package
- keep the original package identity
- avoid copying whole upstream files
- still write compatibility logic as ordinary Go source
This has shown up most clearly in packages such as:
internal/sync
iter
internal/reflectlite
These cases are awkward for alt patch, but overlay is heavier than necessary.
Proposal
Add a source patch mechanism.
Core idea
Before packages.Load / type checking, llgo builds a patched source view for selected stdlib packages:
- original stdlib source remains the base
- patch files come from
runtime/_patch/<pkg>
- selected original declarations are commented out or stubbed in the original stdlib files
- patch code is emitted as separate generated
z_llgo_patch_*.go files in the original stdlib package
- the package path remains the original stdlib package path
This gives us:
- original package identity
- load-time visibility of patched declarations
- much smaller scope than file-level overlay
Source patch semantics
Patch files are normal Go files, with build tags as usual.
The current directive model is intentionally small:
- default: a patch file is active if selected for the current target
//llgo:skipall
- reduce the original stdlib files for that package to package stubs
//llgo:skip name1 name2 ...
- comment out specific original declarations
- same-name declarations in the patch file automatically override original declarations
This means we do not need a separate explicit replace directive. In practice:
- if a patch file declares the same symbol
- the original declaration is commented out in the stdlib source view
- the patch declaration becomes the one seen by the loader/type checker via the generated patch file
Supported top-level declarations
The current implementation supports these top-level declaration kinds:
type
const
var
- top-level
func
- methods, keyed as
T.M and (*T).M
For type, const, and var, matching is by the declared top-level name.
For methods, matching is by receiver-plus-name key, for example:
const / var granularity
There is one important detail for const and var.
If the original code is written as separate specs inside a grouped declaration, each spec can be handled independently:
In that case, replacing or skipping A does not require removing B.
However, if the original code is written as a single multi-name ValueSpec, the current implementation does not split it:
const A, B = 1, 2
var X, Y = f(), g()
If only one name from such a spec is replaced or skipped, the whole original spec is commented out. This keeps the implementation simple and deterministic, but it is a known limitation.
Multiple patch files
If multiple patch files are active for the same package:
- their
skip directives are unioned
- same-name declarations from any patch file participate in the override set
- each patch file is emitted as its own generated
z_llgo_patch_<name>.go
- if any active patch file declares
//llgo:skipall, the original stdlib files are reduced to package stubs and all patch-side code comes from the generated patch files
This keeps patch files independent while still letting the original stdlib source be filtered consistently.
Why this is better than overlay
Compared with overlay, source patch:
- does not require whole-file replacement in the common case
- does not require copying full upstream files just to replace a few declarations
- is less sensitive to upstream file layout churn
- keeps compatibility logic in ordinary Go patch files
- is easier to review and version-gate
So source patch should be the preferred choice when:
- package identity must be preserved
- but only a small subset of declarations actually needs replacement
Why this is better than alt patch for some packages
Compared with alt patch, source patch runs earlier.
That matters when correctness depends on the loader/type checker seeing the patched package shape from the beginning, not after the package shape is already fixed.
This is especially relevant for packages involving:
- unexported stdlib implementation details
- generic instantiation boundaries
- method-set-sensitive behavior
- packages tightly used by other stdlib packages
Important limitation
Source patch must not become a way to import arbitrary llgo internals into stdlib packages.
A source-patched stdlib package should not import:
github.com/goplus/llgo/runtime/internal/...
That breaks the model and creates module/internal visibility problems.
Instead, source patch code should be limited to:
- standard library packages
- standard library
internal/... packages that are already valid for that package
- non-
internal llgo compatibility surfaces such as:
github.com/goplus/llgo/runtime/abi
//go:linkname for runtime hooks where needed
If a package fundamentally depends on llgo/runtime/internal/..., it is a strong sign that it should remain an alt patch, not a source patch.
Positioning relative to existing mechanisms
This proposal does not remove alt patch or overlay.
The intended layering is:
Keep using alt patch for
- symbol additions
- function/variable replacement
- runtime/linkname shims
- packages strongly coupled to
llgo runtime internals
Likely examples:
runtime
reflect
internal/abi
sync/atomic
internal/runtime/maps
internal/runtime/sys
Use source patch for
- packages that need source-level replacement
- package identity must remain the original stdlib package
- patch logic can stay in ordinary Go files without importing
runtime/internal/...
Good candidates include packages like:
iter
internal/sync
internal/reflectlite
unique where appropriate
Keep overlay only as a last resort
Only for cases where even declaration-level source patch is not enough and file-level replacement is truly required.
Implementation outline
A minimal implementation can work like this:
- Declare source-patched package paths centrally in
runtime/build.go
- Before the first
packages.Load, build a patched source view for each selected package
- For each active patch file:
- evaluate Go build tags against the current target
- parse source patch directives
- collect declarations provided by the patch
- Comment out or stub original declarations in the original stdlib source view
- Emit each active patch file as a generated
z_llgo_patch_*.go in the original stdlib package
- Load/typecheck the package as the original stdlib package
Diagnostics and line mapping
This part is important.
Source patch must preserve diagnostics quality as much as possible.
Two rules are especially important:
- source patch directives should be consumed only once
- sanitizing directive lines should preserve line count
Also, the current implementation keeps diagnostics simple:
- original stdlib files keep their original file paths
skip and same-name replacement comment out the old declarations there
skipall reduces the original stdlib files to package stubs
- each generated patch file gets a file-level
//line <patch-file>:1
This keeps diagnostics understandable:
- errors in untouched stdlib code still point to the original stdlib file
- errors in patch-side code point back to the original patch file
Incremental rollout
This should be introduced incrementally.
Recommended rollout:
- keep the mechanism opt-in by package
- migrate a few well-bounded packages first
- validate against the existing CI matrix
- only migrate more packages when it clearly reduces maintenance cost
The point is not “convert everything”. The point is to reduce the set of packages that currently need file-level overlay or awkward alt patch behavior.
Non-goals
This proposal is not trying to:
- eliminate
alt patch
- eliminate
overlay
- allow source-patched stdlib code to depend on arbitrary
llgo internals
- automatically migrate every compatibility package
Expected result
If accepted, llgo will have a cleaner compatibility stack:
alt patch for symbol/runtime-level adaptation
source patch for declaration-level source replacement with preserved package identity
overlay only for rare file-level fallback cases
That should reduce maintenance cost for Go version compatibility, especially for cases where alt patch is too late and overlay is too coarse.
Summary
llgocurrently has two main mechanisms for Go stdlib compatibility work:alt patchoverlayBoth are useful, but they solve different problems and both have hard limits.
This proposal introduces a third mechanism, source patch, intended to sit between them:
alt patchoverlayThe goal is not to replace every existing mechanism. The goal is to use:
alt patchfor symbol-level and runtime-coupled compatibilityoverlayonly as a last resortsource patchfor packages that need source-level replacement while still keeping the original stdlib package identityBackground
Today we mainly rely on two strategies.
1.
alt patchThis is the preferred mechanism when possible.
It works well for:
//llgo:skip///llgo:skipallIts strengths are:
But it also has a limit:
That becomes fragile when compatibility depends on:
This is where
alt patchstarts to become too late in the pipeline.2.
overlayoverlaysolves that by replacing files in the original stdlib package before load/typecheck.Its main strength is:
This is sometimes exactly what we need.
But
overlayis also expensive:So
overlayis powerful, but not a good default.Problem
There is a real gap between
alt patchandoverlay.Some packages need more than symbol-level patching, but less than full file replacement.
Typical examples are packages where we need to:
This has shown up most clearly in packages such as:
internal/synciterinternal/reflectliteThese cases are awkward for
alt patch, butoverlayis heavier than necessary.Proposal
Add a source patch mechanism.
Core idea
Before
packages.Load/ type checking,llgobuilds a patched source view for selected stdlib packages:runtime/_patch/<pkg>z_llgo_patch_*.gofiles in the original stdlib packageThis gives us:
Source patch semantics
Patch files are normal Go files, with build tags as usual.
The current directive model is intentionally small:
//llgo:skipall//llgo:skip name1 name2 ...This means we do not need a separate explicit
replacedirective. In practice:Supported top-level declarations
The current implementation supports these top-level declaration kinds:
typeconstvarfuncT.Mand(*T).MFor
type,const, andvar, matching is by the declared top-level name.For methods, matching is by receiver-plus-name key, for example:
T.M(*T).Mconst/vargranularityThere is one important detail for
constandvar.If the original code is written as separate specs inside a grouped declaration, each spec can be handled independently:
In that case, replacing or skipping
Adoes not require removingB.However, if the original code is written as a single multi-name
ValueSpec, the current implementation does not split it:If only one name from such a spec is replaced or skipped, the whole original spec is commented out. This keeps the implementation simple and deterministic, but it is a known limitation.
Multiple patch files
If multiple patch files are active for the same package:
skipdirectives are unionedz_llgo_patch_<name>.go//llgo:skipall, the original stdlib files are reduced to package stubs and all patch-side code comes from the generated patch filesThis keeps patch files independent while still letting the original stdlib source be filtered consistently.
Why this is better than
overlayCompared with
overlay, source patch:So source patch should be the preferred choice when:
Why this is better than
alt patchfor some packagesCompared with
alt patch, source patch runs earlier.That matters when correctness depends on the loader/type checker seeing the patched package shape from the beginning, not after the package shape is already fixed.
This is especially relevant for packages involving:
Important limitation
Source patch must not become a way to import arbitrary
llgointernals into stdlib packages.A source-patched stdlib package should not import:
github.com/goplus/llgo/runtime/internal/...That breaks the model and creates module/internal visibility problems.
Instead, source patch code should be limited to:
internal/...packages that are already valid for that packageinternalllgocompatibility surfaces such as:github.com/goplus/llgo/runtime/abi//go:linknamefor runtime hooks where neededIf a package fundamentally depends on
llgo/runtime/internal/..., it is a strong sign that it should remain analt patch, not a source patch.Positioning relative to existing mechanisms
This proposal does not remove
alt patchoroverlay.The intended layering is:
Keep using
alt patchforllgoruntime internalsLikely examples:
runtimereflectinternal/abisync/atomicinternal/runtime/mapsinternal/runtime/sysUse
source patchforruntime/internal/...Good candidates include packages like:
iterinternal/syncinternal/reflectliteuniquewhere appropriateKeep
overlayonly as a last resortOnly for cases where even declaration-level source patch is not enough and file-level replacement is truly required.
Implementation outline
A minimal implementation can work like this:
runtime/build.gopackages.Load, build a patched source view for each selected packagez_llgo_patch_*.goin the original stdlib packageDiagnostics and line mapping
This part is important.
Source patch must preserve diagnostics quality as much as possible.
Two rules are especially important:
Also, the current implementation keeps diagnostics simple:
skipand same-name replacement comment out the old declarations thereskipallreduces the original stdlib files to package stubs//line <patch-file>:1This keeps diagnostics understandable:
Incremental rollout
This should be introduced incrementally.
Recommended rollout:
The point is not “convert everything”. The point is to reduce the set of packages that currently need file-level overlay or awkward
alt patchbehavior.Non-goals
This proposal is not trying to:
alt patchoverlayllgointernalsExpected result
If accepted,
llgowill have a cleaner compatibility stack:alt patchfor symbol/runtime-level adaptationsource patchfor declaration-level source replacement with preserved package identityoverlayonly for rare file-level fallback casesThat should reduce maintenance cost for Go version compatibility, especially for cases where
alt patchis too late andoverlayis too coarse.