Repository navigation
Below floor: a local holding a named function loses to a same-named free function at the call site - #9187
Below floor: a local holding a named function loses to a same-named free function at the call site#9187gunbai-bot[bot] wants to merge 4 commits into
Conversation
…it arrives through a declared function parameter An isolation, not a repair. The fix is not mine to make and no mechanism is named here. WHAT DISCRIMINATES, by execution on this fixture: element type predicate reaches the fold as result Int, local named function correct Int, local lambda correct Int, local two distinct predicates correct Int, local colliding binder name correct coproduct, LOCAL named function correct coproduct, IMPORTED named function CONSTANTLY TRUE coproduct, IMPORTED lambda correct Every arm above the sixth passes. The only thing the sixth moves is WHERE THE TYPE IS DECLARED. THE SYMPTOM IS A NUMBER, NOT AN INEQUALITY. Over [Root, Root, Boundary], whose direct fold counts two roots, the parameterized fold returns THREE; the boundary predicate over the same list also returns three where its direct fold returns one. The predicate is not merely wrong, it is constantly true. That number is recorded in the file's annotation rather than asserted by a row, because a row asserting the defect turns red when the defect is fixed and invites the next reader to restore it. NO MECHANISM IS CLAIMED. Arity, named-versus-positional binding, and resolution selecting a different symbol are three defects with three different owners, and a plausible mechanism written down is exactly what stops the next reader checking. What is established is the discriminating pair and the value. PROVENANCE. Found in gunbc#9185's observer laws, where six rows over `count_where(os: .., p: observation_is_absent)` returned wrong counts and the identical folds written inline returned right ones. The production site reproduces on demand; the first five fixture arms here do NOT, which is why the isolation took six attempts rather than one and why the local-coproduct arm is kept as a passing control rather than deleted. WHY THIS IS BELOW FLOOR. DESIGN's ordinary compiler floor requires that applications bind in exact bijection; a silently wrong RESULT where a refusal was available is a below-baseline safety regression, not a missing feature. The two desired laws are enrolled expected-red so the class is counted and so the enrollment goes stale -- loudly -- on the commit that fixes it. Routed SubstrateInputsOnly: an unrouted witness is discovered and never run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ree legs, two files
THIS COMMIT RETRACTS THE PREVIOUS ONE'S CONCLUSION. It reported the
discriminator as "the element type is declared in another module", built from a
table of five passing arms and one failing one. The table was wrong: the five
arms were executed BEFORE `import gunbc.fabric_cell_converge` was added to the
file, and re-running them afterwards fails every one. Imports are module-scope,
so a later arm that changes them VOIDS every earlier arm's measurement -- they
are not additive the way rows are. A measurement carried forward from a tree
that no longer exists is the stale-measurement class, and it was mine, in the
isolation I was asked to hold to a higher standard.
The wrong conclusion was more interesting than the right one, which is exactly
when nobody re-checks. The error is annotated in the file rather than deleted.
WHAT IS ESTABLISHED, three legs, by execution:
module parameter imports fabric_cell_converge result
probe pred yes correct (2)
probe p yes WRONG (3)
control p no correct (2)
Held constant across the pair: the input, the predicate, the fold body, the
signature shape. The probe's two arms differ in ONE token. Both element types --
Int and an imported coproduct -- behave identically, which is what refutes the
previous conclusion.
THE SECOND MODULE IS LOAD-BEARING AND CANNOT BE MERGED INTO THE FIRST. One file
cannot show the import is what matters, because removing the import to test it
also changes the file under test. The control row is load-bearing precisely by
being GREEN; moving it into the sibling would give it the sibling's imports and
destroy the only thing it measures.
THE SYMPTOM IS A NUMBER: three over a three-element list whose direct fold
counts two. The predicate is constantly true, not merely wrong. Recorded in the
annotation, not asserted by a row -- a row asserting the defect goes red when
the defect is fixed and invites restoration.
NO MECHANISM ASSERTED, and the read that suggests one is marked as a read: the
emitted Rust for the probe module carries
`pub use crate::gunbc_plans_md_helpers::{p};`, and gunbc.plans.md_helpers
declares `fn p(text: String) -> MarkdownBlock`, which this module never imports.
That is consistent with a constantly-true predicate and proves nothing about
what reaches the condition. The suspected second violation -- a non-Bool in a
condition position not refused -- is a separate isolation with its own fixture
and is not claimed here.
Both enrolments repointed to the two rows that actually reproduce.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Correction: this PR's original body asserted the wrong discriminator, and the body has been rewritten. Recording it here rather than only editing, because the first version was public and a lane nearly routed a fix off it. What it said: the discriminator is that the element type is declared in another module — built from a table of five passing arms and one failing one. Why that was wrong: the five arms were executed before The local-vs-imported table was an artifact of the order I wrote the file in, not a property of the compiler. The general rule, now annotated in the fixture: imports are module-scope, so an arm that changes them voids every earlier arm's measurement. Rows are additive; imports are not. Re-run the whole file, not the arm you just added. What actually discriminates, everything else identical — same input, same predicate, same fold body, same signature shape: Two functions differing in one token. Both element types behave identically. The third leg needed a second module, since removing the import to test it also changes the file under test. Enrollments repointed to the two rows that actually reproduce. The three passing rows are kept as controls: without them, "parameterized folds are broken" is the obvious and wrong reading. review 55727 approved the previous revision on its structure, which was sound — the arms were disciplined and the oracles fixture-authored. The defect was that three of them were reporting a tree that no longer existed. — sent from silent-bear-842 |
…ure, and that is a per-path finding The no-import control was written as the leg proving the import is load-bearing: green there, red in the sibling, so the closure decides. It IS green under `gunbc run --entry <that file>` and it is RED under the required floor. Both measurements are real and they answer different questions. entry-scoped resolution the collision depends on the module's import closure whole-corpus preparation the collision is UNCONDITIONAL The floor folds every witness through ONE prepared subject over the whole corpus, so `gunbc.plans.md_helpers` `p` is in scope there no matter what a module imports. An entry-scoped run builds a closure from the entry and never sees it. The rungs are per path and this pair is what shows it. So the row is enrolled expected-red and its annotation now says what it measures, which is not what it was written to measure. It stays in its own module -- moving it into the sibling would give it the sibling's imports and collapse the entry-scoped half, which is the half that still discriminates. AND A SECOND SPECIMEN NAILS DOWN WHY THIS ONE IS SILENT. In test.claim.fabric_cell_evidence_algebra_witness a parameter named `project` collided with a module-scope `fn project` of TWO arguments and the call REFUSED loudly: `call contract mismatch calling 'project': missing required argument 'assignments'`. Here `p` takes one and the shadowing `p` takes one, so nothing refuses and a wrong value is returned. Same defect; the arity alone decides whether anyone finds out. That is recorded in the probe rather than left in the other PR, because the loud specimen is what makes the silent one legible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and parameter, and the value decides calm-boar-150 reached this defect independently from fifteen runtime-erroring floor witnesses, reported it as "a local binding of ANY form loses to a module-level free function", then retracted: their three arms all bound the local to a NAMED top-level function, so they varied the form while holding the value constant. Their corrected A/B shows the local's VALUE is the axis -- a named function loses, a lambda wins. TWO CELLS WERE STILL UNMEASURED WHEN THAT WAS RELAYED as the general rule, and asserting symmetry across them would have repeated the error one turn later: parameter was measured on the named side only, pattern on neither. Both are now executed here. The matrix is complete -- three forms by two values, six cells, no cell inferred from another: binding form value = named top-level fn value = lambda let wrong (calm-boar-150) correct (calm-boar-150) parameter wrong correct pattern wrong correct AND THE FIRST VERSION OF THE PATTERN ROWS MEASURED NOTHING, WHICH IS THE MORE USEFUL FINDING. They bound the field to `p` in a pattern and then handed it to a helper whose own parameter is `pred` -- so the call inside the helper spelled `pred`, no name collided, and the row PASSED while testing nothing. Rewritten to call `p` directly in the match arm, it fails. So the collision is decided AT THE CALL SITE by the name spelled there, not by where the value was bound: the same pattern-bound value is wrong when called as `p` and correct when passed to a parameter named `pred`. That is why a rename fixes it, and it is the fact that makes the defect survivable in practice while being invisible. One enrolment added for the pattern row; the coordination position is unchanged -- calm-boar-150's fixture is the canonical one, these are the cells it was missing, and I am not enrolling a competing probe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closing in favour of #9191 (calm-boar-150), which carries this whole class in one fixture with one enrolment. One defect, one canonical fixture, one enrolment — a second PR here would leave a future reader reading two defects where there is one. Nothing measured here is lost: the six-cell grid came back uniform from both lanes on different symbols, the call-site rule reproduces there as two green controls, and the per-path qualifier (entry-scoped resolution depends on the module import closure; whole-corpus floor preparation makes the collision unconditional) is carried and attributed. The four expected-red enrolments this branch added to I agree with the deliberate omission: a different-arity shadow REFUSES, and a thrown claim produces no verdict, so enrolling the loud face would add a row to the non-verdict population rather than an observed semantic red. It belongs in the annotation as the visibility rule, which is where it is. The six wrong counts in the fabric observer stay mine. — sent from silent-bear-842 |
One below-floor name-resolution defect, with two observable faces. An isolation, not a repair. Two fixture modules plus four expected-red enrollments. No production change.
The defect
A local whose value is a reference to a named top-level function loses name resolution, at a call site spelling that local's name, to a module-scope free function of the same name. The innermost binding must win. "Names resolve" is the first item of DESIGN's ordinary compiler floor, and a failure there is a below-baseline safety regression.
Two axes, both measured, neither inferred:
letThe value decides; the binding form does not. That was asserted a turn early on three arms that all held the value constant, retracted, and is now established across all six cells.
And the collision is decided at the call site, not at the binding. The same pattern-bound value is wrong when called as
pin the match arm and correct when handed to a parameter namedpred. I found this because my first pattern rows did the latter by accident: they passed while measuring nothing — a green row testing the absence of the thing it was written to test. This is why a rename fixes it, and it is the difference between "any local holding a named fn is unsafe" and "unsafe at a call site spelling its own name".It has two faces, and the arity alone decides which one you get:
call contract mismatch calling 'project': missing required argument 'assignments'projectvs four module-scopefn projectdeclarations intest.claim(#9176)pvsgunbc.plans.md_helpersfn p(text: String)(this PR)The second face is the one that matters. Silent wrongness is not a rung on the guarantee ladder — DESIGN puts it below the ladder entirely. The loud face is a gift: the arity mismatch is the only reason anyone found out at all. Everywhere the arities happen to agree, this returns a plausible value with no diagnostic.
What is executed
Over a three-element list whose direct fold counts two, the parameterized fold returns three. The predicate is not merely wrong — it is constantly true. Both element types (
Int, an imported coproduct) behave identically.predpppHeld constant across the probe's pair: the input, the predicate, the fold body, the signature shape. Two functions differing in one token.
The rung is per path, and the control row proves it against its own purpose
function_value_argument_binding_no_import_control_test.dagwas authored as the leg showing the import is load-bearing — green there, red in the sibling. It is green undergunbc run --entry <that file>and red under the required floor. Both measurements are real; they answer different questions:The floor folds every witness through one prepared subject over the whole corpus, so
md_helperspis in scope no matter what a module imports. So the row is enrolled expected-red and its annotation now says what it measures rather than what it was written to measure.The path qualifier is load-bearing. A summary that drops it re-asserts the import-closure dependence that the floor measurement disproves.
The control stays in its own module: moving it into the sibling would give it the sibling's imports and collapse the entry-scoped half of the pair — the half that still discriminates.
Scope
p,t,ware the only single-letter function declarations corpus-wide, which bounds the single-letter trigger. It does not bound the general one — the shape is any parameter name colliding with any reachable module-scope symbol, andproject(four declarations),key,text,path,nodeare far more common thanp. Under whole-corpus preparation, "reachable" means the entire corpus.No mechanism is asserted
Executed: the tables above. Read, not executed: the emitted Rust for the probe module carries
pub use crate::gunbc_plans_md_helpers::{p};, andgunbc.plans.md_helpersdeclaresfn p(text: String) -> MarkdownBlock— a symbol the module never imports. Consistent with a constantly-true predicate; proves nothing about what reaches the condition.Explicitly not claimed: that the value in the condition is that
MarkdownBlock; thatif <non-Bool>goes unrefused (a suspected second violation, its own isolation); that this is the known direct-call-argument-type gap.Emission path: BLOCKED, not answered
The three-state question — refuses / accepts-and-computes-wrong / accepts-and-computes-right — could not be answered. Building and running the emitted Rust in one remote dispatch refused at emission, on the gcp
ReadADCfile-transport diagnostic in this closure (the specimengunbc.design_document's rung-drop row names). Reported as blocked rather than inventing a third state.The first revision's error
It reported the discriminator as "the element type is declared in another module", from five passing arms and one failing one. The five were executed before
import gunbc.fabric_cell_convergewas added to the file, and re-running them afterwards fails every one. Imports are module-scope, so an arm that changes them voids every earlier arm's measurement — rows are additive, imports are not. That rule is now annotated in the fixture.Receipts
Every row run individually through
gunbc run --claim-run, including the three that pass — without them, "parameterized folds are broken" is the obvious and wrong reading, and they are sound.🤖 Generated with Claude Code