⏺ 407 pass, clippy clean. Stage 3 complete. #18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "ff-stage-3-span"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stage 3: provenance for the type-checking det builtins
The guard from Stage 2 paid off — each of the 12 raising builtins was a
mechanical two-line change.
both the decl-gen (declare … , i32 for raising ones) and the codegen RtDet
arm (lower::det_builtin_raises(sym) → append g.span's site_id). Non-raising
type checks (var/1, I/O, copy_term/2, …) carry no site_id — provenance
only where raises happen.
atom_chars/2, number_chars/2, number_codes/2, msort/2, sort/2, succ/2,
plus/3) gained site_id: u32 + let _site = ErrorSiteGuard::enter(m,
site_id);. control.rs's query-side dispatch passes NO_SITE; the unit tests
use thin per-module wrappers so they stay site-free.
Verified end-to-end: atom_length(123,) → type_error(atom, 123) … at
prog.pl:2:5; sort(foo,) → type_error(list, foo) … :2:5; succ(-1,_) →
domain_error(not_less_than_zero, -1) … :4:5; query-side → no suffix
(byte-identical v1). Three new integration tests pin these; 407 pass,
clippy clean, golden IR + byte-exact v1 messages preserved.
SPANS.md updated: Layer 3 provenance is now complete for all raising
builtins. The only remaining item is the (non-CI) diff-test
suffix-stripping for checkpoint 5; throw/1 stays excluded. Ready for you to
branch/PR — once the diff-test helper lands, SPANS.md can retire to done/.
Review — Stage 3: provenance for the type-checking det builtins
Mechanical, focused, and the payoff from the prior stages is now visible: each of the 12 raising builtins really is a two-line change (a trailing
site_id: u32andlet _site = ErrorSiteGuard::enter(m, site_id);). Theraisesflag inDET_BUILTINSis the right place to put the metadata — it drives both the IR decl-gen (crates/compiler/src/codegen/program.rs) and the RtDet emit (crates/compiler/src/codegen/clause.rs) from a single bit, so they can't drift against each other by construction.Issues below, ordered by impact.
Real concerns
1. End-to-end provenance is tested for 2 of 12 newly raising builtins. The new integration tests cover
atom_length(type_check_builtin_error_carries_source_location) andsort(sort_type_error_carries_source_location). Wired but never verified end-to-end with a suffix assertion:functor/3,arg/3,=../2(termops)atom_concat/3,atom_chars/2,number_chars/2,number_codes/2(atomops)msort/2(sortops)succ/2,plus/3(miscops)The PR description claims "the golden-IR / integration tests catch a mismatch" — true for the IR shape (the decl-gen / RtDet wire-up is generated from the same
raisesflag, so they agree by construction), but the IR doesn't catch a runtime ABI mismatch. If a future refactor flips("functor", 3, ..., true)tofalsewhile leavingplg_rt_b_functor_3's signature carryingsite_id, you get one i32 of garbage on the stack/register read as site_id with no test firing. The 10 missing tests are each one line of "compile a body that hits the builtin with bad input, assertout.contains(" at prog.pl:")." A table-driven test that walks everyraises: truerow and pins a known-bad invocation would close this — and would automatically extend to the next stage of raising builtins (if any).2.
det_builtin_raises(sym)is O(N) per emit site.crates/compiler/src/codegen/lower.rs:Called once per RtDet goal in the program from
emit_inline_builtin. The(name, arity)lookup already happened inlower_goalwhen populatingLGoalKind::RtDet { sym, args }— the natural place to also storeraisesis right there, alongsidesym:Populated once at lower time, read O(1) at emit time, and the fact "does this raise?" is co-located with the symbol it applies to instead of looked up by string at the emit site. Small refactor; meaningful in a 10k-RtDet program (27 × 10k string compares per build).
Small observations
3.
let _site = ErrorSiteGuard::enter(m, site_id);has a silent footgun. If someone writeslet _ = ErrorSiteGuard::enter(m, site_id);instead (easy mistake —let _looks idiomatic), the guard drops immediately and the function body runs withm.error_siterestored to whatever it was before. No raise → no observable bug; raise → wrong site (or none).#[must_use]doesn't catchlet _binding. Two cheap defenses:ErrorSiteGuard: "Bind to a named variable (e.g.let _site);let _drops the guard immediately and the function runs with the wrong site."entered!(m, site_id)expanding tolet _site = ErrorSiteGuard::enter(m, site_id);makes the pattern unambiguous and self-documenting. With 12 call sites — and more if the fast-follow stretches further — the macro pays for itself.4. The
raisesflag and the runtime fn signature have no compile-time agreement check. Same root cause as #1: the table's metadata claims the wire shape, and the runtime fn signature is the wire shape, but nothing forces them to agree at build time. Astatic_assertions::const_assert!on the count ofraises: truerows is hard without a procedural macro. A pragmatic alternative is a unit test that, at runtime, scans the table and asserts each raising builtin's first call site receives the i32 — but that's what #1's end-to-end coverage already proves indirectly, so closing #1 closes most of this. Tracking only.5. The
DET_BUILTINS3-tuple → 4-tuple migration leaks into thevocab_invarianttests. Bothdet_builtins_are_det_rows_in_sharedandrecognized_names_equal_shared_vocabularyalready updated to the new shape. Fine — the cost is two destructures — but if a future addition (say, anarity_min/arity_maxflag for variadic builtins) adds another field, every consumer updates. A named structDetBuiltin { name, arity, sym, raises }would future-proof this and double as documentation at the call site. Stylistic.What's good
ErrorSiteGuardis used uniformly across all 12 sites. No hand-rolled set/restore lurking anywhere; the PR 17 disposition got the foundation right and Stage 3 leans on it instead of inventing variants. The pattern is self-enforcing exactly where the surface grew most.NO_SITE) quietly solve the nesting concern from PR #17 review #3. Ifplg_rt_b_sort_2ever calls back into another raising builtin (today: leaf, but the architecture survives the change), the outer raise sees its own site again on inner-guard-Drop. Right-shaped now, right-shaped later.INVARIANT:comment onMachine::error_siteaddresses PR 17 review #2 directly. Future contributors who reach form.error_site = ...see the contract.NO_SITE's ABI contract note ("MUST equalplg_compiler::codegen::NO_SITE") addresses PR 15 review #7. Independent consts, same value, documented as a contract — won't drift silently.fn alen,fn acat, …) keep existing behavior tests site-free. The intent is obvious to a reader: "this test exercises behavior, not provenance; site is elided." A single-line wrapper instead of inlineNO_SITEat every call site reads cleaner.control.rs's query-side dispatch passesNO_SITEexplicitly. Documented at the comment, mechanical at each call. Query-side raises stay byte-identical to v1;query_side_type_check_error_has_no_location_suffixpins it.throw/1explicitly excluded with rationale (user-thrown ball isn't a system error). Right call — appending source location to a user's deliberate throw would be wrong.raisesboolean drives decl-gen and emit from one bit. Drift between IR shape and codegen lookup is impossible by construction. The single source of truth for "this builtin has provenance" is exactly the row that names the builtin.Suggested order
#1 (the 10 missing end-to-end pins) is worth doing before merge — it's the only thing standing between "wired" and "verified," and it's a single table-driven test. #2 (carry
raisesinRtDet) is small, mechanical, and avoids fan-out lookups. #3+ are observations.