Repository navigation
A def specialized at a finite parameter also goes to --verdict as a λ-match on it, so the kernel checks that each value has an instance that fits its type (#1186) - #1225
Conversation
|
I ran this locally, merged onto main 1159f0b, and through a /code-review pass. The approach is the right follow-up to #1213, and it holds up: spec_finite_match passes --verdict, and skip, same, look and pick all go out with their covering defs (on main skip is missing from the .bendtt). -o over all 1,558 test files runs with no crash. I haven't run the cluster gates. Two things to fix before merging, and one question. The match path skips item_try (the Reroute retry, and each instance kept apart) root_emit's body calls item_ref(e, k, r.cols, true) directly. Since this branch's base, #1215 put a Reroute retry in item_try (safe.ts:271 on main), and safe_book's loop only catches Scope_Error. So once this is rebased, an instance at a finite value that ends up on a rerouted cycle throws an uncaught Reroute from --verdict/-o instead of regrouping. I couldn't reach that from a test, but nothing rules it out. The same call also changes what happens when an instance fails. On main each instance went through item_try: a failing one was listed out of scope and the rest still went to the kernel. Here the first failing arm throws out of root_emit, so the later instances are never emitted or checked, and the out-of-scope line doesn't say which value failed. The PR lists the single line under Costs, but the instances you lose go further than that. Using item_try in body fixes both. A failing instance keeps its own reason, and the covering def goes out of scope through it, by the usual rule that a def naming an out-of-scope def goes with it. One related nit: fresh(e, name_tt(k)) is reserved before ty/body can throw, so on Scope_Error the catch at safe.ts:172 reserves k_ as well, and the first name is never used. The test can't fail on main spec_finite_match.bend only checks that main prints 3n, which main also does. gates/safe.ts scores main as agree: it prints ALL PROOFS CHECK while skip is silently missing. If root_emit went back to main's behaviour, every gate would stay green, so #1186 can come back unnoticed. I don't see a way to pin this without some check that a non-Base def reached the kernel. That's a gate change, so probably a question for Taelin, but it's worth saying in the PR that the test documents the fix rather than guarding it. Question: what does the kernel actually check here? Each arm of the covering def's type is that instance's own type_drop type, and a datatype's arms come from the same adt.c that root_of enumerated. So the kernel checks that the match is exhaustive over A and that each instance fits its own arm. It doesn't check against k's declared ∀ type: a wrong substitution in type_drop would land on both sides and still check. For Void the kernel sees λ{(,): λ{}} and still never sees skip's body, which I think is intended per your #1213 comment. If that's the claim, say so in the header comment, i.e. "the instances cover A's values". The current wording, "so the kernel checks that they cover it", reads stronger than that. Smaller
|
1d36887 to
58e1426
Compare
|
Unsupervised AI action: rebased by an AI agent, no human checked it. Rebased onto main d71f42e (fix(io) quiet exit, #1492). The four commits are kept as they were (old head 1d36887, new head 58e1426). Only
None of the review's points were addressed in this rebase. Checks (macOS, Bun 1.3.14):
|
58e1426 to
7a5ef60
Compare
|
@Lorenzobattistela, thanks for the review. 7a5ef60, rebased onto main 381dc87, addresses it:
Gates on macOS: repo 54/54, ping 55/55, test 1605/1605, perf 124/124. Every Unsupervised AI review/action. |
|
Pushed ec37001 on top of 2a90300 (
Checks on a merge with main 4ff8545: repo 54/54, ping 55/55, test 1607/1607, perf 124/124. All 1618 test files compile to byte-identical Not done: the cluster Unsupervised AI review. |
|
Thanks. ec37001 reads better, and the outputs are the same as 2a90300 (every One regression:
Still open from the last round:
None of this is a soundness hole on main's paths. The #1186 fix itself holds. |
|
Known regression, left as is on purpose: a template whose |
…λ-match on it, so the kernel checks that the values cover it (bendlang#1186) root_cols enumerated a specialized parameter of a finite type (Quant, or a datatype whose constructors have no fields) and safe.ts sent one instance per value, so --verdict trusted that the enumeration covers the def. At an empty type there were no instances: the def never went to the kernel, and --verdict still printed ALL PROOFS CHECK. root_cols now returns its enumeration as a tree, and root_emit sends a root with branches as one def: a λ-match on each finite parameter whose arms are the instances' bodies (or name the instance, for a datatype, a def with no body or an @unsafe one), at a type that is a λ-match on it too. The kernel checks that the match covers every value, and at an empty type that there are none: λ{(,): λ{}}. Instances still go out where a caller uses them. Closes bendlang#1186 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mit is written like the rest of the file Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… a case two others cover Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-match arms instead of inlining their bodies The instances were never the problem: what main lacked was a check that they cover the root. Naming them keeps main's defs byte-identical, drops the second copy of a recursive body, and the special case for a datatype, a def with no body or an @unsafe one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the values it holds, and shares one λ-match helper, mats, with adt_emit and group_emit
…goes out after them, reports a failing instance's own reason, and --verdict fails a def that reaches no kernel def Each arm of the covering def's type is now the def's type, a λ on its ~ parameters, applied to that instance's arguments, so the kernel checks the substitution instead of trusting tele_fill on both sides. Where the def's type needs its ~ parameters closed (a template call), the arm is the instance's own type, as before. The instances go out before that type is built, so one whose type is out of scope no longer takes the others with it, and a root reports its first failing instance's reason rather than "names <instance>". A match node keeps its kernel labels and whether it is a datatype's, and QS spells Quant's labels once. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, which the kernel checks, in one walk over the telescope
The covering def's arms were checked against a λ-match type built from
the same tree as its body, so a root_emit bug that named one instance
under two labels passed: the kernel checked coverage and each instance
against the def's type at its own arguments, never that the arm under a
label holds that label's instance. Each match is now annotated
∀y:A -> Σz:A -> {z == y : A} and each arm returns its value, so the
kernel ties the label to the instance it names.
The cover is a def at <()> that checks the instances in erased lets,
with G, the def's type as a λ on its ~ parameters, bound once instead of
copied into every arm; an arm names its instance unannotated where G
needs its parameters closed. root_of and root_emit are one walk that
emits each instance at its leaf, so the Root tree is gone. safe.ts is
23,278 ttok (main 22,386).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cannot go out is undone, and the unit type and value are UT and UV A root kept only its first failing name's reason, where main printed one line per failing instance; it now prints each distinct one, and its cover's only when alone (that reason names an instance). Building G registers the names its type mentions, so a Scope_Error there is undone as item_ref undoes a failed emission. The test says no gate runs --verdict to failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aves nothing behind, and fin says whether it has a finite parameter
…ef again, and the comments say what the kernel checks: every value has an arm, and each instance fits the def's type
…nces, else its λ-match, else no kernel def
…hose type can't go out
ec37001 to
29777d1
Compare
|
Thanks @Lorenzobattistela. Pushed
(Changes and this comment by an AI agent, at Nicolas's direction.) |
…er, the λ-match returns (), and an undo costs what it removes
- G's fallback catches only Open_Error (a template argument that depends
on a run-time value); any other reason G can't go out is the def's, so
it is out of scope with that reason instead of silently unchecked
- the λ-match is typed ∀y:A -> <()>: the Σz:A -> {z == y : A} codomain
only re-checked that a value and its label share an index
- each column's kernel term is made once for every leaf below it
- undo pops a log of the keys emission added, rather than copying every
key of names, seen, taken and fail
- G's λs go through lams; the oos fallback no longer reads ns[0]
- tests: the fallback at a non-finite ~ parameter, two Quant parameters
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fixes #1186. Follows up on #1213, closed in favour of this approach.
Cause
root_colsinbend2/safe.tsenumerates a template (~) parameter of a finite type (Quant, or a datatype whose constructors have no fields), and each value goes out as its own instance. Nothing checked that those instances cover the def, so--verdicttrusted the enumeration. At an empty type there are no values: the def never reached the kernel, and--verdictstill printedALL PROOFS CHECK.Fix
root_colsbecomesroot_emit, one walk over the def's telescope that emits each instance at its leaf (throughitem_try, as main does). A def with a finite~parameter also goes out once, at<()>, as a λ-match that checks its instances in erased lets:∀y:A -> <()>;G(a λ on its~parameters, bound once in a let) applied to that instance's arguments, and returns().So the kernel checks that:
Ahas an arm (at an empty type, that there are none:λ{});G v …itself, so a wrong substitution intele_fillno longer cancels out.It does not check which arm names which instance. If every arm named the first value's instance, only that instance would go out, under both labels, and
--verdictwould still pass.Fallback. If the def's type calls a template at one of its
~parameters,Gcan't go out, because those parameters are bound in it.spec_valraisesOpen_Error(aScope_Errorsubclass: "a template argument that depends on a run-time value"), whatGregistered is undone, and the arms name their instances unannotated. There, coverage still holds, but no instance is checked againstG. OnlyOpen_Errorfalls back: any other reasonGcan't go out puts the def out of scope with that reason.(
wrapis the fallback: noG, bare names.)Reporting. A root is out of scope at each distinct reason its instances are out for; else at its λ-match's reason; else, when neither went out, at
no kernel def. An instance out of scope reports its own reason (- flip: uses the @unsafe def flip), and the good instances still go out. The last case is the guard against a revert of this fix: if the λ-match stops going out for a def with no instances,--verdictfails with- skip: no kernel definstead of passing.The instances, the enumeration and how callers pick instances are unchanged. Only defs branch: since main's
root_colsrewrite, a datatype's parameters never do. The match on a Σ chain's unit is one helper,unit. The chain of λ-match arms is another,mats, shared withadt_emitandgroup_emit. Quant's kernel labels are spelled once, inQS, and the unit type and value once, inUTandUV.undonow pops a log of the keys emission added (putlogs each new key innames,seen,takenandfail), instead of copying every key of those four. It costs what it removes, which matters now that a failed root and theGfallback undo too. Each~column's kernel term is made once for every leaf below it.Checks
At head
a29f0c62(macOS):tests/check/spec_finite_match.bendaddssize(the fallback when the template is called at a~Natparameter, which goes out at a constant) andboth(two Quant parameters).--verdictprintsALL PROOFS CHECK.-o .bendtton all 1223 files oftests/,demos/,bench/andevals/that elaborate: byte-identical to29777d11exceptspec_finite_match.bend, where only the λ-match codomain and arm values changed. With logging on the fallback, the only def in that corpus that takes it iswrap, withOpen_Error, so narrowing the catch changes no output.ALL PROOFS CHECK: an alias type, two Quants, a non-finite~before a finite one, a native op in the law type, an empty type between two finite ones..bendtt: an instance annotated at the wrongGarguments, a dropped Quant arm, and an arm atVoideach giveSOME PROOFS FAIL.Open_ErrorScope_Errorwhile buildingsame'sGgives- same: injected(out of scope), whilewrapstill falls back.tsc: no new errors.gates/safe.ts tests(1636 files, 26 nodes): 1054 agree, 88 unsafe, 490 bend2 rejects, 4 out of scope, 0 false rejects, 0 timeouts, 0 infra. The table is identical, line for line, to the29777d11run;spec_finite_match.bendagrees.At head
29777d11, rebased ond0cb504e:tests/check/spec_finite_match.bend(new): the issue's repro (skip,~v: Void), a~ Bool(same), a kind that depends on the parameter (look), a later~parameter whose type names the first one (pick), an empty type after a~ Bool(late), a Quant parameter (mode), and the fallback (wrap). Here,skip,same,look,pick,late,modeandwrapall go out and--verdictprintsALL PROOFS CHECK. On main,skipandlateare missing from the.bendttand--verdictstill printsALL PROOFS CHECK.if (!fin)→if (true)inroot_emit; nowif (G === undefined)),--verdicton that file fails with- skip: no kernel defand- late: no kernel def. No gate runs this: the test gate doesn't run--verdict, andgates/safe.tsonly tallies out-of-scope files. So the guard bites when--verdictis run on this file.-o .bendtton the other 1635 test files gives byte-identical output to main (d0cb504e), including the out-of-scope lines (33 files on both).gates/safe.ts testson29777d11(1636 files, 26 nodes): 1054 agree, 88 unsafe, 490 bend2 rejects, 4 out of scope, 0 false rejects, 0 timeouts, 0 infra. Main gives the same table file by file, excepttests/io/file_write_zero.bend, which main's run marks unsafe. That file comes from fix(io): fail file writes on zero progress #1446, which is newer than this branch's base.safe.tsgrows from 22,578 to 23,500 ttok at29777d11, 23,564 ata29f0c62(cap 24,000).comp.tsis unchanged.Limits and costs
~parameter), no instance is checked against the def's type.no kernel defguard fires only when a def reaches no kernel def at all. A λ-match dropped for a non-empty type would leave the instances checked, and nothing would flag the missing coverage check.~column is empty at some value of an earlier finite~parameter (for ~b: Bool; for ~h: {b == True{} : Bool}) is out of scope even when the body never usesh, sinceGnames every column's constant. On main it is out of scope once the body usesh. Tracked in safe: refute empty ~ columns in a specialized def's cover instead of leaving it out of scope #1544.Ggoes out once per root, and each arm adds an annotation and its value.🤖 Generated with Claude Code