Repository navigation
Hold the argument-to-formal rule in one place: inference bound named arguments by position, refusing valid programs - #9192
Conversation
…ding an argument its neighbour's formal Which declared formal a call argument binds to was answered in two places with different rules. build_call_application_plan matched by authored label with a positional fallback -- the rule call_function_inner uses. The ExprCall inference fold, which chooses the expected type each argument is inferred under, selected purely by source position and read the label only to tag the result node. So a call passing named arguments in a different order than declared inferred each argument against its neighbour's formal, while the type check ran the other rule and reported nothing. The green type check was not evidence against the defect; it was produced by the other authority. Two independent symptoms, both measured: src/v1/05_emit.dag:2993 calls emit_typed_match_unified with seven named arguments, source_indices ahead of recurse. The recurse lambda was inferred against render_pattern's fn(MatchPattern) -> String. take_list(label: "a", xs: []) is correct code that main REFUSES with "empty list literal: expected type is not a collection", because the [] sits at position 1 and was handed label: String's formal. A fabricated refusal, which DESIGN 5 forbids as firmly as fabricated success. The repair is not a second copy of the name-first search. Pasting the rule into the fold would leave two implementations that agree today and drift later, which is the same single-authority failure one layer down. select_formal_for_call_argument is the rule, held once: the fold consumes it directly, and the plan builder consumes it inverted -- a formal's argument is the argument that selects that formal's index -- so neither side can encode a rule the other does not. It returns the formal's index alongside its node precisely so that inversion needs no second search. It is deliberately pre-type. It reads authored labels only, needs no substitution and no inferred types, so it runs before recursive argument inference and call_subst still accumulates in authored source order, leaving generic unification order untouched. Unknown and duplicate labels stay owned by their existing walls; this selector decides correspondence, never admission. One behaviour change, stated rather than left for review to find: routing the plan through the shared selector removes an existing double binding. A labelled argument matching formal 3 also satisfied formal 0's positional fallback, so one argument could answer for two formals. Under one rule formal 0 now binds nothing. Controls, executed against compilers built from committed mirrors with no hand edits in either binary: out-of-order empty list main refuses / P1 clean the discriminating flip declaration order clean both, emitted identical mixed positional+named clean both, emitted identical positional only clean both, emitted identical unknown label 2 hard both, diagnostics identical including spans duplicate label 1 hard both, diagnostic identical including span Both enrolled witnesses assert green because the defect is a fabricated refusal; the out-of-order arm goes red again if the two rules ever diverge. Bootstrap receipt: main compiler regenerating main first_generation_equal=true main compiler regenerating P1 one drifting file, its own mirror self-applied P1 compiler built exit 0 P1 compiler regenerating P1 first_generation_equal=true, 135/135 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRMbwdtHZxTiMNZD5WLS3P
The compiler that runs is the committed mirror, so a change to 04_infer.dag alone changes nothing that executes. This is the emitted mirror from the fixed-point tree: the compiler built from it re-emits it byte-identically (first_generation_equal=true, 135/135). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRMbwdtHZxTiMNZD5WLS3P
Side-chat ruling log (routed here so it is findable on the PR, not only in chat)Scope ruling. This stays a standalone prerequisite; #9182 (B1) does not absorb it. P1 is an inference law, not a Rust-emission law; it must survive even if B1 is reverted; and a combined bootstrap would obscure whether a changed mirror came from corrected inference or newly emitted annotations. Instrument objection, accepted. An earlier measurement used a compiler containing three hand-corrected Rust annotations. Those were not part of the subject, but they were part of the instrument — in a self-hosted experiment that distinction matters. Every measurement now in the PR body was re-taken with compilers built from committed mirrors, no hand edits in either binary. Subject renamed. The 173-file comparison is the v2 compiler closure, not "whole-corpus emission". The whole-bundle path is a separate subject gated elsewhere; keeping the names apart prevents a trajectory splice. Census framing. Carry as exact 13 sites at exact head, with mixed named/positional calls excluded and a regex instrument — not a timeless corpus fact. The known-positive control is load-bearing: two earlier census implementations returned plausible populations while being structurally unable to see lambda-bearing calls. Correction I owe on my own claim. I said the other twelve sites could not change shape because their callees take no callable parameter. That was reasoning past the evidence — a wrong expected formal reaches empty collection literals, record literals, optionals, generics and aliases, none of which need a lambda. The empty-list probe in the PR body is that class executing. Emitted-output delta over those twelve is zero; their typed-graph effects are measured only to that depth. Open, and deliberately not done here. A third authority ( |
…l relation is a bijection in both directions
The selector this branch introduced made the relation FUNCTIONAL - one
argument selects one formal - but said nothing about INJECTIVITY, and its
absence was silent wrongness below the floor. Measured on both compilers:
fn f(_a: Int, b: Int) -> Int { b }
f(a: 1, _a: 2, b: 3) compiles CLEAN, emits f(2, 3)
The argument a: 1 is dropped with no diagnostic anywhere, because a and _a
are two caller spellings of ONE formal and nothing asked whether two
arguments had chosen the same one.
formal_identity_duplicate_diags closes it: an argument whose selected formal
index was already claimed by an earlier argument is refused.
THE TWO DUPLICATE WALLS PARTITION BY CAUSE. duplicate_label_diags asks
whether two arguments produce the same RUNTIME BINDING KEY, via
call_arg_bound_param_at's running counter, and catches collisions this
selector cannot see; it stays separate. This wall asks whether two arguments
select the same SEMANTIC FORMAL. The two questions coincide on exactly one
input shape - two arguments carrying the SAME authored label - and there the
key wall already owns the refusal, so this wall fires only when the labels
DIFFER. Without that guard both fired and f(a: 1, a: 2, b: 3) produced two
byte-identical diagnostics; the guard states which wall owns which cause
rather than deduplicating output after the fact.
Four controls, executed against compilers built from committed mirrors:
control main this
out-of-order named args 1 (fabricated) 0
two spellings, one formal 0 (silent) 1 (refuses)
two anonymous positional slots 0 0
same label twice 1 1
The third confirms the historical "every _ is the same key" false refusal is
not revived; the fourth confirms the walls no longer double-report.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRMbwdtHZxTiMNZD5WLS3P
…ls, so a named argument before a positional one stops fabricating a refusal Review on #9192 (codex/gpt-5.6-sol, review 55769) found that the selector refuses a program main accepts: fn f(a: Int, b: Int) -> Int { a } f(b: 2, 1) The fallback used the argument's RAW SOURCE INDEX, so argument 1 selected formal index 1 -- which argument 0 had already claimed by name -- and the new injectivity wall refused the call. A fabricated refusal introduced by the change that exists to remove one. It escaped because the enrolled controls covered POSITIONAL-THEN-NAMED, which is already refused upstream, and never the reverse order. The PR body generalised from that measurement to "a labelled argument preceding an unlabelled one is already refused", which is false. THE REPAIR: the positional fallback now selects the k-th formal NOT CLAIMED BY ANY LABEL in the call, where k counts unlabelled arguments before this one. That is computed inside the selector rather than by routing through call_arg_bound_param_at -- which answers a different question, and whose underscore handling must not be disturbed. Controls, both compilers built from committed mirrors: control main this out-of-order named args 1 (fabricated) 0 out-of-order underscore alias 1 (fabricated) 0 two spellings, one formal 0 (silent) 1 (refuses) two anonymous positional slots 0 0 same label twice 1 1 named then positional 0 0 <- the review's case WHAT THIS DOES NOT FIX, measured and recorded rather than left implicit: emission ordering for that same call is wrong on main AND here. 05_emit.dag order_typed_call_args returns arguments in SOURCE ORDER whenever any argument is unlabelled, so f(b: 2, 1) emits f(2, 1) while the interpreter binds a == 1 -- the emitted program computes a different answer than the interpreted one. That is a fourth consumer of the same argument-to-formal question with its own third rule. It is pre-existing, its blast radius is corpus-wide call ordering, and it is out of scope here; the enrolled witness says so in its own note so a census cannot read the green as covering mixed calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRMbwdtHZxTiMNZD5WLS3P
|
Confirmed and fixed in A fabricated refusal introduced by the change that exists to remove one. The cause is exactly as diagnosed: the positional fallback used the argument's raw source index, so argument 1 selected formal index 1 — which argument 0 had already claimed by name — and the injectivity wall refused it. Why it escaped my controls, since that matters more than the fix: I had measured The repair. The positional fallback now selects the k-th formal not claimed by any label in the call, where k counts unlabelled arguments before this one. That running rank is computed inside the selector rather than by routing through Controls, both compilers built from committed mirrors:
The case is enrolled as a regression witness in One correction to the review's premise, which makes the finding larger rather than smaller. The review states the interpreter "correctly binds The emitted program computes a different answer than the interpreted one. so whenever any argument is unlabelled the emitter does no reordering at all and emits source order. That is a fourth consumer of the argument-to-formal question, running its own third rule — below-floor silent wrongness, pre-existing on main, unrelated to the namespace work. It is not fixed in this PR, deliberately. Its blast radius is corpus-wide call ordering, while this PR's subject is expected-type propagation and the type-check plan. It is recorded in the PR body with the measurement, and the enrolled witness states in its own note that it covers the source-language half only — so neither the PR nor a witness census can be read as having closed it. It has been handed to the lane cataloguing this class as its own row. Fixed point re-established after the change: regen in flight at time of writing; the prior run refused with — sent from deep-ant-102 |
…ix, the .rs did not The merge commit 4c9c55c resolved the generated-mirror conflict by taking one side whole. That is a SILENT deletion -- no conflict markers, clean tree, and the committed mirror became an OLDER COMPILER than the .dag it claims to mirror. Measured, with the control that makes it a result rather than a name-mangling artifact: fn .dag .rs(before) select_formal_for_call_argument 1 0 call_argument_formal_at_position 1 0 call_formal_claimed_by_a_label 1 0 declared_type_inhabitance 1 1 <- pre-existing, same grep, present in both declared_type_obligation_diags 1 1 <- same and `select_formal_for_call_argument` IS spelled that way in origin/main's mirror, so the grep works and the absence is real. #9192 landed to stop inference binding named arguments by POSITION -- refusing valid programs -- and this branch would have re-shipped the seed without that fix while the .dag said it had it. REGENERATED, NOT RE-RESOLVED AND NOT SPLICED. No hand edit to the .rs. The emitted file was taken from target/stage0-regen-candidate/src/ and installed whole. Receipts, from runs that did not share a candidate directory: round 1 (broken head): first_generation_equal=false FAIL generated surface drift: v1_compiler_infer.rs round 2 (installed): first_generation_equal=true planned=135 executed=135 declared_divergent=1 [main.rs] The fixed point is demonstrated at byte grain, not just by the flag: the installed file's sha256 210e64fb2ad08be2 was recorded BEFORE the confirming run, and that run re-emitted the identical hash. diff -rq over the candidate tree reports no other differing file. CI independently reached the same verdict on the broken head (run 32884947431), with a byte-identical message -- so the mirror-to-source comparison on a PR is intact and this red was the gate working, not a flake.
…rror Main tip 151e771. ONE conflicting path: the generated mirror src/v1/stage0/src/v1_compiler_infer.rs. src/v1/04_infer.dag (+104 from main) and cli_run.rs (-373 from main) both AUTO-MERGED, verified by a tree-wide grep for conflict markers returning only the mirror. THE MERGE DRIVER DID NOT FIRE, and that is the second time on this PR. It is enrolled by BASENAME while the population is defined by a HEADER, so it protects 2 of 136 stage0 mirrors and v1_compiler_infer.rs is outside it. On #9196 the same driver DID refuse a generated .yml and printed the regeneration recipe; here git left ordinary conflict markers and would have accepted a hand-resolution. REGENERATED, NOT HAND-RESOLVED -- and the binary vintage mattered, measurably: binary built from f57bc0e (pre-merge): drift = extdeps_uri.rs, v1_compiler_compile.rs, v1_compiler_infer.rs, v1_rt.rs (4) binary built from the MERGED tree: drift = v1_compiler_infer.rs (1) Same tree, two binaries. Three of the four were EMITTER VINTAGE, not tree drift: this diff touches inference and has nothing to do with extdeps_uri.rs or v1_rt.rs. Installing the first candidate would have written an old emitter's bytes over mirrors main had already advanced -- silently, and it would have read as a clean regen. A drift list wider than the change is a claim about the instrument. So the recipe's order is wrong when the binary predates the merge: REBUILD FIRST, then regenerate once. The binary was dated before use -- it now accepts --required-lane and routes phases by lane, which the pre-merge one rejected. RECEIPTS, from a rebuild off the installed seed: required-regen: first_generation_equal=true planned=135 executed=135 declared_divergent=1 [main.rs] installed sha256 12110f0a3dc440ef... recorded BEFORE the confirming run and re-emitted identically by the rebuilt binary; diff -rq shows no other file differing. v1_src_dag_parse: 3998 file(s) parse-clean. The three #9192 functions stay 1/1 dag-to-rs: select_formal_for_call_argument, call_argument_formal_at_position, call_formal_claimed_by_a_label.
… positions wired, ten declared (#9194) * WIP: declared-type inhabitance obligation carrier (UNVERIFIED, not for push) Carrier types, the single declared_type_inhabitance relation consuming #8876's coproduct_payload_where_parent_required, the DeclaredTypeNotInhabited diagnostic, and the list-element position wired. Mirrors NOT regenerated; the wall does not execute in any built artifact yet. Verification dispatch in flight. * Wire the kernel-at-structured route and its witness (still unverified) * Install the emitted mirrors for the inhabitance carrier * Regenerate both mirrors from the merged authorities, not from a textual merge Git conflicted on v1_std_core.rs and AUTO-MERGED v1_compiler_infer.rs. Resolving only the file git complained about left the pair internally inconsistent: one mirror declared the roster variant and not the inhabitance one, while its sibling used both. That state is not something any emitter produces, and it does not compile. Generated files are projections of one authority and are only consistent as a SET, so both are replaced wholesale by a fresh emit from the merged .dag rather than merged file-by-file. Measured on that emit: both variants present in all three seed files, and all four inhabitance arms hold -- nega and negb refused at the list element, pos accepted, reach refused. Neither wall was eaten by the merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * The wall's first landing found a real one: octet rows declared as bit-records The list-element inhabitance obligation refused six data rows in emit_on_demand_match_loop_fold_family_witness, 30 elements in all. It was right, and the annotation was wrong. data match_expected_octets: List<Byte> = [0, 1, 0, 0, 0] Byte here resolves through v2.std.machine to std.bit's `type Byte { bits: List<Bit> }` -- a product. A plain Int does not inhabit it. The values were never bit-records: their only consumer is `emit_host_octets_byte_string(octets: List<Int>)`, which takes the octets as numbers. So the rows declared one type, held another, and were read as a third name for the second. Nothing in the corpus noticed, because the direct-call argument position is exactly the one still exempted pending gunbc#8925 -- the value flowed into a List<Int> parameter unchecked. Corrected to List<Int>, which is what the consumer's signature already said, and dropped the now-unused Byte import rather than leave a name in scope that no longer means anything here. Verified on the committed tree, both directions, one remote dispatch: fixed exit=0 inhabit_errors=0 compiled: 149 files emitted control exit=1 inhabit_errors=30 30 hard diagnostic(s) The control restores the List<Byte> annotation and nothing else, so the discriminator is the annotation itself and not the harness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * A reachability control that asserted our own class at zero was measuring nothing Review 54993 caught a contradiction between this arm's prose and its assertion, and the prose was the honest half. The comment said the arm keys on a DIFFERENT class than the wall's, deliberately, because keying it on DeclaredTypeNotInhabited would make it a second copy of arm one. The assertion was: violation_count(source: undefined_name_source, wanted: "DeclaredTypeNotInhabited") == 0 That is the wall's own class at zero, and it is satisfied identically by "the position is judged and our wall correctly stayed silent" and by "the position is never reached by anything" -- precisely the distinction the arm exists to draw. It read as coverage while carrying none: DESIGN's reachability-read-as-occupancy failure turned on a control, and a zero that had no nonzero beside it. The class was measured rather than guessed. An unresolved value name is refused by 04_infer through inference_error, which builds InternalError { message } -- compiling this exact probe source yields `undefined variable 'nosuchname_zzz_probe'`. The arm now demands that refusal. InternalError is coarser than the shape deserves, so a positive count alone could come from any unrelated defect in the probe. The paired arm is the discriminator: the same source with the name DEFINED and nothing else changed, asserting zero. The pair is what makes the undefined NAME the measured thing rather than the probe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * Wire the direct-call argument position — MEASUREMENT FIRST, no repair in this diff The list-element position landed in gunbc#8974. This attaches the same DeclaredTypeObligation at the direct-call argument seam, which is the position everything routes through and the one that has never judged inhabitance. WHAT THIS IS NOT: it does not touch module_skips_direct_call_arg_check. That exemption keeps its exact current meaning and population, and the 285k/67k residue behind it is not disturbed. gunbc#8925 landed the correction that deleting that arm is a NECESSARY condition someone had written as a sufficient one; this change sits beside the arm rather than removing it. WHY THE EXEMPTION'S REASON DOES NOT REACH THIS JUDGMENT. The arm exists for the TYPE judgment, whose false-positive classes are representation gaps -- brand aliases, optionality's two forms, anonymous literals, expansion depth. This relation refuses exactly two states, kernel-at-structured and payload-at-parent, and answers Undecidable for generic formals, optional carriers, unresolved formals and identity-erased produced types. All four representation classes are Undecidable or Inhabits under it. That is the same argument direct_call_shape_diags already makes in this file for sitting un-exempted at this very seam (direct_call_shape_wall_note): a label has no representation, so the exemption's reason does not reach it. The precedent is in the file, not built for this case. It attaches beside direct_call_structured_application_mismatch_diags, which is already un-exempted here and already reads app.formal_subst as declared and arg_value(n: ta) as produced -- the exact pair the obligation needs. HOISTED, NOT INLINE, AND THE REASON IS A TRAP WORTH RECORDING. Written inline at the seam it refused at regen: call shape mismatch calling function value 'resolved_type': named argument 'n' is not supported -- use positional arguments The seam binds a local `let resolved_type = match sig { ... }`, which shadows the top-level function of the same name, so the fold was calling a Node value as a function. The shadow is invisible to reading and the diagnostic names the call, not the binding five lines above it. Hoisting to a top-level fn beside the judgment it mirrors is both the fix and this file's existing convention. THE POPULATION IS NOT ASSERTED HERE. Two local attempts to measure it died: required-ci ran 116 minutes against CI's 44 for the same phases, and the whole-tree diagnostic histogram was OOM-killed on the runner (exit 137), whose empty output means the instrument died rather than that the corpus is clean. CI has the resources, so this branch exists to have CI produce the census -- by position, by declared-to-produced shape, by file -- before anything is repaired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * The seed mirror the .dag change requires — without it the floor measured nothing The first push of this branch carried the .dag wiring and not its regenerated mirror, so CI refused at the regen phase: required-regen: first_generation_equal=false ... FAIL generated surface drift: v1_compiler_infer.rs The floor phase therefore never ran, and the corpus reported ZERO DeclaredTypeNotInhabited diagnostics. That zero is not a measurement. It is the two-generation property doing exactly what it is supposed to: cargo builds the COMMITTED mirror, so a .dag edit is invisible until regen emits a new one, and a gate that stops before the floor produces an absence that looks identical to a clean corpus. This is the third instrument in a row on this question to fail toward zero -- a 116-minute buffered run I could not observe, an OOM-killed histogram that printed empty section headers under exit=137, and now a regen refusal that skipped the measuring phase entirely. All three would have supported the sentence "zero direct-call inhabitance defects corpus-wide", and all three would have been fabricating it. A zero is only readable beside a nonzero. The mirror was regenerated remotely and transported back verified rather than rebuilt by hand: exactly one file drifted (v1_compiler_infer.rs, confirmed by comparing every candidate file against its committed pair), and the decoded bytes match the candidate's sha256 07a3256ede2120bf9c65f4934fdc94f25fe258dcc2c7f4531d5db9a43d8d0fae. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * The octet rows say why they are Int, and name the carrier that would replace them Review 55052 approved and asked, non-blocking, for a tracked pointer toward a proper octet carrier, on the ground that no Octet alias exists today. One does, and it changes the shape of the answer: extdeps.network.ipv4 declares `type Octet = Int where range(min: 0, max: 255)` -- already the right shape and already grounded. It is homed in the IPv4 domain, so reaching into a network module for a compiler-emission byte would be a layer inversion rather than reuse. What is missing is a DOMAIN-AGNOSTIC octet carrier, not a new spelling of one that exists, and that is a more useful thing for the next author to know than "no such type". The annotation records three facts a reader of these rows would otherwise have to re-derive: that Byte is a bit-record so the Int literals never inhabited it; that List<Int> is the consumer's own declared type rather than a weakened carrier; and that Int is nonetheless weaker than an octet deserves, with the replacement named and the order stated -- the parameter moves first, since the declaration follows its consumer. It is rationale, not a machine claim, and it is deliberately not a feature: or dissolve-on: tag: no Accepted program can read an annotation, so a tag here would assert tracking that nothing performs. When the carrier lands, the obligation belongs on it. Placement checked against DESIGN 4c rather than assumed: a standalone leading // block attached to a module-scope data declaration, blank line above, none between block and declaration -- the shape this file's other 19 annotation lines already use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * The class was never undecidable — it was unconsulted The census refused 33 correct sites: a kernel integer at a parameter declared std.nat.Nat. Reading it structurally, that is kernel-at-structured, because std.nat declares `type Nat = CommutativeSemiring<Magnitude>` and std.integer declares `type Int = AbelianGroup<GroupCompletion<Nat>>` -- the canonical form of both is an algebraic STRUCTURE while their values are authored as kernel literals. The obvious reading is that this is undecidable and owed a counted advisory: dag/std/magnitude.dag is three lines with no body, so nothing there relates Magnitude to a kernel integer, and "40 is a Nat" looks like a convention the corpus relies on and never declares. That reading is wrong, and stopping at magnitude.dag is what makes it look right. v1.compiler.coercion numeric_realization_declaring_modules already records that dag/std/nat.dag and dag/std/integer.dag realize natively, and decl_file_realizes_natively answers it. So the relation was refusing on a question an authority in the tree already decides. Counting the residue would have recorded a deficit that does not exist and handed the next author a number to explain away. decl_file_realizes_natively is the surface used, and the choice is deliberate: it takes only decl_file. The neighbouring lookup_checkpoint takes a RenderTarget, so it would have made an EMISSION fact answer a SOURCE-level question -- fact in one carrier, operation governed by another, and the arrow between them invented. THREE PROPERTIES, EACH WITH AN ARM RATHER THAN AN INTENTION: It is a conjunction. The produced value must also be a kernel numeric, so a String at a natively-realized Nat stays refused -- without that arm, an implementation admitting anything at such a type would pass the positive arm. It fails closed on unknown identity. decl_file_realizes_natively answers false for the empty string, which is what an unestablished identity yields, and no fallback is added here that would undo it. It is keyed on the declaring module, never the spelling. std.nat.Nat realizes natively; v2.std.nat.Nat is the Peano coproduct Zero | Succ and must NOT. That last pair is enrolled as a permanent RED rather than argued in prose. A kernel integer at the Peano Nat must refuse, and if the discrimination ever decays to a spelling comparison that arm admits and goes green. DESIGN 4b(4) keeps a climb's evidence for exactly this reason. The witness declares SubstrateInputsOnly deliberately. A ReadsLiveTree witness is discovered, counted in declined_live, and never folded -- the sibling direct_call_argument_type_witness calls that state "enrolled and inert, the specification-without-execution state DESIGN 5 names, wearing the costume of a populated probe corpus". A regression control has to run. Acceptance test for the next measurement: cause 1 to 0 refused AND 0 counted; causes 2 and 3 unchanged at 16 and 1 sites. If either of those drops, the arm is too wide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * Every arm in this witness was enrolled, reviewed, cited — and executed by nothing The file declared ReadsLiveTree. A ReadsLiveTree witness is DISCOVERED, counted in declined_live, and NEVER FOLDED by the required floor. So the two REDs, the positive control, the reachability arm and the Undecidable arm have never run. That is worse than having written no test. Two reviews cited these arms as the executable evidence that the class had climbed -- "lands executable RED+GREEN+reachability+undecidable arms per DESIGN 4b's rung-honesty rule" -- and I cited them the same way in the PR body. An unexecuted assertion presented as the reason a rung is real is the rung inflation DESIGN 4b names as worse than sitting low, and it is the specification-without-execution trap section 5 calls the deepest one. I did not find this by reading my own file. I found it while authoring the direct-call witness and checking what its sibling declares: test.claim.direct_call_argument_type_witness -- the same kind of probe, compiling a source string through the same census -- declares SubstrateInputsOnly, and its header explains exactly why: an assertion authored in a live-tree module is "enrolled and inert -- the specification-without-execution state DESIGN 5 names, wearing the costume of a populated probe corpus". Nothing here needs a live read. Every arm hands compile_dag_diagnostic_census a source string this file authors itself, so the declaration was simply wrong about what the module consumes, and correcting it costs no coverage. WHAT I AM NOT CLAIMING. I could not discriminate this from the CI log: passing witnesses are not printed by name, so my grep returned zero for this file AND zero for the known-executing control -- a zero with no nonzero beside it, which is evidence of nothing. The finding rests on the declaration's documented meaning and on the sibling's contrasting declaration, and the next floor run is what turns it into a measurement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * The seed mirror for the decidable admit, transported verified Exactly one file drifts (v1_compiler_infer.rs, established by comparing every candidate file against its committed pair, not by trusting the regen summary), and the decoded bytes match the candidate's sha256 e4170a14243ece044b711f62a7931e3b3cef67703d151ed0ea0587f272736f9f. Without this the .dag change is invisible: cargo builds the COMMITTED mirror, so the floor would run the old relation and report a population that says nothing about the new one. The last push of this branch made exactly that mistake and its zero was not a measurement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * The 17 repairs the wall forces, landing with the wall per the 8876 precedent A wall that correctly refuses 17 real defects refuses the WHOLE CORPUS at floor preparation, so with the wall on and the repairs absent main is red. That makes them one change, not two -- the shape 8876 used when the same ruling applied. SIXTEEN PAYLOAD-AT-PARENT SITES. A Fnv1a64Structural stored where the ContentHash union is declared -- the class 8876 repaired at five production sites and deliberately did not widen to; these are that residue. The repair is 8876's construction reached through the module's own named surface: std.content_hash `as_content_hash_structural(s)` is literally `Fnv1a64(s)`, so it is the same move, not a second idiom. It is also the local convention: the very call sites being repaired already use its sibling as_content_hash_cryptographic for the Sha256 case, one argument above. materialization_provider_witness_test.dag 14 heal_revalidation.dag 1 native_cache_fusion.dag 1 NOT A CODEMOD, AND THAT MATTERS HERE. materialization_provider carries 34 content_hash_atom calls and only 14 are at a ContentHash-declared position; the other 20 legitimately produce Fnv1a64Structural. A global rewrite would have corrupted them silently, so only the flagged lines were touched. heal_revalidation is the one whose producer is not a call: the enclosing function declares `required_roster: Fnv1a64Structural` and passes it to a ContentHash parameter. Wrapped at the call rather than narrowing the declaration -- 8876's ARM A ruling, wrap the construction, since narrowing severs the carrier from the union its peers use. ONE ACCUMULATOR DEFECT, AND IT NEEDED NO NEW HELPER. dag_acceptance's post_front_end_obligations returns List<DagStageObligation> and snocs DagStageObligation, while seeding from no_rows() : List<StageExecution>. The element types disagree; it is latent only because the list is empty, so nothing ever observes an element of the wrong type. The fix is neither a second accumulator nor a generic one. `no_obligations() -> List<DagStageObligation>` ALREADY EXISTS four lines above no_rows(), and the fold now uses it. A generic `no_rows<T>()` would have been worse than the defect: with no element type to fix it is a nickname for `[]`, and the reason these helpers exist at all is to give a fold's init an element type inference cannot supply. The other three folds over no_rows() return List<StageExecution> and are correct and untouched -- one shared helper, four uses, one wrong. That discrimination is what the wall bought. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * 8876's repair shape does NOT apply to these 16 sites — reverting the wrap, keeping the one repair that was real The ruling said to copy 8876's shape and to STOP rather than improvise if it did not apply. It does not apply, and the measurement is how I know rather than a reading: wrapping the 16 sites turned two PASSING witnesses RED. test.claim.materialization_provider_witness.understated_bytes_alone_hold_every_part_digest_fixed test.claim.heal_revalidation_witness.only_exact_healed_head_complete_coverage_admits WHY, AND IT IS THE SAME LATENT-DEFECT MECHANISM ONE LEVEL OUT. witness_hash_list_contains(xs: List<ContentHash>, wanted: ContentHash) declares BOTH sides as the union. It is passed `ds`, a list of o.digest, and dag/std/artifact_store.dag declares those fields as raw Fnv1a64Structural. So the declaration lies on both sides and the comparison was raw-against-raw, which agrees with itself. Wrapping only `wanted` made ONE side honest and the equality stopped matching. That is exactly what DESIGN describes: a declaration that lies is inert while every consumer contradicts it in the same direction, and detonates on the first consumer that takes it at its word. My repair was that first consumer. 8876 is not this. It wrapped a CONSTRUCTION whose consumer genuinely expected the union, so one edit made producer and consumer agree. Here the consumer's own declaration is part of the lie, and the honest repair is to make the PRODUCER a ContentHash -- i.e. change artifact_store's closure_digest/content_digest fields from Fnv1a64Structural to ContentHash and follow every producer and consumer of them. That is a model change in std, in someone else's carrier, and it is not mechanical application of an established shape. KEPT, because it is a real defect and its repair is genuinely mechanical: dag_acceptance's post_front_end_obligations now seeds from no_obligations() rather than no_rows(). Element types agreed nowhere before; they agree now; the helper it should have used already existed four lines away. WHAT THIS LEAVES: the wall still refuses the 16 payload-at-parent sites, so the floor still cannot prepare, and this branch still cannot go green. That is not a reason to soften the wall -- the 16 refusals are correct. It is a reason the repair belongs to the carrier's owner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * One mismatch diagnostic, two defects, opposite repairs — split by the callee's role The floor's preparation named 14 sites, all in one witness file, all reading `declared 'Coproduct(ContentHash)', produced 'Product(Fnv1a64Structural)'`. A blanket wrap over all 14 broke two witnesses that had been green for their whole lives, because the diagnostic names both types and cannot name which one is wrong. The discriminator is the role of the carrier on the DECLARED side, and it is visible only in the callee: ELEVEN CALLER SITES (261-268, 273-280, 414) — the callee is right and the caller is raw. `serve_resolved_graph_stored_disk_probe` takes the union so it can narrow it with a typed cross-family refusal; `provider_admit` compares union against union (`artifact_content_digest` also returns `ContentHash`). Repair: lift the argument through `as_content_hash_structural`. The corpus was already carrying the discriminating control — line 260 of the same call passes `as_content_hash_cryptographic(...)` and is NOT refused, adjacent to the raw argument that is. THREE HELPER SITES (467-469) — `witness_hash_list_contains` declared the union for a comparison over `List<Fnv1a64Structural>` and narrows nothing. Repair: narrow the signature; the arguments stay raw. The 23 further bare `content_hash_atom` calls in the same file are untouched: they flow into parameters already declared `Fnv1a64Structural` and the wall did not name them. The wall is the census. Also reframes the direct-call witness to claim only what executes. A control run built gunbc from origin/main 907f19c and from this branch and ran identical probe sources through both: main admits a kernel 5 at the Peano `Nat`, a String at the natively-realized `Nat`, and a kernel 40 at it, exactly as this branch does. So the two red arms assert refusals that were never there — asserted, not broken — and the sentence claiming a String at a natively-realized type "stays refused" is deleted rather than softened. Both arms are enrolled in `v2.workflow.floor_expected_red` carrying the branch-and-main control table and their next-rung trigger: `kernel_value_declared_type_mismatch` repaired to fire for a kernel value at an algebraic type application. That roster self-empties, so the flip to green announces itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * Widen the enrolment row: it is any type application, not an algebraic one Five probes varying only the declared type's shape, one String argument at every one, measured against a compiler built from the branch head: Int kernel primitive REFUSED NatLike = CommutativeSemiring<Magnitude> 1-arg application admitted OneArg = List<Int> 1-arg application admitted TwoArg = Map<String, Int> 2-arg application admitted Closed = ZzA | ZzB { v: Int } coproduct REFUSED The two-argument `Map` kills the arity story the row's earlier wording rested on, and `List<Int>` is the specimen that makes the gap legible without any appeal to the numeric tower: a `String` reaching a `List<Int>` parameter unremarked is the ordinary compiler floor, not a numeric-tower curiosity. The refusing probes emit BOTH the pre-existing `type mismatch` and this branch's inhabitance diagnostic at the same offset; the admitting ones emit neither. So the blindness is upstream of `declared_type_inhabitance`, which inherits it faithfully — no arm of this relation could have caught it. Class: total at the level examined, blind one level down. The judgment is exhaustive over primitive / coproduct / application, and the application arm never asks what the application expands to, so there is no missing arm for exhaustiveness checking or a reviewer to see. The admitting branch has NOT been read and no line is named — this locates the class and a reproducing input, nothing more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * Round two of the wall's census: two production sites, same class, same caller-side repair Clearing the first 14 let preparation reach further and name two more, both outside the witness tests this time: dag/gunbc/heal_revalidation.dag:154 required_roster src/v2/workflow/native_cache_fusion.dag:36 key Both are the caller-side shape. Judged at the callee, as the class requires: `check_coverage_admits` and the whole `CheckCoverage` family declare `ContentHash` end to end (`merge_admission.dag:360,383,394,407,485,556`) and compare union against union; `EmitOnDemandCacheReceipt.key` is `ContentHash` too. Neither callee is a lying consumer, so neither declaration moves — the arguments are lifted through `as_content_hash_structural`. The census is ITERATIVE. Preparation stops at the modules it refused, so each round of repairs uncovers the next set. 14 → 2 is the wall working through the corpus, not a repair that missed. NOT repaired, and named rather than swept: `heal_revalidation.dag:155` passes `List<Fnv1a64Structural>` to `check_coverage_admits`'s `required_gates: List<ContentHash>`. That is the same mismatch one level inside a list, and the wall did NOT name it — so it is a coverage gap in this relation at the list-element-of-a-direct-call-argument position, not a site anyone repaired. Left alone deliberately: fixing it by hand would hide the gap that its silence is evidence for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * Move the list-element evidence into a fixture, then repair the production site Reversing my own call. `heal_revalidation.dag:155` passed `List<Fnv1a64Structural>` into `check_coverage_admits`'s `required_gates: List<ContentHash>` — the same mismatch as the argument beside it, one level inside a list — and this relation did not name it. I left it broken because its silence was the only evidence the gap existed. That conclusion does not follow from its own premise. DESIGN §4b(4) separates exactly this: a climb deletes the redundant PRODUCTION handling and KEEPS the discriminating RED as enrolled evidence. The evidence is a probe. It is not a live wrong argument in a merge-admission path. So the evidence moved and the site is repaired: - `w_wrong_element_type_in_a_list_at_a_direct_call_argument_is_refused` — a plain record at a coproduct element type inside a list at a direct-call argument. Asserts the refusal; currently fails; enrolled in `floor_expected_red_chunk_15` with its next-rung trigger. - `w_the_same_wrong_pair_directly_at_the_argument_is_refused` — the paired control, deliberately NOT enrolled. Identical two types, identical position, no list. It must stay green: if it ever reds, the probe has stopped measuring lists and the enrolment above is meaningless. This is what separates "the relation cannot judge this pair" from "the relation cannot see inside a list". - `heal_revalidation.dag:155` now lifts each element. Also: the census this wall performs is FAIL-FAST, so its output is a LOWER BOUND and never a population. Preparation stops at the modules it refused and never reaches what lies behind them — "14 sites" was the population visible from the first refusal, and clearing it surfaced two more in different files. Depth unknown; each round gets reported as it surfaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * Mark the realization-keyed admit as an interim and name the model that replaces it `declared_realizes_as_kernel_numeric` decides that `40` inhabits `Nat` by consulting `decl_file_realizes_natively` — because `Nat`'s declaring module is kernel-backed. That is a REALIZATION fact standing in for a TYPING fact, and it amounts to peeling the applied type until a scalar appears. It is not unsound. It fails closed on unknown identity, and the admit is measured behaviour-preserving-or-narrowing against main. It is UNDER-SPECIFIC: it answers "this module's numerics are kernel-backed" where the question is "does this type admit this literal", so it grants literal syntax module-wide permission to inhabit anything whose implementation eventually mentions a numeric carrier. The two answers coincide today for `Nat` and `Int`, and stop coinciding the moment a module declares a numeric type that should not take bare literals. The durable model is an expected-type-directed literal introduction judgment: `40` inhabits `Nat` because `Nat` SUPPLIES a numeral introduction. Lean is the worked precedent — numerals elaborate against the expected type through an `OfNat` obligation, and literal introduction stays separate from coercion insertion. Recorded at the predicate and in `floor_expected_red_chunk_14`'s next-rung trigger so the interim is never cited as the design. Two constraints on whoever builds the replacement, both live here rather than hypothetical: a judgment that peels a declared type to its representation defeats walls that already hold (`TransparentAlias` may be exposed; `OpaqueType`, `SoleConstructorCarrier`, `Refinement`, `Brand` may not — `sole_constructor` is a real executing construction wall), and exposure must derive a canonical view once per type identity and cache it, with cycle detection and a measured expansion budget. No behaviour changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * The control arm went red and caught a mis-designed probe — plus a double-wrap I introduced First execution of these arms. Two failures, both mine, both found by controls rather than by review. 1. THE PAIRED CONTROL FAILED, WHICH IS WHY IT EXISTS. `w_the_same_wrong_pair_directly_at_the_argument_is_refused` asserted that a plain record at a coproduct is refused at a direct-call argument. It is not — `kernel_value_declared_type_mismatch` gates its entire body on `is_kernel_type(actual_name)`, so a RECORD literal is never judged at any position, list or not. The pair was therefore measuring "records are never judged", and its enrolled twin would have been filed as evidence about lists. Both arms rebuilt on a kernel `String`, which is judged at this position by execution (a String at a closed coproduct refuses — measured). The list is now the only difference between the two arms, which is what the pair claimed all along. 2. MY OWN heal_revalidation REPAIR WAS A DOUBLE WRAP. Lifting `required_roster`/`required_gates` through `as_content_hash_structural` wrapped values that were ALREADY the union: the witness passes `Fnv1a64(content_hash_atom(...))` and `check_coverage_admits` takes `ContentHash`. So the comparison became `Fnv1a64(Fnv1a64(x))` against `Fnv1a64(x)` and reddened `only_exact_healed_head_complete_coverage_admits`, green until I touched it. `heal_revalidation`'s own two parameters were the only things in the chain declaring `Fnv1a64Structural`, sandwiched between a caller and a callee that both speak `ContentHash`. The chain is now `ContentHash` end to end and the lifts are gone. Reading the callee is half the discriminator. The caller is the other half, and a declaration sitting between two that agree with each other is the one that is wrong. I read the callee, lifted, and never read the caller. Ledger for the record (run 32664496347): planned=executed=terminal=10705, known_red_held 36→39 — the three enrolled arms held exactly as predicted — interrupted_before_verdict=0, known_red_now_passing=0, failed=2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * Unenrol the list-element arm until its control is green — believed is not measured The row was enrolled while its paired control was RED, and the control's red proved the pair measured the wrong thing entirely: a record literal is never judged at any position, so the list was doing no work. Both arms are rebuilt on a kernel String, and the rebuilt pair HAS NOT EXECUTED. An enrolment asserts a known, real gap. A control that reds and an arm that reds are indistinguishable as evidence, so enrolling now would re-file a claim that is currently believed rather than measured — the exact state that put two unverified reds on this roster earlier today. Until a run shows the control GREEN and the arm RED, the arm fails loudly as an ordinary failure. That is the honest reading of an unproven claim, and a red I have to look at is better than a held row asserting something I cannot support. Re-enrolment is one line once the measurement exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * Withdraw the list-element gap — measured green, the wall was there all along Run 32667623528, whole required CI green: planned=executed=terminal=10705, passed=10423, failed=0, phases_run=3 failed=0, known_red_held=38. The list-element arm was left UNENROLLED precisely so a red would show loudly. It PASSES. So a kernel String at a coproduct element type inside a list at a direct-call argument IS refused, the relation descends into a list literal's elements at this position, and the gap this pair was authored to document does not exist. WHY I BELIEVED IT DID, and the distinction is the correction: the site that started this was `heal_revalidation` passing a `List<Fnv1a64Structural>` VARIABLE into a `List<ContentHash>` parameter. That is list-typed-value compatibility, and the relation answers `Undecidable` for a generic carrier BY DESIGN — its silence there was specified behaviour, not blindness. My probe passes a list LITERAL with a wrong element, which is a different judgment and one the relation makes. I inferred the second from the silence on the first, and the two were never the same question. The list-typed-value case remains UNMEASURED and nothing now claims otherwise. The arms stay as a permanent regression control over a wall shown real (§4b(4): evidence stays enrolled as evidence once the wall is established). `floor_expected_red_chunk_15` stays empty — there is nothing red to enrol. Also green in this run: `heal_revalidation only_exact_healed_head_complete_ coverage_admits`, the witness my double-wrap had reddened, and `w_the_same_wrong_pair_directly_at_the_argument_is_refused`, the control whose red caught the mis-designed probe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QRQgQyZvAgydCNpFGLR2b * WIP: Compiler floor — declared-type inhabitance across every grammar type pos * WIP: counted non-blocking residue for the four undecidable inhabitance reasons (seed mirrors not yet regenerated) * Declare the ten unwired type positions and their next-rung trigger * Regenerate the seed mirrors and add the compile-forced cli_run arms for the new variant * Regenerate the infer mirror the merge dropped: the .dag had #9192's fix, the .rs did not The merge commit 4c9c55c resolved the generated-mirror conflict by taking one side whole. That is a SILENT deletion -- no conflict markers, clean tree, and the committed mirror became an OLDER COMPILER than the .dag it claims to mirror. Measured, with the control that makes it a result rather than a name-mangling artifact: fn .dag .rs(before) select_formal_for_call_argument 1 0 call_argument_formal_at_position 1 0 call_formal_claimed_by_a_label 1 0 declared_type_inhabitance 1 1 <- pre-existing, same grep, present in both declared_type_obligation_diags 1 1 <- same and `select_formal_for_call_argument` IS spelled that way in origin/main's mirror, so the grep works and the absence is real. #9192 landed to stop inference binding named arguments by POSITION -- refusing valid programs -- and this branch would have re-shipped the seed without that fix while the .dag said it had it. REGENERATED, NOT RE-RESOLVED AND NOT SPLICED. No hand edit to the .rs. The emitted file was taken from target/stage0-regen-candidate/src/ and installed whole. Receipts, from runs that did not share a candidate directory: round 1 (broken head): first_generation_equal=false FAIL generated surface drift: v1_compiler_infer.rs round 2 (installed): first_generation_equal=true planned=135 executed=135 declared_divergent=1 [main.rs] The fixed point is demonstrated at byte grain, not just by the flag: the installed file's sha256 210e64fb2ad08be2 was recorded BEFORE the confirming run, and that run re-emitted the identical hash. diff -rq over the candidate tree reports no other differing file. CI independently reached the same verdict on the broken head (run 32884947431), with a byte-identical message -- so the mirror-to-source comparison on a PR is intact and this red was the gate working, not a flake. --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
What this repairs
Which declared formal a call argument binds to was answered in two places with different rules:
build_call_application_planExprCallargument foldSo a call passing named arguments in a different order than declared inferred each argument against its neighbour's formal, while the type check ran a different rule and reported nothing. The green type check was never evidence against the defect — it was produced by the other authority.
A third path,
call_arg_bound_param_at, is not a third authority for this fact and is deliberately left alone. It answers a different question — under which runtime binding key an actual would be stored, for detecting duplicate writes — where a named actual keys on its literal caller label and only unlabeled actuals advance its positional counter. Routing it through the selector would replace a runtime-key question with a formal-identity answer and reintroduce the underscore-shadow false refusal its current form was written to remove.Two independent symptoms, both measured
Wrong emitted type.
src/v1/05_emit.dagcallsemit_typed_match_unifiedwith seven named arguments,source_indicesahead ofrecurse. Therecurselambda was inferred againstrender_pattern'sfn(MatchPattern) -> String.Fabricated refusal — correct code rejected.
take_list(label: "a", xs: [])is valid and main refuses it:because the
[]sits at source position 1 and was handedlabel: String's formal. DESIGN §5 forbids this as firmly as fabricated success. This is what makes the repair provable without the typed-lambda emission route, and it shows the blast radius reaches any expression whose accepted type consumes the expected input — not only lambdas.The construction
select_formal_for_call_argumentis the rule, held once. The fold consumes it directly;build_call_application_planconsumes it inverted — a formal's argument is the argument that selects that formal's index — so neither side can encode a rule the other does not. It returns the formal's index alongside its node precisely so the inversion needs no second search.Deliberately pre-type: it reads authored labels only, so it runs before recursive argument inference,
call_subststill accumulates in authored source order, and generic unification order is untouched.The label test is
call_param_caller_labels, not exact equality — a parameter declared_ctxis addressable by callers asctx. It is notcall_arg_label_matches_parameither: that predicate governs admission and returns true for a bare_against any label, which is correct for "does this label name a declared parameter" and catastrophic for "which parameter does this label select" — an anonymous parameter would claim the first label it saw.Both directions of the bijection
The selector alone makes the relation functional (one argument selects one formal). It does not make it injective, and that gap was silent wrongness below the floor:
compiles clean on both compilers and emits
f(2, 3)— the argumenta: 1is dropped with no diagnostic anywhere, becauseaand_aare two caller spellings of one formal and nothing asked whether two arguments had chosen the same one.formal_identity_duplicate_diagscloses it: an argument whose selected formal index was already claimed by an earlier argument is refused.The two duplicate walls partition by cause.
duplicate_label_diagsasks whether two arguments produce the same runtime binding key, and catches collisions this selector cannot see. This wall asks whether two arguments select the same semantic formal. They coincide on exactly one input shape — two arguments carrying the same authored label — and there the key wall already owns the refusal, so this wall fires only when the labels differ. Without that guard both fired andf(a: 1, a: 2, b: 3)produced two byte-identical diagnostics (measured). The guard states which wall owns which cause rather than deduplicating output after the fact.Controls, executed
Both compilers built from committed mirrors; no hand-edited mirror in either binary.
f(n: 1, ctx: [])vsfn f(_ctx: List<Int>, n: Int))f(a: 1, _a: 2, b: 3))f(2, 3), drops an argumentkeep_first(7, 8, 9))f(a: 1, a: 2, b: 3))f(b: 2, 1)) — review 55769The anonymous-slot control confirms the historical "every
_is the same key" false refusal is not revived. The same-label control confirms the two walls no longer double-report.The underscore-alias control is load-bearing for a reason worth recording: an earlier version of the selector used exact name equality, so
ctxnever matched_ctxand the fabricated refusal survived. An earlier in-order underscore test had passed by luck — the mis-selected argument was anIntliteral, which ignores its expected type.Mixed calls — corrected after review
An earlier revision of this body claimed the divergence between the selector's raw index and
call_arg_bound_param_at's running counter was unwritable, on the evidence that a labelled argument preceding an unlabelled one is already refused upstream:That is positional-then-named, and generalising from it to "a labelled argument preceding an unlabelled one" was wrong. The reverse order is accepted, and review 55769 caught that the selector refused it:
Argument 0 claims formal
bby name; argument 1 took formal index 1 by raw index — the same formal — and the injectivity wall refused a program main accepts. A fabricated refusal introduced by the change that exists to remove one.The repair: the positional fallback now selects the k-th formal not claimed by any label in the call, where k counts unlabelled arguments before this one. Computed inside the selector, not by routing through
call_arg_bound_param_at— which answers a different question and whose underscore handling must not be disturbed.The case is enrolled as a regression witness.
Out of scope, and measured so it is not read as closed
Emission ordering for mixed calls is wrong on main and remains wrong here.
src/v1/05_emit.dagorder_typed_call_argsopens withso if any argument is unlabelled the emitter does no reordering at all and emits source order. Measured on both compilers:
The emitted program computes a different answer than the interpreted one — below-floor silent wrongness, pre-existing, and a fourth consumer of the argument-to-formal question running its own third rule.
It is not fixed here: its blast radius is corpus-wide call ordering, and this PR's subject is expected-type propagation and the type-check plan. The enrolled witness states in its own note that it covers the source-language half only, so a census cannot read its green as covering mixed calls.
Corpus comparison
Both compilers over
src/v1 + dag + src/v2: identical diagnostics (0 blocking, 201 advisory).One
dag-target artifact byte-differs, initem_registry_keysordering at identical file length. A determinism control — the same compiler run twice — reproduces it (differ: byte 100478390), so it is pre-existing nondeterminism, not attributable to this change, and it is filed separately against the producer rather than normalised away here.Consequently
dag-artifact.jsonbyte equality is an excluded oracle for this PR. Rust mirror file-set and byte equality remain valid on the measured subject, as does diagnostic-set equality.Bootstrap receipt
The gate is discriminating rather than passing by default: the run immediately before this one reported
FAIL generated surface drift: v1_compiler_infer.rsagainst a stale mirror.Relationship to #9182
#9182 (B1, Rust lambda annotation) is hard-dependent on this and stays draft. B1 cannot emit a correct type from a malformed typed graph.