⏺ Stage 2 complete and green. Summary (git is yours — ready to branch/PR): #17
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "ff-stage-2-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 2: arithmetic provenance
Mechanism evolution (the interesting part): instead of threading a site_id
param down through the recursive arithmetic evaluator to each error
constructor, I added a Machine::error_site field. The raising builtin sets
it at its ABI boundary and clears it on exit; set_formal — the single
function every error constructor routes through — appends at file:line:col
from it. So:
arg, set/reset error_site around the eval, and every error they can raise
(evaluation_error, type_error, instantiation) gets provenance with zero
evaluator changes.
of an explicit param + helper). This supersedes the planned
append_to_error_msg helper from PR#15 — the centralization in set_formal is
the cleaner form, and NO_SITE (the default) allocates nothing on the
no-provenance path.
two RUNTIME_DECLS updated.
Verified end-to-end on real binaries: compiled-body _ is 1 // 0 →
…evaluation_error(zero_divisor)… at prog.pl:2:5; _ is foo + 1 and 1 < foo →
type_error(evaluable, foo) at …:2:5; query-side _ is 1 // 0 → no suffix
(byte-identical to v1).
Tests: 4 new integration tests (eval/type/comparison provenance +
query-side no-suffix), plus the deferred PR#16 #2 pin
(lowered_goals_have_non_degenerate_spans). The byte-exact v1 unit tests and
golden IR are preserved. 404 pass, clippy clean.
SPANS.md updated to record Stage 2 and the error_site mechanism.
Review — Stage 2: arithmetic provenance via
Machine::error_siteThe mechanism evolution is the part worth stopping on. Threading
site_idthrougharith::evaland every error constructor it can reach (evaluation_error, type_error, instantiation, …) would have been an ugly recursive plumbing job. Instead, moving the suffix-append intoset_formal— the one function every error constructor already routes through — and gating it on aMachine::error_sitefield set at the raising builtin's ABI boundary is a real architectural insight: it exploits an existing structural property of the runtime to make "add provenance to a new raising builtin" a two-line change. The migration ofexistence_errorto the same mechanism is the right move; collapsing two patterns into one shrinks the surface a future contributor has to learn.Issues below, ordered by impact.
Real concerns
1. The set/clear discipline is not RAII-enforced.
plg_rt_b_isandplg_rt_b_arith_cmpboth do:This works for these two, but it's a
try { ... } finally { reset }pattern with no compiler help. The failure modes for future raising builtins:The fast-follow list is long (
functor/3,arg/3,=../2, theatom_*family,number_chars/codes,msort/sort,succ,plus). Each one is one more chance to miss a reset. A small RAII guard would make this self-enforcing:Each raising builtin then does
let _g = ErrorSiteGuard::enter(m, site_id);at the top and the reset is impossible to forget. It also kills the'cmp:labeled block (#4 below) and thelet r = ...; reset; rshuffle. The borrow-checker pain withmref(m)may be why the current shape avoids the guard — worth seeing if it works before the next stage's six builtins land each with their own set/clear pair.2.
set_formalnow has a hidden dependency onm.error_site. Any future code path that callsset_formal(directly or transitively viaresource,throw_term, etc.) and runs whilem.error_sitehappens to be set will silently append a suffix from that site. This is the failure mode of #1 from the consumer side. Worth a load-bearing comment nearset_formal:3.
error_siteassumes raising builtins are leaf operations. Today they are:arith::evalis a pure expression walker that never recurses back into compiled code or into another raising builtin. But the fast-follow list includes type-checking det builtins, and one of them may eventually want to call back into the goal walker (or invoketry_builtin—control.rsalready calls back intopred::plg_rt_b_is). If a nested raise ever happens — outer raise setserror_site = A, inner raise setserror_site = B, returns, outer continues withB— the outer's raise gets the inner's site. Worth a// INVARIANTcomment on the field saying "raising builtins are assumed leaf; if a meta-builtin makes nested raises possible, this needs a save/restore stack." The guard pattern in #1 actually solves this for free if the guard saves the previous value rather than resetting toNO_SITE.Small observations
4. The
'cmp:labeled block inplg_rt_b_arith_cmpexists solely to hop past them.error_site = NO_SITEcleanup at the end. It works, but it's an unusual idiom in this codebase — and it's exactly the pattern RAII would erase. If the guard from #1 lands, this collapses to a plain function body.5. The PR #15 follow-ups don't appear in SPANS.md's "Remaining" section. Specifically,
CodeGen::site_idstill rebuildsSourceMap::newper call (O(N×M) over source) and the srcmap still doesn't dedupe. Out of scope here, but the doc reads as if Stage 2 is the last codegen-side item; worth a one-line "deferred from PR #15 review" note so they don't fall off the radar.6. Two allocations on the hot path.
errors.rs:This formats into a temporary
Stringand then copies it intomessage. A direct write avoids the temporary:Cold path today; matters once a tight loop (arithmetic in a recursive predicate) hits a type error repeatedly. Trivial.
7. The
existence_suffix_appears_when_site_resolvesunit test now setsm.error_site = 0;inline. Cleaner than threading the param. But: each test that wants provenance now has to remember to set it before AND reset after (the same discipline as #1, at the test layer). With per-testmachine()construction the cross-test contamination risk is zero, but a single test that does multiple raises in sequence could trip on it. Mention only if you grow that test pattern.What's good
set_formalexploits an existing choke point. Every error constructor already routes through it; piggybacking the suffix-append on that property turns "each new raising builtin threads site_id through every constructor" into "each new raising builtin sets/clears one field at its boundary." Real architectural insight, and it's the right call.arith::evalis untouched. That's the actual payoff — the recursive evaluator stays a pure arithmetic walker, ignorant of provenance. A future arith refactor (constant folding, bignum optimization) doesn't have to know spans exist.existence_errormigrated to the same field. Two mechanisms collapse to one. Theappend_to_error_msghelper from PR #15's review is correctly superseded — the centralized field is the cleaner form, and SPANS.md records why.NO_SITEas the default field value preserves byte-identical v1 behavior. Runtime-internal raises (query-side,solve.rsdispatch) leaveerror_siteuntouched atNO_SITE→site_locationreturnsNone→ no suffix.query_side_arith_error_has_no_location_suffixpins this contract directly.evaluation_error,type_error(viais/2),type_error(via<), and the query-side no-suffix case. The contract is pinned at the binary-level integration layer, not just at the unit-test layer — that's the right place because it exercises the full ABI dance.lowered_goals_have_non_degenerate_spans). Good follow-through; theg.spaninvariant Stage 2 reads from is now defended.append_to_error_msghelper will see the rationale for not doing so.control.rscallers passNO_SITEexplicitly. Query-sidetry_builtindoesn't accidentally inherit a previous raise's site; the explicit NO_SITE argument documents the intent.Suggested order
#1 (the RAII guard) is worth doing as part of this PR — it's small, and the next round of raising builtins each gets to use it instead of reinventing the set/clear. #2 (the
set_formalinvariant comment) is a one-line addition. #3 (the leaf assumption) is a comment on the field. The rest are observations.