## Summary `llgo` currently has two main mechanisms for Go stdlib compatibility work: - `alt patch` - `overlay` 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: - `T.M` - `(*T).M` ### `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: ```go const ( A = 1 B = 2 ) ``` 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: ```go 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: 1. Declare source-patched package paths centrally in `runtime/build.go` 2. Before the first `packages.Load`, build a patched source view for each selected package 3. For each active patch file: - evaluate Go build tags against the current target - parse source patch directives - collect declarations provided by the patch 4. Comment out or stub original declarations in the original stdlib source view 5. Emit each active patch file as a generated `z_llgo_patch_*.go` in the original stdlib package 6. 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: 1. keep the mechanism opt-in by package 2. migrate a few well-bounded packages first 3. validate against the existing CI matrix 4. 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.