Skip to content

build: prepare read-only package backend state - #2282

Merged
xushiwei merged 3 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/parallel-build-pr6-overlay
Aug 9, 2026
Merged

build: prepare read-only package backend state#2282
xushiwei merged 3 commits into
xgo-dev:mainfrom
zhouguangyuan0718:agent/parallel-build-pr6-overlay

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • collect linknames, exports, closure-env directives, nointerface markers, type backgrounds, package syntax identities, and locality metadata before backend lowering starts
  • create each backend Program with a fresh LLVM Context, TargetMachine, lowering/type caches, ABI state, and C ABI transformer while directly sharing the already-prepared Go metadata read-only
  • precompute caller-frame tracking for every Go SSA package before workers start
  • preload the effective patched package syntax explicitly because patch.Types has a distinct go/types identity

Design boundary

This PR is stacked on #2280. It deliberately replaces the earlier snapshot/overlay approach: there is no freeze API, syntax base/delta, worker merge, syntax validation pass, or metadata copying. One-shot compiler users retain Program-local mutation; the build driver switches to read-only syntax mode only after all package preparation is complete.

Parallel scheduling remains in the following PR; this change only establishes the minimal safe state boundary it needs.

Validation

  • go test ./ssa ./cl ./internal/build -count=1
  • go test ./ssa ./cl ./internal/build -coverprofile=... -count=1
  • targeted race tests for backend Program sharing, preloaded syntax, caller tracking, and build sessions
  • new executable-line patch coverage measured locally at about 92.3%

@fennoai fennoai Bot left a comment

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.

Review: parallel-build backend overlay scaffolding

The "freeze-then-share" design is coherent and applied consistently: each accessor checks its Program-local map first, then the read-only baseline (packageSyntaxBase / typbgBase / frozen locality state), which is the right shape for lock-free concurrent reads. FreezePackageSyntaxState deep-copies into a fresh snapshot so the coordinator's continued local writes don't mutate shared state, and assertMutable() gives a fail-fast guard. Tests cover the sharing/overlay/concurrency invariants well.

Findings are inline. Summary of the notable ones:

  • Latent nil-deref in the worker session path (newSession/newProgram): the "no Python" case is handled gracefully by the coordinator's lazy closure but not by the eager template path — a worker could nil-deref pyget. Not yet live because newSession is unwired, but worth fixing before it lands.
  • Unlanded session machinery (newSession, backendSession, replayProgramState, ctx.backend) is exercised only by tests — no production caller. newProgram is wired in (coordinator). If the session path is intentional staged scaffolding, a tracking reference would help; otherwise it risks silent drift.
  • Redundant work in CallerTracking.Precompute: the inner pkg.Prog.AllPackages() fan-out re-allocates the full package set N times when pkgs already equals AllPackages(). Low severity (runs once per compilation) but easy to eliminate.
  • Doc inaccuracy on FreezeLocalityState: the "parse markers remain local to each Program" sentence is both misplaced (markers moved off localityInfos) and contradicted by the shared-baseline behavior the tests assert.
  • Minor: copy shadows the builtin in two spots.

Additional context (no change requested):

  • Concurrency safety of the lock-free shared reads currently relies on an undocumented happens-before: Precompute/freeze fully completes before any worker spawns, and frozen is a plain (non-atomic) bool with no test spawning writers across the freeze transition. Correct today; worth documenting the invariant (or making frozen atomic) so a future change fails loudly under -race rather than silently.
  • The frozen-coordinator locality path is safe only because ParsePkgSyntax short-circuits on PackageSyntaxParsed; a patched package (different *types.Package pointer) declaring a package var absent from the original could reach assertMutable() and panic. Fragile invariant — an explicit assertion/comment would harden it.

