Skip to content

fail-closed cross-representation == (numeric model↔realization fork) - #5420

Merged
briansrls merged 1 commit into
mainfrom
claude/clever-brahmagupta-valpai
Jun 21, 2026
Merged

briansrls merged 1 commit into
mainfrom
claude/clever-brahmagupta-valpai

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

What

nat_add(85, 32) == 117 silently returned false. nat_add folds via nat_cata(n: a, zero: b, ..), building the non-canonical hybrid Succ^85(Int(32)) — a Variant chain with a native Int leaf — which Value::eq cannot reconcile against the native Int(117), so it falls through its _ => false arm and fabricates false. That is a §5 fail-open (a silent wrong answer), not the §2/§7 migration redundancy the 🟡 dissolve-on markers track.

Per the operator decision (== fail-closed, 2026-06-20), this makes a cross-representation ==/!= a typed, located error instead of a silent false.

How

  • New InterpError::CrossRepresentationEquality, raised at the eval_binop BinOp::Eq/Ne seam when a false result is explained by a native Int/Float vs Nat-coproduct straddle.
  • Value::eq is left infallible — it is the single CanonKey map-key authority and its Hash-consistent false must stand (§3). The guard is a read-only diagnosis over the same structural recursion, run only on a false, so it never becomes a second equality authority.
  • Recursive: catches the top-level fork (nat_add(85,32) == 117), the Variant-vs-Variant non-canonical fork (nat_add(85,32) == nat_add(100,17)), and forked list elements ([nat_add(1,1)] == [2]).
  • Genuine inequalities (1 == 2, Succ{..} == Zero) and reconciled forks ([1,2,3] == Cons{..}) pass through as a plain bool. Scalar/string pairs short-circuit without flattening, so the hot path stays O(1).

Scope

Numeric tower only — the proven, corpus-safe fork. Deliberately not a blanket guard:

  • Bool True|False over Value::Bool: no native-Bool vs True|False == site exists in the corpus.
  • Optional/Witness over Value::Null: Value::Null is the overloaded None/Absent/miss sentinel, and present == None (131 sites) is a legitimate false — a blanket guard would break it. These need the root grounding (§1/§2/§7), recorded in DESIGN.md "Open threads".

Verification (by execution)

  • New cross_representation_equality_test runs the operator's receipts: forks → typed error, controls → true, genuine diffs → false. 3/3 green.
  • cargo clippy --all-targets -- -D warnings and cargo fmt --all --check: clean.
  • Witness corpus (claim_batch --roster-from-discovery): no new failures vs baseline (only the 3 pre-existing hermetic-mock skips).

Note (pre-existing, unrelated)

cargo test --workspace fails fast on regen_stage0's cargo_invalidation_receipt_hashes_workspace_and_stage0_manifests — a test whose setup creates src/v2/stage0/Cargo.toml but asserts src/v1/stage0/... (a stale v1→v2 migration artifact committed in 32bade1, in a file this PR does not touch). Left for a separate fix since the correct migration direction is ambiguous.

🤖 Generated with Claude Code


Generated by Claude Code

`Value::eq` funnels heterogeneous model↔realization pairs through its
`_ => false` arm, so `nat_add(85, 32)` — folded as `Succ^85(Int(32))` by
`nat_cata(n: a, zero: b, ..)` — silently compared unequal to the native
`Int(117)`. That is a §5 fail-open, not the §2/§7 redundancy the 🟡
dissolve-on markers track.

Per operator decision (`==` fail-closed, 2026-06-20): raise a typed,
located `InterpError::CrossRepresentationEquality` at the `eval_binop`
`BinOp::Eq`/`Ne` seam when a `false` result is *explained* by a native
`Int`/`Float` vs `Nat`-coproduct straddle. `Value::eq` stays infallible
— it is the single `CanonKey` map-key authority and its `Hash`-consistent
`false` must stand — so the guard is a read-only diagnosis over the same
structural recursion, run only on a `false`. It recurses, so
`nat_add == nat_add` and `[nat_add(1,1)] == [2]` are caught; genuine
inequalities (`1 == 2`, `Succ{..} == Zero`) and reconciled forks
(`[1,2,3] == Cons{..}`) pass through as a plain bool. Scalar/string pairs
short-circuit without flattening, so the hot path stays O(1).

Scope is the numeric tower — the proven, corpus-safe fork. The corpus has
no native-`Bool` vs `True|False` `==`, and `Optional/Witness` over
`Value::Null` can't take a blanket guard (`Value::Null` is the overloaded
`None`/`Absent`/miss sentinel; `present == None`, 131 sites, is a
legitimate `false`) — those need the root grounding, recorded in DESIGN.md
"Open threads".

Regression: `cross_representation_equality_test` runs the operator's
receipts by execution — forks → typed error, controls → bool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CL6KyE555dMLV9PWMXiEtS
@briansrls
briansrls merged commit dcff87e into main Jun 21, 2026
1 check passed
@briansrls
briansrls deleted the claude/clever-brahmagupta-valpai branch June 21, 2026 01:10
briansrls added a commit that referenced this pull request Jun 21, 2026
… (fixes v1↔v2 FreeMonoid collision)

