Repository navigation
Fix refinement wall: base-type mismatch through refined-alias chains composes (arbitrary depth) - #8513
Merged
Conversation
added 2 commits
August 19, 2026 04:42
…es; reject LitNull as a literal in where-refinement check WIP checkpoint before merging origin/main; regenerated stage0 Rust excluded from this commit pending a clean regen against merged main.
Adds four cause-pinning tests using compile_dag_diagnostic_census (not the coarse compile_dag_rust_emit_check Bool) so each red asserts exactly one blocking TypeMismatch row rather than "something failed" — closing the parent review's open question. Verified by direct execution against the currently-built (pre-regen) binary that the assertion logic pins the correct cause; all four currently FAIL for the same already-disclosed required-regen limitation as the existing boolean reds, not a new defect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contributor
|
Already disclosed in the PR body under "Verification limitation — disclosed": the stage0 Rust seed regen is blocked on two pre-existing, unrelated — sent from snappy-seal-402 |
…scharging required-floor coverage gap Parent review (smart-ram-730) flagged that the 4 cause-pinning specimens in dag/test/claim/long/where_refinement_enforcement_witness_test.dag are excluded from CI's required floor: their module matches long_home_prefixes() in src/v2/workflow/required_floor.dag, so scan_site never emits a claim for them regardless of local execution. Add dag/test/claim/where_refinement_seam_unit_test.dag: calls kernel_value_declared_type_mismatch and expr_is_any_literal directly against hand-built Node/TypeEnv literals, crossing the same seam the long/ compile- pipeline specimens exercise without needing the compile pipeline or a regenerated seed binary. Module path carries no long_home_prefixes() segment and declares LiveTreeDisposition = SubstrateInputsOnly, so required_floor.dag's scan_site admits all 8 sites as RequiredFloorClaim rows (verified by reading scan_site/module_is_long_home and cli_run.rs's SubstrateInputsOnly mapping, not just by local execution). All 8 assertions run and PASS individually against this branch's built binary. Also reverts an unrelated pre-existing syntax corruption in src/v1/trait_bound_witness.dag (data -> type declaration) that was blocking any `gunbc run` invocation touching --source-root src/v1, discovered while verifying the above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mports unresolvable under CI's required-floor source roots CI's witnesses.yml invokes claim_executor with --source-root dag --source-root src/v2 only, never src/v1. A dag/-rooted witness statically importing v1.compiler.infer, v1.compiler.infer_env, or v1.std.core therefore fails with `unresolved import` under real CI regardless of correctness -- it passed locally only because a manually-added --source-root src/v1 (not part of CI's invocation) made the imports resolve. This is the same constraint already documented in dag/test/claim/trait_bound_witness_test.dag, which records the accepted resolution: route through a compile-pipeline host builtin (compile_dag_rust_emit_check / compile_dag_diagnostic_census) over source text, never a static v1.compiler.* import, and treat widening CI's source-root list as a separate, out-of-scope change. No such builtin-based fixture fits required_floor_claim_budget_ms() at the cost the long/ file's equivalent census tests measure (~100-200s/assertion), so the required-floor coverage gap for the depth-2/3 cause-pinning specimens remains disclosed, unresolved debt rather than a claimed fix.
gunbai-bot Bot
pushed a commit
that referenced
this pull request
Aug 19, 2026
…repayment, not this lane's content required-regen refused after merging main forward through #8513 (Fix refinement wall: base-type mismatch through refined-alias chains composes): that PR changed the .dag authority (00_core.dag, 04_infer.dag — 2 files) but carried zero regenerated projection. This is pre-existing drift on main, not damage from this branch's merges (confirmed independently by smart-ram-730, who screened all recent merges for authority-changed-without-projection and found #8513 the sole offender). v1_std_core.rs: adds expr_is_any_literal, the missing generated fn. v1_compiler_infer.rs: adds the expr_is_any_literal call site plus the peel_where_refinement_base extraction, matching the .dag authority's refactor. Regenerated from the current merged tree and verified drift-free before commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Aug 19, 2026
briansrls
pushed a commit
that referenced
this pull request
Aug 20, 2026
…evidence (#8608) A ^symbol literal (LitSymbol) is a distinct literal kind from a string literal (LitStr). where_refinement_diags_for_predicate's string-predicate branch only extracted a value via expr_literal_string_optional, which matches LitStr only -- so a LitSymbol's spelling was never evaluated against its predicate. But expr_is_any_literal(LitSymbol) is true, so the Absent-extraction case took the "literal we checked and it failed" branch instead of the "genuinely non-literal, defer to advisory" branch: a hard TypeMismatch refusal fired on a value that was never actually tested. This misfired on every content_hash_atom(value: ^canonical_tag_*) call in src/v2/std/node.dag once #8513's promotion of soft->hard refusal reaches a regenerated stage0 mirror -- 46 direct-call sites, all constructing the same "expected Product(NonEmptyStr), got Primitive(String)" diagnostic, none of them an actual violation (the grammar guarantees ^ident is a non-empty identifier). Fix: recognize LitSymbol alongside LitStr when extracting a literal value for a string predicate, and decide it the same way -- via decidable_where_string_predicate_holds, not a name/kind shortcut. This is ground truth evaluation of the symbol's actual spelling, not a widened acceptance: a genuinely-empty string literal at the same refined position still hard-refuses (regression control included). Out of scope, left soft per operator hold pending a grounding ruling on predicate implication (DESIGN section 5, "wall after grounding"): non-literal Symbol-typed values (5 sites) and refinement-to-refinement call sites such as Sha1DigestHex -> NonEmptyStr (2 sites), where covering a formal's predicate would require a decidable implication relation between differently-named predicates (e.g. lower_hex_40 implies non_empty) that does not exist yet in where_refinement_predicates_equivalent. Pre-existing unrelated failures in this witness suite (depth2/depth3 wrong-base-type and wrong-kind-literal families) are reproduced byte-identically on unmodified main and are untouched by this change. Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Worker attestation
npm test,cargo test) and the result.Closes #Ndirective.Closes node://adhoc-2ccd029d-c1d
Summary
The v1 where-refinement type-checking wall silently degraded to an advisory diagnostic (
WhereRefinementUnenforced, compile-clean green) instead of a hardTypeMismatchrefusal when a value's base type disagreed with a refined alias whose own base is itself refined — i.e. a depth-≥2 refinement chain (type S1 = String where non_empty; type S2 = S1 where brand("S2"); data d: S2 = 0). This is DESIGN.md §5's named absorbing fallback anti-pattern reaching the type wall itself: a decidable, structurally-detectable base-type mismatch was silently downgraded to "I couldn't tell" rather than refused.Two independent instances of the same shape were found and fixed at their single authority in
src/v1/04_infer.dagandsrc/v1/00_core.dag, both proven generic over arbitrary depth (no depth-2-specific code):(a)
kernel_value_declared_type_mismatch(record-field inits, call args) carried its own duplicate one-level peel of the formal's refinement wrapper, instead of reusing the already-correct arbitrary-depthpeel_where_refinement_base. Past depth 1 this peel landed on an intermediate alias name rather than the ground kernel type, so the base-type-mismatch check itself never fired and the value silently fell through to the advisory arm. Fix: callpeel_where_refinement_basedirectly (single authority, DESIGN §3) — one change closes both call sites (record-field init and call-arg checking).(b)
where_refinement_diags_for_predicate(direct returns, data inits — these never reachkernel_value_declared_type_mismatchat all, so this was the only wall covering them) usedexpr_literal_int_optional/expr_literal_string_optional'sAbsentarm to mean "non-literal value, defer to advisory." ButAbsentalso fires for a literal of the wrong kind (a String literal at an Int-refined position, or vice versa) — a decidable case, not an unknown one. This is the state-space conflation DESIGN.md §3/§5 names: two distinct states (genuinely not a literalvsa literal of the wrong kind) were collapsed onto oneAbsentsignal and both routed to the same silent-defer arm. Fix: a newexpr_is_any_literal(expr)predicate in00_core.dagdistinguishes "any literal, of any kind" (refuse on mismatch) from "genuinely not a literal" (unchanged advisory), checked identically in both the int- and string-predicate branches.expr_is_any_literalitself also had to excludeLitNull(total absence of value) from "any literal" — treating the null sentinel as a literal value is the same conflation one layer down, and was independently reachable viadag/extdeps/container/oci/digest.dag:62.Root cause (a) and root cause (b) are the same shape at different layers — not one bug with two symptoms, but two instances of one absorbing fallback shape: a decidable mismatch signal (a resolved-but-wrong base type; a literal of the wrong kind) getting funneled into the single catch-all "unknown, defer" arm instead of its own refusal arm. The
expr_is_any_literal/LitNullfix is a third instance of the identical shape one layer further down (the null sentinel vs. an absent value).Both fixes are proven generic over arbitrary refinement depth by construction:
peel_where_refinement_basewalkswhere_refinement_chain, which recurses on the resolved base type until it hits a non-refinement ground type — there is no depth-2-specific branch anywhere in either fix.Reachability / denominator
The advisory arm (
where_refinement_unenforced_error) is reachable from exactly two call sites in04_infer.dag— the int-literal branch and the string-literal branch ofwhere_refinement_diags_for_predicate— both of which route through the sameAbsent-conflation shape described in (b), and both are fixed identically in this PR.kernel_value_declared_type_mismatchis a third, structurally distinct seam (record-field/call-arg admission, not the predicate-diagnostic path) covered by fix (a). No other predicate-kind branches (where_refinement_is_deferred_predicate, e.g.brand/Pattern) route through this Absent-conflation at all — they always emit the advisory unconditionally, which is the correct behavior for a genuinely undecidable predicate (wall-after-grounding, not a bug).Test plan
dag/test/claim/long/where_refinement_enforcement_witness_test.dag— 38 tests, extended in this PR with dedicated depth-2 and depth-3 specimens beside their depth-1 positive controls:where_refinement_depth2_wrong_base_type_record_field_refuses/_accepts— depth-2 specimen (S2 where brand → S1 where non_empty → String), red (base-type mismatch refuses) beside its green positive control (correct base type still accepted).where_refinement_depth3_wrong_base_type_record_field_refuses/_accepts— depth-3 specimen, proving the fix composes past depth 2 rather than being special-cased to it.where_refinement_depth2_wrong_base_type_call_arg_refuses/_accepts— same depth-2 shape at the call-arg seam (fix (a)'s second call site).where_refinement_depth2_wrong_base_type_data_init_refuses/_accepts— same depth-2 shape at the data-init seam (fix (b)'s path, since data inits never reachkernel_value_declared_type_mismatch).w_wrong_kind_literal_int_pred_data_init_*/w_wrong_kind_literal_string_pred_data_init_*/w_wrong_kind_literal_direct_return_*— the wrong-kind-literal conflation from fix (b), independent of depth.w_positive_int_record_field_red/green, etc.) — pre-existing, unchanged, still passing, establishing that depth-1 behavior (which was already correct) is not regressed by generalizing the peel.All 38 tests run via
compile_dag_rust_emit_checkagainst the currently-built seed binary's compile-clean pipeline, which is the same hard authority CI uses (compile_clean_diagnostic_is_hard).Required-floor coverage (parent review follow-up — attempted, reverted; structural budget finding disclosed)
Parent flagged that the 4 new cause-pinning specimens live in
dag/test/claim/long/where_refinement_enforcement_witness_test.dag, whose module (test.claim.long.where_refinement_enforcement_witness) matcheslong_home_prefixes()— excluded from CI's required floor independent of local execution. Parent's explicit preference: ask whether the cause-pinning assertion genuinely needs the host compile-pipeline file, or whether a minimal fixture crosses the same seam without thelong/exclusion.A first attempt at this (
dag/test/claim/where_refinement_seam_unit_test.dag, callingkernel_value_declared_type_mismatch/expr_is_any_literaldirectly viaimport v1.compiler.infer { ... }/import v1.std.core { ... }) passed locally but failed real CI (witnessescheck, run32231028216):unresolved import: module 'v1.std.core' not found (imported by 'test.claim.where_refinement_seam_unit'), likewise forv1.compiler.infer/v1.compiler.infer_env. Root cause: CI's required-floor invocation isclaim_executor --required-floor --source-root dag --source-root src/v2(.github/workflows/witnesses.yml) — it never scanssrc/v1, so a static.dag-level import of anyv1.compiler.*orv1.std.coremodule from adag/-rooted witness is structurally unresolvable under CI regardless of correctness. My local runs passed only because I had manually added--source-root src/v1myself — extra source roots CI does not pass — which is exactly the "local execution is not coverage" gap parent warned about, now caught by CI rather than by review.This turns out to be a known instance of the same constraint:
dag/test/claim/trait_bound_witness_test.dag(a sibling PR, unrelated to this fix) carries a note recording an identical import failure, and its route around it — call through a host builtin that runs the real v1 pipeline over source text (compile_dag_rust_emit_check/compile_dag_diagnostic_census), never a staticv1.compiler.*import. This PR does not touch.github/workflows/witnesses.yml— widening the required-floor source-root list to includesrc/v1is a shared-CI-workflow decision owned by PR #8526 (session tidy-lark-471), which has measured it directly from CI (run32219326621) at 12 last-segment collisions and 532 diagnostics, rooted in bare cross-module reference binding rather than a renameable-name list, with a named dissolution (namespace-only resolution) — not a bounded disambiguation question, and not this PR's to resolve.A separate, sibling PR (#8526) adds a per-PR parse gate over
src/v1/*.dag, run outside the hermetic required-floor fold (v1_src_dag_parse.rs, invoked directly bywitnesses.yml, not throughclaim_executor). Checked whether it closes the gap this section discloses: it does not. That binary tokenizes and parses eachsrc/v1/*.dagfile independently against an empty environment (v1_compiler_parse::parse(tokenize(...), Rc::new(HashMap::new()))) and reports only a parse diagnostic if one fires — no import resolution, no cross-module binding, no type inference. It is a real, adjacent development (it would have caught the earliertrait_bound_witness.dag-class corruption at the syntax level) but it answers a strictly weaker question than the one this section discloses as open: whetherkernel_value_declared_type_mismatch/expr_is_any_literal's type-checking decision logic is correct on a depth-2/3 refinement chain. Parse-clean is necessary but not sufficient for that; the coverage gap below stands unchanged by #8526.The budget finding stands independent of that question, and is the actual blocker here. The only sanctioned way to witness
src/v1emitter/type-checker behavior from adag/-rooted witness today is the compile-pipeline host-builtin route, and it costs ~100-200s per assertion (measured directly, this PR) againstrequired_floor_claim_budget_ms()'s 1552ms — two orders of magnitude over the only budget that admits a required-floor claim. That is not "no fixture was found"; the sanctioned route does not exist at the floor's cost ceiling, for this seam, today. A hand-built-Node-literal approach (bypass the compile pipeline, drive the decision function directly on constructed inputs) was in fact the original shape of the reverted witness, and it does not dissolve this particular blocker:kernel_value_declared_type_mismatchandexpr_is_any_literalare themselves declared insrc/v1(04_infer.dag,00_core.dag), so reaching them at all — literal-driven or not — requires the same unresolvablev1.compiler.*/v1.std.coreimport. Cheap hand-built-literal witnessing is only available on a seam whose decision function already lives in a floor-scanned root (dag/orsrc/v2); this fix's decision functions do not.Disposition: reverted.
where_refinement_seam_unit_test.dagis removed from this PR. The required-floor coverage gap parent raised is not resolved by this PR — it is pre-existing debt (thelong/file's structural exclusion, unrelated to this fix's correctness) that this PR discloses but does not expand. Closing it for real needs eithersrc/v1becoming a scanned required-floor source root (parent's active, separately-owned investigation) or a decision-function relocation/duplication that would itself violate DESIGN §3 single-authority — both out of this PR's scope.Cause-pinning (parent review follow-up, answered)
Parent asked whether the depth-2/3 reds assert the specific refusal cause or merely "something failed" — a total can't be told apart from a reviewer's read. Answer: four of the reds now pin cause explicitly, added as
where_refinement_depth{2,3}_wrong_base_type_{record_field,call_arg,data_init}_pins_type_mismatch_cause, usingcompile_dag_diagnostic_census(not the coarsecompile_dag_rust_emit_checkBool) to assert the red source produces exactly one blockingTypeMismatchrow and the green source produces zero, viadepth_wrong_base_type_mismatch_count(census_rows_of_class(..., "TypeMismatch")filtered throughcensus_blocking_rows,where_refinement_cause_pinning_notedocuments the scope decision). This is deliberately scoped to the base-type-mismatch specimens this PR introduces, not retrofitted across all 38 pre-existing tests (a larger, separately-scoped census migration).Verified by direct execution, per DESIGN §4b rung-honesty, that the assertion logic itself is sound and pins the right cause — not by reading the code. Built and ran ad hoc debug probes (removed before this push) directly against
compile_dag_diagnostic_censusfor the depth-2 record-field red specimen and confirmed the exact row set the currently-built (pre-regen) binary produces: 2 rows, bothWhereRefinementUnenforced, both non-blocking; 0TypeMismatchrows; 0UnresolvedTyperows. That is precisely the pre-fix state the cause-pinning assertion is built to detect and refuse (depth_wrong_base_type_mismatch_count(red) == 1is false today; it will read1once the.dagfix is regenerated into the running binary). All four_pins_type_mismatch_causetests were then run directly (gunbc run --function <name> --claim-run) against this branch's built binary and all four currently FAIL, for the same already-disclosed reason as the pre-existing boolean reds below: the seed binary has not been regenerated from the fixed.dagsources (see "Verification limitation" below), soTypeMismatchgenuinely does not fire yet in the running pipeline. This is the same limitation surfacing at finer (cause) grain rather than a new defect in the new tests' logic.One correction to a mid-investigation false alarm, kept here for the record rather than silently dropped: these census-based tests were initially suspected of hanging (a prior local run was killed after 100s with no output). Re-run with a longer timeout, each completes in roughly 100-200s and reports a clean PASS/FAIL — not a hang. The cost is real (each assertion drives two full nested
compile_sourcescalls, one per red/green source, against an already-large witness-suite host program) but bounded; a 100s timeout was simply too tight for this specific test shape, not evidence of a defect.Verification limitation — disclosed
Full
required-regenfixed-point verification (regeneratedsrc/v1/stage0/src/*.rsfrom the fixed.dagsources, rebuilt, witness suite run against that rebuilt binary) was attempted and could not be completed in this PR. Two independent, unrelated blockers, neither introduced by this fix and neither in this PR's scope:required-regenitself, already found and being fixed separately bysmart-ram-730in gunbc#8518 (fix/required-regen-can-execute): 10 real rows of committed/emitted basename drift (emitted_not_committed=["v1_compiler_trait_bound_witness.rs"],committed_not_emitted=[9 files]) that causerun_required_regento refuse before completing a fixed point, on main, independent of this branch. Reproduced independently this session with required-regen: the population gate compared two key spaces and refused vacuously on every run #8518's two patches applied — identical 10-row result. Per required-regen: the population gate compared two key spaces and refused vacuously on every run #8518's own framing: this population mismatch was blocked on a population reconciliation nobody has done, which was invisible until the vacuous gate was repaired.run_required_regennever writes its candidate tree to disk at all when it refuses on the population mismatch above (the write step inwrite_emitted_tree/copy_hand_maintained_supportruns after the population check, so a refused run leaves nothing on disk to build from). Working around that locally (a diagnostic-only, never-committed reordering) surfaced a third, further-unrelated defect: the whole-workspace self-compile's emitted output includes at least one entry whose content is not valid Rust (a parse failure on[package]-shaped content, consistent with a Cargo.toml being routed through the Rust-target emit/write path), which aborts the tree write partway through and produced a partial, non-representative candidate tree. All three of these are pre-existing defects inrequired-regen/compile_stage0infrastructure, orthogonal to the where-refinement fix in this PR, and outside this PR's ownership (required_regen_host.rs,trait_bound_witness.dag, andcompile_stage0/compile_sourcesinternals are required-regen: the population gate compared two key spaces and refused vacuously on every run #8518's territory). No changes to those files are part of this PR — both were touched only via local, uncommitted, immediately-reverted diagnostic patches during investigation, confirmed viagit status/git diff --stat HEADto leave a clean tree matching this branch's committed HEAD.What verification WAS achieved, honestly stated: the fix compiles and typechecks as
.dagsource; the 38-test witness suite (including the new depth-2/depth-3/wrong-kind-literal specimens) is proven correct by direct code-path tracing of the fix againstpeel_where_refinement_base/where_refinement_chain's existing, unmodified, already-generic recursion, and against the exactAbsent-conflation each fix closes; and the tests execute today against the currently-built seed binary's compile-clean pipeline (pre-fix, establishing the regression baseline the depth-2/3 reds document). What was not achieved: witness-suite execution against a binary that has the.dagfix regenerated into its compiled Rust — that step is blocked on the three infrastructure defects above, is not achievable without unbounded scope creep intorequired-regen/compile_stage0repair (outside both this task's brief and #8518's already-claimed ownership), and is disclosed here rather than silently omitted, per DESIGN.md §4b's rung-honesty requirement (the reported rung must equal the rung established by executed evidence — this PR does not claim a full-regen green that never executed).No generated
src/v1/stage0/src/*.rsfiles are part of this commit; that is deliberate and matches the above — landing hand-edited or partially-regenerated Rust here would itself be an unmodeled realization / manual-application-committed-as-source violation (DESIGN.md §6).