Comment thread internal/build/build.go Outdated
prog.SetRuntime(t.runtimePackage)
}
if t.pythonPackage != nil {
prog.SetPython(t.pythonPackage)

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.

Latent nil-deref in the worker session path. The coordinator installs Python via a nil-safe lazy closure (SetPython(func() *types.Package { ... return nil })), so python() degrades gracefully when Python isn't in the build. Here a worker only calls SetPython when t.pythonPackage != nil, leaving both p.py == nil and p.pyget == nil. If any worker code path then reaches Program.python() it calls p.pyget() on a nil func value and panics (ssa/python.go).

This isn't live yet because newSession has no production caller, but it should be fixed before the parallel path lands: install the same nil-returning closure fallback (or set unconditionally). The SetRuntime/runtime() pair has the same asymmetry but is lower risk since runtimePackage is effectively always set (altPkgs[0].Types).

Comment thread internal/build/build.go Outdated
return prog
}

func (t backendProgramTemplate) newSession() (backendSession, error) {

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.

newSession, backendSession, replayProgramState, and the ctx.backend field (written at ctx construction, never read in production) are exercised only by backend_program_test.go — no production code spawns a session. newProgram is wired in as the coordinator program. This is a sizeable chunk of code whose behavior is guaranteed only by unit tests, with no production caller to keep it honest as the surrounding build logic evolves.

If this is intentional staged scaffolding for a follow-up parallel-build PR, a tracking issue/comment reference would make that clear. Otherwise consider gating it behind a clearly-labeled experimental path or deferring it.

Relatedly, replayProgramState calls ParsePkgSyntax (which runs DeclareLocality on the session's own fresh locality map) before UseLocalityState swaps in the frozen shared state, so those writes are discarded. Harmless today only because the shared frozen state already contains those declarations; consider installing the shared locality state first, or documenting why the pre-replay writes are intentionally thrown away.

Comment thread internal/build/build.go Outdated
func newBackendProgramTemplate(target *llssa.Target, conf *Config, funcInfoMetadata, funcInfoSites bool) backendProgramTemplate {
var targetCopy *llssa.Target
if target != nil {
copy := *target

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.

copy shadows the copy builtin here (and again at the newProgram target-copy below). Legal but a maintenance hazard — a later edit needing copy() in this scope would misbehave. Suggest renaming to targetCopy (as already done for the outer variable in this function).

Comment thread cl/instr.go Outdated
}
all[pkg] = true
if pkg.Prog != nil {
for _, programPkg := range pkg.Prog.AllPackages() {

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.

Redundant O(N·M) work. Precompute is called as ctx.callerTracking.Precompute(ctx.progSSA.AllPackages()), so pkgs already is the full program package set. All packages share the same *ssa.Program, so pkg.Prog.AllPackages() returns the identical full set on every iteration — and AllPackages() allocates a fresh slice and re-iterates the package map each call, giving N slice allocations and N·M insertions.

Low severity (runs once per compilation, dwarfed by the analysis it feeds), but pure redundant work: iterate pkgs directly, or hoist a single AllPackages() call as a fallback only if pkgs may be a subset.

Comment thread ssa/locality.go Outdated
}

// FreezeLocalityState freezes p's locality metadata and returns it without
// copying. Package syntax parse markers remain local to each Program.

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.

This comment is inaccurate and misplaced. parsedPackages no longer lives on localityInfos — it moved to Program (ssa/package.go), so a note about parse markers doesn't belong on FreezeLocalityState. More importantly, the claim that parse markers "remain local to each Program" is contradicted by the code: the coordinator's markers are copied into the shared PackageSyntaxState snapshot in FreezePackageSyntaxState and read across all Programs via packageSyntaxBase in PackageSyntaxParsed (and package_syntax_test.go explicitly asserts a coordinator-marked package IS visible through the shared baseline). Only newly worker-discovered markers stay Program-local.

Suggest dropping this sentence from FreezeLocalityState, and if useful adding a corrected version to FreezePackageSyntaxState.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.07143% with 25 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/build/build.go 73.80% 5 Missing and 6 partials ⚠️
cl/import.go 72.22% 5 Missing and 5 partials ⚠️
ssa/type_cvt.go 96.00% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

c1728af80cbb | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 18456 B +0.0% 227.133 ms -1.9% (better) 914.436 us +0.1% (worse)
Linux fmtprintf 1861416 B +1.7% (worse) 2.501 s +0.5% (worse) 2.402 ms +6.0% (worse)
Linux println 68008 B +0.0% 213.468 ms -2.7% (better) 1.147 ms -5.4% (better)
macOS cprintf 84672 B +0.0% 280.425 ms -35.4% (better) 2.335 ms -36.5% (better)
macOS fmtprintf 1869328 B +0.0% 2.178 s -3.5% (better) 10.547 ms +1.9% (worse)
macOS println 121200 B +0.0% 276.985 ms -18.1% (better) 3.113 ms -19.0% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 9.623 ns/op -0.2% (better)
Linux BenchmarkMergeCompilerFlags 119 ns/op -1.0% (better)
Linux BenchmarkMergeLinkerFlags 80.710 ns/op -0.2% (better)
Linux BenchmarkChannelBuffered 49.090 ns/op -10.5% (better)
Linux BenchmarkChannelHandoff 26592 ns/op +4.2% (worse)
Linux BenchmarkDefer 37.520 ns/op -5.2% (better)
Linux BenchmarkDirectCall 1.151 ns/op +13.3% (worse)
Linux BenchmarkGlobalRead 0.962 ns/op +1.7% (worse)
Linux BenchmarkGlobalWrite 7.260 ns/op -0.2% (better)
Linux BenchmarkGoroutine 35854 ns/op -0.4% (better)
Linux BenchmarkInterfaceCall 4.969 ns/op -20.9% (better)
Linux BenchmarkRuntimeGetG 1.427 ns/op +1.3% (worse)
macOS BenchmarkLookupPCRandom 10.790 ns/op +1.8% (worse)
macOS BenchmarkMergeCompilerFlags 95.170 ns/op -12.4% (better)
macOS BenchmarkMergeLinkerFlags 64.700 ns/op -1.8% (better)
macOS BenchmarkChannelBuffered 23.770 ns/op +10.6% (worse)
macOS BenchmarkChannelHandoff 7474 ns/op +12.6% (worse)
macOS BenchmarkDefer 34.380 ns/op +10.2% (worse)
macOS BenchmarkDirectCall 1.155 ns/op +18.4% (worse)
macOS BenchmarkGlobalRead 1.077 ns/op +14.2% (worse)
macOS BenchmarkGlobalWrite 1.130 ns/op +15.8% (worse)
macOS BenchmarkGoroutine 47759 ns/op +73.1% (worse)
macOS BenchmarkInterfaceCall 5.100 ns/op +18.4% (worse)
macOS BenchmarkRuntimeGetG 2.070 ns/op +9.1% (worse)

Compared with 6670dae3884d measured in the same runner job.

@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6-overlay branch from 8f51e28 to a70be63 Compare August 4, 2026 07:33
@zhouguangyuan0718 zhouguangyuan0718 changed the title build: share syntax state with worker overlays build: share package syntax with worker deltas Aug 4, 2026
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6-overlay branch from a70be63 to 4db979f Compare August 4, 2026 13:43
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6-overlay branch from 40ba68d to 9cc28fa Compare August 5, 2026 13:30
@zhouguangyuan0718 zhouguangyuan0718 changed the title build: share package syntax with worker deltas build: prepare read-only package backend state Aug 5, 2026
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6-overlay branch from 9cc28fa to 5b7167c Compare August 5, 2026 13:44
@zhouguangyuan0718
zhouguangyuan0718 force-pushed the agent/parallel-build-pr6-overlay branch from 5b7167c to c1728af Compare August 5, 2026 16:12
@zhouguangyuan0718

Copy link
Copy Markdown
Contributor Author

@xgopilot please review the current PR head (c1728af) from scratch. Focus on whether package syntax, locality, caller-tracking, runtime/Python package data, and frontend options are fully prepared before backend workers start; whether shared state is truly read-only; whether worker-local writes can leak or race; patched/alternate-package behavior; order independence and deterministic output; and regression-test completeness. Please ignore findings that only apply to earlier revisions.

@fennoai

fennoai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

An internal error occurred. Please try again later.

View job run

@zhouguangyuan0718

Copy link
Copy Markdown
Contributor Author

@xgopilot review current head c1728af. Focus on freeze-before-share correctness, worker read-only state, patched/alternate packages, races, determinism, and missing tests.

@fennoai

fennoai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review posted ✅

Reviewed head c1728af against base 6670dae (locked range). Findings submitted as a PR review with 3 inline comments — see the review.

Verdict: solid, well-tested preparatory change. No blocking issues.

  • Freeze-before-share: packageSyntaxData centralizes all Go-owned source metadata behind one RWMutex; NewBackendProgram shares it + localities read-only while giving each backend a fresh LLVM context/TM/caches. Field migration is complete — no stale readers of the old fields.
  • type_cvt.go recursive rewrite (highest risk): verified needsTypeConversion stays in lock-step with cvtType across every shape; converted placeholder is published before descending so mutual-recursion is order-independent; negative-caching can't be poisoned by cycle back-edges. Generic-instance SetUnderlying ordering is safe.
  • Patched/alternate packages: preloadPatchedPackageSyntax handles patch.Types' distinct identity, sorts paths, clones syntax → deterministic. //export and C-package X-prefix handling preserve prior semantics.
  • Races/determinism: no new nondeterminism; caller-tracking sets are membership maps.

Non-blocking points raised: (1) Precompute completeness is a precondition for the concurrent phase, not yet enforced — worth documenting the AllPackages()-is-complete invariant; (2) NewBackendProgram/newBackendSession are test-only until the scheduler PR wires them in; (3) SetPython now captures a possibly-nil package eagerly (more defensive). Also flagged one missing test: no direct assertion that two backends read-only share coordinator syntax mutations.

Verification limitation: could not run go test ./ssa ./cl ./internal/build here — the LLVM C headers required by github.com/xgo-dev/llvm aren't installed in this sandbox, so LLVM-dependent packages don't compile. Findings are from static review; gofmt is clean on all changed sources. The PR's own notes report the suites, race tests, and ~92.3% patch coverage passing.

@fennoai fennoai Bot left a comment

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.

Review — read-only package backend state

Reviewed head c1728af against base 6670dae over the locked range. Focus areas: freeze-before-share correctness, worker read-only state, patched/alternate packages, races, determinism, and missing tests.

Overall: solid, well-tested preparatory change. The state boundary is coherent: packageSyntaxData gathers all Go-owned source metadata (linknames, exports, closure-env, nointerface, type backgrounds, parsed-package set) behind one sync.RWMutex, and NewBackendProgram shares that plus localities read-only while giving each backend a fresh LLVM context, TargetMachine, and lowering caches. Field migration is complete — no stale readers of the old linkname/noInterface/closureEnvDirectives/gocvt.typbg/parsedPackages fields remain. gofmt is clean on all changed sources.

Correctness — verified

  • type_cvt.go recursive-conversion rewrite is the riskiest change and looks correct. The new namedNeedsTypeConversion decides the whole recursive graph up front, then publishes the converted placeholder before descending, so mutually-recursive named types no longer depend on which cycle member is visited first. I checked the needsTypeConversion predicate case-by-case against cvtType/cvtInterface/cvtStruct/cvtTuple/cvtUnion/cvtClosure and they stay in lock-step (Signature/Union always-convert, closure-struct never-convert, interface method+embedded coverage, etc.). The negative-result caching only stores conversionNotNeeded when the top-level query is fully negative, so cycle back-edges can't poison the cache. TestNamedTypeConversionIsIndependentOfTraversalOrder, TestRecursiveGenericNamedTypeConversion, TestTypeConversionRequirementShapes, and TestRecursiveNamedTypesWithoutConversionKeepTheirIdentity cover exactly these hazards.
  • Generic instance underlying ordering is safe. The instance placeholder is published to p.typs before origin.SetUnderlying(tund), but cvtNamed/cvtType back-edges only return the cached pointer without dereferencing .Underlying(), so no caller observes the pre-SetUnderlying (Int) underlying.
  • //export and C-package X-prefix handling preserves prior semantics. Collection now records exports into prog during ParsePkgSyntaxWithOptions, and preloaded initFiles replays them via PackageExportpkg.SetExport. The C-package condition pkg.Name()=="C" matches the old cPkg (pkgName=="C" at compile.go:2293), and PreloadedSyntax gates the split cleanly.
  • Determinism. preloadPatchedPackageSyntax sorts patch paths and clones syntax slices; caller-tracking sets are membership maps (order-independent); namedNeedsTypeConversion's map iteration only writes an order-independent value. No new nondeterminism.

Points worth confirming (non-blocking)

  1. Precompute completeness is a precondition, not yet enforced. Criterion-2 in runtimeCallerFuncSet reaches into callee.Pkg and lazily writes c.base[callee.Pkg]. If a static callee's package is ever absent from the slice passed to Precompute, a worker would mutate the shared map during the concurrent phase (the future PR). Today everything still runs sequentially so there's no race, and AllPackages() is a reasonable superset — but the safety of the shared read-only maps hinges on this invariant. Worth an assertion or a comment tying it to AllPackages() when scheduling lands.
  2. newBackendSession/NewBackendProgram are not wired into the production build path yet (only tests reference them). So the read-only sharing is exercised by tests, not by real builds in this PR — expected for a boundary-only change, just flagging that end-to-end coverage arrives with the scheduler.
  3. SetPython behavior change: it now captures a possibly-nil *types.Package eagerly instead of the previous lazy dedup.Check(PkgPython).Types (which would have panicked on a nil result). This is strictly more defensive; just confirm no downstream code relied on the old non-nil guarantee.

Tests

Coverage is strong for the areas in scope: backend-program state sharing (ssa/backend_program_test.go, internal/build/backend_program_test.go), preloaded-syntax replay (cl/preloaded_syntax_test.go), concurrent caller-tracking reads (cl/caller_tracking_precompute_test.go), and the type-conversion rewrite. One gap: there's no direct test asserting two NewBackendProgram instances observe the same shared syntax mutation done on the coordinator after creation is complete (the invariant that they read, never write, shared metadata) — the existing tests check field identity and value propagation, which is close but not a concurrency assertion.

Limitation: I could not execute go test ./ssa ./cl ./internal/build in this sandbox — the LLVM C headers (llvm-c/Core.h) required by github.com/xgo-dev/llvm are not installed, so the LLVM-dependent packages don't compile here. The findings above are from static review; the PR's own validation notes report the suites, race tests, and ~92.3% patch coverage passing.

No blocking issues found.

Comment thread ssa/type_cvt.go
origin.SetTypeParams(list)
}
named := origin
if typ, ok := Instantiate(origin, t); ok {

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.

Nice fix. Instantiating origin here and publishing the resulting instance as the placeholder before the cvtType(t.Underlying()) descent is what makes recursive generic back-edges (*My[T]) resolve to the converted instance. Verified that no back-edge dereferences .Underlying() during the descent — cvtNamed/cvtType return the cached pointer only — so filling origin.SetUnderlying afterward is safe. Covered by TestRecursiveGenericNamedTypeConversion.

Comment thread cl/import.go
if linkCollected || item.Args == "" {
continue
}
if item.Args != inPkgName && !options.ExportRename {

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.

Equivalence check vs the old initLink export path holds for the common single-token //export name form (Args is the export name; matches SetLinkname+SetExport). One edge case to confirm: the legacy initLink treated //export args by splitting on the first space, whereas directive.Parse puts everything after //export into item.Args verbatim. A two-token //export foo bar directive would now flow through as item.Args == "foo bar" rather than name/link split. Unusual for //export, but worth a line of test or comment if that form is meant to be supported.

Comment thread internal/build/build.go
return nil, err
}
buildSSAPkgs(ctx, append(append(altEntries, pkgEntries...), depEntries...))
ctx.callerTracking.Precompute(ctx.progSSA.AllPackages())

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.

The read-only safety of the shared caller-tracking maps depends on every package that can appear as a static callee during lowering being present here. runtimeCallerFuncSet (criterion 2) lazily writes c.base[callee.Pkg], so a callee package missing from this slice would cause a shared-map write once workers run concurrently (next PR). Sequential today, so no race — but consider asserting or documenting the AllPackages()-is-complete invariant so the scheduler PR can rely on it.

@xushiwei
xushiwei merged commit a4a1222 into xgo-dev:main Aug 9, 2026
42 checks passed
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.

2 participants