The type/constructor environment keyed names by bare interned string and an
implicit bootstrap bridge injected v1 `std.types`' whole transitive env as a
base into every module that didn't import it. Together these let v1's
`std.algebra` FreeMonoid (a record) and v2's `v2.std.algebra` FreeMonoid
(`Empty | Cons` sum) collide — the winner decided by the topological typecheck
order, which the std.types implicit edges coupled across the v1 and v2 trees
into one global sort. Adding import edges (e.g. #5418's 44 extdeps anchors)
perturbed that order, so `v2.std.node_query` bound FreeMonoid to the leaked v1
record and produced `undefined variable 'Empty'` / `variant 'Empty' not found
in type 'FreeMonoid'` across the v2 tree. A §3 single-authority violation.

Fix (authority in src/v1/*.dag, mirrored in the stage0 seed .rs):
- 03_resolve.dag `topological_sort`: drop the implicit `std.types -> *` edges
  and the implicit in-degree term. Ordering is now import-driven only, so the
  two trees decouple and a module is always typechecked after the modules it
  actually imports — no order-sensitivity to re-tune.
- 04_infer.dag `build_type_env` / `build_type_env_unresolved`: drop the
  `std.types` base-injection. Import bindings start empty and are seeded only
  by the module's explicit imports; kernel primitives still come from the
  kernel env. No second, implicit authority for names.

Witness (src/v1/tests/src/module_authority_resolution_test.rs): a module that
never imports `std.types` no longer inherits a type `std.types` declares — the
reference is a hard UnresolvedType (RED without the fix, where the leak silently
resolved it). Plus two §3 invariant guards for same-named types across modules.

Verified by execution: the #5418-anchored discovery-corpus repro
(wet_hermetic_scaffold_roster_filter_uses_dag_prefix_authority) goes green;
regen_stage0 self-compiles the v1 tree clean without the leak; full
`cargo test --workspace` green except the pre-existing regen receipt-hash red
(#5420); clippy + fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Jun 21, 2026
… (fixes v1↔v2 FreeMonoid collision)

The type/constructor environment keyed names by bare interned string and an
implicit bootstrap bridge injected v1 `std.types`' whole transitive env as a
base into every module that didn't import it. Together these let v1's
`std.algebra` FreeMonoid (a record) and v2's `v2.std.algebra` FreeMonoid
(`Empty | Cons` sum) collide — the winner decided by the topological typecheck
order, which the std.types implicit edges coupled across the v1 and v2 trees
into one global sort. Adding import edges (e.g. #5418's 44 extdeps anchors)
perturbed that order, so `v2.std.node_query` bound FreeMonoid to the leaked v1
record and produced `undefined variable 'Empty'` / `variant 'Empty' not found
in type 'FreeMonoid'` across the v2 tree. A §3 single-authority violation.

Fix (authority in src/v1/*.dag, mirrored in the stage0 seed .rs):
- 03_resolve.dag `topological_sort`: drop the implicit `std.types -> *` edges
  and the implicit in-degree term. Ordering is now import-driven only, so the
  two trees decouple and a module is always typechecked after the modules it
  actually imports — no order-sensitivity to re-tune.
- 04_infer.dag `build_type_env` / `build_type_env_unresolved`: drop the
  `std.types` base-injection. Import bindings start empty and are seeded only
  by the module's explicit imports; kernel primitives still come from the
  kernel env. No second, implicit authority for names.

Witness (src/v1/tests/src/module_authority_resolution_test.rs): a module that
never imports `std.types` no longer inherits a type `std.types` declares — the
reference is a hard UnresolvedType (RED without the fix, where the leak silently
resolved it). Plus two §3 invariant guards for same-named types across modules.

Verified by execution: the #5418-anchored discovery-corpus repro
(wet_hermetic_scaffold_roster_filter_uses_dag_prefix_authority) goes green;
regen_stage0 self-compiles the v1 tree clean without the leak; full
`cargo test --workspace` green except the pre-existing regen receipt-hash red
(#5420); clippy + fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Jun 21, 2026
… (fixes v1↔v2 FreeMonoid collision) (#5422)

The type/constructor environment keyed names by bare interned string and an
implicit bootstrap bridge injected v1 `std.types`' whole transitive env as a
base into every module that didn't import it. Together these let v1's
`std.algebra` FreeMonoid (a record) and v2's `v2.std.algebra` FreeMonoid
(`Empty | Cons` sum) collide — the winner decided by the topological typecheck
order, which the std.types implicit edges coupled across the v1 and v2 trees
into one global sort. Adding import edges (e.g. #5418's 44 extdeps anchors)
perturbed that order, so `v2.std.node_query` bound FreeMonoid to the leaked v1
record and produced `undefined variable 'Empty'` / `variant 'Empty' not found
in type 'FreeMonoid'` across the v2 tree. A §3 single-authority violation.

Fix (authority in src/v1/*.dag, mirrored in the stage0 seed .rs):
- 03_resolve.dag `topological_sort`: drop the implicit `std.types -> *` edges
  and the implicit in-degree term. Ordering is now import-driven only, so the
  two trees decouple and a module is always typechecked after the modules it
  actually imports — no order-sensitivity to re-tune.
- 04_infer.dag `build_type_env` / `build_type_env_unresolved`: drop the
  `std.types` base-injection. Import bindings start empty and are seeded only
  by the module's explicit imports; kernel primitives still come from the
  kernel env. No second, implicit authority for names.

Witness (src/v1/tests/src/module_authority_resolution_test.rs): a module that
never imports `std.types` no longer inherits a type `std.types` declares — the
reference is a hard UnresolvedType (RED without the fix, where the leak silently
resolved it). Plus two §3 invariant guards for same-named types across modules.

Verified by execution: the #5418-anchored discovery-corpus repro
(wet_hermetic_scaffold_roster_filter_uses_dag_prefix_authority) goes green;
regen_stage0 self-compiles the v1 tree clean without the leak; full
`cargo test --workspace` green except the pre-existing regen receipt-hash red
(#5420); clippy + fmt clean.

Co-authored-by: Brian Searls <briansrls@gunb.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants