diff --git a/Makefile b/Makefile index 0f7605832..ef8755991 100644 --- a/Makefile +++ b/Makefile @@ -268,23 +268,49 @@ install-hooks: git config merge.sums.driver "scripts/git-merge-sums.sh %O %A %B %L %P" @echo "Registered the 'lgb' + 'sums' merge drivers (core_compiled.lgb / generated.sums)." -# Non-mutating front gate: fail before any target has a chance to refresh -# pkg/rt/generated.sums. This catches the exact drift that a prior go generate -# or lgbgen invocation would otherwise mask in CI. +# Fast local staleness probe: compares the recorded source digest against the +# sources on disk. Used by scripts/pre-commit, which needs an answer in +# milliseconds rather than a full regeneration, and by the build job in CI. +# +# No longer a prerequisite of check-generated. As a prerequisite it aborted the +# content gate — the check that actually binds sources to artifacts — on a +# proxy's verdict, so a stale digest hid the answer you wanted. The two run +# independently now. This does not change whether a stale digest fails CI: it +# still does, here and in TestGeneratedArtifactsAreFresh. check-generated-manifest: $(GO) @go run ./cmd/check-generated +# Committed generated artifacts that the content gate compares. Keep in step +# with scripts/generate.lg. core_go_lowered/ is absent because it is gitignored +# (a build artifact) and gets the behavioral gate instead. +GENERATED-TRACKED := \ + pkg/rt/core_compiled.lgb \ + pkg/ir/op_generated.go \ + pkg/rt/ir_bridge_generated.go \ + pkg/rt/zz_primitives_generated.go \ + pkg/rt/core/ir/data/generated.lg + +# Committed, generated, and deliberately NOT compared. lgbgen's writeBundle +# refreshes this on every regeneration, so `make generate` — and therefore this +# gate — rewrites it as a side effect. The gate stashes and restores it so a +# check-* target does not mutate a tracked file. Its own staleness is covered by +# check-generated-manifest and by TestGeneratedArtifactsAreFresh. +GENERATED-DIGEST := pkg/rt/generated.sums + # Single gate for every generated artifact. One target to remember, and it # treats the two artifacts by their actual nature. VCS-agnostic by design: # it shells out to no `git` (this repo is used with jj, whose secondary # workspaces have no .git, so a `git diff` gate breaks there). # -# * core_compiled.lgb is byte-deterministic, so it gets a CONTENT gate: -# stash the committed bytes, regenerate, and `cmp`. A difference means the -# committed bundle was stale. Survives a fresh checkout (an mtime -# `find -newer` check silently passes after any VCS checkout — in fact the -# old check-lowered-fresh pointed at a path that no longer exists and had -# been a silent no-op). +# * Every artifact in GENERATED-TRACKED is byte-deterministic, so they get a +# CONTENT gate: stash the committed bytes, regenerate, and `cmp`. A +# difference means the committed artifact was stale. Survives a fresh +# checkout, unlike an mtime `find -newer` check. +# +# Regenerating through `make generate` rather than lgbgen alone is what +# widened this from the bundle to all five. lgbgen emits only the .lgb and +# the lowered tree, so op/ir_bridge/primitives/ir-data were never verified — +# and zz_primitives_generated.go had drifted from its generator. # # * core_go_lowered/ (+ the gogen_ir wireup files) is NOT committed — it is # a build artifact, regenerated on demand and gitignored. Its self-lower @@ -297,20 +323,34 @@ check-generated-manifest: $(GO) # # This is the gate CI runs. After a merge/rebase touching pkg/rt/core/**, run # `make check-generated` (or `make generate` to refresh, then commit). -check-generated: check-generated-manifest $(GO) - @echo ">> regenerate bundle + lowered tree from a SINGLE core compile (--target=both)" - @cp pkg/rt/core_compiled.lgb pkg/rt/.core_compiled.lgb.committed - @go run -tags bootstrap ./cmd/lgbgen --target=both >/dev/null - @echo ">> bundle: verify lockstep (content-based, VCS-agnostic)" - @if cmp -s pkg/rt/core_compiled.lgb pkg/rt/.core_compiled.lgb.committed; then \ - rm -f pkg/rt/.core_compiled.lgb.committed; \ - echo "OK: core_compiled.lgb in lockstep with the .lg sources."; \ - else \ - rm -f pkg/rt/.core_compiled.lgb.committed; \ - echo "ERROR: pkg/rt/core_compiled.lgb is stale — the regenerated bytes differ."; \ - echo " Run 'make generate' and commit the regenerated bundle."; \ +check-generated: $(GO) + @echo ">> stash committed artifacts, regenerate all of them, compare" + @stash=$$(mktemp -d) || exit 1; \ + trap 'rm -rf "$$stash"' EXIT HUP INT TERM; \ + for f in $(GENERATED-TRACKED) $(GENERATED-DIGEST); do \ + cp "$$f" "$$stash/$$(echo $$f | tr / _)" || exit 1; \ + done; \ + if ! $(MAKE) --no-print-directory generate >"$$stash/generate.log" 2>&1; then \ + cat "$$stash/generate.log"; \ exit 1; \ - fi + fi; \ + cp "$$stash/$$(echo $(GENERATED-DIGEST) | tr / _)" $(GENERATED-DIGEST); \ + echo ">> verify lockstep (content-based, VCS-agnostic)"; \ + fail=0; \ + for f in $(GENERATED-TRACKED); do \ + if cmp -s "$$f" "$$stash/$$(echo $$f | tr / _)"; then \ + echo " OK $$f"; \ + else \ + echo " STALE $$f — the regenerated bytes differ."; \ + fail=1; \ + fi; \ + done; \ + if [ $$fail -ne 0 ]; then \ + echo "ERROR: committed generated artifacts are stale."; \ + echo " Run 'make generate' and commit the result."; \ + exit 1; \ + fi; \ + echo "OK: all tracked generated artifacts in lockstep with their sources." @echo ">> lowered tree: compile + dispatch natively under -tags gogen_ir" @go build -tags gogen_ir ./pkg/rt/... @out=$$(printf '(require (quote ir.passes.dce)) (println "DCE-TYPE:" (type ir.passes.dce/dce))' \ diff --git a/cmd/lginterop/prims_emit.go b/cmd/lginterop/prims_emit.go index a73892cdf..3dcf9804c 100644 --- a/cmd/lginterop/prims_emit.go +++ b/cmd/lginterop/prims_emit.go @@ -62,13 +62,20 @@ package rt import ( "fmt" + "github.com/nooga/let-go/pkg/vm" `) + // The blank line above separates stdlib from external imports. It has to be + // emitted here: main.go runs the file through gofmt, and gofmt PRESERVES + // existing grouping rather than introducing it — only goimports inserts that + // separator, and it is a linter here, not part of generation. Without the + // blank line every regeneration produced a file the goimports linter would + // rewrite, so the committed copy (hand-grouped) and generator output drifted. + // // Add imports for scanned packages (excluding same-package). Emission order - // here is irrelevant: the final file is run through gofmt (main.go), which - // canonicalizes the import block. Registration STATEMENT order below is NOT - // touched by gofmt, so that path sorts its keys explicitly. + // within the group is irrelevant — gofmt sorts it. Registration STATEMENT + // order below is NOT touched by gofmt, so that path sorts its keys explicitly. for goPkg := range pkgAliasMap { if !isSamePackage(goPkg) { fmt.Fprintf(&b, "\t%q\n", goPkg) diff --git a/pkg/rt/generated.sums b/pkg/rt/generated.sums index b2e29b9cd..29011466f 100644 --- a/pkg/rt/generated.sums +++ b/pkg/rt/generated.sums @@ -2,4 +2,4 @@ # Content digest of all .lg + lgbgen sources that feed the .lgb # bundle and the lowered Go tree. The genmanifest staleness test # fails if this no longer matches the sources on disk. -0a3d203cefa6782f9266fe6aa86a36d1f34dc65170af0eb37708162ced4f4d6d +b6f7838debc9b8c2b9d4822c02e341c4153cdd9cbb0d302d9f5a434b4671d202