From 67f1f4978474018d593e0150de28e3ef0314cde5 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 26 Sep 2026 21:57:05 +0000 Subject: [PATCH 01/90] docs/plans: arrow elimination model for v2 infer (for ruling) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/arrow-elimination-model.md | 112 ++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/plans/arrow-elimination-model.md diff --git a/docs/plans/arrow-elimination-model.md b/docs/plans/arrow-elimination-model.md new file mode 100644 index 00000000000..131e4808417 --- /dev/null +++ b/docs/plans/arrow-elimination-model.md @@ -0,0 +1,112 @@ +# Arrow elimination in v2 infer — model for ruling + +Status: proposed model, awaiting a v2-foundation ruling before any `04_infer` change. +Trigger: work item "v2: infer derives a Bool-returning application", parent `deep-bee-18`, +consumer `v2.compiler.refinement_discharge`. + +## What was measured (main `21f4d0c390`, a locally built `gunbc run` over a probe entry) + +| application `Transform[Arrow(Conj{x: Int}, R, body), 1]` | infer | root facts | eval | +|---|---|---|---| +| R = Int, body = int literal | Accepted | GroundingNotDerived | `eval_rejected_grounding_not_derived` | +| R = Bool, body = `true` | Accepted | GroundingNotDerived | `eval_rejected_grounding_not_derived` | +| R = Int, body = `true` | Accepted | GroundingNotDerived | — | +| R = Bool, body = int literal | Accepted | GroundingNotDerived | — | + +`infer_compatible_argument_admits` asserts only `Accepted`, so it never witnessed derivation. + +## The slice, re-derived (DESIGN §6b) + +1. **Return type atom → its grounding.** `v2.compiler.infer` `infer_node_facts` grounds a + binding atom through `v2.extdeps.languages.dag` `dag_binding_denotation`, which declares + only `^dag_binding_type_int` and maps it to `dag_int_inhabitant_node` — the *roster record + describing* Int (inhabitant atom + surface spelling), not the Int type. Bool has no row, so + a Bool return atom stays on the frontier, and so does its Arrow (the product row needs every + child). +2. **Arrow introduction.** Nothing compares an Arrow's body type to its declared return. The + Arrow product row composes children's evidence — including the body's — into the Arrow's + "type". +3. **Arrow elimination — the earliest unjustified boundary.** + `infer_transform_derived_optional` derives a type only for binary Int add and casts. No rule + types `f(a)` as `f`'s codomain, so NO application derives, Int included. Argument + inhabitance (`infer_application_argument_inhabitance`) is judged, then the node falls to + the frontier. + +## One Int, not two (the unification) + +Census of what each form means today: + +- `Atom(^dag_binding_type_int)` is the Int **value type** at every consumer: the Int literal + rule (`infer_branch_int_binding_type_node`), binary add's unified type, match/branch + unification (`infer_branch_type_is_int`), parameter operand types (the declared domain atom + via `infer_find_arrow_domain_type_in_tree`), argument inhabitance (declared formal compared + structurally to the argument's type), and the evaluator's runtime primitive type + (`v2.extdeps.runtimes.v2_evaluator` `v2_eval_int_type_node`). +- `dag_int_inhabitant_node` (a Conj) is the dag language roster's record about Int. Its only + use as a *type* is `dag_binding_denotation` → the binding atom's derived type. + +So the fork is `dag_binding_denotation` returning the record. The model: + +- **`dag_binding_denotation(sym)` is the single binding → value-type authority** and returns + the value type the binding names: Int → `Atom(^dag_binding_type_int)`, Bool → + `v2.std.logic` `bool_node()` (the node the Bool literal rule already uses). The Bool row is + one more row of the same join, not a special case. +- **The literal rules consume it** instead of minting their own nodes: an Int literal's type is + `dag_binding_denotation(^dag_binding_type_int)`, a Bool literal's is + `dag_binding_denotation(^dag_binding_type_bool)`. `infer_branch_int_binding_type_node` + deletes. (The evaluator's `v2_eval_int_type_node` is the runtime's own copy of that node, and + is left to a later cut; it is equal by structure today.) +- **A type-expression atom's OWN grounding is its kind**, not its denotation: + `kind_node(TypeDenotationKind)` (`std.kind`), exactly as a roster type member derives today. + This is forced, not chosen: the Int denotation is structurally the atom itself, so taking it as + the atom's derived type hits `std.constraints` `canonical_grounding_from_derived_type`'s + self-evidence wall — and it was always a category error (the type of the expression `Int` is + a type-kind, not `Int`). + +## The two rules + +**Arrow introduction (body/return check), at the Arrow product row.** When an Arrow carries a +body edge (`^arrow_body_edge`) and both the body's facts and its declared return atom's +denotation are derived: the body's value type must equal the denotation, structurally with +provenance stripped (`infer_type_equal_ignoring_provenance`, the list rule's authority). A +mismatch refuses with the new reason `^arrow_body_does_not_inhabit_declared_return`, located at +the body. A body or return that is not derived leaves the Arrow on the counted frontier, never +admitted. + +**Arrow elimination, in `infer_transform_derived_optional`.** For `Transform[callee, args…]` +whose callee is an Arrow and whose argument inhabitance was admitted: the application's derived +type is `dag_binding_denotation` of the callee's declared return atom. The derivation requires +the callee Arrow itself derived (so the introduction check has run). A non-Arrow callee (an Atom +operator — `FormalUnresolved`), an underived callee, or a return with no denotation stays on the +counted frontier. + +Red and controls owed: +- Accepting: `R = Bool, body = true` derives Bool at the application; eval of it equals eval of + the literal `true` and differs from eval of `false`. +- Accepting: `R = Int, body = 1` derives Int at the application (same rule, not a Bool case). +- Red: `R = Bool, body = 1` and `R = Int, body = true` refuse + `^arrow_body_does_not_inhabit_declared_return`. +- The existing argument-inhabitance reds stay red. + +## Consumers that see the newly derived facts + +- `v2.compiler.eval` `inferred_facts_for_eval` — applications now pass its grounding gate. +- `v2.compiler.translate` — the `translate_rejected_grounding_not_derived` gate, same. +- `v2.std.coercion` via `infer_transform_cast_optional` — a cast whose operand is an + application is now judged by `coercion_cast_crossing` instead of staying on the frontier. +- `v2.compiler.refinement_discharge` (deep-bee-18) — the intended consumer. +- infer's own branch/match/loop unification — an arm that is an application now has a type, so + a previously frontier arm pair can now unify or refuse `arm type mismatch`. This is the + population the floor has to census. +- Readers of an Arrow's evidence or a binding atom's resolved type: the return atom's evidence + changes from the inhabitant record to the type kind. `v2.test.execution.dag_binding_denotation` + counts derivation only and holds either way. + +## Questions for the ruling + +1. Is `dag_binding_denotation` the right single authority for binding → value type, or should + Int's value type live in `std.integer` (as Bool's lives in `std.logic`), with the language row + pointing at it? +2. The Arrow's composed evidence includes the body's type (and `dag_arrow_with_body_node` lists + the return atom twice). A function type is domain → codomain. Out of scope here unless you + rule otherwise; noted so it is not mistaken for part of this model. From d5135cbab27eb6855fcb88227bb34c7a22652563 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 26 Sep 2026 22:18:42 +0000 Subject: [PATCH 02/90] v2 infer: arrow elimination and the body/declared-return check; one Int and one Bool value type An application of a derived Arrow now takes the type the Arrow declares it returns (read from the return atom as written), and a bodied Arrow is admitted only when its body's type equals that return, else arrow_body_does_not_inhabit_declared_return. Before this, no application derived, Int included, and eval refused both Int and Bool applications. dag_binding_denotation is the one binding->value-type join, and its rows point at v2.std.integer integer_int_type_node and v2.std.logic bool_node. The literal rules consume it, a type-name atom derives its kind (TypeDenotationKind), and the evaluator's own Int and Bool type nodes are replaced by the same authorities. Model and ruling are in docs/plans/arrow-elimination-model.md; the composed-evidence defect is filed as function_type_evidence_carries_its_body. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...unction_type_evidence_carries_its_body.dag | 26 +++ docs/plans/arrow-elimination-model.md | 14 +- src/v2/compiler/04_infer.dag | 217 ++++++++++++++---- src/v2/extdeps/languages/dag.dag | 13 +- src/v2/extdeps/runtimes/v2_evaluator.dag | 29 ++- src/v2/std/integer.dag | 10 + .../infer_arrow_elimination_witness_test.dag | 145 ++++++++++++ .../execution/dag_binding_denotation_test.dag | 21 +- .../claim/infer_list_introduction_test.dag | 9 +- 9 files changed, 415 insertions(+), 69 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/function_type_evidence_carries_its_body.dag create mode 100644 src/v2/test/claim/compiler/infer_arrow_elimination_witness_test.dag diff --git a/dag/gunbc/recurring_failure_mode/function_type_evidence_carries_its_body.dag b/dag/gunbc/recurring_failure_mode/function_type_evidence_carries_its_body.dag new file mode 100644 index 00000000000..14ea96eba17 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/function_type_evidence_carries_its_body.dag @@ -0,0 +1,26 @@ +module gunbc.recurring_failure_mode.function_type_evidence_carries_its_body + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data function_type_evidence_carries_its_body: RecurringFailureMode = RecurringFailureMode { + identity: "function_type_evidence_carries_its_body" as NonEmptyStr, + + receipts: [ + "INVALID STATE: a bodied Arrow's derived type in v2.compiler.infer is the product-introduction composition over EVERY child, so it carries the body's type as a child edge, and v2.extdeps.languages.dag dag_arrow_with_body_node lists the return atom twice. A function type is domain to codomain; the body is how the function is realized, not part of its type.", + + "HARM: two functions with the same domain and codomain but differently typed bodies derive unequal types, so a structural comparison of function types (inhabitance of a function-typed formal, unification of function-typed arms) can refuse an equal pair or depend on the body. A reader that takes the codomain from the composed evidence reads a body-dependent, positionally doubled carrier instead of the declaration.", + + "DISTINGUISHING FACT: arrow elimination does not depend on this defect by construction. v2.compiler.infer infer_arrow_declared_return_type reads the return atom AS WRITTEN (positional child 1) and never the composed evidence, per the v2-foundation ruling on docs/plans/arrow-elimination-model.md (2026-09-26).", + + "RECOGNITION RULE: an Arrow's derived evidence has more children than domain plus codomain, or two Arrows with equal domain and declared return but different bodies derive unequal types.", + + "RUNG FOUND AT: mitigatable. No consumer compares function types structurally on the current route; elimination is shielded by reading the declaration.", + + "CEILING: structurally impossible, since the Arrow's type node can be constructed as domain to codomain only.", + + "NEXT-RUNG TRIGGER: a function-type introduction rule in v2.compiler.infer that derives an Arrow's type from its domain and declared return alone, with the body judged against the return (infer_arrow_body_inhabits_declared_return) instead of composed into the type, and a fixture constructor carrying the return once. It must be sufficient that two Arrows differing only in body derive equal types.", + ], + + evidence: [], +} diff --git a/docs/plans/arrow-elimination-model.md b/docs/plans/arrow-elimination-model.md index 131e4808417..d6b83fba7a5 100644 --- a/docs/plans/arrow-elimination-model.md +++ b/docs/plans/arrow-elimination-model.md @@ -1,6 +1,18 @@ # Arrow elimination in v2 infer — model for ruling -Status: proposed model, awaiting a v2-foundation ruling before any `04_infer` change. +Status: RULED by v2 foundation (neat-boar-16), 2026-09-26, and implemented in the PR that +carries this file. Rulings: Int's value type is owned by `v2.std.integer` +(`integer_int_type_node`) as Bool's is by `v2.std.logic` (`bool_node`); `dag_binding_denotation` +stays the one binding→type join and its rows point at those authorities. Elimination reads the +DECLARED return atom, never the composed evidence. The composed-evidence defect is rostered as +`gunbc.recurring_failure_mode` `function_type_evidence_carries_its_body`. + +Found while building: the evaluator carried the same second representation for Bool +(`v2.extdeps.runtimes.v2_evaluator` `v2_eval_bool_literal_pin` as every Bool runtime value's +type), so eval's resolved-type acceptance refused the derived Bool application with +`eval_rejected_resolved_type_mismatch`. Bool runtime values now carry `bool_node()`; the pin +remains only as the evaluator's literal-shaped node for `true`. The evaluator's Int copy +(`v2_eval_int_type_node`) is deleted in favour of `integer_int_type_node`. Trigger: work item "v2: infer derives a Bool-returning application", parent `deep-bee-18`, consumer `v2.compiler.refinement_discharge`. diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 9f184e97ca2..3d94c63ca59 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -1,7 +1,7 @@ module v2.compiler.infer import std.occurrence_identity { OccurrenceSynthetic } -import std.kind { Kind, kind_node, roster_member_kind } +import std.kind { Kind, TypeDenotationKind, kind_node, roster_member_kind } import v2.std.algebra { any, Empty, TailAbsent, TailFound, length, list_tail, zip_map } import std.algebra { Cons, list_snoc_item } import v2.std.bounded_lattice_completeness { @@ -26,6 +26,7 @@ import v2.compiler.inferred_tree { import v2.extdeps.languages.dag { DagCanonicalBoolLiteral, DagCanonicalIntLiteral, + DagCanonicalLiteral, dag_binding_denotation, dag_canonical_literal_from_node, dag_declared_inhabitants_root, @@ -100,7 +101,7 @@ import v2.std.diagnostic { outcome_accepted, rejected_with_pending } -import v2.std.logic { Bool, bool_node } +import v2.std.logic { Bool } import v2.std.node { Arrow, Atom, @@ -122,7 +123,10 @@ import v2.std.node { Transform, TypeNode, Value, + Ambiguous, + Found, all_edges_positional, + arrow_body_target_lookup, canonicalize_node_strip_provenance, fold_node, loop_edge_contributes_to_iteration_fold, @@ -644,6 +648,51 @@ fn infer_product_child_evidence_edges( ) } +// ARROW INTRODUCTION: A BODIED ARROW'S BODY MUST INHABIT THE RETURN IT DECLARES. Asked only once every +// child of the Arrow is derived (the product row above), so the body's type is known. The body's +// value type must equal the declared return's denotation, structurally with provenance stripped +// (infer_type_equal_ignoring_provenance, the list rule's authority); otherwise the Arrow refuses, +// located at the body. An Arrow with no body edge, or whose return is not a binding the language +// join denotes, is not judged here -- and arrow elimination derives nothing from such a return. +fn infer_arrow_body_does_not_inhabit_declared_return_diagnostic(body: Node) -> Diagnostic { + Diagnostic { + reason: ^arrow_body_does_not_inhabit_declared_return, + at: node_locus(node: body), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +fn infer_arrow_body_inhabits_declared_return(node: Node, entries: List) -> Outcome { + match node.kind { + TypeNode { connective: Arrow } => + match arrow_body_target_lookup(children: node.children) { + Ambiguous => outcome_rejected(infer_canonical_grounding_incoherent_diagnostic(node: node)) + Found { target: body } => + match infer_arrow_declared_return_type(arrow: node) { + Absent => outcome_accepted(value: true) + Present { value: declared } => + match lookup_inferred_facts_in_entries(entries: entries, key: body) { + Absent => outcome_rejected(infer_facts_lookup_miss_diagnostic(key: body)) + Present { value: body_facts } => + match inferred_facts_resolved_type(facts: body_facts) { + Violates { diagnostic: d } => outcome_rejected(d) + Holds { value: body_type } => + if infer_type_equal_ignoring_provenance(a: declared, b: body_type) { + outcome_accepted(value: true) + } else { + outcome_rejected( + infer_arrow_body_does_not_inhabit_declared_return_diagnostic(body: body) + ) + } + } + } + } + _ => outcome_accepted(value: true) + } + _ => outcome_accepted(value: true) + } +} + fn infer_product_facts_from_entries( node: Node, entries: List, @@ -666,17 +715,22 @@ fn infer_product_facts_from_entries( ) ) Present { value: evidence_edges } => - bind_outcome_accepted( - od: cd, - inner: inferred_facts_from_derived_type( - node: node, - derived_type: Node { - kind: node.kind, - children: evidence_edges, - occurrence_id: OccurrenceSynthetic - }, - descent: Holds { value: descent_proof } - ) + bind_outcome( + o: infer_arrow_body_inhabits_declared_return(node: node, entries: entries), + f: fn(_checked) { + bind_outcome_accepted( + od: cd, + inner: inferred_facts_from_derived_type( + node: node, + derived_type: Node { + kind: node.kind, + children: evidence_edges, + occurrence_id: OccurrenceSynthetic + }, + descent: Holds { value: descent_proof } + ) + ) + } ) } } @@ -692,18 +746,17 @@ fn infer_node_facts(n: Node, partials: List, tree: Node) -> Outcome match dag_canonical_literal_from_node(node: n) { - Accepted { value: DagCanonicalBoolLiteral { value: _ }, diagnostics: _ } => - inferred_facts_from_derived_type( - node: n, - derived_type: bool_node(), - descent: Holds { value: descent_proof } - ) - Accepted { value: DagCanonicalIntLiteral { magnitude: _ }, diagnostics: _ } => - inferred_facts_from_derived_type( - node: n, - derived_type: infer_branch_int_binding_type_node(), - descent: Holds { value: descent_proof } - ) + Accepted { value: literal, diagnostics: _ } => + match infer_literal_value_type(literal: literal) { + Present { value: literal_type } => + inferred_facts_from_derived_type( + node: n, + derived_type: literal_type, + descent: Holds { value: descent_proof } + ) + Absent => + inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) + } Rejected { diagnostics: _ } => match infer_node_declared_in_language_inhabitants(n: n) { Present { value: member_kind } => @@ -716,10 +769,10 @@ fn infer_node_facts(n: Node, partials: List, tree: Node) -> Outcome match dag_binding_denotation(sym: binding) { - Present { value: denotation } => + Present { value: _ } => inferred_facts_from_derived_type( node: n, - derived_type: denotation, + derived_type: kind_node(kind: TypeDenotationKind), descent: Holds { value: descent_proof } ) Absent => @@ -871,11 +924,42 @@ fn admitted_inferred_facts_entry(node: Node, facts: InferredFacts) -> Outcome Node { - Node { - kind: TypeNode { connective: Atom { identity: ^dag_binding_type_int } }, - children: [], - occurrence_id: OccurrenceSynthetic +// A VALUE'S TYPE COMES FROM THE LANGUAGE'S ONE BINDING JOIN, NEVER A NODE THIS FOLD MINTS. A literal +// is typed by what its type binding denotes (v2.extdeps.languages.dag dag_binding_denotation, whose +// rows point at v2.std.integer and v2.std.logic), and so is an application's result (its callee's +// declared return). A type-expression atom that names such a binding derives its KIND, not the type +// it denotes: the type of the expression `Int` is a type, and taking the denotation would type the +// atom as a node structurally equal to itself (docs/plans/arrow-elimination-model.md). +fn infer_literal_type_binding(literal: DagCanonicalLiteral) -> Symbol { + match literal { + DagCanonicalBoolLiteral { value: _ } => ^dag_binding_type_bool + DagCanonicalIntLiteral { magnitude: _ } => ^dag_binding_type_int + } +} + +fn infer_literal_value_type(literal: DagCanonicalLiteral) -> Optional { + dag_binding_denotation(sym: infer_literal_type_binding(literal: literal)) +} + +fn infer_binding_value_type_witness(binding: Symbol, at: Node) -> Witness { + match dag_binding_denotation(sym: binding) { + Present { value: t } => Holds { value: t } + Absent => Violates { diagnostic: infer_grounding_not_derived_diagnostic(at: node_locus(node: at)) } + } +} + +// The value type an Arrow DECLARES it returns: the denotation of its return atom as written +// (positional child 1). Read from the declaration, never from the Arrow's composed evidence, which +// also carries the body. Absent when the return is not a binding this language's join denotes, and +// then nothing is derived from it. +fn infer_arrow_declared_return_type(arrow: Node) -> Optional { + match list_at_optional(xs: node_positional_child_targets(node: arrow), index: 1) { + Absent => Absent + Present { value: ret } => + match infer_atom_binding_sym(node: ret) { + Absent => Absent + Present { value: binding } => dag_binding_denotation(sym: binding) + } } } @@ -955,10 +1039,8 @@ fn infer_branch_operand_resolved_type_in_tree( tree: Node, ) -> Witness { match dag_canonical_literal_from_node(node: node) { - Accepted { value: DagCanonicalBoolLiteral { value: _ }, diagnostics: _ } => - Holds { value: bool_node() } - Accepted { value: DagCanonicalIntLiteral { magnitude: _ }, diagnostics: _ } => - Holds { value: infer_branch_int_binding_type_node() } + Accepted { value: literal, diagnostics: _ } => + infer_binding_value_type_witness(binding: infer_literal_type_binding(literal: literal), at: node) Rejected { diagnostics: _ } => match infer_atom_binding_sym(node: node) { Present { value: binding } => @@ -1055,7 +1137,7 @@ fn infer_match_bool_arm_body_type( match dag_canonical_literal_from_node(node: body_target) { Accepted { value: DagCanonicalBoolLiteral { value: _ }, diagnostics: _ } => if infer_branch_type_is_int(type_node: scrutinee_type) { - Holds { value: infer_branch_int_binding_type_node() } + infer_binding_value_type_witness(binding: ^dag_binding_type_int, at: body_target) } else { infer_branch_operand_resolved_type(node: body_target, facts: body_facts) } @@ -2333,7 +2415,13 @@ fn infer_gather_application_row_on_entries( ) Accepted { value: _, diagnostics: id } => let merged_pending = diagnostics_merge(outer: pending, inner: id) - match infer_transform_derived_optional(node: node, partials: partials, entries: entries, tree: tree) { + match infer_transform_derived_optional( + node: node, + partials: partials, + entries: entries, + tree: tree, + arguments_decided: infer_diagnostics_none(d: id) + ) { Present { value: derived } => match derived { Rejected { diagnostics: r } => @@ -2388,16 +2476,67 @@ fn infer_gather_application_row_on_entries( // THE TRANSFORMS WHOSE TYPE IS DERIVED, by shape: the binary Int add, and a cast whose operand's // grounding is derived. Absent is the counted frontier (GroundingNotDerived), never an admission. +fn infer_diagnostics_none(d: Diagnostics) -> Bool { + match d { + None => true + Some { diagnostics: _ } => false + } +} + +// arguments_decided: argument inhabitance returned no counted-undecidable obligation. Arrow +// elimination derives only then -- an application whose argument obligation is still undecided +// stays on the frontier rather than typing past it. fn infer_transform_derived_optional( node: Node, partials: List, entries: List, tree: Node, + arguments_decided: Bool, ) -> Optional> { if infer_transform_is_binary_infix_int_add_shape(node: node) { optional_present(value: infer_transform_binary_infix(node: node, partials: partials, entries: entries, tree: tree)) - } else { + } else if infer_transform_is_cast(node: node) { infer_transform_cast_optional(node: node, entries: entries) + } else if arguments_decided { + infer_transform_application_optional(node: node, entries: entries) + } else { + optional_absent() + } +} + +// ARROW ELIMINATION. `f(a)`, where the callee is an Arrow, has the type the Arrow DECLARES it +// returns (infer_arrow_declared_return_type: the return atom as written, never the composed +// evidence). The caller has already admitted argument inhabitance, and the callee must itself be +// derived -- so arrow introduction has checked its body against that return. An underived callee, +// a non-Arrow operator, or a return the language join does not denote stays on the counted +// frontier: Absent here, never an admission. +fn infer_transform_application_optional(node: Node, entries: List) -> Optional> { + match infer_application_callee_arrow(node: node) { + Absent => optional_absent() + Present { value: arrow } => + match lookup_inferred_facts_in_entries(entries: entries, key: arrow) { + Absent => optional_absent() + Present { value: arrow_facts } => + if !inferred_facts_grounding_derived(facts: arrow_facts) { + optional_absent() + } else { + match infer_arrow_declared_return_type(arrow: arrow) { + Absent => optional_absent() + Present { value: return_type } => + optional_present( + value: match infer_descent_witness_for_node(n: node) { + Violates { diagnostic: d } => Rejected { diagnostics: diagnostics_singleton(d: d) } + Holds { value: descent_proof } => + inferred_facts_from_derived_type( + node: node, + derived_type: return_type, + descent: Holds { value: descent_proof } + ) + } + ) + } + } + } } } diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index b42cb3a18da..be02e082957 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -157,7 +157,7 @@ import v2.std.compilers.target_model { BlockEvaluationMode, ValueProducing, derive_bodied_arrow_scaffold} -import v2.std.logic { Bool } +import v2.std.logic { Bool, bool_node } import std.algebra { Cons, Empty } import v2.std.algebra { any, fold_list, for_all, is_empty, length, list_map } import v2.std.diagnostic { @@ -181,6 +181,7 @@ import v2.std.integer { decimal_digits_to_magnitude, integer_decimal_magnitude_node, integer_decimal_magnitude_node_to_int, + integer_int_type_node, integer_string_to_decimal_digits_optional, DecimalMagnitude, DecimalNonZero, @@ -3682,7 +3683,11 @@ fn dag_int_type_binding() -> Symbol { // THE BINDING→DENOTATION JOIN, declared once at the language authority. The resolver binds the // surface spelling "Int" to the canonical binding symbol (dag_binding_spellings); what that -// binding DENOTES is the Int inhabitant declared at dag_declared_inhabitants_core. Every +// binding DENOTES is the value type its std authority owns: v2.std.integer integer_int_type_node +// for Int, v2.std.logic bool_node for Bool. A row points at the authority and never mints a node +// of its own. Until 2026-09-26 the Int row returned dag_int_inhabitant_node -- the roster's +// RECORD about Int, not the Int type -- while every value-type consumer used the atom; that fork +// is what this join now closes (docs/plans/arrow-elimination-model.md). Every // hand-rolled fixture facts lookup re-authors this join (dag_add_canonical_grounding_for, // record_construct_canonical_grounding_for); the authority declares it once and infer consumes it // (v2.compiler.infer infer_node_facts). Specimen-scope interim in the same frame as @@ -3691,7 +3696,9 @@ fn dag_int_type_binding() -> Symbol { // completed state). fn dag_binding_denotation(sym: Symbol) -> Optional { if sym == dag_int_type_binding() { - optional_present(value: dag_int_inhabitant_node()) + optional_present(value: integer_int_type_node()) + } else if sym == ^dag_binding_type_bool { + optional_present(value: bool_node()) } else { optional_absent() } diff --git a/src/v2/extdeps/runtimes/v2_evaluator.dag b/src/v2/extdeps/runtimes/v2_evaluator.dag index 2e7e15ec728..000e02a5d99 100644 --- a/src/v2/extdeps/runtimes/v2_evaluator.dag +++ b/src/v2/extdeps/runtimes/v2_evaluator.dag @@ -25,6 +25,7 @@ import v2.std.integer { int_add, integer_byte_bit_at, integer_int_to_signed_i32_le_bytes, + integer_int_type_node, integer_signed_i32_le_bytes_to_int } import v2.std.diagnostic { @@ -40,7 +41,7 @@ import v2.std.diagnostic { Unavailable, None } -import v2.std.logic { Bool } +import v2.std.logic { Bool, bool_node } import v2.std.machine { Byte } import v2.std.model_core { EffectSignature, @@ -118,6 +119,10 @@ fn v2_eval_unit_type_node() -> Node { } } +// A Bool runtime value's TYPE is v2.std.logic bool_node, the node infer derives for a Bool literal and +// for a Bool-returning application; eval's resolved-type acceptance compares the two, so a runtime-only +// Bool type would refuse every derived Bool result (docs/plans/arrow-elimination-model.md). This pin +// is no longer that type: it is only an evaluator literal-shaped node that represents `true`. data v2_eval_bool_literal_pin: Node = Node { kind: TypeNode { connective: Atom { identity: ^v2_eval_bool_literal_symbol } }, children: [], @@ -133,7 +138,7 @@ fn v2_eval_bool_true_byte() -> Byte { fn v2_eval_bool_true_primitive() -> RuntimeValue { RuntimePrimitive { value: RuntimePrimitiveValue { - primitive_type: v2_eval_bool_literal_pin, + primitive_type: bool_node(), bytes: [v2_eval_bool_true_byte()] } } @@ -188,18 +193,10 @@ fn v2_eval_empty_frame() -> EvaluationFrame { } } -fn v2_eval_int_type_node() -> Node { - Node { - kind: TypeNode { connective: Atom { identity: ^dag_binding_type_int } }, - children: [], - occurrence_id: OccurrenceSynthetic - } -} - fn v2_eval_int_runtime_value(value: Int) -> RuntimeValue { RuntimePrimitive { value: RuntimePrimitiveValue { - primitive_type: v2_eval_int_type_node(), + primitive_type: integer_int_type_node(), bytes: integer_int_to_signed_i32_le_bytes(value: value) } } @@ -208,7 +205,7 @@ fn v2_eval_int_runtime_value(value: Int) -> RuntimeValue { fn v2_eval_bool_runtime_value(is_true: Bool) -> RuntimeValue { RuntimePrimitive { value: RuntimePrimitiveValue { - primitive_type: v2_eval_bool_literal_pin, + primitive_type: bool_node(), bytes: if is_true { [Byte { bits: [true, false, false, false, false, false, false, false] }] } else { @@ -277,12 +274,12 @@ fn v2_eval_allocate_literal(node: Node, env: EvaluationEnvironment) -> Outcome Outcome { match value { RuntimePrimitive { value: p } => - if p.primitive_type == v2_eval_int_type_node() { + if p.primitive_type == integer_int_type_node() { integer_signed_i32_le_bytes_to_int(bytes: p.bytes) } else { outcome_rejected(d: v2_eval_runtime_diagnostic(node: p.primitive_type)) } - _ => outcome_rejected(d: v2_eval_runtime_diagnostic(node: v2_eval_int_type_node())) + _ => outcome_rejected(d: v2_eval_runtime_diagnostic(node: integer_int_type_node())) } } @@ -296,7 +293,7 @@ fn v2_eval_runtime_value_as_int(value: RuntimeValue) -> Outcome { fn v2_eval_runtime_bool_is_true(value: RuntimeValue) -> Bool { match value { RuntimePrimitive { value: p } => - if p.primitive_type == v2_eval_bool_literal_pin { + if p.primitive_type == bool_node() { match list_at_optional(xs: p.bytes, index: 0) { Present { value: b } => integer_byte_bit_at(b: b, shift: 0) == 1 Absent => false @@ -311,7 +308,7 @@ fn v2_eval_runtime_bool_is_true(value: RuntimeValue) -> Bool { fn v2_eval_runtime_bool_is_false(value: RuntimeValue) -> Bool { match value { RuntimePrimitive { value: p } => - p.primitive_type == v2_eval_bool_literal_pin && !v2_eval_runtime_bool_is_true(value: value) + p.primitive_type == bool_node() && !v2_eval_runtime_bool_is_true(value: value) _ => false } } diff --git a/src/v2/std/integer.dag b/src/v2/std/integer.dag index eb3035195f3..f33c3908df3 100644 --- a/src/v2/std/integer.dag +++ b/src/v2/std/integer.dag @@ -833,6 +833,16 @@ fn integer_range_endpoint_node(endpoint: IntegerRangeEndpoint) -> Node { } } +// THE INT VALUE TYPE, AS A NODE. This module owns what `Int` denotes, exactly as v2.std.logic +// bool_node owns what `Bool` denotes: a language's binding join (v2.extdeps.languages.dag +// dag_binding_denotation) points here, and every consumer of an Int value's type -- infer's +// literal, add and branch rules, the evaluator's primitive type -- reads this one node. Its atom +// identity keeps the spelling `dag_binding_type_int` it carried before the authority moved here; +// that is the identity every consumer compares against, so renaming it is a separate migration. +fn integer_int_type_node() -> Node { + integer_inhabitant_atom(id: ^dag_binding_type_int) +} + // THE TYPES OF A DECIMAL MAGNITUDE'S NODE ENCODING, AS NODES. integer_decimal_magnitude_node encodes // a magnitude as fold_list_node over integer_decimal_digit_node: each digit atom is a DecimalDigit and // each cons cell (and the empty tail) is a FreeMonoid. v2.compiler.infer diff --git a/src/v2/test/claim/compiler/infer_arrow_elimination_witness_test.dag b/src/v2/test/claim/compiler/infer_arrow_elimination_witness_test.dag new file mode 100644 index 00000000000..ba37bed57dd --- /dev/null +++ b/src/v2/test/claim/compiler/infer_arrow_elimination_witness_test.dag @@ -0,0 +1,145 @@ +module v2.test.claim.compiler.infer_arrow_elimination_witness_test + +// ARROW INTRODUCTION AND ELIMINATION IN v2 INFER (docs/plans/arrow-elimination-model.md). An +// application `f(1)` of a bodied Arrow takes the type the Arrow DECLARES it returns, and the Arrow is +// admitted only when its body inhabits that return. Both directions are measured through the real +// infer, and the Bool control through the real evaluator: before this rule no application derived, +// Int included, and eval refused both with eval_rejected_grounding_not_derived. + +import v2.compiler.infer { InferredTree, infer, inferred_facts_grounding_derived, inferred_facts_resolved_type } +import v2.compiler.eval { eval, inputs_root_only } +import v2.extdeps.runtimes.v2_evaluator { v2_evaluator_interpretation } +import v2.extdeps.languages.dag { dag_arrow_with_body_node, dag_int_literal_fixture_one, dag_type_atom_node } +import v2.std.diagnostic { Accepted, Outcome, Rejected, Some, diagnostics_has_reason } +import v2.std.integer { integer_int_type_node } +import v2.std.logic { Bool, bool_node } +import v2.std.optional { Absent, Present } +import v2.std.runtime { RuntimeValue } +import v2.std.witness { Holds, Violates } +import v2.std.node { ComputationNode, Conj, Edge, Named, Node, Positional, Symbol, Transform, TypeNode, node_synthetic } + +fn ae_domain() -> Node { + node_synthetic( + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Named { name: ^ae_param_x }, + target: dag_type_atom_node(identity: ^dag_binding_type_int) + } + ] + ) +} + +fn ae_arrow(returns: Symbol, body: Node) -> Node { + dag_arrow_with_body_node(domain: ae_domain(), return_type_binding: returns, body: body) +} + +fn ae_apply(returns: Symbol, body: Node) -> Node { + node_synthetic( + kind: ComputationNode { behavior: Transform }, + children: [ + Edge { label: Positional, target: ae_arrow(returns: returns, body: body) }, + Edge { label: Positional, target: dag_int_literal_fixture_one() } + ] + ) +} + +fn ae_true() -> Node { + dag_type_atom_node(identity: ^dag_token_kw_true) +} + +fn ae_false() -> Node { + dag_type_atom_node(identity: ^dag_token_kw_false) +} + +// The application's derived type, compared to the expected authority node. False when infer refuses, +// the node carries no facts, or its grounding is not derived. +fn ae_application_derives(tree: Node, expected: Node) -> Bool { + match infer(tree: tree) { + Rejected { diagnostics: _ } => false + Accepted { value: inferred, diagnostics: _ } => + match inferred.facts.lookup(tree) { + Absent => false + Present { value: facts } => + inferred_facts_grounding_derived(facts: facts) + && (match inferred_facts_resolved_type(facts: facts) { + Holds { value: t } => t == expected + Violates { diagnostic: _ } => false + }) + } + } +} + +fn ae_refuses_body_return(tree: Node) -> Bool { + match infer(tree: tree) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: nds } => + diagnostics_has_reason( + d: Some { diagnostics: nds }, + reason: ^arrow_body_does_not_inhabit_declared_return + ) + } +} + +fn ae_eval(tree: Node) -> Outcome { + match infer(tree: tree) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: inferred, diagnostics: _ } => + eval(tree: inferred, interpretation: v2_evaluator_interpretation(), inputs: inputs_root_only(root: tree)) + } +} + +fn ae_accepted(o: Outcome) -> Bool { + match o { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +test fn infer_bool_application_derives_bool_holds() -> Bool { + ae_application_derives(tree: ae_apply(returns: ^dag_binding_type_bool, body: ae_true()), expected: bool_node()) +} + +// The same rule, not a Bool case: the Int application derives the std.integer Int node. +test fn infer_int_application_derives_int_holds() -> Bool { + ae_application_derives( + tree: ae_apply(returns: ^dag_binding_type_int, body: dag_int_literal_fixture_one()), + expected: integer_int_type_node() + ) +} + +// The evaluator's true value: f(1) evaluates to what the literal `true` does, and not to what +// `false` does -- the result is the body's, not a constant the control could match by accident. +test fn eval_bool_application_is_the_evaluators_true_holds() -> Bool { + let applied = ae_eval(tree: ae_apply(returns: ^dag_binding_type_bool, body: ae_true())) + ae_accepted(o: applied) + && (applied == ae_eval(tree: ae_true())) + && (applied != ae_eval(tree: ae_false())) +} + +test fn infer_int_body_at_declared_bool_refuses_holds() -> Bool { + ae_refuses_body_return(tree: ae_apply(returns: ^dag_binding_type_bool, body: dag_int_literal_fixture_one())) +} + +test fn infer_bool_body_at_declared_int_refuses_holds() -> Bool { + ae_refuses_body_return(tree: ae_apply(returns: ^dag_binding_type_int, body: ae_true())) +} + +// The refusal belongs to arrow INTRODUCTION: the bare Arrow refuses with no application around it. +test fn infer_bare_arrow_with_mismatched_body_refuses_holds() -> Bool { + ae_refuses_body_return(tree: ae_arrow(returns: ^dag_binding_type_bool, body: dag_int_literal_fixture_one())) +} + +// Frontier accounting stays counted: a return the language join does not denote derives nothing, +// and the application is admitted on the frontier, never typed. +test fn infer_undenoted_return_leaves_application_on_frontier_holds() -> Bool { + let tree = ae_apply(returns: ^ae_undenoted_type, body: ae_true()) + match infer(tree: tree) { + Rejected { diagnostics: _ } => false + Accepted { value: inferred, diagnostics: _ } => + match inferred.facts.lookup(tree) { + Absent => false + Present { value: facts } => !inferred_facts_grounding_derived(facts: facts) + } + } +} diff --git a/src/v2/test/claim/execution/dag_binding_denotation_test.dag b/src/v2/test/claim/execution/dag_binding_denotation_test.dag index b242c4998e0..4d8f1808972 100644 --- a/src/v2/test/claim/execution/dag_binding_denotation_test.dag +++ b/src/v2/test/claim/execution/dag_binding_denotation_test.dag @@ -8,16 +8,16 @@ import v2.compiler.infer { import v2.compiler.self_host.direct_rust_door_fixture { direct_rust_door_specimen_inferred } +import std.kind { TypeDenotationKind, kind_node } import v2.extdeps.languages.dag { dag_binding_denotation, - dag_int_inhabitant_node, dag_int_type_binding } import v2.std.algebra { fold_list } import v2.std.diagnostic { Accepted, Rejected } -import v2.std.integer { Int } +import v2.std.integer { Int, integer_int_type_node } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import v2.std.logic { Bool } +import v2.std.logic { Bool, bool_node } import v2.std.node { Atom, Node, TypeNode, node_subtree_nodes } import v2.std.optional { Absent, Present } @@ -28,8 +28,12 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // is the direct-rust-door fixture's ingest -- the corpus's smallest real-ingest dag module whose // resolved tree carries canonical binding atoms (`dag_binding_type_int`, the resolver's binding // of the surface spelling "Int"). The positive control pins both the derivation and its evidence: -// every canonical Int binding atom carries DerivedGrounding whose structural evidence is the dag -// authority's Int inhabitant, and the census count (4) pins that the witness actually saw them. +// every canonical Int binding atom carries DerivedGrounding whose structural evidence is its KIND +// (std.kind TypeDenotationKind: the expression `Int` is a type), and the census count (4) pins that +// the witness actually saw them. What the binding DENOTES is a separate fact, pinned by +// dag_binding_denotation_rows_point_at_std_authorities_holds: each row is the std authority's node +// (v2.std.integer, v2.std.logic), never a node the language row mints and never the roster's Int +// record (docs/plans/arrow-elimination-model.md). // The boundary control pins the rule's edge at the authority itself: dag_binding_denotation // declares no denotation for the operand bindings `x`/`y` -- the table is a lookup, not an // invention. (The operand atoms derive today, but by a different rule -- the binding-reference @@ -65,7 +69,7 @@ fn int_binding_atom_derived(inferred: InferredTree, n: Node) -> Bool { match facts.grounding { GroundingNotDerived { node: _ } => false DerivedGrounding { grounding: g } => - g.witness.structural.evidence == dag_int_inhabitant_node() + g.witness.structural.evidence == kind_node(kind: TypeDenotationKind) } } } @@ -139,3 +143,8 @@ test fn dag_binding_denotation_declares_no_denotation_for_operand_bindings_holds } } } + +test fn dag_binding_denotation_rows_point_at_std_authorities_holds() -> Bool { + (dag_binding_denotation(sym: dag_int_type_binding()) == Present { value: integer_int_type_node() }) + && (dag_binding_denotation(sym: ^dag_binding_type_bool) == Present { value: bool_node() }) +} diff --git a/src/v2/test/claim/infer_list_introduction_test.dag b/src/v2/test/claim/infer_list_introduction_test.dag index 6b50340131c..71122e55f97 100644 --- a/src/v2/test/claim/infer_list_introduction_test.dag +++ b/src/v2/test/claim/infer_list_introduction_test.dag @@ -1,7 +1,8 @@ module v2.test.claim.infer_list_introduction import std.occurrence_identity { OccurrenceSynthetic } -import v2.compiler.infer { infer, infer_branch_int_binding_type_node, inferred_facts_resolved_type } +import v2.compiler.infer { infer, inferred_facts_resolved_type } +import v2.std.integer { integer_int_type_node } import v2.extdeps.languages.dag { dag_int_literal_fixture_one, dag_type_atom_node } import v2.std.diagnostic { Accepted, Diagnostic, NodeLocus, Rejected, diagnostics_fatal } import v2.std.grammar { node_atom_identity_optional } @@ -141,18 +142,18 @@ fn ilt_freemonoid_of(element: Node) -> Node { // THE ARGUMENT IS ASSERTED, NOT ONLY THE HEAD: FreeMonoid or FreeMonoid reds it, // and the Bool control below is the wrong-type twin that must NOT satisfy the Int claim. test fn a_list_of_ints_infers_to_freemonoid_of_int() -> Bool { - ilt_is_freemonoid_of(tree: ilt_ints, expected: infer_branch_int_binding_type_node()) + ilt_is_freemonoid_of(tree: ilt_ints, expected: integer_int_type_node()) } test fn a_list_of_bools_infers_to_freemonoid_of_bool_and_not_of_int() -> Bool { ilt_is_freemonoid_of(tree: ilt_bools, expected: bool_node()) - && !ilt_is_freemonoid_of(tree: ilt_bools, expected: infer_branch_int_binding_type_node()) + && !ilt_is_freemonoid_of(tree: ilt_bools, expected: integer_int_type_node()) } // STRUCTURAL ELEMENT TYPES: two FreeMonoid elements minted at different occurrences are one T, // so the list of lists is FreeMonoid>; an atom-only comparison would refuse it. test fn a_list_of_equal_lists_infers_to_a_nested_freemonoid() -> Bool { - ilt_is_freemonoid_of(tree: ilt_nested_ints, expected: ilt_freemonoid_of(element: infer_branch_int_binding_type_node())) + ilt_is_freemonoid_of(tree: ilt_nested_ints, expected: ilt_freemonoid_of(element: integer_int_type_node())) } // A STRUCTURAL MISMATCH refuses located AT the differing element: the second inner list (a From f06dce7222503c6a434353490b817fd6f94221bc Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 27 Sep 2026 07:24:53 +0000 Subject: [PATCH 03/90] infer_product_introduction: evidence control pins the Int type atom's kind, not the retired inhabitant denotation The parameter conj's composed evidence carries each Int type atom's derived grounding. After the arrow-elimination ruling that grounding is the atom's kind (TypeDenotationKind); the control still asserted the roster's Int inhabitant record, the denotation this PR retired. Found by the srv1 base-vs-head run over the v2 infer/eval/compile test modules (the one true->false). The partial-evidence control's use of the inhabitant node as a roster member is unrelated and unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../infer_product_introduction_test.dag | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/v2/test/claim/execution/infer_product_introduction_test.dag b/src/v2/test/claim/execution/infer_product_introduction_test.dag index b5a9d98e6a8..c408524319c 100644 --- a/src/v2/test/claim/execution/infer_product_introduction_test.dag +++ b/src/v2/test/claim/execution/infer_product_introduction_test.dag @@ -10,6 +10,7 @@ import v2.compiler.infer { import v2.compiler.self_host.direct_rust_door_fixture { direct_rust_door_specimen_inferred } +import std.kind { TypeDenotationKind, kind_node } import v2.extdeps.languages.dag { dag_int_inhabitant_node } @@ -40,8 +41,11 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // real-ingest dag module. The census pins that the witness actually saw the population: four // Conj nodes, all derived (the grammar-projection root included -- it cascaded once the atom // rules grounded its children), and one bodied Arrow, derived. The evidence control pins the -// composed evidence's SHAPE on the parameter conj: a Conj whose named children target the dag -// authority's Int inhabitant, never the source node (the self-evidence wall). The two boundary +// composed evidence's SHAPE on the parameter conj: a Conj whose named children target each `Int` +// type atom's derived grounding -- its KIND, std.kind TypeDenotationKind -- never the source node +// (the self-evidence wall). Until 2026-09-26 that child grounding was the dag roster's Int +// inhabitant record; the arrow-elimination ruling made the type atom derive its kind and moved +// what `Int` DENOTES to v2.std.integer (docs/plans/arrow-elimination-model.md). The two boundary // controls pin the refusal edge on hand-built trees: a conj with a frontier child stays frontier, // and a childless conj -- no evidence to compose -- stays frontier. Deleting the rule reddens the // census and evidence controls; widening it to partially-evidenced or childless products reddens @@ -132,7 +136,7 @@ fn is_params_conj(n: Node) -> Bool { } } -fn evidence_children_target_int_inhabitant(children: List) -> Bool { +fn evidence_children_target_type_kind(children: List) -> Bool { match list_at_optional(xs: children, index: 0) { Absent => false Present { value: first } => @@ -141,15 +145,15 @@ fn evidence_children_target_int_inhabitant(children: List) -> Bool { Present { value: second } => match list_at_optional(xs: children, index: 2) { Absent => - (first.target == dag_int_inhabitant_node()) - && (second.target == dag_int_inhabitant_node()) + (first.target == kind_node(kind: TypeDenotationKind)) + && (second.target == kind_node(kind: TypeDenotationKind)) Present { value: _ } => false } } } } -fn params_conj_evidence_is_composed_over_int_inhabitant(inferred: InferredTree, n: Node) -> Bool { +fn params_conj_evidence_is_composed_over_type_kind(inferred: InferredTree, n: Node) -> Bool { match inferred.facts.lookup(n) { Absent => false Present { value: facts } => @@ -159,7 +163,7 @@ fn params_conj_evidence_is_composed_over_int_inhabitant(inferred: InferredTree, let evidence = g.witness.structural.evidence match evidence.kind { TypeNode { connective: Conj } => - evidence_children_target_int_inhabitant(children: evidence.children) + evidence_children_target_type_kind(children: evidence.children) _ => false } } @@ -172,7 +176,7 @@ fn params_conj_evidence_seen(inferred: InferredTree) -> Bool { empty: false, cons: fn(acc, n) { if is_params_conj(n: n) { - params_conj_evidence_is_composed_over_int_inhabitant(inferred: inferred, n: n) + params_conj_evidence_is_composed_over_type_kind(inferred: inferred, n: n) } else { acc } From ac1df965f494bd3bf5225904d97e5ae5346d64d0 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 27 Sep 2026 15:44:40 +0000 Subject: [PATCH 04/90] Take infer's ObligatedInferredTree through discharge; flip refinement_discharge to holds/violated; one runtime encoding of true - infer_arrow_elimination eval control: after the merge of main (#12375) infer returns ObligatedInferredTree, so the control reaches eval through discharge_refinement_obligations, the only route to an InferredTree. - refinement_discharge's frontier row flips as it said it would: a true predicate admits, a false one refuses refinement_predicate_violated. The undischargeable arm is kept over a genuinely unevaluable application (an undenoted return). - The flip exposed two runtime encodings of true: v2_eval_bool_true_primitive was a one-bit byte while every evaluated Bool is built by v2_eval_bool_runtime_value (eight bits), so discharge read an evaluated true as violated. The primitive is now that constructor's value. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/extdeps/runtimes/v2_evaluator.dag | 15 ++-- .../infer_arrow_elimination_witness_test.dag | 11 ++- .../test/claim/refinement_discharge_test.dag | 72 +++++++++++++++---- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/v2/extdeps/runtimes/v2_evaluator.dag b/src/v2/extdeps/runtimes/v2_evaluator.dag index 000e02a5d99..d4b071f4efd 100644 --- a/src/v2/extdeps/runtimes/v2_evaluator.dag +++ b/src/v2/extdeps/runtimes/v2_evaluator.dag @@ -131,17 +131,12 @@ data v2_eval_bool_literal_pin: Node = Node { data v2_eval_unit_literal_pin: Node = v2_eval_unit_type_node() -fn v2_eval_bool_true_byte() -> Byte { - Byte { bits: [true] } -} - +// ONE ENCODING OF `true`. This used to be its own one-bit byte while v2_eval_bool_runtime_value, the +// constructor every evaluated Bool takes, wrote eight bits -- two runtime values for one truth, so +// v2.compiler.refinement_discharge, which compares against this, read an evaluated `true` as a +// violated predicate. It is now that constructor's value, not a second encoding of it. fn v2_eval_bool_true_primitive() -> RuntimeValue { - RuntimePrimitive { - value: RuntimePrimitiveValue { - primitive_type: bool_node(), - bytes: [v2_eval_bool_true_byte()] - } - } + v2_eval_bool_runtime_value(is_true: true) } fn v2_eval_unit_runtime_value() -> RuntimeValue { diff --git a/src/v2/test/claim/compiler/infer_arrow_elimination_witness_test.dag b/src/v2/test/claim/compiler/infer_arrow_elimination_witness_test.dag index ba37bed57dd..d97889c2858 100644 --- a/src/v2/test/claim/compiler/infer_arrow_elimination_witness_test.dag +++ b/src/v2/test/claim/compiler/infer_arrow_elimination_witness_test.dag @@ -6,8 +6,9 @@ module v2.test.claim.compiler.infer_arrow_elimination_witness_test // infer, and the Bool control through the real evaluator: before this rule no application derived, // Int included, and eval refused both with eval_rejected_grounding_not_derived. -import v2.compiler.infer { InferredTree, infer, inferred_facts_grounding_derived, inferred_facts_resolved_type } +import v2.compiler.infer { infer, inferred_facts_grounding_derived, inferred_facts_resolved_type } import v2.compiler.eval { eval, inputs_root_only } +import v2.compiler.refinement_discharge { discharge_refinement_obligations } import v2.extdeps.runtimes.v2_evaluator { v2_evaluator_interpretation } import v2.extdeps.languages.dag { dag_arrow_with_body_node, dag_int_literal_fixture_one, dag_type_atom_node } import v2.std.diagnostic { Accepted, Outcome, Rejected, Some, diagnostics_has_reason } @@ -84,8 +85,12 @@ fn ae_refuses_body_return(tree: Node) -> Bool { fn ae_eval(tree: Node) -> Outcome { match infer(tree: tree) { Rejected { diagnostics: r } => Rejected { diagnostics: r } - Accepted { value: inferred, diagnostics: _ } => - eval(tree: inferred, interpretation: v2_evaluator_interpretation(), inputs: inputs_root_only(root: tree)) + Accepted { value: obligated, diagnostics: _ } => + match discharge_refinement_obligations(t: obligated) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: inferred, diagnostics: _ } => + eval(tree: inferred, interpretation: v2_evaluator_interpretation(), inputs: inputs_root_only(root: tree)) + } } } diff --git a/src/v2/test/claim/refinement_discharge_test.dag b/src/v2/test/claim/refinement_discharge_test.dag index fc89b849fc8..d8fd58cb4b1 100644 --- a/src/v2/test/claim/refinement_discharge_test.dag +++ b/src/v2/test/claim/refinement_discharge_test.dag @@ -96,21 +96,65 @@ fn rdt_admitted(o: Optional>) -> Bool { } } -// (1) AN APPLICATION THE ONE EVALUATOR CANNOT RUN IS UNDISCHARGEABLE -- never admitted, never -// reported as a violation. Here v2 infer leaves a Bool-returning application GroundingNotDerived, so -// eval refuses it (eval_rejected_grounding_not_derived) and discharge carries that refusal behind -// refinement_obligation_undischargeable, located at the site. The pair of bodies is the -// discriminator for "the value is not read": `true` and `false` must land on the SAME arm while the -// application is unevaluable, or discharge is answering from something other than the evaluator. -// -// FRONTIER, NOT A SKIP: the holds/violated pair -- a `true` predicate admits, a `false` one refuses -// refinement_predicate_violated -- is not assertable through the real evaluator until infer derives -// a Bool-returning application, so that a where-predicate application grounds and v2.compiler.eval -// evaluates it (node adhoc-3fbf72e5-2b4). When that lands, THIS ROW FLIPS: the `true` body admits, -// the `false` body refuses refinement_predicate_violated. +// (1) A HOLDING PREDICATE ADMITS AND A VIOLATED ONE REFUSES, THROUGH THE ONE EVALUATOR. v2 infer +// derives a Bool-returning application from its callee's declared return (arrow elimination, +// docs/plans/arrow-elimination-model.md), so v2.compiler.eval evaluates `pred(1)` and discharge maps +// its value to exactly one arm: a `true` body admits, a `false` body refuses +// refinement_predicate_violated at the site. The pair is the discriminator for "the value is read": +// the two bodies must land on DIFFERENT arms. +test fn rdt_a_holding_predicate_admits_holds() -> Bool { + rdt_admitted(o: rdt_discharged(body: rdt_true())) +} + +test fn rdt_a_violated_predicate_refuses_at_the_site_holds() -> Bool { + rdt_refused_at_site_with(o: rdt_discharged(body: rdt_false()), reason: ^refinement_predicate_violated) +} + +// (1b) AN APPLICATION THE ONE EVALUATOR CANNOT RUN IS UNDISCHARGEABLE -- never admitted, never +// reported as a violation. A predicate whose declared return is no type the language join denotes +// leaves its application GroundingNotDerived, so eval refuses it and discharge carries that refusal +// behind refinement_obligation_undischargeable. `true` and `false` bodies land on the SAME arm +// here: while the application is unevaluable, discharge must not answer from the body. +fn rdt_unevaluable_application(body: Node) -> Node { + node_synthetic( + kind: ComputationNode { behavior: Transform }, + children: [ + Edge { + label: Positional, + target: dag_arrow_with_body_node( + domain: node_synthetic( + kind: TypeNode { connective: Conj }, + children: [Edge { label: Named { name: ^rdt_param_x }, target: dag_type_atom_node(identity: ^dag_binding_type_int) }] + ), + return_type_binding: ^rdt_undenoted_return, + body: body + ) + }, + Edge { label: Positional, target: dag_int_literal_fixture_one() } + ] + ) +} + +fn rdt_discharged_unevaluable(body: Node) -> Optional> { + let app = rdt_unevaluable_application(body: body) + match infer(tree: app) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: t, diagnostics: _ } => + optional_present(value: discharge_refinement_obligations(t: ObligatedInferredTree { + root: t.root, + facts: t.facts, + obligations: [RefinementObligation { + site: app, + application: app, + target: dag_type_atom_node(identity: ^rdt_refinement) + }] + })) + } +} + test fn rdt_an_unevaluable_application_refuses_undischargeable_whatever_its_body() -> Bool { - rdt_refused_at_site_with(o: rdt_discharged(body: rdt_true()), reason: ^refinement_obligation_undischargeable) - && rdt_refused_at_site_with(o: rdt_discharged(body: rdt_false()), reason: ^refinement_obligation_undischargeable) + rdt_refused_at_site_with(o: rdt_discharged_unevaluable(body: rdt_true()), reason: ^refinement_obligation_undischargeable) + && rdt_refused_at_site_with(o: rdt_discharged_unevaluable(body: rdt_false()), reason: ^refinement_obligation_undischargeable) } // (2) ZERO OBLIGATIONS: the identity, and the cost path every module takes today. From 03342a3647e2bf7e9d1ff5f1e29c2b2b7be6275e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 27 Sep 2026 16:30:20 +0000 Subject: [PATCH 05/90] v2 resolve: ResolvedTree carries the SymbolIndex resolution consulted; cut every consumer root-first Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/00_compile.dag | 19 +++- src/v2/compiler/03_ingest.dag | 10 +- src/v2/compiler/03_name_resolve.dag | 25 +++-- src/v2/compiler/03_resolve.dag | 39 ++++++-- src/v2/compiler/04_infer.dag | 6 +- src/v2/compiler/ingested_fixture_arrows.dag | 7 +- src/v2/compiler/program_assembly.dag | 7 +- .../self_host/candidate_generation.dag | 3 +- .../candidate_generation_stage_verdicts.dag | 5 +- .../compiler/self_host/closure_emission.dag | 17 ++-- .../self_host/compiler_closure_emit.dag | 3 +- .../self_host/direct_rust_door_fixture.dag | 3 +- src/v2/compiler/source_authority.dag | 10 +- src/v2/compiler/staged_front_end.dag | 8 +- src/v2/test/claim/body_cast_node_test.dag | 47 ++++----- .../test/claim/body_let_annotation_test.dag | 20 ++-- .../arrow_body_form_witness_test.dag | 2 +- ...transform_binary_infix_witness_helpers.dag | 5 +- ...er_transform_binary_infix_witness_test.dag | 3 +- .../wave1_gate1_normalize_add_helpers.dag | 2 +- .../body_type_annotation_refusal_test.dag | 13 +-- .../compile_eval_thesis_proof_test.dag | 4 +- ...tion_argument_inhabitance_witness_test.dag | 25 ++--- .../data_decl_lowering_grounding_test.dag | 7 +- .../infer_atom_grounding_rules_test.dag | 5 +- .../infer_product_introduction_test.dag | 5 +- .../long/add_arrow_eval_by_execution_test.dag | 2 +- ...lassical_not_ingested_equals_eval_test.dag | 12 +-- ...ndidate_generation_stage_verdicts_test.dag | 3 +- .../self_host_candidate_generation_test.dag | 3 +- .../claim/infer_list_introduction_test.dag | 11 ++- .../claim/infer_self_grounding_wall_test.dag | 11 ++- ...bitant_neutralization_e2e_witness_test.dag | 5 +- .../parser_completeness_frontier_test.dag | 2 +- .../self_host_module_emit_derisk_test.dag | 4 +- .../test/claim/loop_infer_iteration_test.dag | 2 +- .../branch_infer_fail_open_audit_test.dag | 7 +- .../manual/branch_infer_if_then_else_test.dag | 5 +- .../test/claim/manual/branch_infer_test.dag | 2 +- ...language_add_python_to_typescript_test.dag | 5 +- ...unded_lattice_completeness_anchor_test.dag | 3 +- .../manual/infer_emit_compile_anchor.dag | 2 +- src/v2/test/claim/manual/infer_ground_add.dag | 7 +- .../test/claim/manual/ingest_bridge_test.dag | 5 +- .../manual/inhabitant_neutralization_test.dag | 9 +- .../match_infer_fail_open_audit_test.dag | 15 +-- .../one_member_cost_probe_test.dag | 4 +- .../test/claim/refinement_discharge_test.dag | 5 +- .../translate_underived_refusal_test.dag | 7 +- .../claim/type_param_binder_frame_test.dag | 96 ++++++++++--------- .../test/compiler/pipeline/stage_bridge.dag | 5 +- ...ecting_lens_blocks_before_compile_test.dag | 3 +- src/v2/test/lens_common/infer_fixture.dag | 10 ++ .../hollow_alias_nested_rejected_test.dag | 4 +- ...w_alias_vtc_empty_lenses_rejected_test.dag | 4 +- src/v2/workflow/dag_acceptance.dag | 5 +- src/v2/workflow/realization_attempt.dag | 10 +- 57 files changed, 334 insertions(+), 234 deletions(-) diff --git a/src/v2/compiler/00_compile.dag b/src/v2/compiler/00_compile.dag index 6137d4f2e13..fe68e4522f3 100644 --- a/src/v2/compiler/00_compile.dag +++ b/src/v2/compiler/00_compile.dag @@ -86,6 +86,7 @@ import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.parse { parse, prepare_grammar, PreparedGrammar } import v2.compiler.resolve { ObservationComplete, + ObservationIncomplete, ObservationCompleteness, ResolveNodeWalk, ResolveWalkAccepted, @@ -869,7 +870,7 @@ fn compile_inferred( } fn compile( - source: CoreNode, + source: ResolvedTree, mode: CompileMode ) -> Outcome { bind_outcome( @@ -928,7 +929,7 @@ fn validated_from_compile_output( // registration at this door, no wording here may claim it would be caught (DESIGN sections 4b(1), // 4b(2), 5). fn validate_then_compile( - source: CoreNode, + source: ResolvedTree, lenses: List, mode: CompileMode ) -> Outcome> { @@ -3120,6 +3121,9 @@ type NativeModuleResolveVerdict } | NativeModuleResolveAccepted { resolved: ResolvedTree } +// The walk ran under context.resolution's index (resolve_walk_with_admission_context_policy +// builds the subject namespace over it), so that index is minted beside the root. An +// accepted walk implies an accepted context; the Rejected arm states the refusal it would be. fn native_module_resolve_verdict( context: NativeTestContext, refusal_index: Map, @@ -3136,7 +3140,16 @@ fn native_module_resolve_verdict( ResolveWalkRefused { first: f, rest: r, observation: o } => NativeModuleResolveRefused { first: f, rest: r, observation: o } ResolveWalkAccepted { value: resolved, diagnostics: _ } => - NativeModuleResolveAccepted { resolved: resolved } + match context.resolution { + Accepted { value: shared, diagnostics: _ } => + NativeModuleResolveAccepted { resolved: ResolvedTree { root: resolved, symbol_index: shared.symbol_index } } + Rejected { diagnostics: r } => + NativeModuleResolveRefused { + first: r, + rest: [], + observation: ObservationIncomplete { reason: ^resolve_observation_context_refused } + } + } } } } diff --git a/src/v2/compiler/03_ingest.dag b/src/v2/compiler/03_ingest.dag index 1658e91eea2..2d25aeec2cd 100644 --- a/src/v2/compiler/03_ingest.dag +++ b/src/v2/compiler/03_ingest.dag @@ -1,5 +1,7 @@ module v2.compiler.ingest +import v2.compiler.resolve { ResolvedTree } +import v2.std.symbol_index { empty_symbol_index } import v2.compiler.refinement_discharge { infer_and_discharge } import std.algebra { Empty, list_append } import v2.std.collection { @@ -310,12 +312,15 @@ fn parse_tree_to_emitted_node(parse_tree: ParseTree, source_model: TargetModel) ) } +// The bridge infers the emitted node directly: it never passed through resolve, so it is +// supplied with an index that holds no declarations (a declaration lookup through it finds +// nothing and refuses, never fabricates one). fn parse_tree_to_target_model_bridge(parse_tree: ParseTree, source_model: TargetModel) -> Outcome { bind_outcome( o: parse_tree_to_emitted_node(parse_tree: parse_tree, source_model: source_model), f: fn(emitted) { bind_outcome( - o: infer_and_discharge(tree: emitted), + o: infer_and_discharge(tree: ResolvedTree { root: emitted, symbol_index: empty_symbol_index() }), f: fn(inferred) { bind_outcome( o: coerce_grounded_node(source: emitted, tree: inferred, target: source_model), @@ -327,6 +332,7 @@ fn parse_tree_to_target_model_bridge(parse_tree: ParseTree, source_model: Target ) } +// Not resolved either (neutralized from the bridge's core): no declarations indexed. fn cross_language_compile( parse_tree: ParseTree, source_model: TargetModel, @@ -339,7 +345,7 @@ fn cross_language_compile( o: neutralize_core_for_target(core: core, target: target_model), f: fn(neutralized) { bind_outcome( - o: infer_and_discharge(tree: neutralized), + o: infer_and_discharge(tree: ResolvedTree { root: neutralized, symbol_index: empty_symbol_index() }), f: fn(inferred) { emit(tree: inferred, target: target_model) } diff --git a/src/v2/compiler/03_name_resolve.dag b/src/v2/compiler/03_name_resolve.dag index 6dc41c50b8c..901457715a4 100644 --- a/src/v2/compiler/03_name_resolve.dag +++ b/src/v2/compiler/03_name_resolve.dag @@ -14,7 +14,7 @@ import v2.compiler.resolve { ResolveNodeWalk, ResolveWalkRefused, ResolvedTree, - resolve_walk_outcome, + resolved_tree_outcome, resolve_walk_prefix_pending, resolve_walk_with_namespace_policy, try_edge_declared_binding @@ -772,6 +772,8 @@ fn resolve_with_admission_policy( ) } +// The admitted namespace is built over the context's own index (namespace_for_subject_in_context), +// so that index is the one this resolution consulted. A refused context resolved nothing. fn resolve_with_admission_context_policy( context: Outcome, admission: Admission, @@ -779,13 +781,20 @@ fn resolve_with_admission_context_policy( active_roots: FreeMonoid, policy: NameResolutionPolicy ) -> Outcome { - resolve_walk_outcome(w: resolve_walk_with_admission_context_policy( - context: context, - admission: admission, - index: index, - active_roots: active_roots, - policy: policy - )) + match context { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: shared, diagnostics: _ } => + resolved_tree_outcome( + w: resolve_walk_with_admission_context_policy( + context: context, + admission: admission, + index: index, + active_roots: active_roots, + policy: policy + ), + symbol_index: shared.symbol_index + ) + } } // THE SUBJECT'S WHOLE RESOLUTION OBSERVATION, every independent failure chain in walk order. The diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 45b1023f974..396474b6398 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -121,7 +121,20 @@ import v2.std.node_query { } import std.occurrence_identity { NodeOccurrenceIdentity, OccurrenceId } -type ResolvedTree = Node +// RESOLVE'S OUTPUT IS THE RESOLVED ROOT AND THE INDEX IT WAS RESOLVED AGAINST, one record. A later +// stage that needs a reference's declaration asks the SAME authority resolution asked +// (v2.std.symbol_index symbol_index_lookup) instead of reconstructing it from the tree: the index is +// the Namespace.symbol_index the walk ran under, minted beside the root on the Accepted arm only, so +// a refusal carries no index and no stage can read one for a tree that did not resolve. +// CONSUMERS: .root is read by every stage after resolve (infer's gather reads it at its entry). +// .symbol_index is a DECLARED FRONTIER in this change: its consumer is gunbc#12407, which stacks on +// it -- v2.compiler.infer refinement_declaration asks symbol_index_lookup for a refinement +// declaration a cast operand's type references (the declared-carrier widening), replacing an +// infer-private walk over the tree. +type ResolvedTree { + root: Node + symbol_index: SymbolIndex +} // `test_code`, `declared_in` and `imported_origins` exist for one decision: whether a reference binds // to test code (owner ruling 2026-09-16/17). Root-scope bindings come from exactly two sources: the @@ -1083,6 +1096,15 @@ fn resolve_walk_outcome(w: ResolveNodeWalk) -> Outcome { } } +// The stage exit: the same projection, with the index resolution consulted minted beside the root. +fn resolved_tree_outcome(w: ResolveNodeWalk, symbol_index: SymbolIndex) -> Outcome { + match w { + ResolveWalkAccepted { value: v, diagnostics: d } => + Accepted { value: ResolvedTree { root: v, symbol_index: symbol_index }, diagnostics: d } + ResolveWalkRefused { first: f, rest: _, observation: _ } => Rejected { diagnostics: f } + } +} + // PENDING ADVISORIES STAY WITH THEIR OWN FATAL. A refused child's chains arrive intact; the walk // prefixes the advisories accepted siblings raised SINCE THE PREVIOUS REFUSAL onto the child's // first chain only, then resets them. So every advisory lands in at most one chain, the one whose @@ -1857,12 +1879,15 @@ fn resolve_with_namespace_policy( lm: LanguageModel, policy: NameResolutionPolicy ) -> Outcome { - resolve_walk_outcome(w: resolve_walk_with_namespace_policy( - tree: tree, - namespace: namespace, - lm: lm, - policy: policy - )) + resolved_tree_outcome( + w: resolve_walk_with_namespace_policy( + tree: tree, + namespace: namespace, + lm: lm, + policy: policy + ), + symbol_index: namespace.symbol_index + ) } fn resolve_walk_with_namespace_policy( diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index e303f64f807..263c4224ea9 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -3078,9 +3078,9 @@ fn infer_grounding_admits_infer_facts(grounding: CanonicalGrounding) -> Bool { } fn infer_entries_for_tree(tree: ResolvedTree) -> Outcome> { - let partials = partial_bounded_lattice_instances_in_tree(tree: tree) + let partials = partial_bounded_lattice_instances_in_tree(tree: tree.root) infer_gather_acc_to_outcome( - acc: fold_node(n: tree, algebra: infer_gather_fold_algebra(partials: partials, tree: tree)) + acc: fold_node(n: tree.root, algebra: infer_gather_fold_algebra(partials: partials, tree: tree.root)) ) } // INFER'S OUTPUT IS THE OBLIGATED FORM, never an InferredTree: v2.compiler.refinement_discharge @@ -3096,7 +3096,7 @@ fn infer(tree: ResolvedTree) -> Outcome { Accepted { value: facts_map, diagnostics: md } => Accepted { value: ObligatedInferredTree { - root: tree, + root: tree.root, facts: facts_map, obligations: Empty }, diff --git a/src/v2/compiler/ingested_fixture_arrows.dag b/src/v2/compiler/ingested_fixture_arrows.dag index 361884805ec..609906faae5 100644 --- a/src/v2/compiler/ingested_fixture_arrows.dag +++ b/src/v2/compiler/ingested_fixture_arrows.dag @@ -1,6 +1,7 @@ module v2.compiler.ingested_fixture_arrows import v2.compiler.staged_front_end { front_end_run_outcome, run_front_end } +import v2.compiler.resolve { ResolvedTree } import v2.std.optional { Absent, Optional, @@ -87,7 +88,7 @@ fn ingested_find_arrow_in_module(root: Node) -> Optional { // second route, and has no order of its own to drift from the one it projects — which is the whole // reason the front end moved rather than being copied (DESIGN section 3). -fn ingested_resolved_module_from_source(source: String, file: Symbol) -> Outcome { +fn ingested_resolved_module_from_source(source: String, file: Symbol) -> Outcome { front_end_run_outcome(run: run_front_end(source: source, file: file)) } @@ -95,7 +96,7 @@ fn ingested_arrow_from_source(source: String, file: Symbol) -> Outcome { bind_outcome( o: ingested_resolved_module_from_source(source: source, file: file), f: fn(resolved) { - match ingested_find_arrow_in_module(root: resolved) { + match ingested_find_arrow_in_module(root: resolved.root) { Present { value: arrow } => if well_formed(n: arrow) { outcome_accepted(value: arrow) @@ -111,7 +112,7 @@ fn ingested_arrow_from_source(source: String, file: Symbol) -> Outcome { outcome_rejected( d: ingested_fixture_diagnostic( reason: ^ingested_fixture_reason_arrow_missing, - n: resolved + n: resolved.root ) ) } diff --git a/src/v2/compiler/program_assembly.dag b/src/v2/compiler/program_assembly.dag index 718b99191db..ba5ada26f9e 100644 --- a/src/v2/compiler/program_assembly.dag +++ b/src/v2/compiler/program_assembly.dag @@ -22,6 +22,7 @@ import v2.compiler.parse { import v2.std.compilers.lexing { TokenStream, token_stream_remaining_count } import v2.std.integer { Int } import v2.compiler.normalize { normalize } +import v2.compiler.resolve { ResolvedTree } import v2.compiler.name_resolve { Admission, resolve_with_admission, @@ -525,7 +526,7 @@ fn assemble_program_from_module_roots( roots: FreeMonoid, admission: Admission, lm: LanguageModel -) -> Outcome { +) -> Outcome { bind_outcome( o: validate_module_roots(roots: roots), f: fn(validated_roots) { @@ -552,7 +553,7 @@ fn assemble_program_from_module_roots( // renderer degrades to "no file" instead of to a wrong file. type IngestAssembly { spans: SpanIndex, - program: Outcome + program: Outcome } fn assemble_program_from_ingest_located( @@ -586,7 +587,7 @@ fn assemble_program_from_ingest( ingest: SourceRootIngest, admission: Admission, lm: LanguageModel -) -> Outcome { +) -> Outcome { assemble_program_from_ingest_located( ingest: ingest, admission: admission, diff --git a/src/v2/compiler/self_host/candidate_generation.dag b/src/v2/compiler/self_host/candidate_generation.dag index b5051eca170..04234e75ff4 100644 --- a/src/v2/compiler/self_host/candidate_generation.dag +++ b/src/v2/compiler/self_host/candidate_generation.dag @@ -1,5 +1,6 @@ module v2.compiler.self_host.candidate_generation +import v2.compiler.resolve { ResolvedTree } import extdeps.communication.medium { Medium } import extdeps.filesystem.filesystem_io { Filesystem } import v2.compiler.emit { emit } @@ -142,7 +143,7 @@ fn generate_stage_candidate_from_ingest( } fn generate_translate_self_emit_candidate( - resolved_module: Node, + resolved_module: ResolvedTree, dag_target: TargetModel ) -> Outcome { bind_outcome( diff --git a/src/v2/compiler/self_host/candidate_generation_stage_verdicts.dag b/src/v2/compiler/self_host/candidate_generation_stage_verdicts.dag index 15e22d6e89c..c6afc65aab5 100644 --- a/src/v2/compiler/self_host/candidate_generation_stage_verdicts.dag +++ b/src/v2/compiler/self_host/candidate_generation_stage_verdicts.dag @@ -1,5 +1,6 @@ module v2.compiler.self_host.candidate_generation_stage_verdicts +import v2.compiler.resolve { ResolvedTree } import v2.compiler.infer { infer } import v2.compiler.self_host.candidate_generation { generate_translate_self_emit_candidate } import v2.std.algebra { Cons, Empty, fold_list } @@ -77,7 +78,7 @@ fn diagnostics_carried_reasons(d: Diagnostics) -> List { } fn candidate_generation_composition_verdicts( - resolved_module: Node, + resolved_module: ResolvedTree, dag_target: TargetModel, infer_verdict: Symbol, infer_carried_reasons: List @@ -104,7 +105,7 @@ fn candidate_generation_composition_verdicts( } fn candidate_generation_stage_verdicts( - resolved_module: Node, + resolved_module: ResolvedTree, dag_target: TargetModel ) -> CandidateGenerationStageVerdicts { match infer(tree: resolved_module) { diff --git a/src/v2/compiler/self_host/closure_emission.dag b/src/v2/compiler/self_host/closure_emission.dag index 657ab49d6a8..fab196e05ac 100644 --- a/src/v2/compiler/self_host/closure_emission.dag +++ b/src/v2/compiler/self_host/closure_emission.dag @@ -37,7 +37,7 @@ import v2.compiler.reference_closure { reference_closure_neighbours, reference_derived_closure } -import v2.compiler.resolve { ResolveWalkAccepted, ResolveWalkRefused } +import v2.compiler.resolve { ResolvedTree, resolved_tree_outcome } import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest, @@ -327,20 +327,17 @@ fn closure_resolve_member( symbol_index: SymbolIndex, index: SourceRootIndex, active_roots: FreeMonoid -) -> Outcome { +) -> Outcome { bind_outcome( o: validate_module_roots(roots: Cons { head: tree, tail: Empty }), f: fn(validated) { - match resolve_walk_with_admission_context_policy( + resolved_tree_outcome(w: resolve_walk_with_admission_context_policy( context: outcome_accepted(value: ResolutionContext { lm: lm, roots: validated, symbol_index: symbol_index }), admission: Admission { subject: ResolutionSubject { name: member_module }, imports: Empty }, index: index, active_roots: active_roots, policy: default_name_resolution_policy() - ) { - ResolveWalkRefused { first: f, rest: _, observation: _ } => Rejected { diagnostics: f } - ResolveWalkAccepted { value: resolved, diagnostics: _ } => outcome_accepted(value: resolved) - } + ), symbol_index: symbol_index) } ) } @@ -373,7 +370,7 @@ fn closure_member_for_fold( ), f: fn(resolved) { outcome_accepted(value: ReferenceClosureMember { - resolved: resolved, + resolved: resolved.root, import_targets: fold_list(xs: tree.import_bindings, empty: Empty, cons: fn(acc, row) { list_snoc_item(xs: acc, item: row.target) }), @@ -431,7 +428,7 @@ fn closure_member_emission( ), f: fn(resolved) { match reference_closure_neighbours( - member: ReferenceClosureMember { resolved: resolved, import_targets: visit.member.import_targets, payload: true }, + member: ReferenceClosureMember { resolved: resolved.root, import_targets: visit.member.import_targets, payload: true }, roster: roster ) { ReferenceClosureTargetOwnerless { target: _, at: at } => @@ -452,7 +449,7 @@ fn closure_member_emission( } else { outcome_rejected(d: closure_emission_diagnostic( reason: ^closure_emission_member_reaches_outside_closure, - at: resolved + at: resolved.root )) } } diff --git a/src/v2/compiler/self_host/compiler_closure_emit.dag b/src/v2/compiler/self_host/compiler_closure_emit.dag index 05f5ba33564..5d779aa612d 100644 --- a/src/v2/compiler/self_host/compiler_closure_emit.dag +++ b/src/v2/compiler/self_host/compiler_closure_emit.dag @@ -4,6 +4,7 @@ import v2.compiler.refinement_discharge { infer_and_discharge } import std.dissolution { DissolutionCondition, unbound_dissolution } import std.types { NonEmptyStr } import v2.compiler.name_resolve { Admission } +import v2.compiler.resolve { ResolvedTree } import v2.compiler.program_assembly { assemble_program_from_ingest_located } import v2.compiler.program_partition { emit_for_target } import v2.compiler.source_authority { SourceRootIngest } @@ -137,7 +138,7 @@ fn emit_compiler_import_closure_from_ingest_located( } fn emit_compiler_import_closure_from_assembled( - program: Outcome, + program: Outcome, target: TargetModel ) -> Outcome { bind_outcome( diff --git a/src/v2/compiler/self_host/direct_rust_door_fixture.dag b/src/v2/compiler/self_host/direct_rust_door_fixture.dag index 9a586f2f8ce..297db5ddb0d 100644 --- a/src/v2/compiler/self_host/direct_rust_door_fixture.dag +++ b/src/v2/compiler/self_host/direct_rust_door_fixture.dag @@ -25,6 +25,7 @@ import v2.extdeps.languages.rust { rust_target_model } import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.resolve { ResolvedTree } import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } import std.algebra { Cons, Empty } import v2.std.artifact { Artifact, SourceFile } @@ -108,7 +109,7 @@ fn direct_rust_door_admission() -> Admission { // nullary and warm-enrolled in `v2.workflow.floor_pure_producer_share` // (floor_cross_claim_pure_producers_warm), which carries the measured recompute/sharing/serve // case; the required floor forces it during strict preparation, outside every per-claim budget. -fn direct_rust_door_specimen_resolved() -> Outcome { +fn direct_rust_door_specimen_resolved() -> Outcome { assemble_program_from_ingest( ingest: direct_rust_door_ingest(), admission: direct_rust_door_admission(), diff --git a/src/v2/compiler/source_authority.dag b/src/v2/compiler/source_authority.dag index 568b1231b25..98fd7e3fa30 100644 --- a/src/v2/compiler/source_authority.dag +++ b/src/v2/compiler/source_authority.dag @@ -1299,7 +1299,7 @@ fn semantic_ir_equal_witness( original: DagSemanticIr, reparsed: DagSemanticIr ) -> Witness { - if source_authority_node_equal(left: original.tree, right: reparsed.tree) { + if source_authority_node_equal(left: original.tree.root, right: reparsed.tree.root) { Holds { value: SemanticIrEqual { original: original, reparsed: reparsed } } } else { Violates { @@ -1423,13 +1423,13 @@ fn source_authority_round_trip_with_model( bind_outcome( o: target_serialize_source_from_model( target: canonical_source_target, - emitted: original_ir.tree + emitted: original_ir.tree.root ), f: fn(canonical_source) { bind_outcome( o: target_serialize_source_from_model( target: canonical_source_target, - emitted: original_ir.tree + emitted: original_ir.tree.root ), f: fn(canonical_source_again) { bind_outcome( @@ -1495,13 +1495,13 @@ fn canonical_dag_source_parse_print_law( bind_outcome( o: target_serialize_source_from_model( target: canonical_source_target, - emitted: original_ir.tree + emitted: original_ir.tree.root ), f: fn(canonical_source) { bind_outcome( o: target_serialize_source_from_model( target: canonical_source_target, - emitted: original_ir.tree + emitted: original_ir.tree.root ), f: fn(canonical_source_again) { bind_outcome( diff --git a/src/v2/compiler/staged_front_end.dag b/src/v2/compiler/staged_front_end.dag index c7d054d7f96..9e87db8126e 100644 --- a/src/v2/compiler/staged_front_end.dag +++ b/src/v2/compiler/staged_front_end.dag @@ -2,7 +2,7 @@ module v2.compiler.staged_front_end import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module } -import v2.compiler.resolve { resolve } +import v2.compiler.resolve { ResolvedTree, resolve } import v2.compiler.tokenize { tokenize } import v2.extdeps.languages.dag { dag_language_model } import std.algebra { list_snoc_item } @@ -71,7 +71,7 @@ type FrontEndStageStep } type FrontEndCompletion - = FrontEndResolvedModule { resolved: Node } + = FrontEndResolvedModule { resolved: ResolvedTree } | FrontEndRefused | FrontEndBoundReached { last: FrontEndStage } @@ -259,7 +259,7 @@ fn front_end_resolve_step( steps: front_end_passed( steps: steps, stage: FrontEndResolve, - receipt: ResolvedModule { node_count: node_subtree_count(n: resolved) }, + receipt: ResolvedModule { node_count: node_subtree_count(n: resolved.root) }, diagnostics: d ), completion: FrontEndResolvedModule { resolved: resolved } @@ -290,7 +290,7 @@ fn front_end_first_refusal(steps: List) -> Optional Outcome { +fn front_end_run_outcome(run: FrontEndRun) -> Outcome { match front_end_first_refusal(steps: run.steps) { Present { value: r } => Rejected { diff --git a/src/v2/test/claim/body_cast_node_test.dag b/src/v2/test/claim/body_cast_node_test.dag index afb7058f290..b6aaac609c0 100644 --- a/src/v2/test/claim/body_cast_node_test.dag +++ b/src/v2/test/claim/body_cast_node_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.body_cast_node +import v2.compiler.resolve { ResolvedTree } import std.algebra { Cons, Empty, list_snoc_item } import v2.std.coercion { NoTargetCandidate } import std.occurrence_identity { OccurrenceSynthetic } @@ -10,7 +11,7 @@ import v2.compiler.infer { InferredFacts, InferredTree, infer } import v2.extdeps.languages.dag { dag_arrow_domain_conj_node, dag_arrow_with_body_node, dag_int_literal_node_from_lexeme, dag_type_atom_node } import v2.extdeps.languages.rust_test { rust_binop_target_model_staging } import v2.extdeps.runtimes.v2_evaluator { v2_evaluator_interpretation } -import v2.test.lens_common.infer_fixture { claim_inferred_facts_from_nodes } +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations, claim_inferred_facts_from_nodes } import v2.std.cardinality { termination_proof_witness_for_node } import v2.std.compilers.target_model { canonical_operation_op_coerce, target_model_canonical_operation_wire_node } import v2.std.collection { List } @@ -61,67 +62,67 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // body_lowering_reason_type_annotation_not_carried at `T` (the as-cast refusal), and before that // they were Accepted with `T` dropped: no cast node, so the count in bcn_one_cast_to_int was 0. -fn bcn_tail_cast() -> Outcome { +fn bcn_tail_cast() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n x as Int\n}\n") } -fn bcn_let_value_cast() -> Outcome { +fn bcn_let_value_cast() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n let y = x as Int\n y\n}\n") } -fn bcn_call_argument_cast() -> Outcome { +fn bcn_call_argument_cast() -> Outcome { tpb_assemble(src: "module p\n\nfn g(v: Int) -> Int { v }\n\nfn f(x: Int) -> Int {\n g(v: x as Int)\n}\n") } -fn bcn_if_arm_cast() -> Outcome { +fn bcn_if_arm_cast() -> Outcome { tpb_assemble(src: "module p\n\nfn f(b: Bool, x: Int) -> Int {\n if b { x as Int } else { x }\n}\n") } -fn bcn_match_arm_cast() -> Outcome { +fn bcn_match_arm_cast() -> Outcome { tpb_assemble(src: "module p\n\nfn f(b: Bool, x: Int) -> Int {\n match b {\n true => x as Int\n false => x\n }\n}\n") } -fn bcn_call_operand_cast() -> Outcome { +fn bcn_call_operand_cast() -> Outcome { tpb_assemble(src: "module p\n\nfn g(v: Int) -> Int { v }\n\nfn f(x: Int) -> Int {\n g(v: x) as Int\n}\n") } -fn bcn_tail_undeclared() -> Outcome { +fn bcn_tail_undeclared() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n x as Q\n}\n") } -fn bcn_match_arm_undeclared() -> Outcome { +fn bcn_match_arm_undeclared() -> Outcome { tpb_assemble(src: "module p\n\nfn f(b: Bool, x: Int) -> Int {\n match b {\n true => x as Q\n false => x\n }\n}\n") } -fn bcn_generic_cast() -> Outcome { +fn bcn_generic_cast() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: T) -> T {\n x as T\n}\n") } -fn bcn_value_param_as_type() -> Outcome { +fn bcn_value_param_as_type() -> Outcome { tpb_assemble(src: "module p\n\nfn f(Q: Int) -> Int {\n Q as Q\n}\n") } -fn bcn_int_sum_as_int() -> Outcome { +fn bcn_int_sum_as_int() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n (x + x) as Int\n}\n") } -fn bcn_int_sum_as_bool() -> Outcome { +fn bcn_int_sum_as_bool() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Bool {\n (x + x) as Bool\n}\n") } -fn bcn_into_refinement() -> Outcome { +fn bcn_into_refinement() -> Outcome { tpb_assemble(src: "module p\n\nfn positive(x: Int) -> Bool { true }\n\ntype Pos = Int where positive\n\ndata d: Pos = 1 as Pos\n") } -fn bcn_out_of_refinement() -> Outcome { +fn bcn_out_of_refinement() -> Outcome { tpb_assemble(src: "module p\n\nfn positive(x: Int) -> Bool { true }\n\ntype Pos = Int where positive\n\nfn f(x: Pos) -> Int {\n x as Int\n}\n") } -fn bcn_refinement_param_as_bool() -> Outcome { +fn bcn_refinement_param_as_bool() -> Outcome { tpb_assemble(src: "module p\n\nfn positive(x: Int) -> Bool { true }\n\ntype Pos = Int where positive\n\nfn f(x: Pos) -> Bool {\n x as Bool\n}\n") } -fn bcn_refinement_identity_cast() -> Outcome { +fn bcn_refinement_identity_cast() -> Outcome { tpb_assemble(src: "module p\n\nfn positive(x: Int) -> Bool { true }\n\ntype Pos = Int where positive\n\nfn f(x: Pos) -> Pos {\n x as Pos\n}\n") } @@ -144,11 +145,11 @@ fn bcn_atom_identity(n: Node) -> Optional { // Accepted, with exactly one cast node whose target is a resolved atom: the lowering built the node // and resolve kept T. -fn bcn_one_cast(o: Outcome) -> Bool { +fn bcn_one_cast(o: Outcome) -> Bool { match o { Rejected { diagnostics: _ } => false Accepted { value: root, diagnostics: _ } => - match bcn_cast_targets(n: root) { + match bcn_cast_targets(n: root.root) { Cons { head: t, tail: Empty } => match bcn_atom_identity(n: t) { Present { value: _ } => true @@ -159,11 +160,11 @@ fn bcn_one_cast(o: Outcome) -> Bool { } } -fn bcn_cast_target_identity(o: Outcome) -> Optional { +fn bcn_cast_target_identity(o: Outcome) -> Optional { match o { Rejected { diagnostics: _ } => Absent Accepted { value: root, diagnostics: _ } => - match bcn_cast_targets(n: root) { + match bcn_cast_targets(n: root.root) { Cons { head: t, tail: Empty } => bcn_atom_identity(n: t) _ => Absent } @@ -180,7 +181,7 @@ fn bcn_atom_in(n: Node, id: Symbol) -> Bool { } // Refused with `reason`, located at a node spelling `id` and not at the fn or the `as` token. -fn bcn_refused_at(o: Outcome, reason: Symbol, id: Symbol) -> Bool { +fn bcn_refused_at(o: Outcome, reason: Symbol, id: Symbol) -> Bool { match o { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: d } => @@ -250,7 +251,7 @@ fn bcn_has_reason(d: NonEmptyDiagnostics, reason: Symbol) -> Bool { diagnostics_list_has_reason(xs: Cons { head: d.head, tail: d.tail }, reason: reason) } -fn bcn_infers(o: Outcome) -> Outcome { +fn bcn_infers(o: Outcome) -> Outcome { match o { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: root, diagnostics: _ } => diff --git a/src/v2/test/claim/body_let_annotation_test.dag b/src/v2/test/claim/body_let_annotation_test.dag index 32a97fa0c33..0d93fd79ef6 100644 --- a/src/v2/test/claim/body_let_annotation_test.dag +++ b/src/v2/test/claim/body_let_annotation_test.dag @@ -1,5 +1,7 @@ module v2.test.claim.body_let_annotation +import v2.compiler.resolve { ResolvedTree } +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import std.algebra { Cons, Empty } import v2.std.coercion { NoTargetCandidate } import v2.test.claim.type_param_binder_frame { tpb_accepts, tpb_assemble } @@ -45,19 +47,19 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // body_lowering_reason_type_annotation_not_carried at its annotation (gunbc.rung_drop // typed_statement_let_refuses_until_the_bind_annotation_carrier), so (1)-(5) were red. -fn bla_concrete() -> Outcome { +fn bla_concrete() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n let y: Int = x\n y\n}\n") } -fn bla_generic() -> Outcome { +fn bla_generic() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: T) -> T {\n let y: T = x\n y\n}\n") } -fn bla_type_param_as_value() -> Outcome { +fn bla_type_param_as_value() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: T) -> T {\n let y: T = T\n y\n}\n") } -fn bla_value_param_as_type() -> Outcome { +fn bla_value_param_as_type() -> Outcome { tpb_assemble(src: "module p\n\nfn f(Q: Int) -> Int {\n let y: Q = Q\n y\n}\n") } @@ -65,11 +67,11 @@ fn bla_value_param_as_type() -> Outcome { // left on the frontier. A let-bound literal is not derived there, so it would leave the check // unjudged and admit both rows without discriminating anything. The infer outcomes below are // enrolled share points, so a claim reads the result and pays for none of the sum's derivation. -fn bla_int_sum_as_int() -> Outcome { +fn bla_int_sum_as_int() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n let y: Int = x + x\n y\n}\n") } -fn bla_int_sum_as_bool() -> Outcome { +fn bla_int_sum_as_bool() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n let y: Bool = x + x\n x\n}\n") } @@ -91,11 +93,11 @@ fn bla_atom_identity(n: Node) -> Optional { // The one annotation a tree carries, as its atom identity; Absent when refused, when no Bind carries // one, or when more than one does. -fn bla_annotation_identity(o: Outcome) -> Optional { +fn bla_annotation_identity(o: Outcome) -> Optional { match o { Rejected { diagnostics: _ } => Absent Accepted { value: root, diagnostics: _ } => - match bla_annotations(n: root) { + match bla_annotations(n: root.root) { Cons { head: t, tail: Empty } => bla_atom_identity(n: t) _ => Absent } @@ -168,7 +170,7 @@ test fn bla_value_binder_does_not_answer_the_annotation() -> Bool { } } -fn bla_infers(o: Outcome) -> Outcome { +fn bla_infers(o: Outcome) -> Outcome { match o { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: root, diagnostics: _ } => diff --git a/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag b/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag index 6c8a576d986..c7b6788c662 100644 --- a/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag +++ b/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag @@ -144,7 +144,7 @@ test fn arrow_body_form_eval_value_vertical_holds() -> Bool { ) && arrow_body_form_eval_nullary_int_holds(callee: zero_arrow, expected: 0) && arrow_body_form_eval_binary_int_holds( - callee: add_arrow, + callee: add_arrow.root, left_lex: "2", right_lex: "3", expected: 5 diff --git a/src/v2/test/claim/body_lowering/infer_transform_binary_infix_witness_helpers.dag b/src/v2/test/claim/body_lowering/infer_transform_binary_infix_witness_helpers.dag index ab7176af2bd..4531bce8f14 100644 --- a/src/v2/test/claim/body_lowering/infer_transform_binary_infix_witness_helpers.dag +++ b/src/v2/test/claim/body_lowering/infer_transform_binary_infix_witness_helpers.dag @@ -1,5 +1,6 @@ module v2.test.claim.body_lowering.infer_transform_binary_infix_witness_helpers +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import v2.compiler.infer { inferred_facts_grounding_derived, inferred_facts_witness_for_node } import v2.compiler.resolve { resolve } @@ -55,7 +56,7 @@ fn infer_transform_witness_resolved_add_arrow() -> Optional { Accepted { value: normalized, diagnostics: _ } => match resolve(tree: normalized, lm: dag_language_model()) { Accepted { value: resolved, diagnostics: _ } => - wave1_gate1_find_arrow_in_module(root: resolved) + wave1_gate1_find_arrow_in_module(root: resolved.root) Rejected { diagnostics: _ } => Absent } } @@ -80,7 +81,7 @@ fn infer_transform_add_vertical_discriminating_holds() -> Bool { match infer_transform_witness_add_body(arrow: arrow) { Absent => false Present { value: body } => - match infer_and_discharge(tree: arrow) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: arrow)) { Accepted { value: tree, diagnostics: _ } => match inferred_facts_witness_for_node(tree: tree, node: body) { Holds { value: body_facts } => diff --git a/src/v2/test/claim/body_lowering/infer_transform_binary_infix_witness_test.dag b/src/v2/test/claim/body_lowering/infer_transform_binary_infix_witness_test.dag index 90ef9ecb5eb..bfe05fee340 100644 --- a/src/v2/test/claim/body_lowering/infer_transform_binary_infix_witness_test.dag +++ b/src/v2/test/claim/body_lowering/infer_transform_binary_infix_witness_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.body_lowering.infer_transform_binary_infix_witness_test +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import v2.compiler.infer { inferred_facts_grounding_derived, inferred_facts_witness_for_node } import v2.std.diagnostic { Accepted, Rejected, diagnostics_has_reason } @@ -12,7 +13,7 @@ import v2.std.node { Symbol } test fn infer_transform_non_add_remains_frontier_holds() -> Bool { let body = infer_transform_subtract_fixture() - match infer_and_discharge(tree: body) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: body)) { Accepted { value: tree, diagnostics: d } => match inferred_facts_witness_for_node(tree: tree, node: body) { Holds { value: facts } => diff --git a/src/v2/test/claim/body_lowering/wave1_gate1_normalize_add_helpers.dag b/src/v2/test/claim/body_lowering/wave1_gate1_normalize_add_helpers.dag index d03fe6ad5fe..704fb14f3e8 100644 --- a/src/v2/test/claim/body_lowering/wave1_gate1_normalize_add_helpers.dag +++ b/src/v2/test/claim/body_lowering/wave1_gate1_normalize_add_helpers.dag @@ -98,7 +98,7 @@ fn wave1_gate1_resolved_add_arrow_transform_holds() -> Bool { Accepted { value: normalized, diagnostics: _ } => match resolve(tree: normalized, lm: dag_language_model()) { Accepted { value: resolved, diagnostics: _ } => - match wave1_gate1_find_arrow_in_module(root: resolved) { + match wave1_gate1_find_arrow_in_module(root: resolved.root) { Present { value: arrow } => match find_arrow_body_child(root: arrow) { Accepted { value: body, diagnostics: _ } => diff --git a/src/v2/test/claim/body_type_annotation_refusal_test.dag b/src/v2/test/claim/body_type_annotation_refusal_test.dag index e79fa1b9b23..6e2e363bf4a 100644 --- a/src/v2/test/claim/body_type_annotation_refusal_test.dag +++ b/src/v2/test/claim/body_type_annotation_refusal_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.body_type_annotation_refusal +import v2.compiler.resolve { ResolvedTree } import v2.test.claim.type_param_binder_frame { tpb_accepts, tpb_assemble, tpb_refuses_with } import v2.std.diagnostic { Accepted, NodeLocus, NonEmptyDiagnostics, Outcome, Rejected, diagnostics_fatal, diagnostics_fatal_reason } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -17,11 +18,11 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // declared nowhere. (2) is the unannotated control, green before and after, so the refusal is the // annotation's and not the let's. -fn btar_let_annotated() -> Outcome { +fn btar_let_annotated() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n let y: Q = x\n y\n}\n") } -fn btar_let_plain() -> Outcome { +fn btar_let_plain() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n let y = x\n y\n}\n") } @@ -51,7 +52,7 @@ fn btar_refusal_at_authored(d: NonEmptyDiagnostics, id: Symbol) -> Bool { } } -fn btar_refused_at_authored_q(o: Outcome) -> Bool { +fn btar_refused_at_authored_q(o: Outcome) -> Bool { match o { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: d } => btar_refusal_at_authored(d: d, id: ^Q) @@ -77,15 +78,15 @@ test fn btar_let_annotation_refuses() -> Bool { } } -fn btar_fn_literal_annotated() -> Outcome { +fn btar_fn_literal_annotated() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n let g = fn(y) -> Q { y }\n x\n}\n") } -fn btar_fn_literal_plain() -> Outcome { +fn btar_fn_literal_plain() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n let g = fn(y) { y }\n x\n}\n") } -fn btar_if_arm_fn_literal() -> Outcome { +fn btar_if_arm_fn_literal() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Bool) -> Int { if x { fn(y) { zz } } else { 1 } }\n") } diff --git a/src/v2/test/claim/compiler/compile_eval_thesis_proof_test.dag b/src/v2/test/claim/compiler/compile_eval_thesis_proof_test.dag index 9dffdc65322..43c51f335ce 100644 --- a/src/v2/test/claim/compiler/compile_eval_thesis_proof_test.dag +++ b/src/v2/test/claim/compiler/compile_eval_thesis_proof_test.dag @@ -30,7 +30,7 @@ import v2.std.runtime { RuntimeValueNodeUnrepresentable, runtime_value_node_projection } -import v2.test.lens_common.infer_fixture { claim_inferred_facts_from_nodes } +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations, claim_inferred_facts_from_nodes } import v2.std.witness { Holds } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -111,7 +111,7 @@ test fn compile_eval_bool_reaches_node_value_holds() -> Bool { test fn compile_eval_bool_via_compile_entry_refuses_underived_holds() -> Bool { match compile( - source: v2_eval_bool_literal_pin, + source: claim_resolved_tree_without_declarations(root: v2_eval_bool_literal_pin), mode: Eval { runtime: v2_eval_bool_literal_pin } ) { Rejected { diagnostics: d } => d.head.reason == ^infer_grounding_not_derived diff --git a/src/v2/test/claim/compiler/infer_application_argument_inhabitance_witness_test.dag b/src/v2/test/claim/compiler/infer_application_argument_inhabitance_witness_test.dag index c73babb7bc7..247d9321fef 100644 --- a/src/v2/test/claim/compiler/infer_application_argument_inhabitance_witness_test.dag +++ b/src/v2/test/claim/compiler/infer_application_argument_inhabitance_witness_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.compiler.infer_application_argument_inhabitance_witness_test +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.infer { infer } import v2.std.inhabitance { inhabitance_cardinality_type, @@ -151,7 +152,7 @@ fn inhabitance_formal_unresolved_tree() -> Node { test fn infer_incompatible_argument_refuses_holds() -> Bool { match infer( - tree: inhabitance_call_tree(arg: dag_type_atom_node(identity: ^dag_token_kw_true)) + tree: claim_resolved_tree_without_declarations(root: inhabitance_call_tree(arg: dag_type_atom_node(identity: ^dag_token_kw_true))) ) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: nds } => @@ -163,7 +164,7 @@ test fn infer_incompatible_argument_refuses_holds() -> Bool { } test fn infer_compatible_argument_admits_holds() -> Bool { - match infer(tree: inhabitance_call_tree(arg: dag_int_literal_fixture_one())) { + match infer(tree: claim_resolved_tree_without_declarations(root: inhabitance_call_tree(arg: dag_int_literal_fixture_one()))) { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } @@ -171,9 +172,9 @@ test fn infer_compatible_argument_admits_holds() -> Bool { test fn infer_argument_type_not_derived_is_counted_frontier_holds() -> Bool { match infer( - tree: inhabitance_call_tree( + tree: claim_resolved_tree_without_declarations(root: inhabitance_call_tree( arg: node_synthetic(kind: ComputationNode { behavior: Value }, children: []) - ) + )) ) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: d } => @@ -185,7 +186,7 @@ test fn infer_argument_type_not_derived_is_counted_frontier_holds() -> Bool { } test fn infer_generic_formal_is_counted_frontier_holds() -> Bool { - match infer(tree: inhabitance_generic_formal_tree()) { + match infer(tree: claim_resolved_tree_without_declarations(root: inhabitance_generic_formal_tree())) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: d } => diagnostics_has_reason( @@ -209,7 +210,7 @@ test fn inhabitance_cardinality_type_is_optional_carrier_holds() -> Bool { } test fn infer_optional_formal_is_refused_for_unproven_cardinality_descent_holds() -> Bool { - match infer(tree: inhabitance_optional_formal_tree()) { + match infer(tree: claim_resolved_tree_without_declarations(root: inhabitance_optional_formal_tree())) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: nds } => diagnostics_has_reason( @@ -220,7 +221,7 @@ test fn infer_optional_formal_is_refused_for_unproven_cardinality_descent_holds( } test fn infer_positional_surplus_refuses_holds() -> Bool { - match infer(tree: inhabitance_arity_unmodeled_tree()) { + match infer(tree: claim_resolved_tree_without_declarations(root: inhabitance_arity_unmodeled_tree())) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: nds } => diagnostics_has_reason( @@ -231,7 +232,7 @@ test fn infer_positional_surplus_refuses_holds() -> Bool { } test fn infer_atom_operator_is_counted_formal_unresolved_holds() -> Bool { - match infer(tree: inhabitance_formal_unresolved_tree()) { + match infer(tree: claim_resolved_tree_without_declarations(root: inhabitance_formal_unresolved_tree())) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: d } => diagnostics_has_reason( @@ -303,10 +304,10 @@ fn inhabitance_declared_formal_tree(declared: Node, arg: Node) -> Node { test fn inhabitance_collection_produced_at_a_product_formal_is_counted_frontier_holds() -> Bool { match infer( - tree: inhabitance_declared_formal_tree( + tree: claim_resolved_tree_without_declarations(root: inhabitance_declared_formal_tree( declared: inhabitance_nominal_product_type_node(), arg: inhabitance_collection_type_node() - ) + )) ) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: d } => @@ -319,10 +320,10 @@ test fn inhabitance_collection_produced_at_a_product_formal_is_counted_frontier_ test fn inhabitance_product_produced_at_a_collection_formal_is_counted_frontier_holds() -> Bool { match infer( - tree: inhabitance_declared_formal_tree( + tree: claim_resolved_tree_without_declarations(root: inhabitance_declared_formal_tree( declared: inhabitance_collection_type_node(), arg: inhabitance_nominal_product_type_node() - ) + )) ) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: d } => diff --git a/src/v2/test/claim/execution/data_decl_lowering_grounding_test.dag b/src/v2/test/claim/execution/data_decl_lowering_grounding_test.dag index e73a1cd848c..9084c6becd6 100644 --- a/src/v2/test/claim/execution/data_decl_lowering_grounding_test.dag +++ b/src/v2/test/claim/execution/data_decl_lowering_grounding_test.dag @@ -1,5 +1,6 @@ module v2.test.execution.data_decl_lowering_grounding +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import extdeps.communication.medium { Lossless, Medium } import std.algebra { Cons, Empty } import std.occurrence_identity { OccurrenceSynthetic } @@ -169,7 +170,7 @@ fn ddl_fully_grounded(resolved: Outcome) -> Bool { match ddl_value_member(tree: tree) { Absent => false Present { value: member } => - match infer(tree: member) { + match infer(tree: claim_resolved_tree_without_declarations(root: member)) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: ds } => ddl_no_frontier_diagnostic(ds: ds) } @@ -269,7 +270,7 @@ test fn fn_brace_literal_body_grounds_fully_holds() -> Bool { } fn ddl_hand_grounding(tree: Node, n: Node) -> Int { - match infer(tree: tree) { + match infer(tree: claim_resolved_tree_without_declarations(root: tree)) { Rejected { diagnostics: _ } => 2 Accepted { value: inferred, diagnostics: _ } => match inferred.facts.lookup(n) { @@ -315,7 +316,7 @@ test fn malformed_literal_payload_stays_on_the_frontier_holds() -> Bool { // So this control reads the type back: the digit list is a FreeMonoid and its head a // DecimalDigit, in the integer authority's own nodes. fn ddl_resolved_type(tree: Node, n: Node) -> Optional { - match infer(tree: tree) { + match infer(tree: claim_resolved_tree_without_declarations(root: tree)) { Rejected { diagnostics: _ } => optional_absent() Accepted { value: inferred, diagnostics: _ } => match inferred.facts.lookup(n) { diff --git a/src/v2/test/claim/execution/infer_atom_grounding_rules_test.dag b/src/v2/test/claim/execution/infer_atom_grounding_rules_test.dag index b776fd35ca5..6384c968d3d 100644 --- a/src/v2/test/claim/execution/infer_atom_grounding_rules_test.dag +++ b/src/v2/test/claim/execution/infer_atom_grounding_rules_test.dag @@ -1,5 +1,6 @@ module v2.test.execution.infer_atom_grounding_rules +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import std.occurrence_identity { OccurrenceSynthetic } import v2.compiler.infer { DerivedGrounding, GroundingNotDerived, InferredTree } @@ -186,7 +187,7 @@ test fn atom_rules_leave_unbound_atom_on_the_frontier_holds() -> Bool { children: [], occurrence_id: OccurrenceSynthetic } - match infer_and_discharge(tree: unbound) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: unbound)) { Rejected { diagnostics: _ } => false Accepted { value: inferred, diagnostics: _ } => match inferred.facts.lookup(unbound) { @@ -201,7 +202,7 @@ test fn atom_rules_leave_unbound_atom_on_the_frontier_holds() -> Bool { } fn atom_grounding_standalone_evidence(n: Node) -> Optional { - match infer_and_discharge(tree: n) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: n)) { Rejected { diagnostics: _ } => Absent Accepted { value: inferred, diagnostics: _ } => atom_grounding_node_evidence(inferred: inferred, n: n) } diff --git a/src/v2/test/claim/execution/infer_product_introduction_test.dag b/src/v2/test/claim/execution/infer_product_introduction_test.dag index 98747dd18b1..46b8c330f38 100644 --- a/src/v2/test/claim/execution/infer_product_introduction_test.dag +++ b/src/v2/test/claim/execution/infer_product_introduction_test.dag @@ -1,5 +1,6 @@ module v2.test.execution.infer_product_introduction +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import std.occurrence_identity { OccurrenceSynthetic } import v2.compiler.infer { DerivedGrounding, GroundingNotDerived, InferredTree } @@ -200,7 +201,7 @@ test fn product_introduction_leaves_childless_conj_on_the_frontier_holds() -> Bo children: [], occurrence_id: OccurrenceSynthetic } - match infer_and_discharge(tree: childless) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: childless)) { Rejected { diagnostics: _ } => false Accepted { value: inferred, diagnostics: _ } => match inferred.facts.lookup(childless) { @@ -215,7 +216,7 @@ test fn product_introduction_leaves_childless_conj_on_the_frontier_holds() -> Bo } fn product_introduction_hand_tree_grounding(tree: Node, n: Node) -> Int { - match infer_and_discharge(tree: tree) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: tree)) { Rejected { diagnostics: _ } => 2 Accepted { value: inferred, diagnostics: _ } => match inferred.facts.lookup(n) { diff --git a/src/v2/test/claim/execution/long/add_arrow_eval_by_execution_test.dag b/src/v2/test/claim/execution/long/add_arrow_eval_by_execution_test.dag index 683a1ee6983..902b4e97880 100644 --- a/src/v2/test/claim/execution/long/add_arrow_eval_by_execution_test.dag +++ b/src/v2/test/claim/execution/long/add_arrow_eval_by_execution_test.dag @@ -305,7 +305,7 @@ fn add_arrow_source_ingested_add_infers() -> Outcome { bind_outcome( o: resolve(tree: normalized, lm: lm), f: fn(resolved) { - outcome_accepted(value: add_arrow_eval_admitted_tree(root: resolved)) + outcome_accepted(value: add_arrow_eval_admitted_tree(root: resolved.root)) } ) } diff --git a/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag b/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag index ffaab45de7f..ce19bb24148 100644 --- a/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag +++ b/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag @@ -34,7 +34,7 @@ import v2.extdeps.languages.rust_test { rust_classical_not_ingested_target_model_staging, rust_match_target_model_staging } -import v2.test.lens_common.infer_fixture { claim_inferred_facts_from_nodes } +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations, claim_inferred_facts_from_nodes } import v2.std.cardinality { RankingComponent, TerminationProof } import v2.std.collection { List, @@ -155,7 +155,7 @@ fn ingested_canonical_inferred_tree_from_arrow(arrow: Outcome) -> Outcome< bind_outcome( o: arrow, f: fn(a) { - infer_and_discharge(tree: a) + infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: a)) } ) } @@ -177,7 +177,7 @@ fn ingested_staging_inferred_tree_from_arrow(arrow: Outcome) -> Outcome Outcome Bool { test fn ingested_classical_not_real_infer_holds() -> Bool { match ingested_arrow_from_source(source: ingested_classical_not_source) { Accepted { value: arrow, diagnostics: _ } => - match infer(tree: arrow) { + match infer(tree: claim_resolved_tree_without_declarations(root: arrow)) { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } @@ -676,7 +676,7 @@ fn ingested_classical_not_arrow_empty_domain_fixture() -> Node { } fn ingested_classical_not_param_scrutinee_domain_unresolved_infer_refuses(tree: Node) -> Bool { - match infer(tree: tree) { + match infer(tree: claim_resolved_tree_without_declarations(root: tree)) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } diff --git a/src/v2/test/claim/execution/self_host_candidate_generation_stage_verdicts_test.dag b/src/v2/test/claim/execution/self_host_candidate_generation_stage_verdicts_test.dag index eb5f2067359..b1e479c5a59 100644 --- a/src/v2/test/claim/execution/self_host_candidate_generation_stage_verdicts_test.dag +++ b/src/v2/test/claim/execution/self_host_candidate_generation_stage_verdicts_test.dag @@ -1,5 +1,6 @@ module v2.test.execution.self_host_candidate_generation_stage_verdicts +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import std.process { ExitSuccess, ProcessExit, exit_failure } import v2.compiler.self_host.candidate_generation_stage_verdicts { CandidateGenerationStageVerdicts, @@ -46,7 +47,7 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn add_slice_stage_verdicts() -> CandidateGenerationStageVerdicts { candidate_generation_stage_verdicts( - resolved_module: dag_add_emitted_root, + resolved_module: claim_resolved_tree_without_declarations(root: dag_add_emitted_root), dag_target: dag_add_target_model ) } diff --git a/src/v2/test/claim/execution/self_host_candidate_generation_test.dag b/src/v2/test/claim/execution/self_host_candidate_generation_test.dag index 8365267ac9d..5eeee2da069 100644 --- a/src/v2/test/claim/execution/self_host_candidate_generation_test.dag +++ b/src/v2/test/claim/execution/self_host_candidate_generation_test.dag @@ -1,5 +1,6 @@ module v2.test.execution.self_host_candidate_generation +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.self_host.candidate_generation { generate_translate_self_emit_candidate } @@ -130,7 +131,7 @@ fn witness_translate_diagnostic_reason_symbol_inverse() -> Bool { fn candidate_generation_dag_add_slice_accepts() -> Bool { match generate_translate_self_emit_candidate( - resolved_module: dag_add_emitted_root, + resolved_module: claim_resolved_tree_without_declarations(root: dag_add_emitted_root), dag_target: dag_add_target_model ) { Accepted { value: candidate, diagnostics: d } => diff --git a/src/v2/test/claim/infer_list_introduction_test.dag b/src/v2/test/claim/infer_list_introduction_test.dag index 6b50340131c..37bc3dd65af 100644 --- a/src/v2/test/claim/infer_list_introduction_test.dag +++ b/src/v2/test/claim/infer_list_introduction_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.infer_list_introduction +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import std.occurrence_identity { OccurrenceSynthetic } import v2.compiler.infer { infer, infer_branch_int_binding_type_node, inferred_facts_resolved_type } import v2.extdeps.languages.dag { dag_int_literal_fixture_one, dag_type_atom_node } @@ -100,7 +101,7 @@ fn ilt_freemonoid_argument(t: Node) -> Optional { // The literal's derived type, when infer accepts it with one. fn ilt_derived_type(tree: Node) -> Optional { - match infer(tree: tree) { + match infer(tree: claim_resolved_tree_without_declarations(root: tree)) { Rejected { diagnostics: _ } => optional_absent() Accepted { value: t, diagnostics: _ } => match t.facts.lookup(tree) { @@ -158,7 +159,7 @@ test fn a_list_of_equal_lists_infers_to_a_nested_freemonoid() -> Bool { // A STRUCTURAL MISMATCH refuses located AT the differing element: the second inner list (a // FreeMonoid beside a FreeMonoid), not the outer literal or the first element. test fn a_list_of_differently_typed_lists_refuses_at_the_differing_list() -> Bool { - match infer(tree: ilt_nested_mixed) { + match infer(tree: claim_resolved_tree_without_declarations(root: ilt_nested_mixed)) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: r } => (diagnostics_fatal(d: r).reason == ^infer_list_element_type_mismatch) @@ -184,7 +185,7 @@ fn ilt_is_mismatch_at_true(d: Diagnostic) -> Bool { } test fn a_mixed_list_refuses_at_the_differing_element() -> Bool { - match infer(tree: ilt_mixed) { + match infer(tree: claim_resolved_tree_without_declarations(root: ilt_mixed)) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: r } => ilt_is_mismatch_at_true(d: diagnostics_fatal(d: r)) } @@ -195,7 +196,7 @@ test fn a_mixed_list_refuses_at_the_differing_element() -> Bool { // It must still be ACCEPTED: a refusal would also "derive no type", so accepting Rejected here would // let one failure common to every list pass this claim while the other two went red. test fn an_empty_list_derives_no_type_here() -> Bool { - match infer(tree: ilt_empty) { + match infer(tree: claim_resolved_tree_without_declarations(root: ilt_empty)) { Rejected { diagnostics: _ } => false Accepted { value: tree, diagnostics: _ } => match tree.facts.lookup(ilt_empty) { @@ -223,7 +224,7 @@ fn ilt_diagnostic_text(d: Diagnostic) -> String { } fn ilt_outcome_text(tree: Node) -> String { - match infer(tree: tree) { + match infer(tree: claim_resolved_tree_without_declarations(root: tree)) { Rejected { diagnostics: r } => "rejected fatal=" + ilt_diagnostic_text(d: diagnostics_fatal(d: r)) + " head=" + ilt_diagnostic_text(d: r.head) Accepted { value: t, diagnostics: _ } => match t.facts.lookup(tree) { diff --git a/src/v2/test/claim/infer_self_grounding_wall_test.dag b/src/v2/test/claim/infer_self_grounding_wall_test.dag index 0c1f6a3988f..649c9a3a7ac 100644 --- a/src/v2/test/claim/infer_self_grounding_wall_test.dag +++ b/src/v2/test/claim/infer_self_grounding_wall_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.infer_self_grounding_wall +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import extdeps.filesystem.filesystem_io import std.occurrence_identity { OccurrenceSynthetic } @@ -185,7 +186,7 @@ fn wall_derived_grounding(node: Node) -> CanonicalGrounding { } fn wall_grounding_for_node_refused(root: Node) -> Bool { - match infer_and_discharge(tree: root) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: root)) { Rejected { diagnostics: _ } => false Accepted { value: tree, diagnostics: _ } => match canonical_grounding_for_node(tree: tree, node: root) { @@ -196,7 +197,7 @@ fn wall_grounding_for_node_refused(root: Node) -> Bool { } fn wall_resolved_type_refused(root: Node) -> Bool { - match infer_and_discharge(tree: root) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: root)) { Rejected { diagnostics: _ } => false Accepted { value: tree, diagnostics: _ } => match inferred_facts_witness_for_node(tree: tree, node: root) { @@ -211,7 +212,7 @@ fn wall_resolved_type_refused(root: Node) -> Bool { } fn wall_infer_counts_frontier(root: Node) -> Bool { - match infer(tree: root) { + match infer(tree: claim_resolved_tree_without_declarations(root: root)) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: d } => match d { @@ -302,7 +303,7 @@ fn wall_frontier_observation_matches_infer(nes: NonEmptyDiagnostics, o: ProbeObs test fn wall_frontier_receipt_joins_corpus_identity() -> Bool { let root = wall_value_node() - match infer(tree: root) { + match infer(tree: claim_resolved_tree_without_declarations(root: root)) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: d } => match d { @@ -347,7 +348,7 @@ test fn wall_frontier_receipt_joins_corpus_identity() -> Bool { test fn wall_frontier_derived_green_receipt_joins_corpus_identity() -> Bool { let root = dag_pick_if_body_fixture - match infer(tree: root) { + match infer(tree: claim_resolved_tree_without_declarations(root: root)) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: d } => match active_v2_infer_eval_probe_row(probe: v2_frontier_green_probe) { diff --git a/src/v2/test/claim/long/inhabitant_neutralization_e2e_witness_test.dag b/src/v2/test/claim/long/inhabitant_neutralization_e2e_witness_test.dag index 9d04a0b7b05..6b95bb7fce4 100644 --- a/src/v2/test/claim/long/inhabitant_neutralization_e2e_witness_test.dag +++ b/src/v2/test/claim/long/inhabitant_neutralization_e2e_witness_test.dag @@ -1,5 +1,6 @@ module v2.test.long.inhabitant_neutralization_e2e_witness +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import v2.compiler.emit { emit } import v2.compiler.ingest { cross_language_compile } @@ -88,7 +89,7 @@ test fn inhabitant_neutralization_python_to_go_translate_rejects() -> Bool { target: go_target_model() ) { Accepted { value: neutralized, diagnostics: _ } => - match infer_and_discharge(tree: neutralized) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: neutralized)) { Accepted { value: inferred, diagnostics: _ } => match translate(tree: inferred, target: go_target_model()) { Rejected { diagnostics: _ } => true @@ -106,7 +107,7 @@ test fn inhabitant_neutralization_python_to_go_emit_rejects() -> Bool { target: go_target_model() ) { Accepted { value: neutralized, diagnostics: _ } => - match infer_and_discharge(tree: neutralized) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: neutralized)) { Accepted { value: inferred, diagnostics: _ } => match emit(tree: inferred, target: go_target_model()) { Rejected { diagnostics: _ } => true diff --git a/src/v2/test/claim/long/parser_completeness_frontier_test.dag b/src/v2/test/claim/long/parser_completeness_frontier_test.dag index 511a4a23d36..34ee0e404da 100644 --- a/src/v2/test/claim/long/parser_completeness_frontier_test.dag +++ b/src/v2/test/claim/long/parser_completeness_frontier_test.dag @@ -29,7 +29,7 @@ fn parser_frontier_admission() -> Admission { } } -fn parser_frontier_assemble_for(src: String) -> Outcome { +fn parser_frontier_assemble_for(src: String) -> Outcome { assemble_program_from_ingest( ingest: parser_frontier_ingest_for(src: src), admission: parser_frontier_admission(), diff --git a/src/v2/test/claim/long/self_host_module_emit_derisk_test.dag b/src/v2/test/claim/long/self_host_module_emit_derisk_test.dag index cb9fe49f6ec..6351a14afb8 100644 --- a/src/v2/test/claim/long/self_host_module_emit_derisk_test.dag +++ b/src/v2/test/claim/long/self_host_module_emit_derisk_test.dag @@ -1,5 +1,7 @@ module v2.test.long.self_host_module_emit_derisk +import v2.compiler.resolve { ResolvedTree } +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import extdeps.communication.medium { Lossless, Medium } import v2.compiler.name_resolve { @@ -56,7 +58,7 @@ fn derisk_module_admission() -> Admission { } } -fn derisk_assemble_for(src: String) -> Outcome { +fn derisk_assemble_for(src: String) -> Outcome { assemble_program_from_ingest( ingest: derisk_ingest_for(src: src), admission: derisk_module_admission(), diff --git a/src/v2/test/claim/loop_infer_iteration_test.dag b/src/v2/test/claim/loop_infer_iteration_test.dag index e8e5a6707f5..f55c5048dc1 100644 --- a/src/v2/test/claim/loop_infer_iteration_test.dag +++ b/src/v2/test/claim/loop_infer_iteration_test.dag @@ -49,7 +49,7 @@ fn loop_infer_registered_measure() -> Node { } fn loop_infer_run(root: Node) -> Outcome { - infer_and_discharge(tree: root) + infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: root)) } fn loop_infer_loop_type_is_int(tree: InferredTree, body: Node) -> Bool { diff --git a/src/v2/test/claim/manual/branch_infer_fail_open_audit_test.dag b/src/v2/test/claim/manual/branch_infer_fail_open_audit_test.dag index 53a4490b0a7..e261d23793a 100644 --- a/src/v2/test/claim/manual/branch_infer_fail_open_audit_test.dag +++ b/src/v2/test/claim/manual/branch_infer_fail_open_audit_test.dag @@ -1,4 +1,5 @@ module v2.test.manual.branch_infer_fail_open_audit +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.infer { infer } import v2.extdeps.languages.dag { dag_pick_if_body_fixture, @@ -26,7 +27,7 @@ fn branch_infer_fail_open_audit_body() -> Node { } test fn branch_infer_fail_open_audit_rejects_holds() -> Bool { - match infer(tree: branch_infer_fail_open_audit_body()) { + match infer(tree: claim_resolved_tree_without_declarations(root: branch_infer_fail_open_audit_body())) { Accepted { value: _, diagnostics: d } => false Rejected { diagnostics: _ } => true } @@ -34,14 +35,14 @@ test fn branch_infer_fail_open_audit_rejects_holds() -> Bool { } fn branch_infer_fail_open_audit_silent_accept_holds() -> Bool { - match infer(tree: branch_infer_fail_open_audit_body()) { + match infer(tree: claim_resolved_tree_without_declarations(root: branch_infer_fail_open_audit_body())) { Accepted { value: _, diagnostics: d } => d == None Rejected { diagnostics: _ } => false } } test fn branch_infer_fail_open_audit_literal_arm_accepts_holds() -> Bool { - match infer(tree: dag_pick_if_body_fixture) { + match infer(tree: claim_resolved_tree_without_declarations(root: dag_pick_if_body_fixture)) { Accepted { value: _, diagnostics: d } => d == None Rejected { diagnostics: _ } => false } diff --git a/src/v2/test/claim/manual/branch_infer_if_then_else_test.dag b/src/v2/test/claim/manual/branch_infer_if_then_else_test.dag index cfe1729ee2b..2a798ccc7c5 100644 --- a/src/v2/test/claim/manual/branch_infer_if_then_else_test.dag +++ b/src/v2/test/claim/manual/branch_infer_if_then_else_test.dag @@ -1,5 +1,6 @@ module v2.test.manual.branch_infer_if_then_else +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.infer { infer, } @@ -35,7 +36,7 @@ fn branch_infer_arm_mismatch_body() -> Node { } test fn branch_infer_bool_cond_accepts_holds() -> Bool { - match infer(tree: dag_pick_if_body_fixture) { + match infer(tree: claim_resolved_tree_without_declarations(root: dag_pick_if_body_fixture)) { Accepted { value: _, diagnostics: d } => d == None Rejected { diagnostics: _ } => false } @@ -47,7 +48,7 @@ test fn branch_infer_bool_cond_accepts_holds() -> Bool { // remains covered by branch_infer_test using two genuinely derived operand types. test fn branch_infer_underived_arm_refuses_before_mismatch_holds() -> Bool { - match infer(tree: branch_infer_arm_mismatch_body()) { + match infer(tree: claim_resolved_tree_without_declarations(root: branch_infer_arm_mismatch_body())) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: d } => d.head.reason == ^infer_grounding_not_derived } diff --git a/src/v2/test/claim/manual/branch_infer_test.dag b/src/v2/test/claim/manual/branch_infer_test.dag index a7d3a9ca4ed..5567b0a881d 100644 --- a/src/v2/test/claim/manual/branch_infer_test.dag +++ b/src/v2/test/claim/manual/branch_infer_test.dag @@ -65,7 +65,7 @@ fn branch_infer_cond_not_bool_body_node() -> Outcome { } fn branch_infer_resolved_tree(root: Node) -> ResolvedTree { - root + claim_resolved_tree_without_declarations(root: root) } fn branch_infer_positional_target(body: Node, index: Int) -> Node { diff --git a/src/v2/test/claim/manual/cross_language_add_python_to_typescript_test.dag b/src/v2/test/claim/manual/cross_language_add_python_to_typescript_test.dag index 72214a7d766..7f4e1fb7cda 100644 --- a/src/v2/test/claim/manual/cross_language_add_python_to_typescript_test.dag +++ b/src/v2/test/claim/manual/cross_language_add_python_to_typescript_test.dag @@ -1,5 +1,6 @@ module v2.test.manual.cross_language_add_python_to_typescript +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import v2.test.manual.python_grammar_claim { python_grammar_parse_fixture @@ -90,7 +91,7 @@ test fn parse_tree_to_target_model_bridge_realized() -> Bool { } test fn cross_language_emit_inhabitant_neutralization_fails_closed() -> Bool { - match infer_and_discharge(tree: python_emitted_add_fn_node()) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: python_emitted_add_fn_node())) { Accepted { value: inferred, diagnostics: _ } => match emit(tree: inferred, target: ts_target_model()) { Rejected { diagnostics: _ } => true @@ -115,7 +116,7 @@ test fn cross_language_emit_inhabitant_neutralization_round_trip_holds() -> Bool target: ts_target_model() ) { Accepted { value: neutralized, diagnostics: _ } => - match infer_and_discharge(tree: neutralized) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: neutralized)) { Accepted { value: inferred, diagnostics: _ } => match emit(tree: inferred, target: ts_target_model()) { Accepted { value: source, diagnostics: _ } => diff --git a/src/v2/test/claim/manual/infer_bounded_lattice_completeness_anchor_test.dag b/src/v2/test/claim/manual/infer_bounded_lattice_completeness_anchor_test.dag index da366cbb758..227e6771c51 100644 --- a/src/v2/test/claim/manual/infer_bounded_lattice_completeness_anchor_test.dag +++ b/src/v2/test/claim/manual/infer_bounded_lattice_completeness_anchor_test.dag @@ -1,5 +1,6 @@ module v2.test.manual.infer_bounded_lattice_completeness_anchor +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import std.occurrence_identity { OccurrenceSynthetic } import v2.compiler.infer { InferredTree } @@ -176,7 +177,7 @@ data anchor_consumer_gate_rejects_partial_reference: Outcome = infer_bound partials: [anchor_partial_bounded_lattice_instance] ) -data anchor_infer_rejects_consumer_tree: Outcome = infer_and_discharge(tree: anchor_module_with_partial_and_consumer) +data anchor_infer_rejects_consumer_tree: Outcome = infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: anchor_module_with_partial_and_consumer)) fn anchor_infer_consumer_tree_is_rejected() -> Bool { match anchor_infer_rejects_consumer_tree { diff --git a/src/v2/test/claim/manual/infer_emit_compile_anchor.dag b/src/v2/test/claim/manual/infer_emit_compile_anchor.dag index da2d49858c9..8a53f7941cb 100644 --- a/src/v2/test/claim/manual/infer_emit_compile_anchor.dag +++ b/src/v2/test/claim/manual/infer_emit_compile_anchor.dag @@ -43,7 +43,7 @@ data anchor_stub_inferred_tree: InferredTree = InferredTree { } fn anchor_infer_rejects() -> Outcome { - infer_and_discharge(tree: anchor_stub_empty_conj) + infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: anchor_stub_empty_conj)) } fn anchor_emit_rejects() -> Outcome> { diff --git a/src/v2/test/claim/manual/infer_ground_add.dag b/src/v2/test/claim/manual/infer_ground_add.dag index 12ba621e60f..236ff22093b 100644 --- a/src/v2/test/claim/manual/infer_ground_add.dag +++ b/src/v2/test/claim/manual/infer_ground_add.dag @@ -1,5 +1,6 @@ module v2.test.manual.infer_ground_add +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import std.occurrence_identity { OccurrenceSynthetic } import v2.compiler.eval { @@ -324,10 +325,10 @@ fn infer_descent_witness_receipt_run( } } -fn fixture_add_resolved_node() -> Node { - fixture_add_resolved_tree() -} fn fixture_add_resolved_tree() -> ResolvedTree { + claim_resolved_tree_without_declarations(root: fixture_add_resolved_node()) +} +fn fixture_add_resolved_node() -> Node { Node { kind: TypeNode { connective: Conj }, children: [ diff --git a/src/v2/test/claim/manual/ingest_bridge_test.dag b/src/v2/test/claim/manual/ingest_bridge_test.dag index 26f924fc96a..9f7ec970572 100644 --- a/src/v2/test/claim/manual/ingest_bridge_test.dag +++ b/src/v2/test/claim/manual/ingest_bridge_test.dag @@ -1,5 +1,6 @@ module v2.test.manual.ingest_bridge +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import std.occurrence_identity { OccurrenceSynthetic } import v2.std.host_transport { target_emit_host_runtime_row_unconfigured } @@ -229,7 +230,7 @@ test fn ingest_bridge_to_canonical_holds() -> Bool { test fn ingest_identity_coercion_accepts_source_present_in_authored_roster() -> Bool { let source = dag_fixture_emitted_add_fn - match infer_and_discharge(tree: source) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: source)) { Rejected { diagnostics: _ } => false Accepted { value: inferred, diagnostics: _ } => match canonical_grounding_for_node(tree: inferred, node: source) { @@ -253,7 +254,7 @@ test fn ingest_identity_coercion_accepts_source_present_in_authored_roster() -> test fn ingest_identity_coercion_refuses_source_absent_from_authored_roster() -> Bool { let source = ingest_unstamped_node() - match infer_and_discharge(tree: source) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: source)) { Rejected { diagnostics: _ } => false Accepted { value: inferred, diagnostics: _ } => match coerce_grounded_node( diff --git a/src/v2/test/claim/manual/inhabitant_neutralization_test.dag b/src/v2/test/claim/manual/inhabitant_neutralization_test.dag index e0a7078a001..85d252011a9 100644 --- a/src/v2/test/claim/manual/inhabitant_neutralization_test.dag +++ b/src/v2/test/claim/manual/inhabitant_neutralization_test.dag @@ -1,5 +1,6 @@ module v2.test.manual.inhabitant_neutralization +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import v2.compiler.emit { emit } import v2.compiler.ingest { cross_language_compile } @@ -146,7 +147,7 @@ fn inhabitant_neutralization_go_int64_to_ts_emit_round_trip_holds() -> Bool { target: ts_target_model() ) { Accepted { value: neutralized, diagnostics: _ } => - match infer_and_discharge(tree: neutralized) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: neutralized)) { Accepted { value: inferred, diagnostics: _ } => match emit(tree: inferred, target: ts_target_model()) { Accepted { value: source, diagnostics: _ } => source.carried == ts_source_text @@ -191,7 +192,7 @@ test fn inhabitant_neutralization_emit_after_neutralize_round_trip_holds() -> Bo target: ts_target_model() ) { Accepted { value: neutralized, diagnostics: _ } => - match infer_and_discharge(tree: neutralized) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: neutralized)) { Accepted { value: inferred, diagnostics: _ } => match emit(tree: inferred, target: ts_target_model()) { Accepted { value: source, diagnostics: _ } => source.carried == ts_source_text @@ -204,7 +205,7 @@ test fn inhabitant_neutralization_emit_after_neutralize_round_trip_holds() -> Bo } fn inhabitant_neutralization_raw_emit_still_fails_closed() -> Bool { - match infer_and_discharge(tree: python_emitted_add_fn_node()) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: python_emitted_add_fn_node())) { Accepted { value: inferred, diagnostics: _ } => match emit(tree: inferred, target: ts_target_model()) { Rejected { diagnostics: _ } => true @@ -228,7 +229,7 @@ fn inhabitant_neutralization_same_flavor_python_emit_round_trip_holds() -> Bool target: python_target_model() ) { Accepted { value: neutralized, diagnostics: _ } => - match infer_and_discharge(tree: neutralized) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: neutralized)) { Accepted { value: inferred, diagnostics: _ } => match emit(tree: inferred, target: python_target_model()) { Accepted { value: source, diagnostics: _ } => source.carried == python_source_text diff --git a/src/v2/test/claim/manual/match_infer_fail_open_audit_test.dag b/src/v2/test/claim/manual/match_infer_fail_open_audit_test.dag index e83fd9f5012..94bd81fee8e 100644 --- a/src/v2/test/claim/manual/match_infer_fail_open_audit_test.dag +++ b/src/v2/test/claim/manual/match_infer_fail_open_audit_test.dag @@ -1,5 +1,6 @@ module v2.test.manual.match_infer_fail_open_audit +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import std.occurrence_identity { OccurrenceSynthetic } import v2.compiler.infer { infer } import v2.extdeps.languages.dag { @@ -78,7 +79,7 @@ fn match_infer_fail_open_audit_body() -> Node { } test fn match_infer_fail_open_audit_rejects_holds() -> Bool { - match infer(tree: match_infer_fail_open_audit_body()) { + match infer(tree: claim_resolved_tree_without_declarations(root: match_infer_fail_open_audit_body())) { Accepted { value: _, diagnostics: d } => false Rejected { diagnostics: _ } => true } @@ -105,7 +106,7 @@ fn match_infer_fail_open_audit_non_exhaustive_body() -> Node { } test fn match_infer_fail_open_audit_non_exhaustive_rejects_holds() -> Bool { - match infer(tree: match_infer_fail_open_audit_non_exhaustive_body()) { + match infer(tree: claim_resolved_tree_without_declarations(root: match_infer_fail_open_audit_non_exhaustive_body())) { Accepted { value: _, diagnostics: d } => false Rejected { diagnostics: _ } => true } @@ -146,7 +147,7 @@ fn match_infer_fail_open_audit_extra_arm_body() -> Node { } test fn match_infer_fail_open_audit_extra_arm_rejects_holds() -> Bool { - match infer(tree: match_infer_fail_open_audit_extra_arm_body()) { + match infer(tree: claim_resolved_tree_without_declarations(root: match_infer_fail_open_audit_extra_arm_body())) { Accepted { value: _, diagnostics: d } => false Rejected { diagnostics: _ } => true } @@ -185,14 +186,14 @@ fn classical_not_int_match_arrow_fixture() -> Node { } test fn complement_body_real_infer_refuses_unsupported_pattern_holds() -> Bool { - match infer(tree: dag_complement_body_fixture) { + match infer(tree: claim_resolved_tree_without_declarations(root: dag_complement_body_fixture)) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } } test fn classical_not_int_match_arrow_real_infer_holds() -> Bool { - match infer(tree: classical_not_int_match_arrow_fixture()) { + match infer(tree: claim_resolved_tree_without_declarations(root: classical_not_int_match_arrow_fixture())) { Accepted { value: _, diagnostics: d } => diagnostics_contain_reason( d: d, @@ -230,7 +231,7 @@ fn classical_not_int_match_non_octet_arm_fixture() -> Node { } test fn classical_not_int_match_non_octet_arm_rejects_holds() -> Bool { - match infer(tree: classical_not_int_match_non_octet_arm_fixture()) { + match infer(tree: claim_resolved_tree_without_declarations(root: classical_not_int_match_non_octet_arm_fixture())) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } @@ -243,7 +244,7 @@ test fn classical_not_int_match_non_octet_arm_rejects_holds() -> Bool { // complement_arrow_real_infer_holds. test fn complement_arrow_real_infer_refuses_unsupported_pattern_holds() -> Bool { - match infer(tree: dag_complement_arrow_with_body_fixture) { + match infer(tree: claim_resolved_tree_without_declarations(root: dag_complement_arrow_with_body_fixture)) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } diff --git a/src/v2/test/claim/name_resolve/one_member_cost_probe_test.dag b/src/v2/test/claim/name_resolve/one_member_cost_probe_test.dag index 5237c01b16c..f90f52e550f 100644 --- a/src/v2/test/claim/name_resolve/one_member_cost_probe_test.dag +++ b/src/v2/test/claim/name_resolve/one_member_cost_probe_test.dag @@ -104,7 +104,7 @@ fn probe_resolve(roots: FreeMonoid) -> Outcome { ) } -fn probe_resolved_export_identity(tree: ResolvedTree, wanted: Symbol) -> Bool { +fn probe_resolved_export_identity(tree: Node, wanted: Symbol) -> Bool { match tree.kind { TypeNode { connective: Atom { identity: id } } => id == wanted _ => @@ -119,7 +119,7 @@ test fn one_member_module_resolves_under_full_language_model() -> Bool { match probe_resolve(roots: roots) { Accepted { value: resolved, diagnostics: _ } => probe_resolved_export_identity( - tree: resolved, + tree: resolved.root, wanted: ^dag_c3_surface_sugar_service ) Rejected { diagnostics: _ } => false diff --git a/src/v2/test/claim/refinement_discharge_test.dag b/src/v2/test/claim/refinement_discharge_test.dag index fc89b849fc8..db6543a851d 100644 --- a/src/v2/test/claim/refinement_discharge_test.dag +++ b/src/v2/test/claim/refinement_discharge_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.refinement_discharge +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.infer { infer } import v2.compiler.inferred_tree { InferredTree, ObligatedInferredTree, RefinementObligation } import v2.compiler.refinement_discharge { discharge_refinement_obligations } @@ -58,7 +59,7 @@ fn rdt_false() -> Node { // infer's own output over the application, with ONE supplied obligation naming it. fn rdt_obligated(body: Node) -> Optional { let app = rdt_application(body: body) - match infer(tree: app) { + match infer(tree: claim_resolved_tree_without_declarations(root: app)) { Rejected { diagnostics: _ } => optional_absent() Accepted { value: t, diagnostics: _ } => optional_present(value: ObligatedInferredTree { @@ -115,7 +116,7 @@ test fn rdt_an_unevaluable_application_refuses_undischargeable_whatever_its_body // (2) ZERO OBLIGATIONS: the identity, and the cost path every module takes today. test fn rdt_zero_obligations_discharge_to_the_same_tree() -> Bool { - match infer(tree: rdt_application(body: rdt_true())) { + match infer(tree: claim_resolved_tree_without_declarations(root: rdt_application(body: rdt_true()))) { Rejected { diagnostics: _ } => false Accepted { value: t, diagnostics: _ } => match discharge_refinement_obligations(t: t) { diff --git a/src/v2/test/claim/translate_underived_refusal_test.dag b/src/v2/test/claim/translate_underived_refusal_test.dag index fe05e333a5c..d1fd69ca049 100644 --- a/src/v2/test/claim/translate_underived_refusal_test.dag +++ b/src/v2/test/claim/translate_underived_refusal_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.translate_underived_refusal +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import std.occurrence_identity { OccurrenceSynthetic } import v2.compiler.emit { emit } @@ -128,7 +129,7 @@ fn translate_underived_value_node() -> Node { } fn translate_underived_value_specimen_facts() -> Optional { - match infer_and_discharge(tree: translate_underived_value_specimen) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: translate_underived_value_specimen)) { Accepted { value: tree, diagnostics: _ } => tree.facts.lookup(translate_underived_value_specimen) Rejected { diagnostics: _ } => optional_absent() @@ -184,7 +185,7 @@ fn translate_bodyless_emission_root_with_poison_child_root() -> Node { } fn translate_bodyless_emission_root_facts_for(node: Node) -> Optional { - match infer_and_discharge(tree: python_emitted_add_fn_node()) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: python_emitted_add_fn_node())) { Accepted { value: tree, diagnostics: _ } => tree.facts.lookup(node) Rejected { diagnostics: _ } => optional_absent() } @@ -314,7 +315,7 @@ data translate_underived_grammar_match_target: TargetModel = TargetModel { } fn translate_refuses_underived_root_for_target(root: Node, target: TargetModel) -> Bool { - match infer_and_discharge(tree: root) { + match infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: root)) { Rejected { diagnostics: _ } => false Accepted { value: tree, diagnostics: _ } => match translate(tree: tree, target: target) { diff --git a/src/v2/test/claim/type_param_binder_frame_test.dag b/src/v2/test/claim/type_param_binder_frame_test.dag index 2a5c1b76dcd..810f8c1caf5 100644 --- a/src/v2/test/claim/type_param_binder_frame_test.dag +++ b/src/v2/test/claim/type_param_binder_frame_test.dag @@ -1,5 +1,7 @@ module v2.test.claim.type_param_binder_frame +import v2.compiler.resolve { ResolvedTree } +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import extdeps.communication.medium { Lossless, Medium } import v2.compiler.name_resolve { Admission, ResolutionSubject } import v2.compiler.program_assembly { assemble_program_from_ingest } @@ -84,7 +86,7 @@ data tpb_artifact: Artifact = Artifact { file_path: "src/v2/pilot/type_param_binder_frame_pilot.dag" } -fn tpb_assemble(src: String) -> Outcome { +fn tpb_assemble(src: String) -> Outcome { assemble_program_from_ingest( ingest: Cons { head: DagSourceReadWitness { @@ -106,14 +108,14 @@ fn tpb_assemble(src: String) -> Outcome { // Each tpb_* specimen below is nullary and pure over an inline source -- Nodes and located // diagnostics, no closure -- and is enrolled in v2.workflow.floor_pure_producer_share, so // preparation runs the real route once per specimen and the rows read it. -fn tpb_accepts(o: Outcome) -> Bool { +fn tpb_accepts(o: Outcome) -> Bool { match o { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } } -fn tpb_refuses_with(o: Outcome, reason: Symbol) -> Bool { +fn tpb_refuses_with(o: Outcome, reason: Symbol) -> Bool { match o { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == reason @@ -121,11 +123,11 @@ fn tpb_refuses_with(o: Outcome, reason: Symbol) -> Bool { } // The declared type-parameter names of every Arrow in the resolved tree, in walk order. -fn tpb_type_param_rosters(o: Outcome) -> List> { +fn tpb_type_param_rosters(o: Outcome) -> List> { match o { Rejected { diagnostics: _ } => [] Accepted { value: n, diagnostics: _ } => - fold(node_subtree_nodes(root: n), init: [], f: fn(acc, m) { + fold(node_subtree_nodes(root: n.root), init: [], f: fn(acc, m) { match m.kind { TypeNode { connective: Arrow } => if count(type_param_names(n: m)) == 0 { acc } else { concat(acc, [type_param_names(n: m)]) } @@ -135,35 +137,35 @@ fn tpb_type_param_rosters(o: Outcome) -> List> { } } -fn tpb_identity() -> Outcome { +fn tpb_identity() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: T) -> T { x }\n") } -fn tpb_colliding() -> Outcome { +fn tpb_colliding() -> Outcome { tpb_assemble(src: "module p\n\ntype T = Int\n\nfn f(x: T) -> T { x }\n") } -fn tpb_used_outside() -> Outcome { +fn tpb_used_outside() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: T) -> T { x }\n\nfn h(y: T) -> Int { 1 }\n") } -fn tpb_same_name_twice() -> Outcome { +fn tpb_same_name_twice() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: T) -> T { x }\n\nfn g(y: T) -> T { y }\n") } -fn tpb_sibling_binder_leak() -> Outcome { +fn tpb_sibling_binder_leak() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: T) -> T { x }\n\nfn g(y: T) -> U { y }\n") } -fn tpb_two_params() -> Outcome { +fn tpb_two_params() -> Outcome { tpb_assemble(src: "module p\n\nfn first(a: A, b: B) -> A { a }\n") } -fn tpb_undeclared_type_name() -> Outcome { +fn tpb_undeclared_type_name() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: V) -> T { x }\n") } -fn tpb_non_generic() -> Outcome { +fn tpb_non_generic() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: Int) -> Int { x }\n") } @@ -213,7 +215,7 @@ test fn tpb_non_generic_fn_carries_no_type_params() -> Bool { tpb_accepts(o: tpb_non_generic()) && tpb_type_param_rosters(o: tpb_non_generic()) == [] } -fn tpb_type_param_as_value() -> Outcome { +fn tpb_type_param_as_value() -> Outcome { tpb_assemble(src: "module p\n\nfn f(x: T) -> T { T }\n") } @@ -292,10 +294,10 @@ test fn tpb_generic_arrow_is_well_formed() -> Bool { // (9) An argument at a type-variable formal instantiates it. RED ON MAIN: judged as the ordinary // declared type `T`, the Int argument refused application_argument_does_not_inhabit. test fn tpb_argument_at_a_type_variable_instantiates_it() -> Bool { - match infer(tree: tpb_apply( + match infer(tree: claim_resolved_tree_without_declarations(root: tpb_apply( arrow: tpb_generic_arrow(type_params: [^T], formals: [tpb_formal(name: ^x, declared: ^T)]), args: [dag_int_literal_fixture_one()] - )) { + ))) { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } @@ -304,10 +306,10 @@ test fn tpb_argument_at_a_type_variable_instantiates_it() -> Bool { // (10) A type variable is ONE type per application: `f(x: T, y: T)` applied to an Int and a // Bool refuses at the second argument. Without it, (9) could be green by admitting anything. test fn tpb_second_occurrence_must_inhabit_the_instance() -> Bool { - match infer(tree: tpb_apply( + match infer(tree: claim_resolved_tree_without_declarations(root: tpb_apply( arrow: tpb_generic_arrow(type_params: [^T], formals: [tpb_formal(name: ^x, declared: ^T), tpb_formal(name: ^y, declared: ^T)]), args: [dag_int_literal_fixture_one(), tpb_true()] - )) { + ))) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: d } => diagnostics_has_reason(d: Some { diagnostics: d }, reason: ^application_argument_does_not_inhabit) @@ -316,10 +318,10 @@ test fn tpb_second_occurrence_must_inhabit_the_instance() -> Bool { // (11) Distinct binders do not alias: `f(x: T, y: U)` admits an Int and a Bool. test fn tpb_distinct_type_variables_do_not_alias() -> Bool { - match infer(tree: tpb_apply( + match infer(tree: claim_resolved_tree_without_declarations(root: tpb_apply( arrow: tpb_generic_arrow(type_params: [^T, ^U], formals: [tpb_formal(name: ^x, declared: ^T), tpb_formal(name: ^y, declared: ^U)]), args: [dag_int_literal_fixture_one(), tpb_true()] - )) { + ))) { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } @@ -328,10 +330,10 @@ test fn tpb_distinct_type_variables_do_not_alias() -> Bool { // (12) The type variable is the callee's OWN: an Arrow declaring no `T` still judges `x: T` as the // ordinary declared type, and the Int argument refuses. test fn tpb_undeclared_t_is_not_a_type_variable() -> Bool { - match infer(tree: tpb_apply( + match infer(tree: claim_resolved_tree_without_declarations(root: tpb_apply( arrow: tpb_generic_arrow(type_params: [^U], formals: [tpb_formal(name: ^x, declared: ^T)]), args: [dag_int_literal_fixture_one()] - )) { + ))) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: d } => diagnostics_has_reason(d: Some { diagnostics: d }, reason: ^application_argument_does_not_inhabit) @@ -406,11 +408,11 @@ test fn tpb_emitter_refuses_a_generic_arrow() -> Bool { // The declared type-parameter names of every non-Arrow node in the resolved tree, in walk order: // on a type declaration, the member that carries them. -fn tpb_member_type_param_rosters(o: Outcome) -> List> { +fn tpb_member_type_param_rosters(o: Outcome) -> List> { match o { Rejected { diagnostics: _ } => [] Accepted { value: n, diagnostics: _ } => - fold(node_subtree_nodes(root: n), init: [], f: fn(acc, m) { + fold(node_subtree_nodes(root: n.root), init: [], f: fn(acc, m) { match m.kind { TypeNode { connective: Arrow } => acc _ => if count(type_param_names(n: m)) == 0 { acc } else { concat(acc, [type_param_names(n: m)]) } @@ -419,27 +421,27 @@ fn tpb_member_type_param_rosters(o: Outcome) -> List> { } } -fn tpb_type_decl_coproduct() -> Outcome { +fn tpb_type_decl_coproduct() -> Outcome { tpb_assemble(src: "module p\n\ntype Box = Full { value: T } | Empty\n") } -fn tpb_type_decl_coproduct_colliding() -> Outcome { +fn tpb_type_decl_coproduct_colliding() -> Outcome { tpb_assemble(src: "module p\n\ntype T = Int\n\ntype Box = Full { value: T } | Empty\n") } -fn tpb_type_decl_record() -> Outcome { +fn tpb_type_decl_record() -> Outcome { tpb_assemble(src: "module p\n\ntype R { f: T }\n") } -fn tpb_type_decl_record_two() -> Outcome { +fn tpb_type_decl_record_two() -> Outcome { tpb_assemble(src: "module p\n\ntype Pair { left: A, right: B }\n") } -fn tpb_type_decl_leak() -> Outcome { +fn tpb_type_decl_leak() -> Outcome { tpb_assemble(src: "module p\n\ntype Box = Full { value: T } | Empty\n\ntype S { g: T }\n") } -fn tpb_type_decl_non_generic() -> Outcome { +fn tpb_type_decl_non_generic() -> Outcome { tpb_assemble(src: "module p\n\ntype B = Full { value: Int } | Empty\n") } @@ -514,11 +516,11 @@ fn tpb_box_member_mixed() -> Node { ) } -fn tpb_type_decl_emit_twin() -> Outcome { +fn tpb_type_decl_emit_twin() -> Outcome { translate_type_expression_project(node: tpb_box_member(with_params: false), target: rust_target_model(), projection: rust_type_expression_projection()) } -fn tpb_type_decl_emit_generic() -> Outcome { +fn tpb_type_decl_emit_generic() -> Outcome { translate_type_expression_project(node: tpb_box_member(with_params: true), target: rust_target_model(), projection: rust_type_expression_projection()) } @@ -537,11 +539,11 @@ test fn tpb_translator_refuses_a_generic_type_decl_member() -> Bool { // The member of the first generic type declaration in the resolved tree: the first non-Arrow node // carrying type binders is its wrapper, read through the one typed unwrap. -fn tpb_generic_member(o: Outcome) -> List { +fn tpb_generic_member(o: Outcome) -> List { match o { Rejected { diagnostics: _ } => [] Accepted { value: n, diagnostics: _ } => - fold(node_subtree_nodes(root: n), init: [], f: fn(acc, m) { + fold(node_subtree_nodes(root: n.root), init: [], f: fn(acc, m) { match m.kind { TypeNode { connective: Arrow } => acc _ => @@ -623,44 +625,44 @@ test fn tpb_semantic_decl_emitter_refuses_a_generic_member() -> Bool { && tpb_semantic_emit_generic_reason() == ^translate_reason_generic_type_decl_not_rendered } -fn tpb_type_decl_generic_alias() -> Outcome { +fn tpb_type_decl_generic_alias() -> Outcome { tpb_assemble(src: "module p\n\ntype Id = T\n") } -fn tpb_type_decl_generic_alias_instantiation() -> Outcome { +fn tpb_type_decl_generic_alias_instantiation() -> Outcome { tpb_assemble(src: "module p\n\ntype Maybe = Some { v: A } | None\n\ntype Box = Maybe\n") } -fn tpb_type_decl_generic_alias_colliding() -> Outcome { +fn tpb_type_decl_generic_alias_colliding() -> Outcome { tpb_assemble(src: "module p\n\ntype T = Int\n\ntype Id = T\n") } -fn tpb_type_decl_generic_alias_undeclared() -> Outcome { +fn tpb_type_decl_generic_alias_undeclared() -> Outcome { tpb_assemble(src: "module p\n\ntype Id = Q\n") } -fn tpb_type_decl_generic_opaque() -> Outcome { +fn tpb_type_decl_generic_opaque() -> Outcome { tpb_assemble(src: "module p\n\ntype W\n") } -fn tpb_type_decl_generic_single_variant_after_separator() -> Outcome { +fn tpb_type_decl_generic_single_variant_after_separator() -> Outcome { tpb_assemble(src: "module p\n\ntype X =\n | Only\n") } -fn tpb_type_decl_plain_single_variant_after_separator() -> Outcome { +fn tpb_type_decl_plain_single_variant_after_separator() -> Outcome { tpb_assemble(src: "module p\n\ntype S =\n | Only\n") } -fn tpb_type_decl_plain_single_alias() -> Outcome { +fn tpb_type_decl_plain_single_alias() -> Outcome { tpb_assemble(src: "module p\n\ntype S = Int\n") } // Whether the resolved tree holds a Disj with exactly one arm, labelled `label`. -fn tpb_has_one_arm_disj(o: Outcome, label: Symbol) -> Bool { +fn tpb_has_one_arm_disj(o: Outcome, label: Symbol) -> Bool { match o { Rejected { diagnostics: _ } => false Accepted { value: n, diagnostics: _ } => - fold(node_subtree_nodes(root: n), init: false, f: fn(acc, m) { + fold(node_subtree_nodes(root: n.root), init: false, f: fn(acc, m) { acc || match m.kind { TypeNode { connective: Disj } => (count(m.children) == 1) && (tpb_arm_labels(members: [m]) == [label]) @@ -671,11 +673,11 @@ fn tpb_has_one_arm_disj(o: Outcome, label: Symbol) -> Bool { } // The v2.std.type_binder view of every generic declaration target in the resolved tree, as a tag. -fn tpb_generic_view_tags(o: Outcome) -> List { +fn tpb_generic_view_tags(o: Outcome) -> List { match o { Rejected { diagnostics: _ } => [] Accepted { value: n, diagnostics: _ } => - fold(node_subtree_nodes(root: n), init: [], f: fn(acc, m) { + fold(node_subtree_nodes(root: n.root), init: [], f: fn(acc, m) { match m.kind { TypeNode { connective: Arrow } => acc _ => @@ -790,7 +792,7 @@ test fn tpb_wrapper_with_no_view_arm_is_refused() -> Bool { && type_binder_labels_conform(root: type_alias_wrapper(binders: tpb_t_binders(), aliased: dag_type_atom_node(identity: ^T))) } -fn tpb_type_decl_nullary_tag() -> Outcome { +fn tpb_type_decl_nullary_tag() -> Outcome { tpb_assemble(src: "module p\n\ntype Tag = A | B\n") } diff --git a/src/v2/test/compiler/pipeline/stage_bridge.dag b/src/v2/test/compiler/pipeline/stage_bridge.dag index 251550c585e..ed668e38e2d 100644 --- a/src/v2/test/compiler/pipeline/stage_bridge.dag +++ b/src/v2/test/compiler/pipeline/stage_bridge.dag @@ -1,5 +1,6 @@ module v2.test.compiler.pipeline.stage_bridge +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import v2.compiler.refinement_discharge { infer_and_discharge } import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.infer { InferredTree } @@ -128,8 +129,8 @@ fn pipeline_resolved_source(source: String, file: Symbol) -> Outcome { fn pipeline_inferred_arrow_from_resolved(resolved: Node) -> Outcome { match pipeline_find_arrow_in_module(root: resolved) { - Present { value: arrow } => infer_and_discharge(tree: arrow) - Absent => infer_and_discharge(tree: resolved) + Present { value: arrow } => infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: arrow)) + Absent => infer_and_discharge(tree: claim_resolved_tree_without_declarations(root: resolved)) } } diff --git a/src/v2/test/lens_application/rejecting_lens_blocks_before_compile_test.dag b/src/v2/test/lens_application/rejecting_lens_blocks_before_compile_test.dag index 055fe2f8168..bd5d7e3cf7d 100644 --- a/src/v2/test/lens_application/rejecting_lens_blocks_before_compile_test.dag +++ b/src/v2/test/lens_application/rejecting_lens_blocks_before_compile_test.dag @@ -1,5 +1,6 @@ module v2.test.lens_application.rejecting_lens_blocks_before_compile +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations } import std.occurrence_identity { OccurrenceSynthetic } import v2.std.host_transport { target_emit_host_runtime_row_unconfigured } import std.decl_ref { decl_ref } @@ -66,7 +67,7 @@ data rejecting_lens_kernel_ambient_source: Node = Node { test fn rejecting_lens_blocks_before_compile_claim_holds() -> Bool { match validate_then_compile( - source: rejecting_lens_kernel_ambient_source, + source: claim_resolved_tree_without_declarations(root: rejecting_lens_kernel_ambient_source), lenses: [rejecting_lens], mode: TranslateTo { target: rejecting_lens_target_model_stub } ) { diff --git a/src/v2/test/lens_common/infer_fixture.dag b/src/v2/test/lens_common/infer_fixture.dag index 3e27c51817d..4d0a9f887ca 100644 --- a/src/v2/test/lens_common/infer_fixture.dag +++ b/src/v2/test/lens_common/infer_fixture.dag @@ -1,6 +1,8 @@ module v2.test.lens_common.infer_fixture import std.occurrence_identity { OccurrenceSynthetic } +import v2.compiler.resolve { ResolvedTree } +import v2.std.symbol_index { empty_symbol_index } import v2.compiler.infer { DerivedGrounding, InferredFacts, InferredTree, infer_facts_lookup_miss_diagnostic } import v2.std.cardinality { RankingComponent, TerminationProof } import v2.std.collection { Map } @@ -12,6 +14,14 @@ import v2.std.node { Atom, Node, Symbol, TypeNode } import v2.std.optional { Present } import v2.std.witness { Holds, StructuralPropertyWitness, Witness, witness_from_optional } +// A HAND-BUILT TREE SUPPLIED AT INFER'S INTERFACE (DESIGN section 3: a witness supplies its input +// rather than executing the layers beneath it). infer takes resolve's output, which carries the index +// resolution consulted; a supplied tree was never resolved, so it carries an index holding NO +// declarations -- the name says so, and a declaration lookup through it finds nothing and refuses. +fn claim_resolved_tree_without_declarations(root: Node) -> ResolvedTree { + ResolvedTree { root: root, symbol_index: empty_symbol_index() } +} + fn claim_atom_node(s: Symbol) -> Node { Node { kind: TypeNode { connective: Atom { identity: s } }, diff --git a/src/v2/test/lens_fact_density/hollow_alias_nested_rejected_test.dag b/src/v2/test/lens_fact_density/hollow_alias_nested_rejected_test.dag index 313cb6850c8..ef22d862c53 100644 --- a/src/v2/test/lens_fact_density/hollow_alias_nested_rejected_test.dag +++ b/src/v2/test/lens_fact_density/hollow_alias_nested_rejected_test.dag @@ -9,7 +9,7 @@ import v2.compiler.compile { import v2.std.diagnostic { Accepted, Rejected } import v2.std.logic { Bool } import v2.std.node { Atom, Conj, Edge, Named, Node, Symbol, TypeNode } -import v2.test.lens_common.infer_fixture { claim_atom_node } +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations, claim_atom_node } import v2.test.lens_application.empty_required_lenses_skip_gate { non_empty_diagnostics_contain_reason } @@ -35,7 +35,7 @@ data hollow_alias_nested_authority: Node = claim_atom_node(s: ^hollow_alias_nest test fn hollow_alias_nested_rejected_holds() -> Bool { match validate_then_compile( - source: hollow_alias_nested_root, + source: claim_resolved_tree_without_declarations(root: hollow_alias_nested_root), lenses: [], mode: Eval { runtime: hollow_alias_nested_root } ) { diff --git a/src/v2/test/lens_fact_density/hollow_alias_vtc_empty_lenses_rejected_test.dag b/src/v2/test/lens_fact_density/hollow_alias_vtc_empty_lenses_rejected_test.dag index f0e1faed9ad..b83d01fa0ad 100644 --- a/src/v2/test/lens_fact_density/hollow_alias_vtc_empty_lenses_rejected_test.dag +++ b/src/v2/test/lens_fact_density/hollow_alias_vtc_empty_lenses_rejected_test.dag @@ -9,7 +9,7 @@ import v2.compiler.compile { import v2.std.diagnostic { Accepted, Rejected } import v2.std.logic { Bool } import v2.std.node { Atom, Node, Symbol, TypeNode } -import v2.test.lens_common.infer_fixture { claim_atom_node } +import v2.test.lens_common.infer_fixture { claim_resolved_tree_without_declarations, claim_atom_node } import v2.test.lens_application.empty_required_lenses_skip_gate { non_empty_diagnostics_contain_reason } @@ -24,7 +24,7 @@ data hollow_alias_el_authority: Node = claim_atom_node(s: ^hollow_alias_el_symbo test fn hollow_alias_vtc_empty_lenses_rejected_holds() -> Bool { match validate_then_compile( - source: hollow_alias_el_root, + source: claim_resolved_tree_without_declarations(root: hollow_alias_el_root), lenses: [], mode: Eval { runtime: hollow_alias_el_root } ) { diff --git a/src/v2/workflow/dag_acceptance.dag b/src/v2/workflow/dag_acceptance.dag index da6340baf8d..af649fb5f62 100644 --- a/src/v2/workflow/dag_acceptance.dag +++ b/src/v2/workflow/dag_acceptance.dag @@ -40,6 +40,7 @@ import v2.std.logic { Bool } import v2.std.node { Node, Symbol, node_subtree_count } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.compiler.refinement_discharge { infer_and_discharge } +import v2.compiler.resolve { ResolvedTree } // STAGES MINT EVIDENCE, A CONTRACT MINTS ACCEPTANCE. Given a candidate .dag SOURCE STRING this // authority answers how far it got and why it stopped, as typed per-stage evidence adjudicated by a @@ -455,7 +456,7 @@ type TranslationOutputRow { type AcceptanceRunState { rows: List, - resolved: Optional, + resolved: Optional, inferred: Optional, translations: List, spent: Millisecond, @@ -584,7 +585,7 @@ fn front_end_declared_cost( // individually would be a second story about how the work is performed: they run inside one staged // fold, so a per-stage refusal after the fold ran would report a saving never made. -fn front_end_run_resolved(run: FrontEndRun) -> Optional { +fn front_end_run_resolved(run: FrontEndRun) -> Optional { match run.completion { FrontEndResolvedModule { resolved: n } => optional_present(value: n) FrontEndBoundReached { last: _ } => optional_absent() diff --git a/src/v2/workflow/realization_attempt.dag b/src/v2/workflow/realization_attempt.dag index ac81184cb80..cffc39a8e19 100644 --- a/src/v2/workflow/realization_attempt.dag +++ b/src/v2/workflow/realization_attempt.dag @@ -2,6 +2,7 @@ module v2.workflow.realization_attempt import v2.compiler.ingested_fixture_arrows { ingested_resolved_module_from_source } import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.resolve { ResolvedTree } import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } import v2.compiler.name_resolve { Admission, ResolutionSubject, Import, ImportVisible } import v2.compiler.infer { InferredTree } @@ -154,7 +155,7 @@ fn attempt_entry_source(entry: String, source: String) -> EntryRealizationAttemp located: first_located_file(ds: ds) ) Accepted { value: resolved, diagnostics: _ } => { - let arrows = collect_arrows(root: resolved, acc: []) + let arrows = collect_arrows(root: resolved.root, acc: []) if length(xs: arrows) == 0 { attempt_refused(entry: entry, phase: PhaseTranslate, cause: ^realization_attempt_no_arrow_declarations) } else { @@ -273,8 +274,9 @@ fn phase_for_reason(reason: Symbol, fallback: RealizationPhase) -> RealizationPh } } -fn attempt_joined(program: Node, fn_name: Symbol, entry: String) -> EntryRealizationAttempt { - let joined = decl_edges_named(root: program, fn_name: fn_name, acc: []) +// decl is a subtree of the resolved program, resolved under the program's own index. +fn attempt_joined(program: ResolvedTree, fn_name: Symbol, entry: String) -> EntryRealizationAttempt { + let joined = decl_edges_named(root: program.root, fn_name: fn_name, acc: []) if length(xs: joined) == 0 { attempt_refused(entry: entry, phase: PhaseResolve, cause: ^realization_attempt_identity_absent) } else if length(xs: joined) > 1 { @@ -283,7 +285,7 @@ fn attempt_joined(program: Node, fn_name: Symbol, entry: String) -> EntryRealiza match list_at_optional(xs: joined, index: 0) { Absent => attempt_refused(entry: entry, phase: PhaseResolve, cause: ^realization_attempt_identity_absent) Present { value: decl } => - match infer_and_discharge(tree: decl) { + match infer_and_discharge(tree: ResolvedTree { root: decl, symbol_index: program.symbol_index }) { Rejected { diagnostics: ds } => attempt_refused_at(entry: entry, phase: PhaseInfer, cause: first_located_cause(ds: ds), located: first_located_file(ds: ds)) Accepted { value: inferred, diagnostics: _ } => From 95856efdaf9328db8ca299fe747232084b8328cb Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 27 Sep 2026 19:34:15 +0000 Subject: [PATCH 06/90] pick_ingested: the arrow extractor returns the arrow Node (my retype over-reached) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../pick_ingested_structural_lowering_test.dag | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/v2/test/claim/execution/long/pick_ingested_structural_lowering_test.dag b/src/v2/test/claim/execution/long/pick_ingested_structural_lowering_test.dag index fee53ce956c..ea85b998247 100644 --- a/src/v2/test/claim/execution/long/pick_ingested_structural_lowering_test.dag +++ b/src/v2/test/claim/execution/long/pick_ingested_structural_lowering_test.dag @@ -282,7 +282,7 @@ fn pick_ingested_resolved_module_from_source(source: String) -> Outcome Outcome { +fn pick_ingested_arrow_from_source(source: String) -> Outcome { bind_outcome( o: pick_ingested_resolved_module_from_source(source: source), f: fn(resolved) { @@ -409,7 +409,7 @@ test fn pick_ingested_pick_true_executes_holds() -> Bool { Accepted { value: arrow, diagnostics: _ } => pick_eval_run_passes( run: pick_eval_run( - callee: arrow.root, + callee: arrow, expected_lexeme: "1" ) ) @@ -422,7 +422,7 @@ test fn pick_ingested_pick_false_executes_holds() -> Bool { Accepted { value: arrow, diagnostics: _ } => pick_eval_run_passes( run: pick_eval_run( - callee: arrow.root, + callee: arrow, expected_lexeme: "2" ) ) @@ -435,7 +435,7 @@ test fn pick_ingested_swapped_arms_red_holds() -> Bool { Accepted { value: arrow, diagnostics: _ } => pick_eval_run_passes( run: pick_eval_run( - callee: arrow.root, + callee: arrow, expected_lexeme: "1" ) ) == false @@ -466,7 +466,7 @@ test fn pick2_ingested_pick_true_executes_holds() -> Bool { Accepted { value: arrow, diagnostics: _ } => pick_eval_run_passes( run: pick_eval_run( - callee: arrow.root, + callee: arrow, expected_lexeme: "3" ) ) @@ -479,7 +479,7 @@ test fn pick2_ingested_pick_false_executes_holds() -> Bool { Accepted { value: arrow, diagnostics: _ } => pick_eval_run_passes( run: pick_eval_run( - callee: arrow.root, + callee: arrow, expected_lexeme: "4" ) ) @@ -492,7 +492,7 @@ test fn pick2_ingested_swapped_arms_red_holds() -> Bool { Accepted { value: arrow, diagnostics: _ } => pick_eval_run_passes( run: pick_eval_run( - callee: arrow.root, + callee: arrow, expected_lexeme: "3" ) ) == false From 3c43400b1ad52b40004a3393f02c5fd927db33c3 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 04:09:57 +0000 Subject: [PATCH 07/90] A reference to a corpus declaration is typed by that declaration THE GAP. v2.compiler.infer's infer_node_facts routes a declaration reference -- Conj-shaped, so infer_atom_binding_sym answers Absent -- straight to inferred_facts_not_derived. An entry IS admitted for the node and its grounding is GroundingNotDerived, so inference ACCEPTS the tree and eval refuses later at whatever consumes it. Nothing typed a reference to a function. THE REPAIR, IN ONE ARM. Resolution answers WHICH declaration a reference denotes and carries it as a declaring path; this arm answers WHAT TYPE that declaration establishes for the use. Two questions, two stages: nothing here re-resolves a name and resolution mints no types. declaration_reference_path_optional(n) the declaring path, never the leaf symbol_index_lookup(resolved.symbol_index, path) the GUARDED read: a path with more than one bound declaring answers Absent, so a contested binding cannot yield a type arrow_domain_binder_labels(declared.children) evidence check inferred_facts_from_derived_type(n, declared) evidence attached to the USE A LOOKUP HIT IS NOT TYPE EVIDENCE. The index is built from validated normalized roots, which does not establish that every indexed declaration carries usable type evidence, so this arm does not ground on presence. A callable's evidence is its Arrow and the check is that its domain reads; an unsupported or unresolved signature stays explicitly ungrounded. Non-callable declarations are left to their own derivation rather than stamped. THE EVIDENCE ATTACHES TO THE USE. derived_type is the DECLARATION's node while the entry is keyed by the REFERENCE node, so the use keeps its own occurrence and locus and canonical_grounding_from_derived_type's self-evidence refusal still holds. THE CARRIER IS #12432's, CONSUMED NOT REBUILT. ResolvedTree { root, symbol_index } already reaches fn infer(tree: ResolvedTree); it was never threaded past there -- symbol_index appeared exactly once in 04_infer.dag, in a comment. The thread is `resolved: ResolvedTree` under a NEW name at every site, not a second `index: SymbolIndex` parameter: passing root and index side by side lets them come from different trees and disagree, and nothing would stop it, while the paired carrier makes the mismatch unwritable (DESIGN section 5, construction over validation). Functions that want the root read resolved.root. ELEVEN FUNCTIONS, NOT THE SIX ESTIMATED. The compiler found the other five -- infer_gather_transform_row_on_entries, infer_gather_application_row_on_entries, infer_gather_bind_annotation_row_on_entries, infer_gather_fold_step_merged, infer_gather_settled_row -- which is the argument for renaming at every site rather than adding a parallel parameter. Non-path callees keep `tree: Node` and receive resolved.root, so their contracts are untouched. The thread landed first as a 41/41 behaviour-neutral change, verified by the regression guards passing with the control still red, so any guard breakage would be attributable to the derivation rather than the rename. PARAMETER TYPING IS NOT REPLACED. infer_parameter_scope_search / infer_parameter_type_in_scope stay. Their comment names "the SymbolIndex / ResolvedTree.bindings lookup" as their dissolution trigger, and it is tempting to read this change as that trigger; it is not. A local use resolves to a canonical Atom at its own occurrence and never acquires a declaring path, so the index answers a different question. What was established here is only that symbol_index_fill puts Arrow DOMAINS in the index -- a fact about fill, not about what a parameter use resolves to. Deleting the walk on that basis would have reintroduced the defect its comment records: `fn positive(x: Int)` beside `fn f(x: Pos)` grounding every `x` in `f` as Int. EVIDENCE. dre_a_reference_grounds_to_its_declarations_contract_holds FAIL -> PASS bcn_cast_into_a_refinement_refuses_at_infer PASS unchanged bcn_cast_out_of_a_refinement_refuses_until_carrier_widening PASS unchanged bcn_identity_cast_into_a_refinement_admits PASS unchanged The control asserts the CONTRACT, not the grounding tag: it selects every node whose decoded declaring path ends in the wanted leaf, requires EXACTLY ONE, and requires the derived type's Arrow domain to bind exactly the name the fixture text specifies. A DerivedGrounding carrying the wrong type fails it. NOT QUALIFIED, STATED AS SUCH. dre_a_same_leaf_reference_gets_its_own_declarations _contract_holds passes but is NOT yet an identity-collapse detector. The forced- collapse mutation turned BOTH controls red rather than only the same-leaf one, because the mutation's path does not exist in the first fixture either -- it broke everything instead of specifically collapsing identity. Within a single-module fixture the discriminating case cannot be built: a reference that reaches this arm denotes a module-level callable whose declaring path IS [module, leaf], so path and leaf coincide. Qualifying it needs a two-module fixture where each module declares the same leaf. Until that runs, this control is specified, not qualified. NOT DONE HERE. The application connection (#12379's rule wants a callee Arrow, and a reference now grounds to one) and the s3 end-to-end assertion are the next step, and the outer equality may still lack a typing rule of its own. Based on #12432 (ResolvedTree carrier) and #12379 (application-result typing); rebases onto #12379, which lands first. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 142 +++++++++----- .../declaration_reference_evidence_test.dag | 173 ++++++++++++++++++ 2 files changed, 270 insertions(+), 45 deletions(-) create mode 100644 src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 115cf2e3716..3ef8dc6d440 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -15,6 +15,7 @@ import v2.std.compilers.target_model { target_model_canonical_operation_member_declared_type } import v2.compiler.resolve { ResolvedTree } +import v2.std.symbol_index { symbol_index_lookup } import v2.compiler.inferred_tree { DerivedGrounding, GroundingNotDerived, @@ -57,7 +58,7 @@ import v2.std.optional { optional_present } import v2.std.qualified_name { declaration_reference_node, declaration_reference_path_optional } -import v2.std.node { arrow_signature_order_label, edge_label_of } +import v2.std.node { arrow_signature_order_label, edge_label_of, DomainBinderLabels, DomainBinderLabelsUnreadable, arrow_domain_binder_labels } import v2.std.compilers.body_lowering { ArrowParameterOrderAbsent, ArrowParameterOrderDeclared, @@ -757,7 +758,7 @@ fn infer_product_facts_from_entries( } } -fn infer_node_facts(n: Node, partials: List, tree: Node) -> Outcome { +fn infer_node_facts(n: Node, partials: List, resolved: ResolvedTree) -> Outcome { match infer_bounded_lattice_consumer_gate(consumer: n, partials: partials) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: _, diagnostics: cd } => @@ -804,7 +805,7 @@ fn infer_node_facts(n: Node, partials: List, tree: Node) -> Outcome - match infer_parameter_type_in_scope(tree: tree, reference: n, binding: binding) { + match infer_parameter_type_in_scope(tree: resolved.root, reference: n, binding: binding) { Present { value: domain_ty } => inferred_facts_from_derived_type( node: n, @@ -820,9 +821,10 @@ fn infer_node_facts(n: Node, partials: List, tree: Node) -> Outcome - inferred_facts_not_derived( - node: n, - descent: Holds { value: descent_proof } + infer_declaration_reference_facts( + n: n, + resolved: resolved, + descent_proof: descent_proof ) } } @@ -831,6 +833,56 @@ fn infer_node_facts(n: Node, partials: List, tree: Node) -> Outcome Outcome { + match declaration_reference_path_optional(node: n) { + Absent => + inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) + Present { value: path } => + match symbol_index_lookup(index: resolved.symbol_index, qualified_path: path) { + Absent => + inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) + Present { value: declared } => + match arrow_domain_binder_labels(children: declared.children) { + DomainBinderLabelsUnreadable => + inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) + DomainBinderLabels { labels: _ } => + inferred_facts_from_derived_type( + node: n, + derived_type: declared, + descent: Holds { value: descent_proof } + ) + } + } + } +} + fn lookup_inferred_facts_in_entries( entries: List, key: Node, @@ -2343,9 +2395,9 @@ fn infer_gather_transform_frontier_on_entries( entries: List, pending: Diagnostics, partials: List, - tree: Node, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { - match infer_node_facts(n: node, partials: partials, tree: tree) { + match infer_node_facts(n: node, partials: partials, resolved: resolved) { Rejected { diagnostics: r } => infer_gather_fold_acc_failed( node: node, @@ -2487,15 +2539,15 @@ fn infer_gather_transform_row_on_entries( entries: List, pending: Diagnostics, partials: List, - tree: Node, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { match list_introduction_elements_optional(node: node) { Absent => - infer_gather_application_row_on_entries(node: node, entries: entries, pending: pending, partials: partials, tree: tree) + infer_gather_application_row_on_entries(node: node, entries: entries, pending: pending, partials: partials, resolved: resolved) Present { value: elements } => - match infer_transform_freemonoid_introduction(node: node, elements: elements, entries: entries, tree: tree) { + match infer_transform_freemonoid_introduction(node: node, elements: elements, entries: entries, tree: resolved.root) { Absent => - infer_gather_transform_frontier_on_entries(node: node, entries: entries, pending: pending, partials: partials, tree: tree) + infer_gather_transform_frontier_on_entries(node: node, entries: entries, pending: pending, partials: partials, resolved: resolved) Present { value: introduced } => match introduced { Rejected { diagnostics: r } => @@ -2542,7 +2594,7 @@ fn infer_gather_application_row_on_entries( entries: List, pending: Diagnostics, partials: List, - tree: Node, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { match infer_application_argument_inhabitance(node: node, entries: entries) { Rejected { diagnostics: r } => @@ -2561,7 +2613,7 @@ fn infer_gather_application_row_on_entries( node: node, partials: partials, entries: entries, - tree: tree, + tree: resolved.root, arguments_decided: infer_diagnostics_none(d: id) ) { Present { value: derived } => @@ -2610,7 +2662,7 @@ fn infer_gather_application_row_on_entries( entries: entries, pending: merged_pending, partials: partials, - tree: tree + resolved: resolved ) } } @@ -2670,7 +2722,7 @@ fn infer_gather_bind_annotation_row_on_entries( entries: List, pending: Diagnostics, partials: List, - tree: Node, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { match infer_bind_annotation_check(node: node, entries: entries) { Rejected { diagnostics: r } => @@ -2689,7 +2741,7 @@ fn infer_gather_bind_annotation_row_on_entries( entries: entries, pending: diagnostics_merge(outer: pending, inner: d), partials: partials, - tree: tree + resolved: resolved ) } } @@ -2816,13 +2868,13 @@ fn infer_transform_cast_optional(node: Node, entries: List) // added without deciding, at this site, whether it derives its type or joins the counted frontier // -- never defaulted into by omission (DESIGN §4 closed vocabulary, §5 unwritable-by-construction). -fn infer_gather_fold_not_derived(n: Node, partials: List, tree: Node) -> InferGatherFoldAcc { +fn infer_gather_fold_not_derived(n: Node, partials: List, resolved: ResolvedTree) -> InferGatherFoldAcc { infer_gather_transform_frontier_on_entries( node: n, entries: empty_inferred_facts_entry_list(), pending: None, partials: partials, - tree: tree + resolved: resolved ) } @@ -2834,7 +2886,7 @@ fn infer_gather_fold_not_derived(n: Node, partials: List, tree: Node) -> I // -- a grounding this fold does not derive -- and it takes that arm. Deriving the reference's type // FROM its declaration by path is the end-state the roster lookups above already name as their // dissolution, not this arm. -fn infer_gather_fold_init(n: Node, partials: List, tree: Node) -> InferGatherFoldAcc { +fn infer_gather_fold_init(n: Node, partials: List, resolved: ResolvedTree) -> InferGatherFoldAcc { match n.kind { ComputationNode { behavior: Branch } => infer_gather_fold_acc_ok( @@ -2869,7 +2921,7 @@ fn infer_gather_fold_init(n: Node, partials: List, tree: Node) -> InferGat await_transform_row: false, children_remaining: length(xs: n.children) ) - ComputationNode { behavior: Value } => infer_gather_fold_not_derived(n: n, partials: partials, tree: tree) + ComputationNode { behavior: Value } => infer_gather_fold_not_derived(n: n, partials: partials, resolved: resolved) ComputationNode { behavior: Transform } => if length(xs: n.children) == 0 { infer_gather_transform_row_on_entries( @@ -2877,7 +2929,7 @@ fn infer_gather_fold_init(n: Node, partials: List, tree: Node) -> InferGat entries: empty_inferred_facts_entry_list(), pending: None, partials: partials, - tree: tree + resolved: resolved ) } else { infer_gather_fold_acc_ok( @@ -2904,12 +2956,12 @@ fn infer_gather_fold_init(n: Node, partials: List, tree: Node) -> InferGat await_transform_row: false, children_remaining: length(xs: n.children) ) - Absent => infer_gather_fold_not_derived(n: n, partials: partials, tree: tree) + Absent => infer_gather_fold_not_derived(n: n, partials: partials, resolved: resolved) } - TypeNode { connective: Atom { identity: _ } } => infer_gather_fold_not_derived(n: n, partials: partials, tree: tree) + TypeNode { connective: Atom { identity: _ } } => infer_gather_fold_not_derived(n: n, partials: partials, resolved: resolved) TypeNode { connective: Conj } => match declaration_reference_path_optional(node: n) { - Present { value: _ } => infer_gather_fold_not_derived(n: n, partials: partials, tree: tree) + Present { value: _ } => infer_gather_fold_not_derived(n: n, partials: partials, resolved: resolved) Absent => if infer_type_node_awaits_product_row(n: n) { infer_gather_fold_acc_ok( @@ -2923,10 +2975,10 @@ fn infer_gather_fold_init(n: Node, partials: List, tree: Node) -> InferGat children_remaining: length(xs: n.children) ) } else { - infer_gather_fold_not_derived(n: n, partials: partials, tree: tree) + infer_gather_fold_not_derived(n: n, partials: partials, resolved: resolved) } } - TypeNode { connective: Disj } => infer_gather_fold_not_derived(n: n, partials: partials, tree: tree) + TypeNode { connective: Disj } => infer_gather_fold_not_derived(n: n, partials: partials, resolved: resolved) TypeNode { connective: Arrow } => if infer_type_node_awaits_product_row(n: n) { infer_gather_fold_acc_ok( @@ -2940,10 +2992,10 @@ fn infer_gather_fold_init(n: Node, partials: List, tree: Node) -> InferGat children_remaining: length(xs: n.children) ) } else { - infer_gather_fold_not_derived(n: n, partials: partials, tree: tree) + infer_gather_fold_not_derived(n: n, partials: partials, resolved: resolved) } - TypeNode { connective: Cardinality } => infer_gather_fold_not_derived(n: n, partials: partials, tree: tree) - TypeNode { connective: Instantiation } => infer_gather_fold_not_derived(n: n, partials: partials, tree: tree) + TypeNode { connective: Cardinality } => infer_gather_fold_not_derived(n: n, partials: partials, resolved: resolved) + TypeNode { connective: Instantiation } => infer_gather_fold_not_derived(n: n, partials: partials, resolved: resolved) } } @@ -3162,7 +3214,7 @@ fn infer_gather_fold_step( edge: Edge, child: InferGatherFoldAcc, partials: List, - tree: Node, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { if acc.failed { acc @@ -3172,7 +3224,7 @@ fn infer_gather_fold_step( merged_entries: acc.entries, merged_pending: acc.pending, partials: partials, - tree: tree + resolved: resolved ) } else if child.failed { infer_gather_fold_acc_failed( @@ -3205,7 +3257,7 @@ fn infer_gather_fold_step( merged_entries: list_snoc_item(xs: acc.entries, item: domain_entry), merged_pending: acc.pending, partials: partials, - tree: tree + resolved: resolved ) } } else if infer_literal_edge_diagnostics_derived(parent: acc.node, edge: edge) { @@ -3226,7 +3278,7 @@ fn infer_gather_fold_step( merged_entries: concat_inferred_facts_entries(a: acc.entries, b: payload_entries), merged_pending: acc.pending, partials: partials, - tree: tree + resolved: resolved ) } } else { @@ -3235,7 +3287,7 @@ fn infer_gather_fold_step( merged_entries: concat_inferred_facts_entries(a: acc.entries, b: child.entries), merged_pending: diagnostics_merge(outer: acc.pending, inner: child.pending), partials: partials, - tree: tree + resolved: resolved ) } } @@ -3245,7 +3297,7 @@ fn infer_gather_fold_step_merged( merged_entries: List, merged_pending: Diagnostics, partials: List, - tree: Node, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { let children_remaining = acc.children_remaining - 1 if acc.await_branch_row && children_remaining == 0 { @@ -3261,7 +3313,7 @@ fn infer_gather_fold_step_merged( entries: merged_entries, pending: merged_pending, partials: partials, - tree: tree + tree: resolved.root ) } else if acc.await_loop_row && children_remaining == 0 { infer_gather_loop_row_on_entries( @@ -3276,10 +3328,10 @@ fn infer_gather_fold_step_merged( entries: merged_entries, pending: merged_pending, partials: partials, - tree: tree + resolved: resolved ) } else if children_remaining == 0 { - infer_gather_settled_row(acc: acc, merged_entries: merged_entries, merged_pending: merged_pending, partials: partials, tree: tree) + infer_gather_settled_row(acc: acc, merged_entries: merged_entries, merged_pending: merged_pending, partials: partials, resolved: resolved) } else { infer_gather_fold_acc_ok( node: acc.node, @@ -3302,7 +3354,7 @@ fn infer_gather_settled_row( merged_entries: List, merged_pending: Diagnostics, partials: List, - tree: Node, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { match type_annotation_optional(n: acc.node) { Present { value: _ } => @@ -3311,7 +3363,7 @@ fn infer_gather_settled_row( entries: merged_entries, pending: merged_pending, partials: partials, - tree: tree + resolved: resolved ) Absent => if infer_gather_acc_awaits_product_row(node: acc.node, entries: merged_entries) { @@ -3336,13 +3388,13 @@ fn infer_gather_settled_row( } } -fn infer_gather_fold_algebra(partials: List, tree: Node) -> NodeFold { +fn infer_gather_fold_algebra(partials: List, resolved: ResolvedTree) -> NodeFold { NodeFold { init: fn(n0) { - infer_gather_fold_init(n: n0, partials: partials, tree: tree) + infer_gather_fold_init(n: n0, partials: partials, resolved: resolved) }, step: fn(acc, e, child) { - infer_gather_fold_step(acc: acc, edge: e, child: child, partials: partials, tree: tree) + infer_gather_fold_step(acc: acc, edge: e, child: child, partials: partials, resolved: resolved) } } } @@ -3354,7 +3406,7 @@ fn infer_grounding_admits_infer_facts(grounding: CanonicalGrounding) -> Bool { fn infer_entries_for_tree(tree: ResolvedTree) -> Outcome> { let partials = partial_bounded_lattice_instances_in_tree(tree: tree.root) infer_gather_acc_to_outcome( - acc: fold_node(n: tree.root, algebra: infer_gather_fold_algebra(partials: partials, tree: tree.root)) + acc: fold_node(n: tree.root, algebra: infer_gather_fold_algebra(partials: partials, resolved: tree)) ) } // INFER'S OUTPUT IS THE OBLIGATED FORM, never an InferredTree: v2.compiler.refinement_discharge diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag new file mode 100644 index 00000000000..49a9802fc86 --- /dev/null +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -0,0 +1,173 @@ +module v2.test.claim.reference_evidence.declaration_reference_evidence + +import v2.compiler.resolve { ResolvedTree } +import v2.std.symbol_index { symbol_index_lookup } +import v2.compiler.infer { infer, inferred_facts_grounding_derived } +import v2.compiler.inferred_tree { DerivedGrounding, GroundingNotDerived, InferredFacts } +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.extdeps.languages.dag { dag_language_model } +import extdeps.communication.medium { Lossless, Medium } +import std.algebra { Cons, Empty } +import v2.std.algebra { fold_list } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.logic { Bool } +import v2.std.node { DomainBinderLabels, DomainBinderLabelsUnreadable, Node, NodeFold, arrow_domain_binder_labels, fold_node } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.qualified_name { QualifiedName, declaration_reference_path_optional } + +// THE REFERENCE-EVIDENCE BOUNDARY. A reference to a corpus declaration reaches +// v2.compiler.infer's `infer_node_facts` as a declaration-reference Conj. No arm handles it: it falls +// to `inferred_facts_not_derived`, so an entry IS admitted for the node carrying GroundingNotDerived. +// Inference ACCEPTS that tree -- the refusal is eval's, whose `inferred_facts_for_eval` gate reports +// eval_rejected_grounding_not_derived only when the lookup SUCCEEDS -- so a claim asserting that +// `infer` accepted is VACUOUS here. An earlier draft of this file did exactly that and passed while +// the defect stood; the assertions below read the use's own facts instead. +// +// TWO PROPERTIES, DELIBERATELY SEPARATE. That a reference grounds AT ALL, and that it grounds to the +// contract of ITS OWN declaration. The second cannot be credited until the first is repaired -- both +// currently fail on the same missing prerequisite -- so this file specifies it now and it earns the +// name "detects declaration-identity collapse" only after the mutation that forces both uses onto one +// declaration is exercised against a repaired implementation. +// +// LOCAL PARAMETER TYPING IS NOT THIS SUBJECT. A parameter use resolves to a canonical Atom at its own +// occurrence, not to a declaring path, so `infer_parameter_scope_search` answers a different question +// and is not replaced here. The refinement rows in v2.test.claim.body_cast_node are its standing +// control and they pass on this base. +data dre_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^declaration_reference_evidence_artifact, + file_path: "src/v2/pilot/declaration_reference_evidence_pilot.dag" +} + +fn dre_assemble(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: dre_artifact, + compilation_unit: ^declaration_reference_evidence_cu, + source_root: V2Tree + }, + tail: Empty + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +// `callee` names its parameter `only_arg`, so the expected contract is independently specified by the +// fixture text and is not read back out of the implementation under test. +fn dre_reference_source() -> Outcome { + dre_assemble(src: "module p\n\nfn callee(only_arg: Int) -> Int {\n only_arg\n}\n\nfn consumer(y: Int) -> Int {\n callee(only_arg: y)\n}\n") +} + +// The leaf is the path's last segment; QualifiedName is a FreeMonoid carrying no +// last-element reader, so the fold below keeps the final one. +// EVERY use whose decoded path ends in the wanted leaf, so the claim can require EXACTLY ONE and +// never silently select an annotation or an unrelated reference a later change introduces. +fn dre_leaf_of(path: QualifiedName) -> Optional { + fold_list(xs: path, empty: optional_absent(), cons: fn(_acc, seg) { optional_present(value: seg) }) +} + +fn dre_use_here(n: Node, wanted: Symbol) -> List { + match declaration_reference_path_optional(node: n) { + Absent => [] + Present { value: path } => + match dre_leaf_of(path: path) { + Absent => [] + Present { value: leaf } => if leaf == wanted { [n] } else { [] } + } + } +} + +fn dre_uses_of(root: Node, wanted: Symbol) -> List { + fold_node( + n: root, + algebra: NodeFold { + init: fn(n0) { dre_use_here(n: n0, wanted: wanted) }, + step: fn(acc, _e, child) { concat(acc, child) } + } + ) +} + +// The derived type's contract, read through the existing Arrow reader. A grounding tag alone is not +// the property: DerivedGrounding carrying the WRONG type must fail this control. +fn dre_grounded_domain_labels(facts: InferredFacts) -> Optional> { + match facts.grounding { + GroundingNotDerived { node: _ } => optional_absent() + DerivedGrounding { grounding: g } => + match arrow_domain_binder_labels(children: g.witness.structural.evidence.children) { + DomainBinderLabelsUnreadable => optional_absent() + DomainBinderLabels { labels: ls } => optional_present(value: ls) + } + } +} + +fn dre_single_use_domain_labels(o: Outcome, wanted: Symbol) -> Optional> { + match o { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: inferred, diagnostics: _ } => + match dre_uses_of(root: inferred.root, wanted: wanted) { + Cons { head: use_node, tail: rest } => + match rest { + Empty => + match inferred.facts.lookup(use_node) { + Absent => optional_absent() + Present { value: facts } => dre_grounded_domain_labels(facts: facts) + } + Cons { head: _, tail: _ } => optional_absent() + } + Empty => optional_absent() + } + } + } +} + +// RED ON THIS BASE: the single use of `callee` grounds to a callable contract whose domain binds +// `only_arg`. Fails if the reference is ungrounded, if more than one use matches, or if the derived +// type is not that Arrow. +test fn dre_a_reference_grounds_to_its_declarations_contract_holds() -> Bool { + match dre_single_use_domain_labels(o: dre_reference_source(), wanted: ^callee) { + Absent => false + Present { value: labels } => + match labels { + Cons { head: first, tail: rest } => + match rest { + Empty => first == ^only_arg + Cons { head: _, tail: _ } => false + } + Empty => false + } + } +} + +// SAME LEAF, DIFFERENT DECLARING PATHS. `shared` is declared twice in this module: as a top-level +// function (p.shared, whose domain binds `alpha`) and as a field of Wrap (p.Wrap.shared). The use in +// `consumer` denotes p.shared. A leaf-keyed lookup can answer either; the declaring path answers only +// one. This control is SPECIFIED but not yet QUALIFIED as an identity-collapse detector: that claim +// requires the forced-collapse mutation to turn it red, which is exercised separately. +fn dre_same_leaf_source() -> Outcome { + dre_assemble(src: "module p\n\nfn shared(alpha: Int) -> Int {\n alpha\n}\n\ntype Wrap {\n shared: Int\n}\n\nfn consumer(y: Int) -> Int {\n shared(alpha: y)\n}\n") +} + +test fn dre_a_same_leaf_reference_gets_its_own_declarations_contract_holds() -> Bool { + match dre_single_use_domain_labels(o: dre_same_leaf_source(), wanted: ^shared) { + Absent => false + Present { value: labels } => + match labels { + Cons { head: first, tail: rest } => + match rest { + Empty => first == ^alpha + Cons { head: _, tail: _ } => false + } + Empty => false + } + } +} From 1b73daad48bc5ca6a94d36e4a28bacf730bcd25b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 04:46:09 +0000 Subject: [PATCH 08/90] The reference's guard becomes the one an application will ask MY COMMENT CLAIMED MORE THAN MY GUARD CHECKED. The arm grounded a reference when arrow_domain_binder_labels could read the declaration's parameter names, and the comment beside it said an unsupported or unresolved signature stayed ungrounded. That was false of the guard. That reader takes only `children`, so it never establishes the declaration IS an Arrow, and it inspects no parameter type, no return type, no scope and no body. "I can read the parameter names" is a different property from "this declaration establishes this callable type", and the comment asserted the second while the code checked the first -- rung inflation in the annotation, caught in review rather than by a control, because no control distinguished the two. THE GUARD IS NOW THE APPLICATION PATH'S OWN REQUIREMENTS, reused rather than restated: infer_operator_arrow (the node IS an Arrow), infer_formals_from_domain (every formal is named), and arrow_declared_parameter_order, where both Absent and Malformed refuse -- the domain is sorted by label for identity, so its stored sequence is not the declared order and an Arrow without the order edge is one a binder already refuses. Grounding a reference whose declaration cannot satisfy those would mint evidence no consumer can use. WHAT IT STILL DOES NOT ESTABLISH, stated rather than implied: the type references inside that signature are not resolved in the declaration's scope here, and no body or return obligation is discharged. Those stay with the existing inference contract; a reference consuming a declared signature does not recheck a body at every use. The claim is the structural callable contract and nothing wider. Controls unchanged in outcome and now discriminating for the right reason: dre_a_reference_grounds_to_its_declarations_contract_holds PASS dre_a_same_leaf_reference_gets_its_own_declarations_contract_holds PASS bcn_cast_into_a_refinement_refuses_at_infer PASS bcn_cast_out_of_a_refinement_refuses_until_carrier_widening PASS bcn_identity_cast_into_a_refinement_admits PASS Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 61 +++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 3ef8dc6d440..5995b7d11e1 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -58,7 +58,7 @@ import v2.std.optional { optional_present } import v2.std.qualified_name { declaration_reference_node, declaration_reference_path_optional } -import v2.std.node { arrow_signature_order_label, edge_label_of, DomainBinderLabels, DomainBinderLabelsUnreadable, arrow_domain_binder_labels } +import v2.std.node { arrow_signature_order_label, edge_label_of } import v2.std.compilers.body_lowering { ArrowParameterOrderAbsent, ArrowParameterOrderDeclared, @@ -846,16 +846,49 @@ fn infer_node_facts(n: Node, partials: List, resolved: ResolvedTree) -> Ou // path, and it is the GUARDED read: a path with more than one bound declaring answers Absent, so a // contested binding cannot silently yield a type. // -// A LOOKUP HIT IS NOT TYPE EVIDENCE. The index is built from validated normalized roots, which does -// not establish that every indexed declaration carries usable type evidence, so this arm does not -// ground on presence. A callable's evidence is its Arrow, and the check is that the Arrow's domain -// is readable; a declaration whose signature is unsupported or unresolved stays explicitly -// ungrounded rather than being stamped derived. Non-callable declarations are likewise left -// ungrounded here -- their evidence is a separate derivation, not this connection. +// A LOOKUP HIT IS NOT TYPE EVIDENCE, AND READABLE PARAMETER NAMES ARE NOT EITHER. The index is built +// from validated normalized roots, which does not establish that an indexed declaration carries usable +// type evidence. An earlier draft of this arm guarded with arrow_domain_binder_labels and claimed that +// an unsupported signature stayed ungrounded; that claim was FALSE OF THE GUARD. That reader takes +// only `children`, so it never establishes the declaration IS an Arrow, and it inspects no parameter +// type, no return type, no scope and no body -- "I can read the parameter names" is a different +// property from "this declaration establishes this callable type", and the comment asserted the second +// while the code checked the first. // -// THE EVIDENCE ATTACHES TO THE USE. derived_type is the DECLARATION's node while the facts entry is -// keyed by the REFERENCE node, so the use keeps its own occurrence and diagnostic locus and -// canonical_grounding_from_derived_type's self-evidence refusal still holds. +// THE GUARD IS NOW THE ONE AN APPLICATION WILL ASK. A reference is grounded only if the declaration +// satisfies what v2.compiler.infer's own application path requires of a callee: infer_operator_arrow +// (the node IS an Arrow), a domain whose formals are all named (infer_formals_from_domain), and a +// DECLARED parameter order (arrow_declared_parameter_order -- Absent or Malformed both refuse, because +// the domain is sorted by label for identity so its stored sequence is not the declared one). Grounding +// a reference whose declaration cannot satisfy those would mint evidence no consumer can use. +// +// WHAT THIS STILL DOES NOT ESTABLISH, stated rather than implied: the parameter and return TYPE +// references inside that signature are not resolved in the declaration's scope here, and no body or +// return obligation is discharged. Those remain the existing inference contract's, and a reference +// consuming a declared signature does not recheck a body at every use. This arm's claim is the +// structural callable contract, nothing wider. +// THE CALLABLE EVIDENCE OF A DECLARATION, or its absence. Reuses the application path's readers so a +// reference cannot ground to a callee shape that path would refuse. +fn infer_declaration_callable_evidence(declared: Node) -> Optional { + match infer_operator_arrow(operator: declared) { + Absent => Absent + Present { value: arrow } => + match list_at_optional(xs: node_positional_child_targets(node: arrow), index: 0) { + Absent => Absent + Present { value: domain } => + match infer_formals_from_domain(domain: domain) { + Absent => Absent + Present { value: _formals } => + match arrow_declared_parameter_order(arrow: arrow) { + ArrowParameterOrderAbsent => Absent + ArrowParameterOrderMalformed => Absent + ArrowParameterOrderDeclared { labels: _ } => Present { value: arrow } + } + } + } + } +} + fn infer_declaration_reference_facts( n: Node, resolved: ResolvedTree, @@ -869,13 +902,13 @@ fn infer_declaration_reference_facts( Absent => inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) Present { value: declared } => - match arrow_domain_binder_labels(children: declared.children) { - DomainBinderLabelsUnreadable => + match infer_declaration_callable_evidence(declared: declared) { + Absent => inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) - DomainBinderLabels { labels: _ } => + Present { value: callable } => inferred_facts_from_derived_type( node: n, - derived_type: declared, + derived_type: callable, descent: Holds { value: descent_proof } ) } From 707cc495b702ecd3d74e73ff532a8673a89d0b99 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 05:41:20 +0000 Subject: [PATCH 09/90] The same-leaf discriminator is not qualified, and the file says so FIVE ATTEMPTS, NO DISCRIMINATING CONTROL. A single-module fixture cannot produce one: a reference reaching this arm denotes a module-level callable whose declaring path IS [module, leaf], so path and leaf coincide and a leaf-keyed lookup is accidentally right. A two-module fixture reaches the right shape -- two modules each declaring `shared(alpha: ...)` with different types, the consumer importing one -- but the assertion needs the parameter's declared TYPE read out of the derived Arrow's domain, and neither a walk-order atom search nor find_named_child on the domain produced it. The control stayed GREEN under a mutation that forced the wrong declaration, and then went RED on correct code once the reader changed: both arms wrong, so it distinguished nothing. A green control that does not discriminate is worse than no control, because it would be cited as coverage. A red one blocks the PR while asserting nothing. So neither ships; the gap is recorded where the control would have been. WHAT IS THEREFORE NOT CLAIMED: that this arm resists declaration-identity collapse. The declaring path is what it looks up and symbol_index_lookup is the guarded read, but no executed control here demonstrates that a leaf-keyed answer would be caught. Qualifying it needs a reliable reader for a parameter's declared type inside a derived Arrow domain; that reader is the missing piece. Retained and passing: the two controls that do discriminate their own properties, and the three refinement guards. Co-Authored-By: Claude Opus 5 (1M context) --- .../declaration_reference_evidence_test.dag | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 49a9802fc86..8cab3c3542c 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -13,10 +13,13 @@ import std.algebra { Cons, Empty } import v2.std.algebra { fold_list } import v2.std.cross_tree.import_model { V2Tree } import v2.std.artifact { Artifact, SourceFile } +import v2.compiler.source_authority { SourceRootIngest } import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.logic { Bool } -import v2.std.node { DomainBinderLabels, DomainBinderLabelsUnreadable, Node, NodeFold, arrow_domain_binder_labels, fold_node } +import v2.std.node { Node, NodeFold, TypeNode, Atom, fold_node } +import v2.std.node_query { find_named_child, node_positional_child_targets } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.collection { list_at_optional } import v2.std.qualified_name { QualifiedName, declaration_reference_path_optional } // THE REFERENCE-EVIDENCE BOUNDARY. A reference to a corpus declaration reaches @@ -171,3 +174,20 @@ test fn dre_a_same_leaf_reference_gets_its_own_declarations_contract_holds() -> } } } + +// THE TWO-MODULE SAME-LEAF DISCRIMINATOR IS NOT IN THIS FILE, AND THAT IS A GAP, NOT AN OMISSION. +// Five attempts did not produce one that discriminates. A single-module fixture cannot: a reference +// reaching this arm denotes a module-level callable whose declaring path IS [module, leaf], so path +// and leaf coincide and a leaf-keyed lookup is accidentally right. A two-module fixture reaches the +// right shape -- two modules each declaring `shared(alpha: ...)` with different types, the consumer +// importing one -- but the assertion needs the parameter's TYPE read out of the derived Arrow's +// domain, and neither a walk-order atom search nor find_named_child on the domain produced the +// declared type: the control stayed green under a mutation that forced the wrong declaration, then +// went red on correct code once the reader changed. Both arms were wrong, so it distinguished +// nothing. +// +// WHAT IS THEREFORE NOT CLAIMED: that this arm resists declaration-identity collapse. The path is +// what it looks up and symbol_index_lookup is the guarded read, but no executed control here +// demonstrates that a leaf-keyed answer would be caught. Qualifying it needs a reliable reader for a +// parameter's declared type inside a derived Arrow domain, which is the missing piece and is worth +// finding before the claim is made. From 7fa6d601ec6a43a3e7a9fb6be2bf01cf54da6b62 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 12:11:49 +0000 Subject: [PATCH 10/90] The application path can now see a reference's callable evidence; it still is not enough THE CONNECTION WAS ABSENT IN CODE, not merely unmeasured. infer_application_callee_arrow read the callee EXPRESSION's own kind through infer_operator_arrow, and a resolved declaration reference stays a Conj however well typed it is -- so the helper answered Absent for it and every consumer (formals, type parameters, argument inhabitance, result typing) fell through to the undecidable-accepted arm. Giving the reference callable facts did not make any of them read those facts. Adding evidence and consuming evidence are two changes and only the first had landed. infer_application_callee_arrow_with_facts falls back to the callee's own facts entry when the node is not itself an Arrow, taking ONLY the type from DerivedGrounding's structural evidence. The use keeps its node and occurrence; the declaration's body and identity are not substituted. Wired at the three sites that asked the old helper, with entries threaded into infer_application_formals and infer_application_type_params -- the other two callers already carried entries. NECESSARY, NOT SUFFICIENT, AND THE CONTROL SAYS SO. A named call still does not ground. dre_a_named_call_is_grounded_expected_red is enrolled as an executed expected-red rather than a passing claim or a deleted one: the boundary is real, its cause is not yet identified, and naming it is the next step rather than widening the reader until something goes green. What is missing between a grounded callee and a grounded application is unestablished -- I did not determine whether the call's facts entry is absent or present-and-ungrounded, and that distinction picks the repair. Guards unchanged, including the two application-typing rows: bcn_cast_into_a_refinement_refuses_at_infer PASS bcn_cast_out_of_a_refinement_refuses_until_carrier_widening PASS bcn_identity_cast_into_a_refinement_admits PASS bcn_infer_admits_int_to_int PASS bcn_infer_refuses_int_to_bool PASS dre_a_reference_grounds_to_its_declarations_contract_holds PASS Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 43 ++++++++++++--- .../declaration_reference_evidence_test.dag | 55 ++++++++++++++++++- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 5995b7d11e1..9bc0bbbceb9 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -1816,6 +1816,35 @@ fn infer_formals_from_domain(domain: Node) -> Optional> } } +// THE CALLEE'S ARROW, FROM THE CALLEE'S OWN EVIDENCE WHEN THE NODE IS NOT ONE ITSELF. A resolved +// declaration reference stays a Conj however well typed it is, so infer_operator_arrow answers Absent +// for it and every consumer of this helper -- formals, type parameters, argument inhabitance, result +// typing -- fell through to the undecidable-accepted arm. Giving the reference callable facts does not +// make this path read them; this is where it reads them. The use keeps its own node and occurrence: +// only the TYPE is taken from its facts entry, never the declaration's body or identity. +fn infer_application_callee_arrow_with_facts( + node: Node, + entries: List +) -> Optional { + match list_at_optional(xs: node_positional_child_targets(node: node), index: 0) { + Absent => Absent + Present { value: operator } => + match infer_operator_arrow(operator: operator) { + Present { value: arrow } => Present { value: arrow } + Absent => + match lookup_inferred_facts_in_entries(entries: entries, key: operator) { + Absent => Absent + Present { value: facts } => + match facts.grounding { + GroundingNotDerived { node: _ } => Absent + DerivedGrounding { grounding: g } => + infer_operator_arrow(operator: g.witness.structural.evidence) + } + } + } + } +} + fn infer_application_callee_arrow(node: Node) -> Optional { match list_at_optional(xs: node_positional_child_targets(node: node), index: 0) { Absent => Absent @@ -1833,8 +1862,8 @@ type InferApplicationFormals | FormalsUnresolved | FormalsOrderRefused { reason: Symbol } -fn infer_application_formals(node: Node) -> InferApplicationFormals { - match infer_application_callee_arrow(node: node) { +fn infer_application_formals(node: Node, entries: List) -> InferApplicationFormals { + match infer_application_callee_arrow_with_facts(node: node, entries: entries) { Absent => FormalsUnresolved Present { value: arrow } => match list_at_optional(xs: node_positional_child_targets(node: arrow), index: 0) { @@ -1929,8 +1958,8 @@ fn infer_pairing_formal(p: InferFormalPairing) -> InhabitanceFormal { } // The callee's declared type parameters; empty when the operator is not an Arrow or declares none. -fn infer_application_type_params(node: Node) -> List { - match infer_application_callee_arrow(node: node) { +fn infer_application_type_params(node: Node, entries: List) -> List { + match infer_application_callee_arrow_with_facts(node: node, entries: entries) { Absent => [] Present { value: arrow } => type_param_names(n: arrow) } @@ -2075,7 +2104,7 @@ fn infer_application_argument_inhabitance( node: Node, entries: List ) -> Outcome { - match infer_application_formals(node: node) { + match infer_application_formals(node: node, entries: entries) { FormalsUnresolved => infer_inhabitance_undecidable_accepted( application: node, @@ -2094,7 +2123,7 @@ fn infer_application_argument_inhabitance( ) ) } else { - let type_params = infer_application_type_params(node: node) + let type_params = infer_application_type_params(node: node, entries: entries) match fold( zip_map( a: formals, @@ -2816,7 +2845,7 @@ fn infer_transform_derived_optional( // a non-Arrow operator, or a return the language join does not denote stays on the counted // frontier: Absent here, never an admission. fn infer_transform_application_optional(node: Node, entries: List) -> Optional> { - match infer_application_callee_arrow(node: node) { + match infer_application_callee_arrow_with_facts(node: node, entries: entries) { Absent => optional_absent() Present { value: arrow } => match lookup_inferred_facts_in_entries(entries: entries, key: arrow) { diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 8cab3c3542c..74d83902cab 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -16,7 +16,7 @@ import v2.std.artifact { Artifact, SourceFile } import v2.compiler.source_authority { SourceRootIngest } import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.logic { Bool } -import v2.std.node { Node, NodeFold, TypeNode, Atom, fold_node } +import v2.std.node { Node, NodeFold, TypeNode, Atom, ComputationNode, Transform, fold_node } import v2.std.node_query { find_named_child, node_positional_child_targets } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.std.collection { list_at_optional } @@ -191,3 +191,56 @@ test fn dre_a_same_leaf_reference_gets_its_own_declarations_contract_holds() -> // demonstrates that a leaf-keyed answer would be caught. Qualifying it needs a reliable reader for a // parameter's declared type inside a derived Arrow domain, which is the missing piece and is worth // finding before the claim is made. + +// THE APPLICATION CONSUMER. A grounded reference is not a grounded application: at the base, +// infer_application_callee_arrow read the callee EXPRESSION's own kind, and a resolved declaration +// reference stays a Conj however well typed, so every consumer -- formals, type parameters, argument +// inhabitance, result typing -- fell through. This asserts the CALL's facts, not the reference's. +fn dre_call_is_grounded(o: Outcome) -> Bool { + match o { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: _ } => false + Accepted { value: inferred, diagnostics: _ } => + match dre_first_application_optional(n: inferred.root) { + Absent => false + Present { value: call } => + match inferred.facts.lookup(call) { + Absent => false + Present { value: facts } => inferred_facts_grounding_derived(facts: facts) + } + } + } + } +} + +fn dre_first_application_optional(n: Node) -> Optional { + fold_node( + n: n, + algebra: NodeFold { + init: fn(n0) { + match n0.kind { + ComputationNode { behavior: Transform } => + match dre_uses_of(root: n0, wanted: ^callee) { + Cons { head: _, tail: _ } => optional_present(value: n0) + Empty => optional_absent() + } + _ => optional_absent() + } + }, + step: fn(acc, _e, child) { + match acc { Present { value: _ } => acc Absent => child } + } + } + ) +} + +// EXPECTED RED, ENROLLED AS THE NEXT BOUNDARY. The entries-aware callee reader is NECESSARY -- without +// it the application path cannot see a reference's callable evidence at all -- and it is NOT SUFFICIENT: +// this call still does not ground. What remains is unidentified, and naming it is the next step rather +// than widening the reader. Recorded as a claim so the boundary is executed and reported rather than +// described. +fn dre_a_named_call_is_grounded_expected_red() -> Bool { + dre_call_is_grounded(o: dre_reference_source()) +} From 32ea98eea9bf4526cda1797519415e6a2cbeef1a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 15:49:08 +0000 Subject: [PATCH 11/90] A named call grounds: the use carries the facts, the arrow carries the type THE REMAINING FAILURE WAS ONE MISKEYED LOOKUP. After the application path could SEE a reference's callable evidence, the call still did not ground, and the cause was in infer_transform_application_optional: match infer_application_callee_arrow_with_facts(...) { Present { value: arrow } => match lookup_inferred_facts_in_entries(entries: entries, key: arrow) { That key is right only while an arrow can be the callee node itself. Once the arrow may be a DECLARATION's Arrow reached through the use's facts, it is a node of the declaring module with no entry in this tree, so the lookup answered Absent and the application dropped to the frontier however well the callee was typed. The grounding question is about the CALLEE USE; the arrow supplies only the TYPE. They are two things and only the first has facts here. infer_application_callee_use names the first; the second stays what it was. dre_a_named_call_is_grounded_holds goes from an enrolled expected-red to a passing claim on that one change. A BOOL-RETURNING CALL STILL DOES NOT GROUND, AND IT IS A DIFFERENT BOUNDARY. Measured three ways on this base: an Int-returning call grounds; a Bool-returning call with a literal body does not; a Bool-returning call whose body is its own parameter does not either. The variable is the RETURN TYPE, not the body, and the reference itself grounds in every one of the three -- so this sits downstream of the reference repair, in the application's return derivation, infer_arrow_declared_return_type -> dag_binding_denotation. Enrolled as an executed expected-red rather than deleted or chased: which binding symbol a Bool return actually carries is the next question and answering it is a separate change. THE EXECUTION CONTROL IS DELIBERATELY BOOL-RETURNING, which is why the boundary surfaced here rather than later: an Int-returning call compared with `==` would have coupled the first execution proof to equality, which has its own unproven typing. Guards unchanged, including both application-typing rows: bcn_cast_into_a_refinement_refuses_at_infer PASS bcn_cast_out_of_a_refinement_refuses_until_carrier_widening PASS bcn_identity_cast_into_a_refinement_admits PASS bcn_infer_admits_int_to_int PASS bcn_infer_refuses_int_to_bool PASS dre_a_reference_grounds_to_its_declarations_contract_holds PASS dre_a_same_leaf_reference_gets_its_own_declarations_contract_holds PASS Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 16 +++- .../declaration_reference_evidence_test.dag | 95 +++++++++++++++++-- 2 files changed, 104 insertions(+), 7 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 9bc0bbbceb9..c3b49eb3a74 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -1822,6 +1822,10 @@ fn infer_formals_from_domain(domain: Node) -> Optional> // typing -- fell through to the undecidable-accepted arm. Giving the reference callable facts does not // make this path read them; this is where it reads them. The use keeps its own node and occurrence: // only the TYPE is taken from its facts entry, never the declaration's body or identity. +fn infer_application_callee_use(node: Node) -> Optional { + list_at_optional(xs: node_positional_child_targets(node: node), index: 0) +} + fn infer_application_callee_arrow_with_facts( node: Node, entries: List @@ -2845,10 +2849,19 @@ fn infer_transform_derived_optional( // a non-Arrow operator, or a return the language join does not denote stays on the counted // frontier: Absent here, never an admission. fn infer_transform_application_optional(node: Node, entries: List) -> Optional> { + match infer_application_callee_use(node: node) { + Absent => optional_absent() + Present { value: callee_use } => match infer_application_callee_arrow_with_facts(node: node, entries: entries) { Absent => optional_absent() Present { value: arrow } => - match lookup_inferred_facts_in_entries(entries: entries, key: arrow) { + // THE GROUNDING QUESTION IS ABOUT THE CALLEE USE, THE TYPE COMES FROM ITS EVIDENCE. This lookup + // was keyed by `arrow`. That was right while an arrow could only be the callee node itself, and + // wrong as soon as the arrow may be a DECLARATION's Arrow reached through the use's facts: that + // node belongs to the declaring module, is not a node of this tree, and so has no entry here -- + // the lookup answered Absent and the application dropped to the frontier however well the callee + // was typed. The use and its type evidence are two things; only the first has facts in this tree. + match lookup_inferred_facts_in_entries(entries: entries, key: callee_use) { Absent => optional_absent() Present { value: arrow_facts } => if !inferred_facts_grounding_derived(facts: arrow_facts) { @@ -2872,6 +2885,7 @@ fn infer_transform_application_optional(node: Node, entries: List T (v2.std.type_binder). Its type is T, and diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 74d83902cab..05308478844 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -236,11 +236,94 @@ fn dre_first_application_optional(n: Node) -> Optional { ) } -// EXPECTED RED, ENROLLED AS THE NEXT BOUNDARY. The entries-aware callee reader is NECESSARY -- without -// it the application path cannot see a reference's callable evidence at all -- and it is NOT SUFFICIENT: -// this call still does not ground. What remains is unidentified, and naming it is the next step rather -// than widening the reader. Recorded as a claim so the boundary is executed and reported rather than -// described. -fn dre_a_named_call_is_grounded_expected_red() -> Bool { +// THE CALL GROUNDS. Two changes were needed and only the first is obvious: the application path had to +// SEE the callee's callable evidence (infer_application_callee_arrow_with_facts), and the grounding +// check had to be asked of the CALLEE USE rather than of the arrow. That second lookup was keyed by +// `arrow`, which is correct only while an arrow can only be the callee node itself; once the arrow may +// be a DECLARATION's Arrow reached through the use's facts, it is a node of the declaring module with +// no entry in this tree, so the lookup answered Absent and the application dropped to the frontier +// however well the callee was typed. +test fn dre_a_named_call_is_grounded_holds() -> Bool { dre_call_is_grounded(o: dre_reference_source()) } + +// EXECUTION, DELIBERATELY BOOL-RETURNING. The point is that a named call EXECUTES and returns its +// callee's value -- not that equality types. An Int-returning call compared with `==` would couple this +// first execution proof to the equality operation, which has its own typing rule and its own unproven +// standing; a Bool-returning call is its own assertion. +fn dre_execution_source() -> Outcome { + dre_assemble(src: "module p\n\nfn truth(only_arg: Int) -> Bool {\n true\n}\n\nfn consumer(y: Int) -> Bool {\n truth(only_arg: y)\n}\n") +} + +// A BOOL-RETURNING CALL DOES NOT GROUND, AND THAT IS A SEPARATE BOUNDARY FROM THIS REPAIR. Measured +// three ways on this base: an Int-returning call grounds; a Bool-returning call with a literal body +// does not; a Bool-returning call whose body is its parameter does not either. So the variable is the +// RETURN TYPE, not the body. The reference itself grounds in every case, so this is downstream of the +// reference repair, in the application's return derivation -- +// infer_arrow_declared_return_type -> dag_binding_denotation. Enrolled executed rather than deleted, +// and NOT diagnosed further here: which binding symbol a Bool return actually carries is the next +// question, and answering it is a separate change from this one. +fn dre_a_named_call_to_a_bool_fn_is_grounded_expected_red() -> Bool { + dre_call_is_grounded_for(o: dre_execution_source(), wanted: ^truth) +} + +fn dre_call_is_grounded_for(o: Outcome, wanted: Symbol) -> Bool { + match o { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: _ } => false + Accepted { value: inferred, diagnostics: _ } => + match dre_first_application_for(n: inferred.root, wanted: wanted) { + Absent => false + Present { value: call } => + match inferred.facts.lookup(call) { + Absent => false + Present { value: facts } => inferred_facts_grounding_derived(facts: facts) + } + } + } + } +} + +fn dre_first_application_for(n: Node, wanted: Symbol) -> Optional { + fold_node( + n: n, + algebra: NodeFold { + init: fn(n0) { + match n0.kind { + ComputationNode { behavior: Transform } => + match dre_uses_of(root: n0, wanted: wanted) { + Cons { head: _, tail: _ } => optional_present(value: n0) + Empty => optional_absent() + } + _ => optional_absent() + } + }, + step: fn(acc, _e, child) { + match acc { Present { value: _ } => acc Absent => child } + } + } + ) +} + +fn dre_diag_bool_reference_grounds() -> Bool { + match dre_single_use_domain_labels(o: dre_execution_source(), wanted: ^truth) { + Absent => false + Present { value: labels } => + match labels { + Cons { head: first, tail: _ } => first == ^only_arg + Empty => false + } + } +} + +// Bool param AND Bool return, body is the parameter -- the same shape as the working Int case, so the +// only variable against dre_execution_source is the literal body. +fn dre_bool_param_source() -> Outcome { + dre_assemble(src: "module p\n\nfn truth(only_arg: Bool) -> Bool {\n only_arg\n}\n\nfn consumer(y: Bool) -> Bool {\n truth(only_arg: y)\n}\n") +} + +fn dre_diag_bool_param_call_grounds() -> Bool { + dre_call_is_grounded_for(o: dre_bool_param_source(), wanted: ^truth) +} From 53022c28ca88b8313979059cbb8fa1a46ad2dbd9 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 17:24:41 +0000 Subject: [PATCH 12/90] A return that is already a type is consumed, not denoted again INT MASKED THE READER'S ASSUMPTION AND BOOL EXPOSED IT. infer_arrow_declared_return_type sent every return Atom's identity to dag_binding_denotation, which is a BINDING-to-type operation. Int survives that because its canonical type constructor retains the historical spelling ^dag_binding_type_int, so its binding and type identities coincide and a second denotation is a no-op. Bool arrives as the DENOTED node -- v2.std.logic bool_node, ^bool_node_symbol -- so the binding lookup answered Absent and BOTH consumers of this shared reader lost the return: application result typing dropped to the frontier, and the body-versus-declared-return check skipped its comparison. MEASURED BEFORE REPAIRING. An Int-returning call grounds; a Bool-returning call with a literal body does not; a Bool-returning call whose body is its own parameter does not either. The reference itself grounds in all three, so the variable is the RETURN TYPE and not the body. The return atom was then read directly: Int carries ^dag_binding_type_int, Bool carries ^bool_node_symbol. THE REPAIR RECOGNISES BY AUTHORITY, NOT BY SPELLING. The established case is compared against v2.std.logic's own bool_node() through the existing structural equality, rather than teaching a second meaning for ^bool_node_symbol here or widening dag_binding_denotation to accept a denoted symbol -- that lookup stays strictly binding-to-type, so a specimen fix does not become a muddied contract. Ordered denotation-first, so the Int path is byte-identical and only a return the binding lookup cannot denote reaches the established-type question. ONE reader, so introduction and elimination cannot disagree about the same signature. dre_a_named_call_to_a_bool_fn_is_grounded_holds: expected-red -> PASS. THE MISMATCH NEGATIVES ARE RED, AND THAT IS PRE-EXISTING, NOT INTRODUCED. infer ACCEPTS a Bool-declared function with an Int body and the converse. The cause is upstream of this reader: infer_arrow_body_inhabits_declared_return is only reached when the Arrow carries evidence edges; without them the arm answers inferred_facts_not_derived, and a frontier is not a refusal, so the comparison never runs. Verified by reverting ONLY the return reader and re-running -- both rows fail identically. Enrolled as executed expected-reds rather than deleted: they are exactly the controls that would catch a return recognition which admitted nodes without activating the check, they cannot discharge that duty while the check is unreachable, and when the evidence-edge condition is repaired they become its guard without anyone rediscovering the shape. The eight input-inspection diagnostics that located this are removed; their results are recorded above rather than left as permanent obligations. Guards unchanged: bcn_cast_into_a_refinement_refuses_at_infer PASS bcn_cast_out_of_a_refinement_refuses_until_carrier_widening PASS bcn_identity_cast_into_a_refinement_admits PASS bcn_infer_admits_int_to_int PASS bcn_infer_refuses_int_to_bool PASS dre_a_reference_grounds_to_its_declarations_contract_holds PASS dre_a_same_leaf_reference_gets_its_own_declarations_contract_holds PASS dre_a_named_call_is_grounded_holds PASS Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 48 ++++++++-- .../declaration_reference_evidence_test.dag | 91 ++++++++++++++++--- 2 files changed, 117 insertions(+), 22 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index c3b49eb3a74..14773ab36fe 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -116,7 +116,7 @@ import v2.std.diagnostic { outcome_accepted, rejected_with_pending } -import v2.std.logic { Bool } +import v2.std.logic { Bool, bool_node } import v2.std.node { Arrow, Atom, @@ -1057,13 +1057,43 @@ fn infer_binding_value_type_witness(binding: Symbol, at: Node) -> Witness // (positional child 1). Read from the declaration, never from the Arrow's composed evidence, which // also carries the body. Absent when the return is not a binding this language's join denotes, and // then nothing is derived from it. +// A RETURN EXPRESSION THAT IS ALREADY AN ESTABLISHED TYPE IS CONSUMED, NOT DENOTED AGAIN. This reader +// sent every return Atom's identity to dag_binding_denotation, which is a BINDING-to-type operation. +// Int survived that because its canonical type constructor retains the historical spelling +// ^dag_binding_type_int, so its binding and type identities coincide and a second denotation is a +// no-op. Bool does not: it arrives as the DENOTED node (v2.std.logic bool_node, ^bool_node_symbol), +// the binding lookup answered Absent, and both consumers of this reader lost the return -- the +// application's result typing dropped to the frontier, and the body-versus-declared-return check +// skipped its comparison. So Int masked the reader's assumption and Bool exposed it. +// +// THE RECOGNITION IS BY AUTHORITY, NOT BY SPELLING. The established case is compared against +// v2.std.logic's own bool_node() through the existing structural equality, rather than teaching a +// second meaning for ^bool_node_symbol here or widening dag_binding_denotation to accept a denoted +// symbol -- that lookup stays strictly binding-to-type. An arbitrary Atom is not a resolved value type +// and still answers Absent. +// +// ORDERED DENOTATION-FIRST so the Int path is byte-identical: only a return the binding lookup cannot +// denote reaches the established-type question. ONE reader, so introduction and elimination cannot +// disagree about the same signature. +fn infer_established_return_type_optional(ret: Node) -> Optional { + if infer_type_equal_ignoring_provenance(a: ret, b: bool_node()) { + optional_present(value: bool_node()) + } else { + optional_absent() + } +} + fn infer_arrow_declared_return_type(arrow: Node) -> Optional { match list_at_optional(xs: node_positional_child_targets(node: arrow), index: 1) { Absent => Absent Present { value: ret } => match infer_atom_binding_sym(node: ret) { - Absent => Absent - Present { value: binding } => dag_binding_denotation(sym: binding) + Absent => infer_established_return_type_optional(ret: ret) + Present { value: binding } => + match dag_binding_denotation(sym: binding) { + Present { value: denoted } => Present { value: denoted } + Absent => infer_established_return_type_optional(ret: ret) + } } } } @@ -2848,6 +2878,12 @@ fn infer_transform_derived_optional( // derived -- so arrow introduction has checked its body against that return. An underived callee, // a non-Arrow operator, or a return the language join does not denote stays on the counted // frontier: Absent here, never an admission. +// THE GROUNDING QUESTION IS ABOUT THE CALLEE USE, THE TYPE COMES FROM ITS EVIDENCE. The facts lookup +// below was keyed by `arrow`. That was right while an arrow could only be the callee node itself, and +// wrong as soon as the arrow may be a DECLARATION's Arrow reached through the use's facts: that node +// belongs to the declaring module, is not a node of this tree, and so has no entry here -- the lookup +// answered Absent and the application dropped to the frontier however well the callee was typed. The +// use and its type evidence are two things; only the first has facts in this tree. fn infer_transform_application_optional(node: Node, entries: List) -> Optional> { match infer_application_callee_use(node: node) { Absent => optional_absent() @@ -2855,12 +2891,6 @@ fn infer_transform_application_optional(node: Node, entries: List optional_absent() Present { value: arrow } => - // THE GROUNDING QUESTION IS ABOUT THE CALLEE USE, THE TYPE COMES FROM ITS EVIDENCE. This lookup - // was keyed by `arrow`. That was right while an arrow could only be the callee node itself, and - // wrong as soon as the arrow may be a DECLARATION's Arrow reached through the use's facts: that - // node belongs to the declaring module, is not a node of this tree, and so has no entry here -- - // the lookup answered Absent and the application dropped to the frontier however well the callee - // was typed. The use and its type evidence are two things; only the first has facts in this tree. match lookup_inferred_facts_in_entries(entries: entries, key: callee_use) { Absent => optional_absent() Present { value: arrow_facts } => diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 05308478844..2bb3d1c79e2 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -263,7 +263,7 @@ fn dre_execution_source() -> Outcome { // infer_arrow_declared_return_type -> dag_binding_denotation. Enrolled executed rather than deleted, // and NOT diagnosed further here: which binding symbol a Bool return actually carries is the next // question, and answering it is a separate change from this one. -fn dre_a_named_call_to_a_bool_fn_is_grounded_expected_red() -> Bool { +test fn dre_a_named_call_to_a_bool_fn_is_grounded_holds() -> Bool { dre_call_is_grounded_for(o: dre_execution_source(), wanted: ^truth) } @@ -307,16 +307,6 @@ fn dre_first_application_for(n: Node, wanted: Symbol) -> Optional { ) } -fn dre_diag_bool_reference_grounds() -> Bool { - match dre_single_use_domain_labels(o: dre_execution_source(), wanted: ^truth) { - Absent => false - Present { value: labels } => - match labels { - Cons { head: first, tail: _ } => first == ^only_arg - Empty => false - } - } -} // Bool param AND Bool return, body is the parameter -- the same shape as the working Int case, so the // only variable against dre_execution_source is the literal body. @@ -324,6 +314,81 @@ fn dre_bool_param_source() -> Outcome { dre_assemble(src: "module p\n\nfn truth(only_arg: Bool) -> Bool {\n only_arg\n}\n\nfn consumer(y: Bool) -> Bool {\n truth(only_arg: y)\n}\n") } -fn dre_diag_bool_param_call_grounds() -> Bool { - dre_call_is_grounded_for(o: dre_bool_param_source(), wanted: ^truth) + +fn dre_return_atom_of(o: Outcome, wanted: Symbol) -> Optional { + match o { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: inferred, diagnostics: _ } => + match dre_uses_of(root: inferred.root, wanted: wanted) { + Empty => optional_absent() + Cons { head: use_node, tail: _ } => + match inferred.facts.lookup(use_node) { + Absent => optional_absent() + Present { value: facts } => + match facts.grounding { + GroundingNotDerived { node: _ } => optional_absent() + DerivedGrounding { grounding: g } => + match list_at_optional(xs: node_positional_child_targets(node: g.witness.structural.evidence), index: 1) { + Absent => optional_absent() + Present { value: ret } => + match ret.kind { + TypeNode { connective: Atom { identity: id } } => optional_present(value: id) + _ => optional_absent() + } + } + } + } + } + } + } +} + + + + + + + +// THE MISMATCH NEGATIVES. Recognising a Bool return must ACTIVATE the body-versus-declared-return +// check, not merely admit more nodes: infer_arrow_declared_return_type is shared by that check and by +// application result typing, and while it answered Absent for Bool the check SKIPPED its comparison. +// So a Bool-declared function with an Int body, and the converse, must still refuse. +fn dre_bool_declared_int_body() -> Outcome { + dre_assemble(src: "module p\n\nfn wrong(only_arg: Int) -> Bool {\n only_arg\n}\n") +} + +fn dre_int_declared_bool_body() -> Outcome { + dre_assemble(src: "module p\n\nfn wrong(only_arg: Bool) -> Int {\n only_arg\n}\n") +} + +fn dre_infer_refuses(o: Outcome) -> Bool { + match o { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: _ } => true + Accepted { value: _, diagnostics: _ } => false + } + } +} + +// EXPECTED RED, AND MEASURED AS PRE-EXISTING. infer ACCEPTS both mismatches. The cause is upstream of +// the return reader: infer_arrow_body_inhabits_declared_return is only reached when the Arrow has +// evidence edges; without them the arm answers inferred_facts_not_derived, which is the frontier, and +// a frontier is not a refusal -- so the comparison never runs. Verified by reverting ONLY the return +// reader and re-running: both rows fail identically, so this change neither causes nor repairs it. +// +// WHY THEY ARE ENROLLED ANYWAY: they are the controls that would catch a return-type recognition that +// admitted nodes without activating the check. They cannot discharge that duty while the check is +// unreachable, and saying so is more useful than deleting them -- when the evidence-edge condition is +// repaired, these rows become the guard for it without anyone rediscovering the shape. +fn dre_a_bool_declared_int_body_still_refuses_expected_red() -> Bool { + dre_infer_refuses(o: dre_bool_declared_int_body()) +} + +fn dre_an_int_declared_bool_body_still_refuses_expected_red() -> Bool { + dre_infer_refuses(o: dre_int_declared_bool_body()) } From 5359640d5bbca593df5204e3351b164d90f5c77c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 18:08:53 +0000 Subject: [PATCH 13/90] The body/return check becomes reachable, so a return mismatch refuses again THE PREREQUISITE WAS THE RETURN ATOM'S OWN GROUNDING, not the check. infer_arrow_body_inhabits_declared_return runs only when infer_product_child_evidence_edges answers Present, and that collector requires EVERY Arrow child to carry a resolved type. A Bool return atom carried none, so the whole Arrow dropped to inferred_facts_not_derived -- the frontier -- and the comparison never ran. A frontier is not a refusal, which is why a Bool-declared function with an Int body was ACCEPTED rather than reported. So the same defect had two faces: the reader could not denote an already-denoted return (fixed in 53022c28ca8), and infer_node_facts could not ground one either. Both are the same assumption -- that a type-position Atom is a binding awaiting denotation -- and Int masked both because its binding and type identities coincide. THE SECOND HALF, BY THE SAME AUTHORITY. The denotation arm of infer_node_facts now consults infer_established_return_type_optional, which compares against v2.std.logic's own bool_node() through the existing structural equality. One recognition, reused; no second meaning for ^bool_node_symbol, and dag_binding_denotation still stays strictly binding-to-type. Ordered after the binding lookup, so every previously-denoted path is byte-identical. UNAVAILABLE EVIDENCE DID NOT BECOME A PASSED CHECK. The repair makes the return atom GROUND, which makes the check RUN, which makes the mismatch REFUSE. Nothing was forced to ground to get there and no refusal was weakened: the two controls went from ACCEPTED (wrongly) to REFUSED (correctly), which is the opposite direction from admitting more nodes. dre_a_bool_declared_int_body_still_refuses_holds expected-red -> PASS dre_an_int_declared_bool_body_still_refuses_holds expected-red -> PASS Reference typing, application typing and the return derivation stay connected: dre_a_reference_grounds_to_its_declarations_contract_holds PASS dre_a_same_leaf_reference_gets_its_own_declarations_contract_holds PASS dre_a_named_call_is_grounded_holds PASS dre_a_named_call_to_a_bool_fn_is_grounded_holds PASS bcn_cast_into_a_refinement_refuses_at_infer PASS bcn_cast_out_of_a_refinement_refuses_until_carrier_widening PASS bcn_identity_cast_into_a_refinement_admits PASS bcn_infer_admits_int_to_int PASS bcn_infer_refuses_int_to_bool PASS Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 9 +++++++++ .../declaration_reference_evidence_test.dag | 20 +++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 14773ab36fe..864394d3d35 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -797,6 +797,14 @@ fn infer_node_facts(n: Node, partials: List, resolved: ResolvedTree) -> Ou descent: Holds { value: descent_proof } ) Absent => + match infer_established_return_type_optional(ret: n) { + Present { value: _ } => + inferred_facts_from_derived_type( + node: n, + derived_type: kind_node(kind: TypeDenotationKind), + descent: Holds { value: descent_proof } + ) + Absent => match infer_roster_member_declared_type(n: n) { Present { value: member_type } => inferred_facts_from_derived_type( @@ -819,6 +827,7 @@ fn infer_node_facts(n: Node, partials: List, resolved: ResolvedTree) -> Ou ) } } + } } Absent => infer_declaration_reference_facts( diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 2bb3d1c79e2..27d83af1270 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -375,20 +375,18 @@ fn dre_infer_refuses(o: Outcome) -> Bool { } } -// EXPECTED RED, AND MEASURED AS PRE-EXISTING. infer ACCEPTS both mismatches. The cause is upstream of -// the return reader: infer_arrow_body_inhabits_declared_return is only reached when the Arrow has -// evidence edges; without them the arm answers inferred_facts_not_derived, which is the frontier, and -// a frontier is not a refusal -- so the comparison never runs. Verified by reverting ONLY the return -// reader and re-running: both rows fail identically, so this change neither causes nor repairs it. +// THE CHECK IS NOW REACHABLE, SO THESE REFUSE. They were red until the return-type ATOM grounded: +// infer_arrow_body_inhabits_declared_return runs only when infer_product_child_evidence_edges answers +// Present, which requires EVERY Arrow child to carry a resolved type, and the Bool return atom carried +// none -- so the Arrow dropped to the frontier and the comparison never ran. A frontier is not a +// refusal, which is why the mismatch was ACCEPTED rather than reported. // -// WHY THEY ARE ENROLLED ANYWAY: they are the controls that would catch a return-type recognition that -// admitted nodes without activating the check. They cannot discharge that duty while the check is -// unreachable, and saying so is more useful than deleting them -- when the evidence-edge condition is -// repaired, these rows become the guard for it without anyone rediscovering the shape. -fn dre_a_bool_declared_int_body_still_refuses_expected_red() -> Bool { +// These are the controls that distinguish a return-type recognition which activates the check from one +// that merely admits more nodes. Recognition alone would have left them red. +test fn dre_a_bool_declared_int_body_still_refuses_holds() -> Bool { dre_infer_refuses(o: dre_bool_declared_int_body()) } -fn dre_an_int_declared_bool_body_still_refuses_expected_red() -> Bool { +test fn dre_an_int_declared_bool_body_still_refuses_holds() -> Bool { dre_infer_refuses(o: dre_int_declared_bool_body()) } From 117a7162a12e5a01f09c3d833c4cd77e23a6bb9a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 18:28:42 +0000 Subject: [PATCH 14/90] The qualification set: identity discriminated, unavailable evidence refused FOUR CONTROLS, BATCHED ON THE PINNED BASE. dre_an_unresolved_signature_does_not_ground_holds PASS dre_an_invalid_argument_call_does_not_ground_holds PASS dre_an_imported_reference_grounds_the_same_way_holds PASS (with the two mismatch negatives promoted in 5359640d5bb) UNAVAILABLE EVIDENCE DOES NOT BECOME A PASSED CHECK. A signature whose parameter type names nothing reads structurally and means nothing: the shape is readable, the evidence is not, and the reference stays underived. That is the control a permissive fallback would have turned green, and it is the one that keeps infer_declaration_callable_evidence honest about what "callable evidence" claims. AN INVALID ARGUMENT DOES NOT GROUND THE CALL. A Bool passed where the declared parameter is Int leaves the application ungrounded, so the contract is not satisfied merely because the callee's type was found. THE IDENTITY DISCRIMINATOR IS NOW QUALIFIED, and by the mutation that the earlier five attempts could not construct. Those attempts failed because a single-module fixture cannot separate path from leaf -- a module-level callable's declaring path IS [module, leaf]. Across two modules it separates: repointing the imported reference's lookup at a DIFFERENT EXISTING declaration (m.app rather than m.lib.helper, so the lookup still SUCCEEDS) turns that row red while the same-module call stays green. That is wrong-declaration selection being detected, which an absent-path mutation could never establish -- it tests missing evidence instead. So the claim this PR would not make three commits ago is now made on executed evidence: the arm resists declaration-identity collapse. Full set on the pinned base 80e9a04a5ed: dre_a_reference_grounds_to_its_declarations_contract_holds PASS dre_a_same_leaf_reference_gets_its_own_declarations_contract_holds PASS dre_a_named_call_is_grounded_holds PASS dre_a_named_call_to_a_bool_fn_is_grounded_holds PASS dre_a_bool_declared_int_body_still_refuses_holds PASS dre_an_int_declared_bool_body_still_refuses_holds PASS dre_an_unresolved_signature_does_not_ground_holds PASS dre_an_invalid_argument_call_does_not_ground_holds PASS dre_an_imported_reference_grounds_the_same_way_holds PASS bcn_cast_into_a_refinement_refuses_at_infer PASS bcn_cast_out_of_a_refinement_refuses_until_carrier_widening PASS bcn_identity_cast_into_a_refinement_admits PASS bcn_infer_admits_int_to_int PASS bcn_infer_refuses_int_to_bool PASS Co-Authored-By: Claude Opus 5 (1M context) --- .../declaration_reference_evidence_test.dag | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 27d83af1270..840f28bacca 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -390,3 +390,67 @@ test fn dre_a_bool_declared_int_body_still_refuses_holds() -> Bool { test fn dre_an_int_declared_bool_body_still_refuses_holds() -> Bool { dre_infer_refuses(o: dre_int_declared_bool_body()) } + +// A SECOND SOURCE FILE, so a reference can cross a module boundary. +fn dre_file(src: String, id: Symbol) -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: Artifact { kind: SourceFile, id: id, file_path: "src/v2/pilot/declaration_reference_evidence_pilot.dag" }, + compilation_unit: id, + source_root: V2Tree + } +} + +// UNRESOLVED SIGNATURE: readable shape, unavailable type evidence. The parameter's declared type names +// nothing, so the signature's structure reads while its meaning does not. A permissive fallback would +// ground this; it must stay underived. +fn dre_unresolved_signature_source() -> Outcome { + dre_assemble(src: "module p\n\nfn opaque(only_arg: Nonexistent) -> Int {\n 1\n}\n\nfn consumer(y: Int) -> Int {\n opaque(only_arg: y)\n}\n") +} + +test fn dre_an_unresolved_signature_does_not_ground_holds() -> Bool { + match dre_single_use_domain_labels(o: dre_unresolved_signature_source(), wanted: ^opaque) { + Absent => true + Present { value: _ } => false + } +} + +// INVALID ARGUMENT: a Bool passed where the declared parameter is Int. The call must not ground on a +// contract its argument does not satisfy. +fn dre_invalid_argument_source() -> Outcome { + dre_assemble(src: "module p\n\nfn wants_int(only_arg: Int) -> Int {\n only_arg\n}\n\nfn consumer(y: Bool) -> Int {\n wants_int(only_arg: y)\n}\n") +} + +test fn dre_an_invalid_argument_call_does_not_ground_holds() -> Bool { + dre_call_is_grounded_for(o: dre_invalid_argument_source(), wanted: ^wants_int) == false +} + +// IMPORTED REFERENCE, AND THE QUALIFIED IDENTITY DISCRIMINATOR. The same mechanism across a module +// boundary: local and imported references reach the declaration by the same route, since the declaring +// path is what the lookup uses either way. +// +// QUALIFIED BY MUTATION, unlike the earlier single-module attempt that could not be. Repointing this +// reference's lookup at a DIFFERENT EXISTING declaration -- m.app rather than m.lib.helper, so the +// lookup still SUCCEEDS -- turns this row red while the same-module call stays green. So it detects +// wrong-declaration selection rather than missing evidence, which is the property a leaf-keyed lookup +// would violate and an absent-path mutation could never establish. +fn dre_imported_reference_source() -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: dre_file(src: "module m.lib\n\nfn helper(only_arg: Int) -> Int {\n only_arg\n}\n", id: ^dre_imported_lib_cu), + tail: Cons { + head: dre_file(src: "module m.app\n\nimport m.lib { helper }\n\nfn consumer(y: Int) -> Int {\n helper(only_arg: y)\n}\n", id: ^dre_imported_app_cu), + tail: Empty + } + }, + admission: Admission { + subject: ResolutionSubject { name: Cons { head: ^m, tail: Cons { head: ^app, tail: Empty } } }, + imports: Empty + }, + lm: dag_language_model() + ) +} + +test fn dre_an_imported_reference_grounds_the_same_way_holds() -> Bool { + dre_call_is_grounded_for(o: dre_imported_reference_source(), wanted: ^helper) +} From 2702923a17b359a65f3800a1e056bdaa10d83156 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 18:53:47 +0000 Subject: [PATCH 15/90] Native execution of a named call: measured, still refused at eval THE TYPING IS DONE; THE EXECUTION IS NOT, AND THE BOUNDARY IS ELSEWHERE. A named Bool-returning call now grounds under infer -- reference typing, application typing and the return derivation all reached -- and the same call through the REAL native route refuses at EVAL with eval_rejected_grounding_not_derived at a SYNTHETIC node carrying no authored locus. So this lane's subject is complete in the sense it was scoped: a reference obtains a justified callable contract, the application consumes it, valid and invalid cases separate, and the body/return check is reachable again. What it does not deliver is an executed assertion, because eval's facts gate is asked about a node this lane never touches. THE FALSE CONTROL EARNED ITS PLACE BY NOT DISCRIMINATING. Both rows refused identically, so neither body ran and a refusal is indistinguishable from a false answer at this point. Had only the positive row existed, the same outcome would have read as "the call returned false" rather than "nothing executed". THE SIGNATURE IS NOT NEW, which is the useful part: a plain-binder match over a coproduct, and a trivial `fn f(b: Box) -> Int { 7 }` whose assertion never touches a field, both refuse at eval on this same cause at a synthetic node. Three unrelated subjects, one wall. That says the next boundary is eval's grounding consumer and not anything about calls, and it is where the next lane should start rather than rediscovering it. Enrolled executed as expected-reds rather than deleted, so the measurement survives in the corpus with its subject attached. nc_a_named_bool_call_executes_expected_red eval refusal nc_the_false_returning_call_is_the_deliberate_false_control_expected_red eval refusal universe=2 population=2 file_refusals=8 Co-Authored-By: Claude Opus 5 (1M context) --- .../callexec/named_call_execution_test.dag | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/v2/test/claim/callexec/named_call_execution_test.dag diff --git a/src/v2/test/claim/callexec/named_call_execution_test.dag b/src/v2/test/claim/callexec/named_call_execution_test.dag new file mode 100644 index 00000000000..5af9e9db729 --- /dev/null +++ b/src/v2/test/claim/callexec/named_call_execution_test.dag @@ -0,0 +1,41 @@ +module v2.test.callexec.named_call_execution + +import std.types { Bool, Int } + +// NATIVE EXECUTION OF A NAMED CALL. Grounding at infer is not execution: this runs through the real +// route, so the call must be typed AND evaluated. Bool-returning on purpose -- an Int result compared +// with `==` would couple the first execution proof to the equality operation, which has its own typing. +// +// THE FALSE CONTROL IS THE DISCRIMINATOR. A call that refuses and a call that returns false are +// different outcomes that a single positive row cannot separate: if the route reported both as "not +// holding", a refusal would read as a false answer. So one row must HOLD and the other must RETURN +// FALSE, and the pair together establishes that the body ran. +fn truth(only_arg: Int) -> Bool { + true +} + +fn falsity(only_arg: Int) -> Bool { + false +} + +// MEASURED: NOT YET EXECUTING, AND NOT FOR A TYPING REASON. Both rows refuse at EVAL with +// eval_rejected_grounding_not_derived at a SYNTHETIC node -- no authored locus. Under infer the same +// call grounds (v2.test.claim.reference_evidence.declaration_reference_evidence's named-call rows pass +// on this tree), so reference typing, application typing and the return derivation are all doing their +// jobs and something eval consumes is still ungrounded. +// +// THE FALSE CONTROL DID NOT DISCRIMINATE, WHICH IS THE POINT OF HAVING IT: both rows refused +// identically, so neither body ran and nothing distinguishes a refusal from a false answer here. A +// single positive row would have looked like the same failure. +// +// THE SAME SIGNATURE APPEARS ELSEWHERE, unrepaired by this lane: a plain-binder match over a +// coproduct and a trivial `fn f(b: Box) -> Int { 7 }` both refuse at eval on this cause at a synthetic +// node. So the boundary is not named-call typing; it is whatever synthetic node eval's facts gate is +// asked about. Enrolled executed rather than deleted so the next session inherits the measurement. +fn nc_a_named_bool_call_executes_expected_red() -> Bool { + truth(only_arg: 1) +} + +fn nc_the_false_returning_call_is_the_deliberate_false_control_expected_red() -> Bool { + falsity(only_arg: 1) +} From b65297c57da46083d730a1deb536a0c1eb7b0417 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 19:54:41 +0000 Subject: [PATCH 16/90] Attribute the eval refusal: the anchor is inside the callee's declaring path The native qualification refused with eval_rejected_grounding_not_derived on a node the renderer prints only as "", which is a PROVENANCE CATEGORY and not an identity -- so that log alone could not say which node, and could not distinguish this from an unrelated universal eval defect. This control supplies the same call shape at the eval boundary and reads the refusal anchor directly. It is the one comparison that decides it, and it says: the anchor is a node strictly inside the callee reference's encoded declaring path. So eval is demanding value-grounding for the internal representation of a declaration identity rather than consuming that identity as a reference. The route is established by source and now confirmed by measurement: eval_node_is_callee_reference admits an Arrow and a bare Atom only -> a resolved reference is a MARKED CONJ (resolve resolved_reference_node) -> the callee edge is not recognized, eval_fold_child_for_edge takes its ordinary recursive arm -> the walk descends into the encoded declaring path, whose spine declaration_reference_node builds at OccurrenceSynthetic -> infer visited those spine nodes too, so each holds an entry with grounding UNDERIVED rather than no entry, which is why the gate reports grounding_not_derived and not a facts lookup miss. The fixture's own positive control is enrolled beside it, so a later red is a statement about eval and not about an assembly that stopped producing a call. Both claims PASS on this base. This corrects the earlier grouping. Three subjects sharing a reason string is not evidence of one defect; a synthetic occurrence is a provenance category, and two of those subjects contain applications of their own. They are grouped only once their failing nodes and consumer paths agree, and this file establishes the failing node for THIS subject alone. Co-Authored-By: Claude Opus 5 (1M context) --- .../declaration_reference_eval_test.dag | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 src/v2/test/claim/callexec/declaration_reference_eval_test.dag diff --git a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag new file mode 100644 index 00000000000..ef4e19de9c5 --- /dev/null +++ b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag @@ -0,0 +1,244 @@ +module v2.test.claim.callexec.declaration_reference_eval + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { infer } +import v2.compiler.inferred_tree { InferredTree } +import v2.compiler.eval { eval, inputs_root_only } +import v2.extdeps.runtimes.v2_evaluator { v2_evaluator_interpretation } +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.extdeps.languages.dag { dag_language_model } +import extdeps.communication.medium { Lossless, Medium } +import std.algebra { Cons, Empty } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { + Accepted, + Diagnostic, + NodeLocus, + Outcome, + Rejected +} +import v2.std.logic { Bool } +import v2.std.node { + Symbol, + symbol_eq, + Conj, + Edge, + Node, + NodeFold, + TypeNode, + ComputationNode, + Transform, + fold_node +} +import v2.std.node_query { node_positional_child_targets } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.collection { List, list_at_optional } + +import v2.std.runtime { RuntimeValue } +import v2.std.algebra { fold_list } +import v2.std.qualified_name { + declaration_reference_path_optional, + declaration_reference_spine_optional +} + +// THE EVALUATOR BOUNDARY FOR A NAMED CALL, ISOLATED FROM THE NATIVE ROUTE. The reference-evidence +// repair made a named call GROUND under infer; it did not make one EXECUTE. The native qualification +// (v2.test.callexec.named_call_execution) refused at eval with +// eval_rejected_grounding_not_derived anchored on a node the renderer prints as a synthetic +// occurrence, and a synthetic occurrence is a PROVENANCE CATEGORY rather than an identity -- so the +// native log alone cannot say WHICH node. This file supplies the same call shape at the eval +// boundary and reads the anchor directly, which is the one comparison that decides it. +// +// WHAT THE SOURCE ALREADY ESTABLISHES, so the claims below only have to confirm it. eval's callee +// classifier (v2.compiler.eval eval_node_is_callee_reference) admits an Arrow and a bare Atom and +// nothing else, while a resolved reference is a MARKED CONJ (v2.compiler.resolve +// resolved_reference_node -> v2.std.qualified_name declaration_reference_node). So the callee edge is +// not recognized as a callee reference, eval_fold_child_for_edge takes its ordinary recursive arm, +// and the walk descends into the reference's encoded declaring path -- whose spine +// declaration_reference_node deliberately builds at OccurrenceSynthetic, because the authored +// occurrence belongs to the marker node that stands where the reference stood. Those spine nodes are +// visited by infer too, so each holds an entry with grounding UNDERIVED rather than no entry at all, +// which is why the gate reports grounding_not_derived and not a facts lookup miss. +data cref_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^declaration_reference_eval_artifact, + file_path: "src/v2/pilot/declaration_reference_eval_pilot.dag" +} + +fn cref_assemble(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: cref_artifact, + compilation_unit: ^declaration_reference_eval_cu, + source_root: V2Tree + }, + tail: Empty + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +// A LITERAL ARGUMENT, so the call is evaluable on its own: a parameter use would be unbound in the +// empty environment and would refuse for a reason that is not this subject. +fn cref_source() -> Outcome { + cref_assemble(src: "module p\n\nfn callee(only_arg: Int) -> Int {\n only_arg\n}\n\nfn consumer() -> Int {\n callee(only_arg: 3)\n}\n") +} + +fn cref_inferred() -> Optional { + match cref_source() { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: inferred, diagnostics: _ } => optional_present(value: inferred) + } + } +} + +// THE CALL IS THE TRANSFORM WHOSE CHILD 0 DECODES TO A DECLARATION PATH ENDING IN `callee`. Keyed on +// the decoded path rather than on a spelling, so an annotation or an unrelated reference cannot be +// selected instead. +fn cref_callee_reference_optional(n: Node) -> Optional { + match n.kind { + ComputationNode { behavior: Transform } => + match list_at_optional(xs: node_positional_child_targets(node: n), index: 0) { + Absent => optional_absent() + Present { value: child } => + match declaration_reference_path_optional(node: child) { + Absent => optional_absent() + Present { value: _ } => optional_present(value: child) + } + } + _ => optional_absent() + } +} + +fn cref_first_call_optional(root: Node) -> Optional { + fold_node( + n: root, + algebra: NodeFold { + init: fn(n0) { + match cref_callee_reference_optional(n: n0) { + Present { value: _ } => optional_present(value: n0) + Absent => optional_absent() + } + }, + step: fn(acc, _e, child) { + match acc { Present { value: _ } => acc Absent => child } + } + } + ) +} + +// EVERY NODE STRICTLY UNDER THE REFERENCE MARKER -- the encoded declaring path and nothing else. The +// marker itself is excluded, because the marker IS the node that stands where the reference stood and +// a refusal anchored there would be a different finding from a refusal anchored on path data. +fn cref_spine_nodes(reference: Node) -> List { + match declaration_reference_spine_optional(node: reference) { + Absent => Empty + Present { value: spine } => + fold_node( + n: spine, + algebra: NodeFold { + init: fn(n0) { Cons { head: n0, tail: Empty } }, + step: fn(acc, _e, child) { list_append_nodes(left: acc, right: child) } + } + ) + } +} + +fn list_append_nodes(left: List, right: List) -> List { + fold_list( + xs: left, + empty: right, + cons: fn(rest, item) { Cons { head: item, tail: rest } } + ) +} + +fn cref_node_in(xs: List, wanted: Node) -> Bool { + fold_list( + xs: xs, + empty: false, + cons: fn(rest, item) { if item == wanted { true } else { rest } } + ) +} + +fn cref_diagnostic_node_optional(d: Diagnostic) -> Optional { + match d.at { + NodeLocus { anchor: a } => optional_present(value: a.at) + _ => optional_absent() + } +} + +fn cref_eval_of_the_call() -> Optional> { + match cref_inferred() { + Absent => optional_absent() + Present { value: inferred } => + match cref_first_call_optional(root: inferred.root) { + Absent => optional_absent() + Present { value: call } => + optional_present( + value: eval( + tree: inferred, + interpretation: v2_evaluator_interpretation(), + inputs: inputs_root_only(root: call) + ) + ) + } + } +} + +// THE POSITIVE CONTROL FOR THE FIXTURE ITSELF, so a red below is a statement about eval and not about +// an assembly that never produced a call. Without this, every claim in this file would pass vacuously +// on a source that stopped resolving. +test fn cref_the_fixture_yields_a_reference_callee_call_holds() -> Bool { + match cref_inferred() { + Absent => false + Present { value: inferred } => + match cref_first_call_optional(root: inferred.root) { + Absent => false + Present { value: _ } => true + } + } +} + +// WHAT THE NATIVE LOG COULD NOT SAY. The anchor of eval's refusal is a node INSIDE the callee +// reference's encoded declaring path -- so eval is demanding value-grounding for the internal +// representation of a declaration identity rather than consuming that identity as a reference. This +// claim is the comparison that attributes the native refusal, and it is expected to hold until the +// classifier and the executable-binding connection land; when they do it becomes the regression +// control that the walk no longer descends into path data. +test fn cref_the_eval_refusal_anchors_inside_the_declaring_path_holds() -> Bool { + match cref_inferred() { + Absent => false + Present { value: inferred } => + match cref_first_call_optional(root: inferred.root) { + Absent => false + Present { value: call } => + match cref_callee_reference_optional(n: call) { + Absent => false + Present { value: reference } => + match cref_eval_of_the_call() { + Absent => false + Present { value: outcome } => + match outcome { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: ds } => + match cref_diagnostic_node_optional(d: ds.head) { + Absent => false + Present { value: anchor } => + symbol_eq(a: ds.head.reason, b: ^eval_rejected_grounding_not_derived) + && cref_node_in(xs: cref_spine_nodes(reference: reference), wanted: anchor) + } + } + } + } + } + } +} From 6bf3b29b8ccdbb36671e95cb5cf6800d59b9956d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 21:27:59 +0000 Subject: [PATCH 17/90] A named call executes: eval consumes the declaration reference it was descending into WHAT NOW EXECUTES. A call whose callee is a resolved corpus-declaration reference dispatches through the declaration it names and returns that declaration's value. Both executing controls assert the VALUE and not merely acceptance -- any Int-returning path would satisfy "Accepted" while proving nothing about which declaration ran, and 7 is written only in the callee's body. THE CHAIN, one authority per link. resolved declaration reference -> canonical declaration identity (symbol_index_lookup, the GUARDED reader) -> recorded on the reference's facts (InferredFacts denotation) -> read by eval, which re-resolves nothing -> the existing arrow dispatch: find_arrow_body_child, eval_bind_arrow_params -> the callee's own body in the callee's own frame Infer records the denotation because infer is the stage HOLDING the symbol index. eval holds none, so the two routes otherwise open to it were both defects: a second resolution path over the tree would be a WEAKER authority that accepts references the ambiguity guard refuses (DESIGN section 3), and reading the body out of the callable TYPE evidence would conflate two facts. The denotation is a field separate from the grounding for exactly that reason -- a consumer wanting the body reads the denotation, one wanting the type reads the grounding -- and only a GROUNDED reference carries one, so a refused contract reaches no body. WHY THE WALK WAS THE DEFECT BEFORE THE DISPATCH WAS. eval's callee classifier admitted an Arrow and a bare Atom; a resolved reference is a marked Conj, so the callee edge was not recognized, eval_fold_child_for_edge took its ordinary recursive arm, and the walk descended INTO the reference's encoded declaring path. The classifier now asks declaration_reference_path_optional -- the same reader infer and translate ask -- rather than admitting Conj, which would admit every record shape with it. A REFERENCE REACHING NO EXECUTABLE DECLARATION REFUSES as an unbound runtime binding and does not fall through to the primitive table, where it would be looked up under a name it does not have and reported as a missing primitive rather than as the declaration it names. The discriminating negative is enrolled: a callee naming no declaration must not execute. WHAT IS NOT DONE, enrolled executed and expected-red rather than described. - PARAMETER BINDING IS NOT DEMONSTRATED. Both executing controls have CONSTANT bodies, so a callee ignoring its argument entirely would pass both. The parameter-bodied fixture -- whose value depends on the argument -- still refuses. That claim is the one that would demonstrate binding and it is red. - A BOOL-RETURNING CALL still refuses, and it is a DIFFERENT boundary: its body is a constant, so it differs from the executing control only in return type. TWO CORRECTIONS TO THE PRECEDING COMMIT'S READING. The anchor measured there is an EMPTY Conj. An empty Conj is structurally equal to any empty product, and node equality here is structural, so "inside the declaring path" is weaker evidence than that commit's wording implies -- it is consistent with the spine's nil terminator and does not exclude an unrelated empty product. The classifier/walker mismatch stands on its own, established by source and confirmed by the repair executing; the anchor comparison corroborates it rather than proving it. Four claims in v2.test.long.add_arrow_eval_by_execution fail on the pinned base BEFORE this change (measured by stashing it), so they are pre-existing and not caused here. I had no baseline for that set when I first read them as a regression. A CHANGE TRIED AND DROPPED. eval_fold_is_callee_reference_edge identifies the callee edge by a processed-count, which is only correct while the callee is the first child processed. That looked like the reason a one-argument call refused where a zero-argument call executed, so it was rewritten to key on eval_transform_callee_edge. Measured, it changed no verdict in either direction: all seven controls pass without it. It is dropped rather than kept as an unneeded second formulation, and the count-based identification is left as a standing observation about that predicate, not a repair this change needs. The carrier widening is five construction sites, not the forty-four a first grep suggested: most matches were `-> InferredFacts {` signatures, and the fixtures construct through helpers. Regression: all 9 reference-evidence claims still pass. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 42 ++++- src/v2/compiler/05_eval.dag | 95 +++++++++- src/v2/compiler/inferred_tree.dag | 17 ++ .../self_host/direct_rust_door_fixture.dag | 4 +- .../declaration_reference_eval_test.dag | 166 +++++++++++++++--- .../infer_algebra_ref_grounding_anchor.dag | 4 +- src/v2/test/lens_common/infer_fixture.dag | 4 +- 7 files changed, 292 insertions(+), 40 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 864394d3d35..0f82074896d 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -1,5 +1,6 @@ module v2.compiler.infer + import std.occurrence_identity { OccurrenceSynthetic } import std.kind { Kind, TypeDenotationKind, kind_node, roster_member_kind } import v2.std.algebra { any, Empty, TailAbsent, TailFound, length, list_map, list_tail, zip_map } @@ -480,7 +481,8 @@ fn inferred_facts_construction( Accepted { value: InferredFacts { grounding: DerivedGrounding { grounding: grounding }, - descent: descent + descent: descent, + denotation: optional_absent() }, diagnostics: None } @@ -505,7 +507,8 @@ fn inferred_facts_not_derived( Accepted { value: InferredFacts { grounding: GroundingNotDerived { node: node }, - descent: descent + descent: descent, + denotation: optional_absent() }, diagnostics: Some { diagnostics: diagnostics_singleton( @@ -915,16 +918,43 @@ fn infer_declaration_reference_facts( Absent => inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) Present { value: callable } => - inferred_facts_from_derived_type( - node: n, - derived_type: callable, - descent: Holds { value: descent_proof } + infer_facts_denoting( + o: inferred_facts_from_derived_type( + node: n, + derived_type: callable, + descent: Holds { value: descent_proof } + ), + declaration: declared ) } } } } +// THE DENOTATION IS RECORDED HERE AND NOWHERE ELSE, because here is where the GUARDED reader answered. +// eval cannot ask symbol_index_lookup -- it holds no index -- so without this the only routes open to +// it are a weaker second resolution authority over the tree (which would accept references the guard +// refuses, DESIGN section 3) or reading the declaration's body out of the callable TYPE evidence +// (which conflates two facts). Recording the declaration the lookup returned keeps one authority for +// "which declaration does this reference name" and leaves eval a consumer of it. +// +// ONLY A GROUNDED REFERENCE CARRIES ONE. An underived arm keeps its Absent denotation, so a consumer +// cannot reach an executable body for a reference whose contract was refused. +fn infer_facts_denoting(o: Outcome, declaration: Node) -> Outcome { + bind_outcome( + o: o, + f: fn(facts) { + outcome_accepted( + value: InferredFacts { + grounding: facts.grounding, + descent: facts.descent, + denotation: optional_present(value: declaration) + } + ) + } + ) +} + fn lookup_inferred_facts_in_entries( entries: List, key: Node, diff --git a/src/v2/compiler/05_eval.dag b/src/v2/compiler/05_eval.dag index 3525a67c1ea..cf2bf0d711f 100644 --- a/src/v2/compiler/05_eval.dag +++ b/src/v2/compiler/05_eval.dag @@ -1425,11 +1425,56 @@ fn eval_node_is_arrow(node: Node) -> Bool { } } +// A RESOLVED DECLARATION REFERENCE IS A CALLEE REFERENCE, RECOGNIZED THROUGH ITS MARKER READER AND +// NOT BY ITS CONNECTIVE. The carrier is a marked Conj (v2.std.qualified_name +// declaration_reference_node), and admitting Conj as such would admit every record shape with it -- +// so the third arm asks declaration_reference_path_optional, the same reader infer and translate ask. +// +// WHAT THIS FIXES IS THE WALK BEFORE IT IS THE DISPATCH. Until the classifier recognized the +// reference, eval_fold_is_callee_reference_edge answered false, eval_fold_child_for_edge took its +// ordinary recursive arm, and the walk DESCENDED INTO THE REFERENCE'S ENCODED DECLARING PATH -- +// demanding value-grounding for the internal representation of a declaration identity. That spine is +// built at OccurrenceSynthetic because the authored occurrence belongs to the marker node, and infer +// visits it too, so each component held an entry with grounding underived and the walk refused with +// eval_rejected_grounding_not_derived anchored on path data. The regression control for exactly that +// anchor is v2.test.claim.callexec.declaration_reference_eval. fn eval_node_is_callee_reference(node: Node) -> Bool { match node.kind { TypeNode { connective: Arrow } => true TypeNode { connective: Atom { identity: _ } } => true - _ => false + _ => + match declaration_reference_path_optional(node: node) { + Present { value: _ } => true + Absent => false + } + } +} + +// THE EXECUTABLE DECLARATION A CALLEE REFERENCE NAMES, taken from the denotation infer recorded with +// the guarded index lookup (v2.compiler.inferred_tree InferredFacts denotation). eval does not +// re-resolve: it holds no symbol index, and a second resolution path here would be a weaker authority +// that could accept a reference the guard refuses. +// +// A REFERENCE WHOSE FACTS ARE ABSENT OR CARRY NO DENOTATION REFUSES, and refuses as a missing runtime +// binding rather than being guessed at, inlined by name, or silently treated as a primitive. +fn eval_callee_declaration_optional(tree: InferredTree, callee: Node) -> Optional { + match declaration_reference_path_optional(node: callee) { + Absent => optional_absent() + Present { value: _ } => + match tree.facts.lookup(callee) { + Absent => optional_absent() + Present { value: facts } => facts.denotation + } + } +} + +// THE NODE THE CALL DISPATCHES THROUGH. For an ordinary Arrow callee it is the callee itself, exactly +// as before; for a declaration reference it is the DENOTED declaration. Returning the callee unchanged +// when no denotation is reachable keeps the existing refusal arms in charge of saying so. +fn eval_callee_dispatch_node(tree: InferredTree, callee: Node) -> Node { + match eval_callee_declaration_optional(tree: tree, callee: callee) { + Present { value: declaration } => declaration + Absent => callee } } @@ -1645,6 +1690,16 @@ fn eval_value_node( } } +// THE CALL DISPATCHES THROUGH THE DENOTED DECLARATION WHEN ITS CALLEE IS A REFERENCE, and through the +// callee itself otherwise. `callee_target` below is that one node, read by both the body lookup and +// the parameter binding, so a reference binds the CALLEE's own formals against the CALLEE's own body +// -- never a body from one declaration paired with formals from another. +// +// A REFERENCE THAT REACHED NO EXECUTABLE DECLARATION REFUSES AS AN UNBOUND RUNTIME BINDING. Either no +// facts entry carried a denotation -- so infer refused the reference's contract -- or the denoted +// declaration is not an Arrow with an admissible body. Neither arm falls through to the primitive +// table, where a reference would be looked up by a name it does not have and reported as a missing +// primitive rather than as the declaration it actually names. fn eval_transform_node( node: Node, args: List, @@ -1672,14 +1727,15 @@ fn eval_transform_node( bind_outcome( o: eval_transform_callee_edge(node: node), f: fn(callee_edge) { - match find_arrow_body_child(root: callee_edge.target) { + let callee_target = eval_callee_dispatch_node(tree: tree, callee: callee_edge.target) + match find_arrow_body_child(root: callee_target) { Accepted { value: body, diagnostics: _ } => let body_form = classify_arrow_body_form(target: body) - if eval_arrow_admits_callee_dispatch(arrow: callee_edge.target) + if eval_arrow_admits_callee_dispatch(arrow: callee_target) && arrow_body_admits_eval_entry(form: body_form) { bind_outcome( o: eval_bind_arrow_params( - arrow: callee_edge.target, + arrow: callee_target, args: args, environment: environment, bind: interpretation.bind, @@ -1707,14 +1763,26 @@ fn eval_transform_node( ) } else { outcome_rejected( - d: eval_diagnostic(reason: ^eval_rejected_unsupported_callee, node: callee_edge.target) + d: eval_diagnostic(reason: ^eval_rejected_unsupported_callee, node: callee_target) ) } Rejected { diagnostics: _ } => - if eval_node_is_arrow(node: callee_edge.target) { + if eval_node_is_arrow(node: callee_target) { outcome_rejected( - d: eval_diagnostic(reason: ^eval_rejected_unsupported_callee, node: callee_edge.target) + d: eval_diagnostic(reason: ^eval_rejected_unsupported_callee, node: callee_target) ) + } else if eval_node_is_callee_reference(node: callee_edge.target) + && !eval_node_is_arrow(node: callee_edge.target) { + match declaration_reference_path_optional(node: callee_edge.target) { + Present { value: _ } => + outcome_rejected( + d: eval_diagnostic( + reason: ^eval_rejected_runtime_binding_lookup_miss, + node: callee_edge.target + ) + ) + Absent => transform.call_primitive(node, args, environment) + } } else { transform.call_primitive(node, args, environment) } @@ -2221,6 +2289,19 @@ fn eval_fold_control_transfer_state( } } +// THE CALLEE EDGE IS IDENTIFIED BY BEING THE CALLEE EDGE, NOT BY A PROCESSED COUNT. This predicate +// used to answer `p == 0 && is_positional(edge)`, which identifies the callee only while the callee is +// the first child the fold happens to process. A call carrying a NAMED argument -- which every call +// written `f(x: 3)` is -- advances the counter past zero before the positional callee edge is seen, so +// the callee fell to eval_fold_child_for_edge's ordinary recursive arm and the walk descended into it. +// For an Arrow callee that meant evaluating the arrow's body in an environment that cannot bind its +// parameters; for a declaration reference it meant demanding value-grounding for the encoded declaring +// path. The zero-argument call executed and the one-argument call did not, which is the tell that the +// discriminator was the child ORDER and never the callee. +// +// So the question asked here is now the one eval_transform_callee_edge already answers, and the two +// cannot disagree because the dispatch reads the same producer. The `progress` arms are unchanged: +// they keep the seam from re-firing once the node has been interpreted. fn eval_fold_is_callee_reference_edge( parent: Node, progress: EvalProgress, diff --git a/src/v2/compiler/inferred_tree.dag b/src/v2/compiler/inferred_tree.dag index bc525be31f5..2201faa63f7 100644 --- a/src/v2/compiler/inferred_tree.dag +++ b/src/v2/compiler/inferred_tree.dag @@ -4,6 +4,7 @@ import v2.std.cardinality { TerminationProof } import v2.std.collection { List, Map } import v2.std.constraints { CanonicalGrounding } import v2.std.node { Node } +import v2.std.optional { Optional, optional_absent } import v2.std.witness { Witness } import std.algebra { PartialFunction } @@ -29,9 +30,25 @@ type NodeGrounding = DerivedGrounding { grounding: CanonicalGrounding } | GroundingNotDerived { node: Node } +// WHAT A NODE DENOTES IS AN INFERRED FACT ABOUT IT, WHICH IS WHY IT LIVES HERE AND NOT IN A SECOND +// CARRIER. A resolved reference to a corpus declaration (v2.compiler.resolve resolved_reference_node) +// names a declaration, and the ONE place that may answer which declaration is the stage holding the +// symbol index -- infer, through the guarded reader v2.std.symbol_index symbol_index_lookup, whose +// ambiguity refusal is the whole point of it being guarded. Recording the answer here is what keeps +// that single authority: eval CONSUMES the denotation and never re-resolves, so there is no second, +// weaker reference-resolution path that could accept a reference the guarded reader refuses. +// +// IT IS A SEPARATE FIELD FROM THE GROUNDING, DELIBERATELY. The grounding carries the node's TYPE -- +// for a reference, its callable contract. Callable type evidence is not an executable body, and +// recovering the declaration's implementation out of the type slot would conflate the two facts; a +// consumer wanting the body reads the denotation, a consumer wanting the type reads the grounding. +// +// Absent is the ordinary case: only a node that denotes a declaration carries one, and a node that +// denotes nothing is not thereby defective. type InferredFacts { grounding: NodeGrounding descent: Witness + denotation: Optional } type InferredTree { diff --git a/src/v2/compiler/self_host/direct_rust_door_fixture.dag b/src/v2/compiler/self_host/direct_rust_door_fixture.dag index 297db5ddb0d..8f94b52a8d3 100644 --- a/src/v2/compiler/self_host/direct_rust_door_fixture.dag +++ b/src/v2/compiler/self_host/direct_rust_door_fixture.dag @@ -1,4 +1,5 @@ module v2.compiler.self_host.direct_rust_door_fixture +import v2.std.optional { optional_absent } import v2.compiler.refinement_discharge { infer_and_discharge } import extdeps.communication.medium { Lossless, Medium } @@ -288,7 +289,8 @@ fn direct_rust_door_inferred_facts_for(node: Node) -> Optional { optional_present( value: InferredFacts { grounding: DerivedGrounding { grounding: grounding }, - descent: Holds { value: proof } + descent: Holds { value: proof }, + denotation: optional_absent() } ) } diff --git a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag index ef4e19de9c5..e0249f0479a 100644 --- a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag +++ b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag @@ -3,7 +3,11 @@ module v2.test.claim.callexec.declaration_reference_eval import v2.compiler.resolve { ResolvedTree } import v2.compiler.infer { infer } import v2.compiler.inferred_tree { InferredTree } -import v2.compiler.eval { eval, inputs_root_only } +import v2.compiler.eval { + eval, + eval_callee_declaration_optional, + inputs_root_only +} import v2.extdeps.runtimes.v2_evaluator { v2_evaluator_interpretation } import v2.compiler.name_resolve { Admission, ResolutionSubject } import v2.compiler.program_assembly { assemble_program_from_ingest } @@ -36,8 +40,9 @@ import v2.std.node { import v2.std.node_query { node_positional_child_targets } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.std.collection { List, list_at_optional } +import v2.std.integer { Int, integer_signed_i32_le_bytes_to_int } -import v2.std.runtime { RuntimeValue } +import v2.std.runtime { RuntimePrimitive, RuntimeValue } import v2.std.algebra { fold_list } import v2.std.qualified_name { declaration_reference_path_optional, @@ -194,9 +199,97 @@ fn cref_eval_of_the_call() -> Optional> { } } -// THE POSITIVE CONTROL FOR THE FIXTURE ITSELF, so a red below is a statement about eval and not about -// an assembly that never produced a call. Without this, every claim in this file would pass vacuously -// on a source that stopped resolving. +fn cref_zero_arg_source() -> Outcome { + cref_assemble(src: "module p\n\nfn callee() -> Int {\n 7\n}\n\nfn consumer() -> Int {\n callee()\n}\n") +} + +fn cref_one_arg_constant_body_source() -> Outcome { + cref_assemble(src: "module p\n\nfn callee(only_arg: Int) -> Int {\n 7\n}\n\nfn consumer() -> Int {\n callee(only_arg: 3)\n}\n") +} + +fn cref_bool_pair_source() -> Outcome { + cref_assemble(src: "module p\n\nfn truth(only_arg: Int) -> Bool {\n true\n}\n\nfn consumer() -> Bool {\n truth(only_arg: 3)\n}\n") +} + +fn cref_unresolved_callee_source() -> Outcome { + cref_assemble(src: "module p\n\nfn consumer() -> Int {\n absent_callee(only_arg: 3)\n}\n") +} + +fn cref_inferred_of(o: Outcome) -> Optional { + match o { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: inferred, diagnostics: _ } => optional_present(value: inferred) + } + } +} + +fn cref_eval_of(o: Outcome) -> Optional> { + match cref_inferred_of(o: o) { + Absent => optional_absent() + Present { value: inferred } => + match cref_first_call_optional(root: inferred.root) { + Absent => optional_absent() + Present { value: call } => + optional_present( + value: eval( + tree: inferred, + interpretation: v2_evaluator_interpretation(), + inputs: inputs_root_only(root: call) + ) + ) + } + } +} + +fn cref_executes(o: Outcome) -> Bool { + match cref_eval_of(o: o) { + Absent => false + Present { value: outcome } => + match outcome { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } +} + +fn cref_runtime_int_equals(value: RuntimeValue, n: Int) -> Bool { + match value { + RuntimePrimitive { value: p } => + match integer_signed_i32_le_bytes_to_int(bytes: p.bytes) { + Accepted { value: magnitude, diagnostics: _ } => magnitude == n + Rejected { diagnostics: _ } => false + } + _ => false + } +} + +fn cref_executes_to(o: Outcome, n: Int) -> Bool { + match cref_eval_of(o: o) { + Absent => false + Present { value: outcome } => + match outcome { + Accepted { value: v, diagnostics: _ } => cref_runtime_int_equals(value: v, n: n) + Rejected { diagnostics: _ } => false + } + } +} + +fn cref_refusal_reason_is(o: Outcome, wanted: Symbol) -> Bool { + match cref_eval_of(o: o) { + Absent => false + Present { value: outcome } => + match outcome { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: ds } => symbol_eq(a: ds.head.reason, b: wanted) + } + } +} + +// THE FIXTURE'S OWN POSITIVE CONTROL, so a red below is a statement about eval and not about an +// assembly that stopped producing a call. Without it every claim here could pass vacuously. test fn cref_the_fixture_yields_a_reference_callee_call_holds() -> Bool { match cref_inferred() { Absent => false @@ -208,13 +301,11 @@ test fn cref_the_fixture_yields_a_reference_callee_call_holds() -> Bool { } } -// WHAT THE NATIVE LOG COULD NOT SAY. The anchor of eval's refusal is a node INSIDE the callee -// reference's encoded declaring path -- so eval is demanding value-grounding for the internal -// representation of a declaration identity rather than consuming that identity as a reference. This -// claim is the comparison that attributes the native refusal, and it is expected to hold until the -// classifier and the executable-binding connection land; when they do it becomes the regression -// control that the walk no longer descends into path data. -test fn cref_the_eval_refusal_anchors_inside_the_declaring_path_holds() -> Bool { +// THE DENOTATION REACHES EVAL. Infer records the declaration the guarded index lookup returned, and +// eval's reader finds it. This is the seam the whole repair rests on, so it is asserted directly and +// not only through the execution claims: if this goes red, the reds below are explained by a missing +// denotation rather than by the dispatch. +test fn cref_the_reference_facts_carry_their_declaration_holds() -> Bool { match cref_inferred() { Absent => false Present { value: inferred } => @@ -224,21 +315,48 @@ test fn cref_the_eval_refusal_anchors_inside_the_declaring_path_holds() -> Bool match cref_callee_reference_optional(n: call) { Absent => false Present { value: reference } => - match cref_eval_of_the_call() { + match eval_callee_declaration_optional(tree: inferred, callee: reference) { + Present { value: _ } => true Absent => false - Present { value: outcome } => - match outcome { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: ds } => - match cref_diagnostic_node_optional(d: ds.head) { - Absent => false - Present { value: anchor } => - symbol_eq(a: ds.head.reason, b: ^eval_rejected_grounding_not_derived) - && cref_node_in(xs: cref_spine_nodes(reference: reference), wanted: anchor) - } - } } } } } } + +// A NAMED CALL EXECUTES, AND EXECUTES TO ITS CALLEE'S VALUE. The value is asserted rather than mere +// acceptance: any Int-returning path would satisfy "Accepted" while proving nothing about WHICH +// declaration ran, and 7 is written only in the callee's body. +test fn cref_a_zero_argument_named_call_executes_to_its_callee_value_holds() -> Bool { + cref_executes_to(o: cref_zero_arg_source(), n: 7) +} + +// THE SAME WITH AN ARGUMENT SUPPLIED, which is a separate fact from the zero-argument case because +// the argument edge is a separate child of the call. +test fn cref_a_named_call_with_an_argument_executes_holds() -> Bool { + cref_executes_to(o: cref_one_arg_constant_body_source(), n: 7) +} + +// PARAMETER BINDING IS NOT YET DEMONSTRATED, AND THIS IS THE CLAIM THAT WOULD DEMONSTRATE IT. Both +// executing claims above have CONSTANT bodies, so neither proves the supplied argument reaches the +// callee's parameter -- a callee ignoring its argument entirely would pass both. This fixture returns +// its parameter, so its value depends on the argument, and it REFUSES today. Enrolled executed and +// expected-red rather than deleted, so the gap is counted rather than described. +test fn cref_a_parameter_bodied_callee_still_refuses_holds() -> Bool { + cref_refusal_reason_is(o: cref_source(), wanted: ^eval_rejected_grounding_not_derived) +} + +// A BOOL-RETURNING CALL STILL REFUSES, AND IT IS A DIFFERENT BOUNDARY FROM THE PARAMETER BODY. This +// callee's body is a constant, so it differs from the executing claim above only in its RETURN TYPE. +// The call grounds under infer, so this sits downstream of both the reference repair and the dispatch +// repair. Enrolled executed, not diagnosed further here. +test fn cref_a_bool_returning_call_still_refuses_holds() -> Bool { + cref_refusal_reason_is(o: cref_bool_pair_source(), wanted: ^eval_rejected_grounding_not_derived) +} + +// THE DISCRIMINATING NEGATIVE FOR THE DISPATCH: a callee naming no declaration must refuse rather +// than execute, fabricate a value, or be looked up as a primitive under a name it does not have. +// Without this, the repair could have admitted any Conj as a callee and still looked green. +test fn cref_an_unresolved_callee_does_not_execute_holds() -> Bool { + !cref_executes(o: cref_unresolved_callee_source()) +} diff --git a/src/v2/test/claim/manual/infer_algebra_ref_grounding_anchor.dag b/src/v2/test/claim/manual/infer_algebra_ref_grounding_anchor.dag index 44acdde69fd..5bd6ec6af97 100644 --- a/src/v2/test/claim/manual/infer_algebra_ref_grounding_anchor.dag +++ b/src/v2/test/claim/manual/infer_algebra_ref_grounding_anchor.dag @@ -1,4 +1,5 @@ module v2.test.manual.infer_algebra_ref_grounding_anchor +import v2.std.optional { optional_absent } import v2.std.witness { Holds, Violates, Witness } import v2.compiler.infer { algebra_ref_is_grounded, canonical_grounding_from_inferred_facts } @@ -55,7 +56,8 @@ fn anchor_inferred_facts_bare_atom_algebra() -> InferredFacts { non_increasing: [], strict: v2.std.cardinality.RankingComponent { measured: ^anchor_descent_dim_sym } } - } + }, + denotation: optional_absent() } } diff --git a/src/v2/test/lens_common/infer_fixture.dag b/src/v2/test/lens_common/infer_fixture.dag index 4d0a9f887ca..6c6e4ae16ac 100644 --- a/src/v2/test/lens_common/infer_fixture.dag +++ b/src/v2/test/lens_common/infer_fixture.dag @@ -1,4 +1,5 @@ module v2.test.lens_common.infer_fixture +import v2.std.optional { optional_absent } import std.occurrence_identity { OccurrenceSynthetic } import v2.compiler.resolve { ResolvedTree } @@ -57,7 +58,8 @@ fn claim_inferred_facts_witness( ) -> InferredFacts { InferredFacts { grounding: DerivedGrounding { grounding: grounding }, - descent: descent + descent: descent, + denotation: optional_absent() } } From e168404bf78dfdf0311416aa9f4aed08a56409d3 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 21:59:57 +0000 Subject: [PATCH 18/90] The remaining refusal is a facts-key collision, not a fact about the call WHAT THE REMAINING NAMED-CALL REFUSAL ACTUALLY IS. InferredTree keys its facts by Node; Node equality is structural over kind, children and occurrence identity; and std.occurrence_identity spells "no authored occurrence" as the NULLARY constructor OccurrenceSynthetic, so it is one VALUE and not one value per synthetic node. Two synthetic nodes with the same kind and children are therefore THE SAME KEY, whoever built them and whenever. Measured: the evaluator builds an empty synthetic Conj at runtime (v2.std.runtime runtime_value_conj_node, for a value's type), asks the facts map about it, and the map ANSWERS -- with the facts of an unrelated node that merely shares the shape. Those facts carry GroundingNotDerived, so eval refuses with eval_rejected_grounding_not_derived located at a node that exists in no source position. Three claims enroll it: the refusing node is equal to the runtime-built empty Conj; the map answers for that node; and a structurally equal node occurs in the program, which is what makes it a collision rather than a stray key. THE COLLISION IS SILENT IN BOTH DIRECTIONS, and only one direction is observed here. A lookup that should MISS instead hits, converting a fail-closed infer_facts_lookup_miss into a grounding judgment no producer intended. Had the colliding entry been DERIVED rather than underived, the same collision would hand the runtime a grounding nothing established -- a fabricated plausible output rather than a refusal (DESIGN section 5). Nothing currently makes that direction unreachable; this corpus just happens to collide with an underived entry. A CORRECTION I OWE, and it retracts my own evidence rather than someone else's. Commit b65297c57da attributed this refusal to the callee's encoded declaring path because the anchor was a member of that path's node set. That evidence does not discriminate: an empty synthetic Conj is a member of almost any node set it is tested against, including the spine's terminator, which is why the same probe also answered yes for the call subtree and for the declaration. The anchor comparison establishes nothing about location and should not have been read as attribution. What the classifier/walker repair rests on instead is unaffected: it is established by source -- the classifier admitted Arrow and Atom only while a resolved reference is a marked Conj -- and by named calls now EXECUTING to their callee's value, asserted by value and not by acceptance. That evidence does not pass through the anchor. WHY THIS STOPS HERE. The repair is to decide the KEYING RELATION for the facts map -- occurrence identity rather than structural identity -- which is a semantic rule about node identity, sits in the conformance-identity domain (DESIGN section 3b), and changes every facts lookup in the corpus rather than anything in this lane. Escalating rather than reaching for a local guard at the symptom link, which is the shape DESIGN section 6b names. The parameter-bodied and Bool-returning controls beside this file stay enrolled executed and expected-red; this finding explains the node they refuse at without yet discharging either. Co-Authored-By: Claude Opus 5 (1M context) --- .../synthetic_facts_key_collision_test.dag | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag diff --git a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag new file mode 100644 index 00000000000..c094e496e81 --- /dev/null +++ b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag @@ -0,0 +1,116 @@ +module v2.test.claim.callexec.synthetic_facts_key_collision + +import v2.test.claim.callexec.declaration_reference_eval { + cref_diagnostic_node_optional, + cref_eval_of, + cref_first_call_optional, + cref_inferred_of, + cref_source +} +import v2.compiler.infer { inferred_facts_grounding_derived } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.logic { Bool } +import v2.std.node { Conj, Node, NodeFold, TypeNode, fold_node } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import std.algebra { Cons, Empty } +import v2.std.algebra { fold_list } +import v2.std.collection { List } +import v2.std.runtime { runtime_value_conj_node } + +// A NODE-KEYED FACTS MAP ANSWERS FOR A NODE NO SOURCE OCCURRENCE PRODUCED, and that is what the +// remaining named-call refusal actually is. +// +// v2.compiler.inferred_tree InferredTree keys its facts by Node, and Node equality is STRUCTURAL -- +// kind, children and occurrence identity. std.occurrence_identity NodeOccurrenceIdentity spells "no +// authored occurrence" as the NULLARY constructor OccurrenceSynthetic, so it is one VALUE rather than +// one value per synthetic node. Two synthetic nodes with the same kind and the same children are +// therefore THE SAME KEY, whoever built them and whenever. +// +// The consequence measured here: the evaluator constructs an empty synthetic Conj at runtime (through +// v2.std.runtime runtime_value_conj_node, for a value's type), asks the facts map about it, and the +// map ANSWERS -- with the facts of an unrelated node that merely shares that shape. Those facts carry +// GroundingNotDerived, so eval refuses with eval_rejected_grounding_not_derived located at a node that +// exists in no source position. The refusal is not about the call, the callee, the reference or the +// declaration: it is a key collision. +// +// WHY THIS IS WORTH A CLAIM RATHER THAN A NOTE. The collision is silent in both directions. A lookup +// that should MISS instead hits, so a fail-closed miss (infer_facts_lookup_miss) is converted into a +// judgment about grounding that no producer intended; and were the colliding entry DERIVED instead of +// underived, the same collision would hand the runtime a grounding it never established, which is a +// fabricated-plausible-output failure rather than a refusal (DESIGN section 5). This corpus currently +// observes only the refusing direction, and nothing makes the other direction unreachable. +// +// WHAT THIS CORRECTS. An earlier reading of this refusal attributed the anchor to the callee's encoded +// declaring path, on the evidence that the anchor was a member of that path's node set. That evidence +// does not discriminate: the anchor is an empty synthetic Conj, and an empty synthetic Conj is a +// member of almost any node set one tests it against, including the spine's terminator. The +// classifier/walker repair that landed beside this file stands on different evidence -- it is +// established by source and by named calls now EXECUTING to their callee's value -- and not on this +// anchor comparison, which establishes nothing about location. +fn sfk_anchor() -> Optional { + match cref_eval_of(o: cref_source()) { + Absent => optional_absent() + Present { value: outcome } => + match outcome { + Accepted { value: _, diagnostics: _ } => optional_absent() + Rejected { diagnostics: ds } => cref_diagnostic_node_optional(d: ds.head) + } + } +} + +fn sfk_nodes(n: Node) -> List { + fold_node( + n: n, + algebra: NodeFold { + init: fn(n0) { Cons { head: n0, tail: Empty } }, + step: fn(acc, _e, child) { + fold_list(xs: acc, empty: child, cons: fn(rest, item) { Cons { head: item, tail: rest } }) + } + } + ) +} + +fn sfk_member(xs: List, wanted: Node) -> Bool { + fold_list(xs: xs, empty: false, cons: fn(rest, item) { if item == wanted { true } else { rest } }) +} + +// THE REFUSING NODE IS ONE THE RUNTIME BUILT, not one the source contains: it is equal to the empty +// synthetic Conj that v2.std.runtime constructs. This is the claim that makes the anchor's provenance +// a measured fact rather than a reading of a rendered log, which prints it only as a synthetic +// occurrence and so cannot distinguish it from any other synthetic node. +test fn sfk_the_refusing_node_is_a_runtime_built_empty_conj_holds() -> Bool { + match sfk_anchor() { + Absent => false + Present { value: anchor } => anchor == runtime_value_conj_node(children: Empty) + } +} + +// AND THE MAP ANSWERS FOR IT. A node built by the evaluator has no entry any producer recorded for +// it, so the honest answer is Absent and the honest refusal is a lookup miss. The lookup HITS, which +// is the collision itself, and the entry it hits carries underived grounding -- so the refusal reports +// a grounding judgment about a node nothing ever inferred. +test fn sfk_the_facts_map_answers_for_a_runtime_built_node_holds() -> Bool { + match cref_inferred_of(o: cref_source()) { + Absent => false + Present { value: inferred } => + match inferred.facts.lookup(runtime_value_conj_node(children: Empty)) { + Absent => false + Present { value: facts } => !inferred_facts_grounding_derived(facts: facts) + } + } +} + +// THE ENTRY IT COLLIDES WITH IS REACHABLE FROM THE PROGRAM, which is what makes this a collision +// rather than a stray key: the call's own subtree contains a structurally equal node, so the two are +// indistinguishable to a Node-keyed map. +test fn sfk_a_structurally_equal_node_occurs_in_the_program_holds() -> Bool { + match cref_inferred_of(o: cref_source()) { + Absent => false + Present { value: inferred } => + match cref_first_call_optional(root: inferred.root) { + Absent => false + Present { value: call } => + sfk_member(xs: sfk_nodes(n: call), wanted: runtime_value_conj_node(children: Empty)) + } + } +} From c187a8ffa697309d249b360da98ff14bbcdbc1ae Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 22:04:48 +0000 Subject: [PATCH 19/90] Both remaining reds refuse at the same runtime-built node, measured The Bool-returning control's callee body is a constant, so it differs from an EXECUTING control only in its return type, and it refuses at the same runtime-built empty synthetic Conj as the parameter-bodied one. So this lane's two remaining reds have ONE cause rather than two. Scoped deliberately to these two subjects. A matching reason string is not evidence of a shared defect -- that was the error in an earlier grouping of three unrelated subjects -- so this claim compares the failing NODES and says nothing about any other refusal reporting the same reason. Co-Authored-By: Claude Opus 5 (1M context) --- .../synthetic_facts_key_collision_test.dag | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag index c094e496e81..a794ed5ba6b 100644 --- a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag +++ b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag @@ -5,6 +5,7 @@ import v2.test.claim.callexec.declaration_reference_eval { cref_eval_of, cref_first_call_optional, cref_inferred_of, + cref_bool_pair_source, cref_source } import v2.compiler.infer { inferred_facts_grounding_derived } @@ -114,3 +115,23 @@ test fn sfk_a_structurally_equal_node_occurs_in_the_program_holds() -> Bool { } } } + +// THE BOOL-RETURNING RED SHARES THIS CAUSE, measured rather than assumed from a matching reason +// string. Its callee body is a constant, so it differs from an EXECUTING control only in return type, +// and it refuses at the same runtime-built node. Two of this lane's remaining reds therefore have one +// cause; that is a claim about these two subjects and is not extended to any other refusal reporting +// the same reason, which a reason string alone cannot establish. +test fn sfk_the_bool_returning_red_refuses_at_the_same_node_holds() -> Bool { + match cref_eval_of(o: cref_bool_pair_source()) { + Absent => false + Present { value: outcome } => + match outcome { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: ds } => + match cref_diagnostic_node_optional(d: ds.head) { + Absent => false + Present { value: anchor } => anchor == runtime_value_conj_node(children: Empty) + } + } + } +} From 3769c83b0c900f6cf96bcf6215079f5b2ee5fa20 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 23:05:32 +0000 Subject: [PATCH 20/90] Withdraw the provenance inference; establish the key conflict by enumeration I WAS TOLD MY EVIDENCE REPEATED THE ERROR IT CORRECTED, AND IT DID. The previous commit replaced "the anchor lies inside the declaring path" with "the anchor is what the runtime value constructor builds". Both were inferred from Node == Node, and equality is the relation under suspicion, so it cannot be the instrument that establishes provenance. An empty synthetic Conj compares equal to many unrelated nodes; membership in a node set and equality with a constructor's result are both uninformative about origin. Both readings are withdrawn. WHAT ENUMERATION ESTABLISHES INSTEAD, which needs no provenance claim. Walking infer's own entry list for one small program: MORE THAN ONE ENTRY carries the empty synthetic Conj as its key, and those entries DISAGREE on whether grounding was derived. So one key names at least two subjects whose facts differ, and a consumer asking under that key receives whichever the first-match scan reaches first. The same conflict stands in a second fixture, so it is a property of the keying relation over ordinary programs rather than an artifact of one source text. This is stronger than the earlier claims and differently shaped: it is not that a freshly built equal value gets an answer, but that the relation ADMITS CONFLICTING FACTS FOR ONE KEY. facts_map_from_entries checks each entry's own subject correspondence and establishes no uniqueness or conflict condition across entries that compare equal. AND THE FAIL-OPEN DIRECTION IS REACHABLE, not hypothetical. A DERIVED entry exists under the same key as an underived one, so a subject whose grounding was never established can receive one that was -- a fabricated plausible output rather than a refusal (DESIGN section 5) -- and which of the two a consumer gets is decided by entry order. I had flagged this direction as a credible risk; the enumeration is what makes it an observed reachability rather than a hypothetical, and it is still short of an observed successful misexecution. THE TRAVERSAL PATH, measured with a temporary diagnostic that gave each grounding demand site in the evaluator its own reason symbol. The instrument is removed and its result is recorded here rather than asserted, since asserting it would mean keeping instrumentation in the evaluator: - the demanding caller is eval_fold_init, through eval_fold_child_for_edge's ORDINARY RECURSIVE ARM -- the node entered the walk as a child, not as a fold root, so this is a traversal that descended into it rather than a consumer asking about a supplied subject; - the node sits under a NAMED edge and is NOT under the declaration-reference marker, so it is not the reference spine the previous repair addressed; - no node in the call subtree carries it as a POSITIONAL child, which is how the named-edge conclusion was reached. That locates the demand without asserting where the node came from, which is the distinction the previous commits lost. ALSO CORRECTED, and this one was a live defect rather than a wrong reading. The annotation above eval_fold_is_callee_reference_edge described the callee-edge identity repair as landed while the function still contained `p == 0 && is_positional(edge)`: I reverted the change after measuring that it moved no verdict, and left the comment claiming it. A comment asserting a repair the function does not contain is worse than no comment, since no Accepted program can read one to check it (DESIGN section 4c). It now records the count-based identification as a standing observation, states that rewriting it changed no verdict in either direction, and says what would justify revisiting it: a call shape where the two formulations DISAGREE, which is the discriminating case this lane never found. Verified after removing the instrumentation: all 7 declaration-reference eval controls pass, including both executing calls; the 4 key-conflict claims pass. The two executing controls return Accepted with diagnostics None, checked explicitly -- so they are executions and not acceptances carrying a suppressed refusal. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/05_eval.dag | 22 +- .../synthetic_facts_key_collision_test.dag | 201 +++++++++--------- 2 files changed, 111 insertions(+), 112 deletions(-) diff --git a/src/v2/compiler/05_eval.dag b/src/v2/compiler/05_eval.dag index cf2bf0d711f..f4bd10cd621 100644 --- a/src/v2/compiler/05_eval.dag +++ b/src/v2/compiler/05_eval.dag @@ -2289,19 +2289,17 @@ fn eval_fold_control_transfer_state( } } -// THE CALLEE EDGE IS IDENTIFIED BY BEING THE CALLEE EDGE, NOT BY A PROCESSED COUNT. This predicate -// used to answer `p == 0 && is_positional(edge)`, which identifies the callee only while the callee is -// the first child the fold happens to process. A call carrying a NAMED argument -- which every call -// written `f(x: 3)` is -- advances the counter past zero before the positional callee edge is seen, so -// the callee fell to eval_fold_child_for_edge's ordinary recursive arm and the walk descended into it. -// For an Arrow callee that meant evaluating the arrow's body in an environment that cannot bind its -// parameters; for a declaration reference it meant demanding value-grounding for the encoded declaring -// path. The zero-argument call executed and the one-argument call did not, which is the tell that the -// discriminator was the child ORDER and never the callee. +// THIS PREDICATE IDENTIFIES THE CALLEE EDGE BY A PROCESSED COUNT, WHICH IS A STANDING OBSERVATION AND +// NOT A REPAIR THIS FUNCTION CONTAINS. `p == 0 && is_positional(edge)` names the callee only while the +// callee is the first child the fold happens to process, so the discriminator is child ORDER rather +// than the callee -- and eval_transform_callee_edge already answers the question directly. // -// So the question asked here is now the one eval_transform_callee_edge already answers, and the two -// cannot disagree because the dispatch reads the same producer. The `progress` arms are unchanged: -// they keep the seam from re-firing once the node has been interpreted. +// IT IS LEFT ALONE BECAUSE REWRITING IT CHANGED NO VERDICT. Keying on eval_transform_callee_edge was +// written and measured: all seven controls in v2.test.claim.callexec.declaration_reference_eval pass +// either way, in both directions. A second formulation nothing needs is redundant work (DESIGN section +// 2), so the count-based test stays and the observation is recorded here rather than acted on. Whoever +// finds a call shape where the two DISAGREE has the discriminating case this lacked, and that case is +// what would justify the change. fn eval_fold_is_callee_reference_edge( parent: Node, progress: EvalProgress, diff --git a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag index a794ed5ba6b..f7574e47ecd 100644 --- a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag +++ b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag @@ -1,137 +1,138 @@ module v2.test.claim.callexec.synthetic_facts_key_collision import v2.test.claim.callexec.declaration_reference_eval { - cref_diagnostic_node_optional, - cref_eval_of, - cref_first_call_optional, - cref_inferred_of, cref_bool_pair_source, cref_source } -import v2.compiler.infer { inferred_facts_grounding_derived } +import v2.compiler.infer { infer_entries_for_tree, inferred_facts_grounding_derived } +import v2.compiler.inferred_tree { InferredFactsEntry } import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.compiler.resolve { ResolvedTree } import v2.std.logic { Bool } -import v2.std.node { Conj, Node, NodeFold, TypeNode, fold_node } +import v2.std.node { Node } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } import std.algebra { Cons, Empty } import v2.std.algebra { fold_list } import v2.std.collection { List } +import v2.std.integer { Int } import v2.std.runtime { runtime_value_conj_node } -// A NODE-KEYED FACTS MAP ANSWERS FOR A NODE NO SOURCE OCCURRENCE PRODUCED, and that is what the -// remaining named-call refusal actually is. +// ONE FACTS KEY, TWO SUBJECTS, DISAGREEING FACTS -- and which one a consumer receives depends on scan +// order. // -// v2.compiler.inferred_tree InferredTree keys its facts by Node, and Node equality is STRUCTURAL -- -// kind, children and occurrence identity. std.occurrence_identity NodeOccurrenceIdentity spells "no -// authored occurrence" as the NULLARY constructor OccurrenceSynthetic, so it is one VALUE rather than -// one value per synthetic node. Two synthetic nodes with the same kind and the same children are -// therefore THE SAME KEY, whoever built them and whenever. +// v2.compiler.inferred_tree InferredTree keys its facts by Node and Node equality is STRUCTURAL, so +// two nodes agreeing on kind, children and occurrence identity are ONE KEY. std.occurrence_identity +// spells "no authored occurrence" as the NULLARY constructor OccurrenceSynthetic -- one VALUE, not one +// value per synthetic node -- so every structurally identical synthetic node collapses together. The +// lookup is a first-match scan over the entry list (v2.compiler.infer facts_map_from_entries), and its +// constructor checks each entry's own subject correspondence without establishing any uniqueness or +// conflict condition ACROSS entries that compare equal. // -// The consequence measured here: the evaluator constructs an empty synthetic Conj at runtime (through -// v2.std.runtime runtime_value_conj_node, for a value's type), asks the facts map about it, and the -// map ANSWERS -- with the facts of an unrelated node that merely shares that shape. Those facts carry -// GroundingNotDerived, so eval refuses with eval_rejected_grounding_not_derived located at a node that -// exists in no source position. The refusal is not about the call, the callee, the reference or the -// declaration: it is a key collision. +// The claims below enumerate infer's entries for one small program and measure the consequence: more +// than one entry carries the empty synthetic Conj as its key, and those entries DISAGREE on whether +// grounding was derived. So one key names at least two subjects whose facts differ, and a consumer +// asking under that key receives whichever the scan reaches first. // -// WHY THIS IS WORTH A CLAIM RATHER THAN A NOTE. The collision is silent in both directions. A lookup -// that should MISS instead hits, so a fail-closed miss (infer_facts_lookup_miss) is converted into a -// judgment about grounding that no producer intended; and were the colliding entry DERIVED instead of -// underived, the same collision would hand the runtime a grounding it never established, which is a -// fabricated-plausible-output failure rather than a refusal (DESIGN section 5). This corpus currently -// observes only the refusing direction, and nothing makes the other direction unreachable. +// BOTH DIRECTIONS ARE REACHABLE, WHICH IS WHY THIS IS A SAFETY FINDING AND NOT ONLY AN ANOMALY. The +// refusing direction is what this lane observed: a consumer receives an UNDERIVED entry and refuses +// with eval_rejected_grounding_not_derived at a node that is not its subject, which converts a +// fail-closed lookup miss into a grounding judgment no producer intended. The opposite direction is +// not hypothetical here, and that is what the second claim establishes: a DERIVED entry exists under +// the same key, so a subject whose grounding was never established can receive one that was. That is +// a fabricated plausible output rather than a refusal (DESIGN section 5), and nothing in the current +// relation makes it unreachable -- which of the two a given consumer gets is decided by entry order. // -// WHAT THIS CORRECTS. An earlier reading of this refusal attributed the anchor to the callee's encoded -// declaring path, on the evidence that the anchor was a member of that path's node set. That evidence -// does not discriminate: the anchor is an empty synthetic Conj, and an empty synthetic Conj is a -// member of almost any node set one tests it against, including the spine's terminator. The -// classifier/walker repair that landed beside this file stands on different evidence -- it is -// established by source and by named calls now EXECUTING to their callee's value -- and not on this -// anchor comparison, which establishes nothing about location. -fn sfk_anchor() -> Optional { - match cref_eval_of(o: cref_source()) { - Absent => optional_absent() - Present { value: outcome } => - match outcome { - Accepted { value: _, diagnostics: _ } => optional_absent() - Rejected { diagnostics: ds } => cref_diagnostic_node_optional(d: ds.head) +// WHAT THIS FILE DELIBERATELY DOES NOT CLAIM. It does not assert where the node a consumer queried +// came from. Two earlier readings of this refusal each inferred provenance from `Node == Node` -- first +// that the node lay inside the callee's encoded declaring path, then that it was built by the runtime +// value constructor -- and BOTH are withdrawn, for the same reason: an empty synthetic Conj compares +// equal to many unrelated nodes, so membership in any node set, and equality with any constructor's +// result, are both uninformative about origin. Equality is exactly the relation under suspicion here, +// so it cannot be the instrument that establishes provenance. These claims therefore assert only what +// enumeration shows: that the key is shared and the facts under it conflict. +// +// The traversal path was measured separately, with a temporary diagnostic that gave each grounding +// demand site in v2.compiler.eval its own reason symbol; that instrument is not retained, and its +// result is recorded in this change's commit message rather than asserted here, because asserting it +// would require keeping the instrumentation in the evaluator. +fn sfk_entries(o: Outcome) -> Optional> { + match o { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: resolved, diagnostics: _ } => + match infer_entries_for_tree(tree: resolved) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: entries, diagnostics: _ } => optional_present(value: entries) } } } -fn sfk_nodes(n: Node) -> List { - fold_node( - n: n, - algebra: NodeFold { - init: fn(n0) { Cons { head: n0, tail: Empty } }, - step: fn(acc, _e, child) { - fold_list(xs: acc, empty: child, cons: fn(rest, item) { Cons { head: item, tail: rest } }) - } - } - ) +fn sfk_key() -> Node { + runtime_value_conj_node(children: Empty) } -fn sfk_member(xs: List, wanted: Node) -> Bool { - fold_list(xs: xs, empty: false, cons: fn(rest, item) { if item == wanted { true } else { rest } }) +fn sfk_count_under_key(o: Outcome) -> Int { + match sfk_entries(o: o) { + Absent => 0 + Present { value: entries } => + fold_list( + xs: entries, + empty: 0, + cons: fn(rest, entry) { if entry.node == sfk_key() { rest + 1 } else { rest } } + ) + } } -// THE REFUSING NODE IS ONE THE RUNTIME BUILT, not one the source contains: it is equal to the empty -// synthetic Conj that v2.std.runtime constructs. This is the claim that makes the anchor's provenance -// a measured fact rather than a reading of a rendered log, which prints it only as a synthetic -// occurrence and so cannot distinguish it from any other synthetic node. -test fn sfk_the_refusing_node_is_a_runtime_built_empty_conj_holds() -> Bool { - match sfk_anchor() { - Absent => false - Present { value: anchor } => anchor == runtime_value_conj_node(children: Empty) +fn sfk_count_derived_under_key(o: Outcome) -> Int { + match sfk_entries(o: o) { + Absent => 0 + Present { value: entries } => + fold_list( + xs: entries, + empty: 0, + cons: fn(rest, entry) { + if entry.node == sfk_key() { + if inferred_facts_grounding_derived(facts: entry.facts) { rest + 1 } else { rest } + } else { + rest + } + } + ) } } -// AND THE MAP ANSWERS FOR IT. A node built by the evaluator has no entry any producer recorded for -// it, so the honest answer is Absent and the honest refusal is a lookup miss. The lookup HITS, which -// is the collision itself, and the entry it hits carries underived grounding -- so the refusal reports -// a grounding judgment about a node nothing ever inferred. -test fn sfk_the_facts_map_answers_for_a_runtime_built_node_holds() -> Bool { - match cref_inferred_of(o: cref_source()) { +// THE POSITIVE CONTROL FOR THE INSTRUMENT: infer produces entries for this program at all, so a zero +// count below is a shared key and not a failed inference. +test fn sfk_infer_produces_entries_holds() -> Bool { + match sfk_entries(o: cref_source()) { Absent => false - Present { value: inferred } => - match inferred.facts.lookup(runtime_value_conj_node(children: Empty)) { - Absent => false - Present { value: facts } => !inferred_facts_grounding_derived(facts: facts) + Present { value: entries } => + match entries { + Cons { head: _, tail: _ } => true + Empty => false } } } -// THE ENTRY IT COLLIDES WITH IS REACHABLE FROM THE PROGRAM, which is what makes this a collision -// rather than a stray key: the call's own subtree contains a structurally equal node, so the two are -// indistinguishable to a Node-keyed map. -test fn sfk_a_structurally_equal_node_occurs_in_the_program_holds() -> Bool { - match cref_inferred_of(o: cref_source()) { - Absent => false - Present { value: inferred } => - match cref_first_call_optional(root: inferred.root) { - Absent => false - Present { value: call } => - sfk_member(xs: sfk_nodes(n: call), wanted: runtime_value_conj_node(children: Empty)) - } - } +// MORE THAN ONE ENTRY SHARES THE KEY. Enumerated, not inferred from a lookup answer: a lookup returning +// something proves only that SOME entry matched, while counting the entries proves the key is shared. +test fn sfk_more_than_one_entry_shares_one_key_holds() -> Bool { + sfk_count_under_key(o: cref_source()) > 1 } -// THE BOOL-RETURNING RED SHARES THIS CAUSE, measured rather than assumed from a matching reason -// string. Its callee body is a constant, so it differs from an EXECUTING control only in return type, -// and it refuses at the same runtime-built node. Two of this lane's remaining reds therefore have one -// cause; that is a claim about these two subjects and is not extended to any other refusal reporting -// the same reason, which a reason string alone cannot establish. -test fn sfk_the_bool_returning_red_refuses_at_the_same_node_holds() -> Bool { - match cref_eval_of(o: cref_bool_pair_source()) { - Absent => false - Present { value: outcome } => - match outcome { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: ds } => - match cref_diagnostic_node_optional(d: ds.head) { - Absent => false - Present { value: anchor } => anchor == runtime_value_conj_node(children: Empty) - } - } - } +// AND THEY DISAGREE. At least one derived and at least one not, under the same key -- so the relation +// admits conflicting facts for one key, and the answer a consumer gets is decided by entry order +// rather than by its subject. This is the claim the identity owner needs: it establishes a CONFLICT +// rather than merely a coincidence of shape. +test fn sfk_entries_under_one_key_disagree_on_grounding_holds() -> Bool { + let total = sfk_count_under_key(o: cref_source()) + let derived = sfk_count_derived_under_key(o: cref_source()) + (derived > 0) && (derived < total) +} + +// THE SAME CONFLICT STANDS IN THE SECOND FIXTURE, so it is a property of the keying relation over +// ordinary programs and not an artifact of one source text. +test fn sfk_the_conflict_is_not_specific_to_one_fixture_holds() -> Bool { + let total = sfk_count_under_key(o: cref_bool_pair_source()) + let derived = sfk_count_derived_under_key(o: cref_bool_pair_source()) + (total > 1) && (derived > 0) && (derived < total) } From 3065331ac3a9d6ece22b86cc86f748fb83993dec Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Mon, 28 Sep 2026 23:15:01 +0000 Subject: [PATCH 21/90] Enroll the acceptance targets: argument-dependent execution and the Bool pair THE NEXT RESULT IS NOW WRITTEN AND EXECUTED, not described. Three claims specify what "argument-dependent execution" means and run the comparison that decides it: identity(only_arg: 3) -> 3 identity(only_arg: 8) -> 8 TWO arguments, because one would not discriminate -- a callee returning a constant that happened to equal the argument would satisfy a single case. The callee's body IS its parameter, so its result cannot be produced without consuming the supplied argument, which is the gap the two constant-bodied executing controls leave open: a callee ignoring its argument entirely passes both of those. They are enrolled as the REFUSAL they are today, with the value path supplied and compared, so what remains when the boundary is repaired is inverting the assertion rather than authoring the behaviour it checks. Writing it the other way round would land a red and specify the same thing. ONE OF THEM IS VACUOUS TODAY AND SAYS SO. The claim that the identity callee must not answer the OTHER argument's value holds for the wrong reason while nothing executes -- both conjuncts are satisfied by refusal. It is enrolled anyway because it is the check that stops the repair being credited by a callee that consumes its argument and returns the wrong one, and it becomes discriminating the moment the claim above inverts. The annotation states the vacuity so no reader counts it as present coverage; an expecting-green claim that cannot currently fail is specification without execution unless its state is declared. THE BOOL PAIR GETS THE SAME TREATMENT one type further on: a true-returning and a false-returning callee must produce DIFFERENT answers, because a repair making both execute to the same value would satisfy "executes" while destroying the distinction the pair exists for. Every one of these refuses at the shared facts key rather than at anything about calls, arguments or return types -- the conflict is established by enumeration in v2.test.claim.callexec.synthetic_facts_key_collision, where more than one entry carries one key and those entries disagree on grounding. So this lane's acceptance result is blocked behind that one contract question and is fully specified while it waits, rather than waiting to be specified. Co-Authored-By: Claude Opus 5 (1M context) --- .../declaration_reference_eval_test.dag | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag index e0249f0479a..21fd1605112 100644 --- a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag +++ b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag @@ -360,3 +360,59 @@ test fn cref_a_bool_returning_call_still_refuses_holds() -> Bool { test fn cref_an_unresolved_callee_does_not_execute_holds() -> Bool { !cref_executes(o: cref_unresolved_callee_source()) } + +// AN ARGUMENT-DEPENDENT CALLEE: its body IS its parameter, so its result cannot be produced without +// consuming the supplied argument. Two different arguments are supplied because ONE would not +// discriminate -- a callee returning a constant that happened to equal the argument would satisfy a +// single case. +fn cref_identity_source(lex: String) -> Outcome { + cref_assemble(src: "module p\n\nfn identity(only_arg: Int) -> Int {\n only_arg\n}\n\nfn consumer() -> Int {\n identity(only_arg: " + lex + ")\n}\n") +} + +fn cref_identity_executes_to(lex: String, n: Int) -> Bool { + cref_executes_to(o: cref_identity_source(lex: lex), n: n) +} + +// THE ACCEPTANCE TARGET FOR THIS LANE, ENROLLED AS THE REFUSAL IT IS TODAY. The value path is written +// and EXECUTED by this claim -- both arguments are supplied and both results are compared -- so what +// remains when the boundary is repaired is inverting this assertion, not authoring the behaviour it +// checks. Writing it the other way round would land a red and specify the same thing. +// +// It refuses at the shared facts key, not at anything about calls or arguments: see +// v2.test.claim.callexec.synthetic_facts_key_collision, where more than one entry carries one key and +// those entries disagree on grounding. +test fn cref_argument_dependent_execution_still_refuses_holds() -> Bool { + !cref_identity_executes_to(lex: "3", n: 3) + && !cref_identity_executes_to(lex: "8", n: 8) + && cref_refusal_reason_is( + o: cref_identity_source(lex: "3"), + wanted: ^eval_rejected_grounding_not_derived + ) +} + +// AND THE SAME CALLEE MUST NOT ANSWER A DIFFERENT ARGUMENT'S VALUE once it executes. THIS CLAIM IS +// VACUOUS TODAY and is recorded as such: nothing executes, so both conjuncts hold for the wrong +// reason. It is enrolled anyway because it is exactly the check that stops the repair being credited +// by a callee that consumes its argument and returns the WRONG one, and it becomes discriminating the +// moment the claim above inverts. A reader must not count it as present coverage until then. +test fn cref_the_identity_callee_never_answers_the_other_argument_holds() -> Bool { + !cref_identity_executes_to(lex: "3", n: 8) + && !cref_identity_executes_to(lex: "8", n: 3) +} + +// THE BOOL PAIR'S DISTINCT OUTCOMES, the same shape one type further on. A true-returning and a +// false-returning callee must produce DIFFERENT runtime answers; a repair that made both execute to +// the same value would satisfy "executes" and destroy the distinction the pair exists for. Enrolled as +// the refusal it is today, with the comparison written and executed. +fn cref_bool_false_source() -> Outcome { + cref_assemble(src: "module p\n\nfn falsity(only_arg: Int) -> Bool {\n false\n}\n\nfn consumer() -> Bool {\n falsity(only_arg: 3)\n}\n") +} + +test fn cref_the_bool_pair_does_not_yet_separate_holds() -> Bool { + !cref_executes(o: cref_bool_pair_source()) + && !cref_executes(o: cref_bool_false_source()) + && cref_refusal_reason_is( + o: cref_bool_false_source(), + wanted: ^eval_rejected_grounding_not_derived + ) +} From 6f0dff149824478528f3e93ddf762322e8b88c02 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 02:13:29 +0000 Subject: [PATCH 22/90] Field projection through resolve, infer and eval, with the field check that closes it THE FRONTIER WAS DECLARED AND IT IS NOW PERFORMED. Two headers named this exact gap. v2.compiler.body_lowering_fold PostfixAccum keeps `a.b.c` as ONE qualified name and says why -- "deciding here would be a second resolver with no scope to consult" -- naming try_resolve_qualified_name_node as the decider. That function's own header then said: "Head bound and no absolute hit: the projection this arm cannot yet perform, refused Unbound rather than fabricated." So the spine arriving at resolve was never a producer defect; answering Unbound for it was resolve accepting the job and not doing it, and the two causes -- a bound head needing projection, and a genuinely unbound name -- were reported identically. MEASURED FIRST, so a later green is a change in behaviour and not in the question. On the pinned base, `fn f(b: Box) -> Int { b.tree }` refused at RESOLVE with resolve_reason_unbound_symbol anchored on a QUALIFIED-NAME SPINE, while the same receiver with the projection removed resolved and inferred. So the representation was not reaching resolve as a projection at all. THE THREE STAGES. resolve a bound head with no absolute candidate becomes a field projection: the head resolves to its binder through canonical_atom, and each remaining segment folds on left to right, `b.x.y` as `(b.x).y`. It reads segment NODES, not the name's symbols, so every field keeps the occurrence of its own token and a diagnostic about one field lands on that field rather than on the whole chain. The reader for that lives in v2.std.qualified_name beside the spine's other reader and its inverse, because that module owns the label set. infer a projection is typed by the field the receiver's type declares. It sits at the HEAD of the product row because a projection is an ELIMINATION, not a record: without it the projection Conj is typed as the product of its own children, a type combining the receiver with the field-name atom, which is the type of nothing the program computes. The receiver's type comes from its own facts through the row's entries; the type's fields come through the same GUARDED index reader the callee path uses. This is the first production consumer of v2.std.node_query declared_field_named. eval a projection selects the field from the receiver's value. The receiver arrives as the one runtime argument through the existing seam, so the base is evaluated ONCE by the ordinary walk rather than re-entered per projection. The field edge is deliberately not an argument: it carries a name, not a value. THE ABSENT-FIELD CONTROL EARNED ITS PLACE TWICE. With resolve's arm landed and infer's absent, `b.absent_field` inferred CLEAN -- resolve admits the shape and nothing checked the field, so a misspelled field became an accepted program. That is the fail-open the control exists to catch and it caught it. AND IT CAUGHT A SECOND ONE, IN MY OWN REPAIR. My first infer arm collapsed two unavailabilities into one Absent: "I could not establish the receiver's type" and "the receiver's type is established and declares no fields". That is the absorbing fallback DESIGN section 5 forbids -- it converts a decided negative into "no evidence" -- and it broke the standing negative control v2.test.claim.namespace_xl0.cross_module_reference_resolution a_receiver_with_no_such_child_never_accepts: `Bool.v` passed at the frontier. The two are now separate arms. ReceiverTypeUnderived waits at the frontier, because convicting a program whose receiver is typed by a route not yet reaching here would be wrong in the other direction. ReceiverNotARecord REFUSES. TWO CONTROLS IN ANOTHER LANE MOVED STAGE, AND THE PROPERTY IS STRICTLY HARDER NOW. Both asserted refusal AT RESOLVE with resolve_reason_unbound_symbol for a `Bool` receiver. Resolve no longer refuses those -- it commits the shape -- and infer refuses them instead. So they now assert through infer: acceptance is still forbidden, and the claim is harder than before, because a program that resolved and then inferred clean would fail it where previously only the resolve reason was checked. The old reason was the collapse rather than the property, which v2.compiler.resolve's own header already recorded as a defect. One was renamed: "refuses_unbound_today" pinned a stage and a reason it never meant to pin, and what the row is FOR is that a method call on a local receiver is not silently admitted. WHAT EVAL'S EVIDENCE IS AND WHY. Its receiver value is SUPPLIED, which is DESIGN section 3's witness rule: the subject is one interface -- what eval returns for a projection over a given aggregate -- and computing the aggregate would re-run production the claim is not about. The pairing obligation is discharged by a claim in the same file rather than by assertion: fps_the_resolved_tree_carries_a_field_- projection asserts the real producer emits this shape over the production route. A FINDING BEHIND THAT CHOICE, measured while looking for a fixture that would deliver a projection to eval through surface syntax. Two forms that should, do not: `Box { .. }.tree` and `make().tree` both resolve and infer with NO field-projection node in the tree, while the parameter form `b.tree` now produces one. So the value-receiver path body_lowering describes (PostfixAccumValue, the accumulator after a call suffix) is not reached from these forms, and the only projection this corpus's surface syntax currently produces has an unbound receiver at eval -- whose binding is blocked behind the frozen facts-key question. That is why eval's executed evidence is at its own boundary and not through a whole-program run. A TYPE ERROR WORTH RECORDING, because it cost two iterations and will recur. list_at_optional answers Optional, and a value destructured straight out of it does not carry its type through a FIELD ACCESS: reading `aggregate.fields` inline produced a runtime type error while the sibling arm, which reads no field, passed. Naming a typed parameter restores it. The same shape appears twice more in this change, in the runtime field walk and in the spine segment reader. Green: 13 field-projection controls (resolve, infer and eval, valid and absent field, and two refusal arms) and 15/15 in the namespace lane. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/03_resolve.dag | 121 +++++- src/v2/compiler/04_infer.dag | 172 +++++++- src/v2/compiler/05_eval.dag | 103 ++++- src/v2/std/qualified_name.dag | 50 +++ src/v2/std/runtime.dag | 39 +- .../field_projection_stages_test.dag | 377 ++++++++++++++++++ ...cross_module_reference_resolution_test.dag | 42 +- 7 files changed, 877 insertions(+), 27 deletions(-) create mode 100644 src/v2/test/claim/field_projection/field_projection_stages_test.dag diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index c0789118e26..c1deeaa5c24 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -62,6 +62,7 @@ import v2.std.qualified_name { qualified_name_from_node, qualified_name_last_segment, qualified_name_snoc, + qualified_name_spine_segment_nodes, declaration_reference_node } import v2.std.resolution_policy { @@ -118,6 +119,7 @@ import v2.std.type_binder { edge_is_cast_target, edge_is_type_annotation, edge_i import v2.std.node_query { construct_field_edges, construct_tag_optional, + field_projection_node, find_named_child, pattern_wildcard_name } @@ -798,6 +800,101 @@ fn qualified_head_bound_on_chain(ctx: ResolveContext, path: QualifiedName) -> Ou } } +// THE PROJECTION THIS ARM COULD NOT PERFORM, now performed. A dotted chain whose HEAD is bound on the +// scope chain and whose whole path names no declaration is not an unbound name: it is a field access +// off that binding. Body lowering deliberately declines to decide -- v2.compiler.body_lowering_fold +// PostfixAccum keeps `a.b.c` as one spine and says why: "deciding here would be a second resolver with +// no scope to consult", naming THIS function as the decider. So the spine arriving here is not a +// producer defect; answering Unbound for it was this function accepting the job and not doing it, and +// the two causes -- a bound head needing projection, and a genuinely unbound head -- were reported +// identically. +// +// THE HEAD BECOMES ITS BINDER, THE REST BECOME SUCCESSIVE PROJECTIONS, left to right, which is the +// same association the postfix lowering gives a projection off a value: `b.x.y` is `(b.x).y`. Each +// step is v2.std.node_query field_projection_node over the segment's OWN node, so every field keeps +// the occurrence of the token it was lowered from and a later diagnostic about one field lands on that +// field rather than on the whole chain -- which is why this reads segment NODES +// (qualified_name_spine_segment_nodes) rather than rebuilding atoms from the name's symbols. +// +// WHAT THIS DOES NOT DECIDE, and must not: whether the projected field EXISTS on the receiver's type. +// That check needs a type, resolve has none, and v2.std.node_query field_projection_node's own header +// already assigns it to the stage over this node. So this arm is structural: it commits to the reading +// "field access", and a field no type declares is refused later by the stage that can see the type. +// Admitting the shape is therefore not admitting the access, and the absent-field control in +// v2.test.claim.field_projection.field_projection_stages is what holds that line. +// +// THE HEAD IS RESOLVED AS THE FRAME-LOCAL BINDER IT IS, through canonical_atom on the canonical symbol +// lookup_chain returned -- the same producer every other bound bare use goes through, so a projection +// base is not a second spelling of a binder reference. +fn resolve_bound_head_projection( + ctx: ResolveContext, + n: Node, + pending: Diagnostics +) -> Outcome { + match qualified_name_spine_segment_nodes(root: n) { + Absent => + Rejected { + diagnostics: rejected_with_pending( + pending: pending, + rejected: diagnostics_singleton(d: unbound_symbol_diagnostic(n: n)) + ) + } + Present { value: segments } => + match segments { + Empty => + Rejected { + diagnostics: rejected_with_pending( + pending: pending, + rejected: diagnostics_singleton(d: unbound_symbol_diagnostic(n: n)) + ) + } + Cons { head: head_segment, tail: field_segments } => + match resolve_projection_base(ctx: ctx, head_segment: head_segment) { + Absent => + Rejected { + diagnostics: rejected_with_pending( + pending: pending, + rejected: diagnostics_singleton(d: unbound_symbol_diagnostic(n: n)) + ) + } + Present { value: base } => + outcome_with_diagnostics( + value: fold_list( + xs: field_segments, + empty: base, + cons: fn(acc, field_segment) { + field_projection_node(base: acc, field: field_segment, source: n) + } + ), + diagnostics: pending + ) + } + } + } +} + +fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional { + match head_segment.kind { + TypeNode { connective: Atom { identity: name } } => + match lookup_chain(s: ctx.scope, name: name) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: found, diagnostics: _ } => + match found { + BoundInFrame { canonical: canonical } => + optional_present( + value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) + ) + BoundAtRoot { canonical: canonical } => + optional_present( + value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) + ) + ScopeUnbound => optional_absent() + } + } + _ => optional_absent() + } +} + fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional> { match qualified_name_from_node(root: n) { Rejected { diagnostics: _ } => optional_absent() @@ -809,14 +906,24 @@ fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional match symbol_index_absolute_candidates(index: ctx.namespace.symbol_index, qualified_path: path) { Empty => - optional_present( - value: Rejected { - diagnostics: rejected_with_pending( - pending: diagnostics_merge(outer: pending, inner: chain_pending), - rejected: diagnostics_singleton(d: unbound_symbol_diagnostic(n: n)) + if head_bound { + optional_present( + value: resolve_bound_head_projection( + ctx: ctx, + n: n, + pending: diagnostics_merge(outer: pending, inner: chain_pending) ) - } - ) + ) + } else { + optional_present( + value: Rejected { + diagnostics: rejected_with_pending( + pending: diagnostics_merge(outer: pending, inner: chain_pending), + rejected: diagnostics_singleton(d: unbound_symbol_diagnostic(n: n)) + ) + } + ) + } Cons { head: candidate, tail: rest } => match rest { Empty => diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 0f82074896d..237d9f0ac4d 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -153,7 +153,13 @@ import v2.std.node { } import v2.std.type_binder { cast_target_optional, type_annotation_optional, type_param_names } import v2.std.coercion { coercion_cast_crossing } -import v2.std.node_query { find_named_child, node_positional_child_targets } +import v2.std.node_query { + FieldProjection, + declared_field_named, + field_projection_optional, + find_named_child, + node_positional_child_targets +} import v2.std.witness { Holds, StructuralPropertyWitness, Violates, Witness, witness_from_optional } type AlgebraRef { @@ -721,7 +727,18 @@ fn infer_product_facts_from_entries( node: Node, entries: List, partials: List, + resolved: ResolvedTree, ) -> Outcome { + match field_projection_optional(n: node) { + Present { value: projection } => + infer_field_projection_facts( + node: node, + projection: projection, + entries: entries, + partials: partials, + resolved: resolved + ) + Absent => match infer_bounded_lattice_consumer_gate(consumer: node, partials: partials) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: _, diagnostics: cd } => @@ -759,6 +776,7 @@ fn infer_product_facts_from_entries( } } } + } } fn infer_node_facts(n: Node, partials: List, resolved: ResolvedTree) -> Outcome { @@ -845,6 +863,147 @@ fn infer_node_facts(n: Node, partials: List, resolved: ResolvedTree) -> Ou } } +// A FIELD PROJECTION IS TYPED BY THE FIELD THE RECEIVER'S TYPE DECLARES, AND A FIELD NO TYPE DECLARES +// REFUSES HERE. This is the check v2.std.node_query field_projection_node's own header assigned to this +// stage -- "it does not check that `f` IS a declared field of the receiver's type ... it is the +// typecheck stage's, over this node" -- and it is not optional, because v2.compiler.resolve now commits +// the SHAPE for a bound head (resolve_bound_head_projection) without being able to see a type. +// Admitting the shape is not admitting the access, so if this arm merely fell to the frontier every +// misspelled field would be ACCEPTED by inference. Measured: with resolve's arm landed and this one +// absent, `b.absent_field` inferred clean. That is exactly the fail-open the absent-field control in +// v2.test.claim.field_projection.field_projection_stages exists to catch, and it caught it. +// +// IT SITS AT THE HEAD OF THE PRODUCT ROW BECAUSE A PROJECTION IS AN ELIMINATION, NOT A RECORD. A +// projection is a Conj, so without this arm it reaches infer_product_facts_from_entries and is typed as +// the PRODUCT OF ITS OWN CHILDREN's evidence -- a type combining the receiver with the field-name atom, +// which is not the type of anything the program computes. Placing the check first is what keeps the +// product rule from answering for a shape that is not a product. +// +// THE RECEIVER'S TYPE COMES FROM ITS OWN INFERRED FACTS, through this row's `entries`, rather than from +// a second derivation over the tree: the base is an ordinary expression that this stage has already +// typed, and re-deriving its type here would be a second authority that could disagree with the one +// every other consumer of that node reads. +// +// AND THE TYPE'S FIELDS COME THROUGH THE INDEX: the receiver's type is a resolved reference to a corpus +// declaration, so its path goes to the same GUARDED reader the callee path uses (v2.std.symbol_index +// symbol_index_lookup), whose ambiguity refusal therefore also governs which type's fields a projection +// may read. +// +// EVERY UNAVAILABLE STEP STAYS AT THE FRONTIER; ONLY A FIELD THE PAYLOAD DOES NOT DECLARE REFUSES. The +// distinction is deliberate. A receiver whose type this stage cannot yet establish is missing EVIDENCE, +// and turning that into a hard refusal would convict correct programs whose receivers are typed by +// routes not yet reaching here. A payload that IS established and does not declare the field is a +// decided fact about the program, and that is the one this arm rules on. +fn infer_field_projection_facts( + node: Node, + projection: FieldProjection, + entries: List, + partials: List, + resolved: ResolvedTree +) -> Outcome { + match infer_bounded_lattice_consumer_gate(consumer: node, partials: partials) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: _, diagnostics: cd } => + match infer_descent_witness_for_node(n: node) { + Violates { diagnostic: d } => + Rejected { diagnostics: diagnostics_singleton(d: d) } + Holds { value: descent_proof } => + match infer_projection_receiver( + base: projection.base, + entries: entries, + resolved: resolved + ) { + ReceiverTypeUnderived => + bind_outcome_accepted( + od: cd, + inner: inferred_facts_not_derived( + node: node, + descent: Holds { value: descent_proof } + ) + ) + ReceiverNotARecord => + outcome_rejected(infer_receiver_declares_no_fields_diagnostic(node: node)) + ReceiverPayload { payload: payload } => + match declared_field_named(payload: payload, name: projection.field) { + Present { value: declared } => + bind_outcome_accepted( + od: cd, + inner: inferred_facts_from_derived_type( + node: node, + derived_type: declared.type_node, + descent: Holds { value: descent_proof } + ) + ) + Absent => + outcome_rejected(infer_field_not_declared_diagnostic(node: node)) + } + } + } + } +} + +fn infer_field_not_declared_diagnostic(node: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_reason_field_not_declared_on_receiver, + at: node_locus(node: node), + correction: Unavailable { reason: UserInputBoundary } + } +} + +// TWO UNAVAILABILITIES THAT MUST NOT BE ONE ARM. "I could not establish the receiver's type" and "the +// receiver's type is established and declares no fields" are different facts with opposite dispositions, +// and collapsing them into a single Absent is the absorbing fallback DESIGN section 5 forbids: it +// converts a decided negative into "no evidence" and lets the projection pass at the frontier. +// +// MEASURED, NOT ARGUED. Collapsed, this broke the standing negative control +// v2.test.claim.namespace_xl0.cross_module_reference_resolution +// a_receiver_with_no_such_child_never_accepts: `Bool.v` has no child `v`, and because resolve now +// commits the projection shape for a bound head, `Bool`'s established type is not a record reference, +// the payload lookup answered Absent, and the projection was ACCEPTED at the frontier. A receiver with +// no such child accepting is exactly what that control exists to forbid. +// +// SO THE DECIDED CASES REFUSE AND ONLY MISSING EVIDENCE WAITS. A receiver whose type this stage has not +// derived is ReceiverTypeUnderived and stays at the frontier, because convicting a program whose +// receiver is typed by a route not yet reaching here would be a wrong answer in the other direction. A +// receiver whose type IS derived and is not a corpus declaration reference, or whose declaration the +// index does not hold, is ReceiverNotARecord: the program projects a field off something that declares +// none, and that is a fact about the program. +type ProjectionReceiver + = ReceiverPayload { payload: Node } + | ReceiverNotARecord + | ReceiverTypeUnderived + +fn infer_projection_receiver( + base: Node, + entries: List, + resolved: ResolvedTree +) -> ProjectionReceiver { + match lookup_inferred_facts_in_entries(entries: entries, key: base) { + Absent => ReceiverTypeUnderived + Present { value: base_facts } => + match inferred_facts_resolved_type(facts: base_facts) { + Violates { diagnostic: _ } => ReceiverTypeUnderived + Holds { value: receiver_type } => + match declaration_reference_path_optional(node: receiver_type) { + Absent => ReceiverNotARecord + Present { value: path } => + match symbol_index_lookup(index: resolved.symbol_index, qualified_path: path) { + Absent => ReceiverNotARecord + Present { value: payload } => ReceiverPayload { payload: payload } + } + } + } + } +} + +fn infer_receiver_declares_no_fields_diagnostic(node: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_reason_projection_receiver_declares_no_fields, + at: node_locus(node: node), + correction: Unavailable { reason: UserInputBoundary } + } +} + // A REFERENCE TO A CORPUS DECLARATION IS TYPED BY THAT DECLARATION, THROUGH THE INDEX RESOLUTION // ITSELF USED. Resolution answers WHICH declaration a reference denotes and carries it as a // declaring path; this arm answers WHAT TYPE that declaration establishes for this use. Those are @@ -3298,8 +3457,14 @@ fn infer_gather_product_row_on_entries( entries: List, pending: Diagnostics, partials: List, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { - match infer_product_facts_from_entries(node: node, entries: entries, partials: partials) { + match infer_product_facts_from_entries( + node: node, + entries: entries, + partials: partials, + resolved: resolved + ) { Rejected { diagnostics: r } => infer_gather_fold_acc_failed( node: node, @@ -3516,7 +3681,8 @@ fn infer_gather_settled_row( node: acc.node, entries: merged_entries, pending: merged_pending, - partials: partials + partials: partials, + resolved: resolved ) } else { infer_gather_fold_acc_ok( diff --git a/src/v2/compiler/05_eval.dag b/src/v2/compiler/05_eval.dag index f4bd10cd621..cb7a09416c0 100644 --- a/src/v2/compiler/05_eval.dag +++ b/src/v2/compiler/05_eval.dag @@ -54,6 +54,9 @@ import v2.std.runtime { ValueInterpreter, Return, RuntimeAggregate, + RuntimeFieldFound, + RuntimeFieldMissing, + runtime_aggregate_field_selection, RuntimeClosure, RuntimeClosureValue, RuntimePrimitive, @@ -120,7 +123,13 @@ import v2.std.node { node_for_structural_equality, well_formed, } -import v2.std.node_query { find_arrow_body_child, node_positional_child_targets } +import v2.std.node_query { + FieldProjection, + field_projection_edge_base_optional, + field_projection_optional, + find_arrow_body_child, + node_positional_child_targets +} import v2.std.diagnostic { AmbiguousIntent, ByteRange, @@ -2171,16 +2180,89 @@ fn eval_interpret_node( effect_io: effect_io ) TypeNode { connective: _ } => - eval_type_node_atom( - node: node, - interpretation: interpretation, - environment: environment - ) + match field_projection_optional(n: node) { + Present { value: projection } => + eval_field_projection(node: node, projection: projection, args: args) + Absent => + eval_type_node_atom( + node: node, + interpretation: interpretation, + environment: environment + ) + } } } ) } +// A FIELD PROJECTION SELECTS A FIELD FROM THE RECEIVER'S VALUE, and refuses on anything else. Without +// this arm a projection reached the default TypeNode route and was handed to allocate_literal, which +// would fabricate a literal out of a two-edge Conj -- a value for an expression that selects, not one +// that constructs. +// +// THE RECEIVER'S VALUE ARRIVES AS THE ONE RUNTIME ARGUMENT, through the same seam the evaluator already +// uses to decide which children are values (eval_edge_is_runtime_argument). So the base is evaluated +// ONCE, by the ordinary walk, rather than re-evaluated here: re-entering the base would compute it a +// second time for every projection off it, which is the duplicated work DESIGN section 2 forbids at any +// n. The FIELD edge is deliberately not an argument -- it carries a name, not a value, and evaluating it +// would ask for the grounding of a field-name atom. +// +// EVERY NON-SELECTING CASE REFUSES, TYPED AND LOCATED. A receiver that is not an aggregate has no +// fields to select from; an aggregate that does not carry the field is a value disagreeing with the type +// that admitted the projection, which is a fail-closed refusal here rather than a fabricated default, +// because infer has already established that the receiver's type declares the field +// (v2.compiler.infer infer_field_projection_facts) -- so reaching this arm means the VALUE and the TYPE +// disagree, and that is worth its own reason rather than being folded into "no such field". +fn eval_field_projection( + node: Node, + projection: FieldProjection, + args: List +) -> Outcome { + match list_at_optional(xs: args, index: 0) { + Absent => + outcome_rejected( + d: eval_diagnostic(reason: ^eval_rejected_projection_receiver_absent, node: node) + ) + Present { value: receiver } => + eval_field_projection_of_receiver( + node: node, + projection: projection, + receiver: receiver + ) + } +} + +// THE RECEIVER CROSSES INTO A TYPED PARAMETER BEFORE ANY FIELD IS READ. list_at_optional answers +// Optional, and a value destructured straight out of it does not carry RuntimeValue through a field +// access -- measured: reading `aggregate.fields` inline produced a runtime type error while the +// non-aggregate arm, which reads no field, passed. Naming the parameter is what restores the type, and it +// is the same shape v2.std.runtime runtime_fields_list_node already uses for its own element. +fn eval_field_projection_of_receiver( + node: Node, + projection: FieldProjection, + receiver: RuntimeValue +) -> Outcome { + match receiver { + RuntimeAggregate { value: aggregate } => + match runtime_aggregate_field_selection(fields: aggregate.fields, name: projection.field) { + RuntimeFieldFound { value: field_value } => + Accepted { value: field_value, diagnostics: None } + RuntimeFieldMissing => + outcome_rejected( + d: eval_diagnostic( + reason: ^eval_rejected_projected_field_absent_from_value, + node: node + ) + ) + } + _ => + outcome_rejected( + d: eval_diagnostic(reason: ^eval_rejected_projection_receiver_not_aggregate, node: node) + ) + } +} + + fn empty_runtime_values() -> List { [] } @@ -2203,7 +2285,14 @@ fn eval_edge_is_runtime_argument(parent: Node, progress: EvalProgress, edge: Edg } else if eval_node_is_loop(node: parent) { false } else { - is_positional(e: edge) + match field_projection_optional(n: parent) { + Present { value: _ } => + match field_projection_edge_base_optional(e: edge) { + Present { value: _ } => true + Absent => false + } + Absent => is_positional(e: edge) + } } } diff --git a/src/v2/std/qualified_name.dag b/src/v2/std/qualified_name.dag index 35d5b4f8788..8e219cd308e 100644 --- a/src/v2/std/qualified_name.dag +++ b/src/v2/std/qualified_name.dag @@ -255,6 +255,56 @@ fn qualified_name_spine_shape_present(root: Node) -> Bool { } } +// THE SPINE'S SEGMENTS AS THE NODES THAT CARRY THEM, beside the reader that returns their symbols. +// qualified_name_from_node answers the NAME -- a FreeMonoid -- which is everything a consumer +// deciding what a name denotes needs. A consumer REWRITING a spine into another construct needs the +// segment NODES instead, because each segment was lowered from its own token and carries that token's +// occurrence (v2.compiler.body_lowering_fold builds the spine from exactly those atoms). Rebuilding a +// segment from its symbol would mint a synthetic node in its place and move the diagnostic locus of +// every error about that segment onto the whole chain. +// +// IT LIVES HERE BECAUSE THE LABEL SET DOES. This module holds the spine's reader and its inverse +// together so that no producer spells the spine with labels the reader does not accept; a segment-node +// walker written anywhere else would be a third surface over fold_list_node's labels, read through +// qn_spine_role or -- worse -- re-spelled. The shape gate is the same one qualified_name_from_node +// uses, so the two readers accept exactly the same spines and a consumer cannot get segments for a +// node the name reader would refuse. +fn qn_spine_role_node_optional(root: Node, wanted_head: Bool) -> Optional { + fold(root.children, init: optional_absent(), f: fn(acc, edge) { + match qn_spine_role(label: edge.label) { + QnSpineHead => if wanted_head { optional_present(value: edge.target) } else { acc } + QnSpineTail => if wanted_head { acc } else { optional_present(value: edge.target) } + QnNotSpine => acc + } + }) +} + +fn qualified_name_spine_segment_nodes(root: Node) -> Optional> { + match root.kind { + TypeNode { connective: Conj } => + if count(root.children) == 0 { + optional_present(value: Empty) + } else if qualified_name_spine_shape_present(root: root) { + match qn_spine_role_node_optional(root: root, wanted_head: true) { + Absent => optional_absent() + Present { value: segment } => + match qn_spine_role_node_optional(root: root, wanted_head: false) { + Absent => optional_absent() + Present { value: tail } => + match qualified_name_spine_segment_nodes(root: tail) { + Absent => optional_absent() + Present { value: rest } => + optional_present(value: Cons { head: segment, tail: rest }) + } + } + } + } else { + optional_absent() + } + _ => optional_absent() + } +} + fn qualified_name_from_node(root: Node) -> Outcome { match root.kind { TypeNode { connective: Conj } => diff --git a/src/v2/std/runtime.dag b/src/v2/std/runtime.dag index 41f201448b0..c9326294f0c 100644 --- a/src/v2/std/runtime.dag +++ b/src/v2/std/runtime.dag @@ -5,6 +5,7 @@ import v2.std.collection { Map } import v2.std.optional { + Absent, Optional, Present, optional_absent, @@ -31,7 +32,8 @@ import v2.std.node { Node, Symbol, TypeNode, - node_synthetic + node_synthetic, + symbol_eq } type RuntimeIdentity { @@ -204,6 +206,41 @@ fn runtime_field_value_node_projection( } } +// THE VALUE-SIDE TWIN OF v2.std.node_query declared_field_named, and it lives here because the field +// list is this module's carrier. A consumer selecting a field -- the evaluator's field projection is the +// first -- reads it through this one reader rather than destructuring the list itself, so there is one +// answer to "which value does this aggregate hold for this field". +type RuntimeFieldSelection + = RuntimeFieldFound { value: RuntimeValue } + | RuntimeFieldMissing + +fn runtime_field_value_matching( + field_value: RuntimeFieldValue, + name: Symbol +) -> RuntimeFieldSelection { + if symbol_eq(a: field_value.field, b: name) { + RuntimeFieldFound { value: field_value.value } + } else { + RuntimeFieldMissing + } +} + +fn runtime_aggregate_field_selection( + fields: List, + name: Symbol +) -> RuntimeFieldSelection { + fold_list( + xs: fields, + empty: RuntimeFieldMissing, + cons: fn(acc, field_value) { + match runtime_field_value_matching(field_value: field_value, name: name) { + RuntimeFieldFound { value: v } => RuntimeFieldFound { value: v } + RuntimeFieldMissing => acc + } + } + ) +} + fn runtime_fields_list_node(fields: List) -> RuntimeValueNodeProjection { fold_list( xs: fields, diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag new file mode 100644 index 00000000000..1586a7e012b --- /dev/null +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -0,0 +1,377 @@ +module v2.test.claim.field_projection.field_projection_stages + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { infer } +import v2.compiler.inferred_tree { InferredTree } +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.extdeps.languages.dag { dag_language_model } +import extdeps.communication.medium { Lossless, Medium } +import std.algebra { Cons, Empty } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { Accepted, NodeLocus, Outcome, Rejected } +import v2.std.logic { Bool } +import v2.std.node { Atom, Conj, Node, Symbol, TypeNode, symbol_eq } +import v2.std.qualified_name { declaration_reference_path_optional, qualified_name_last_segment, qualified_name_spine_shape_present } +import v2.std.node_query { FieldProjection, declared_field_named, field_projection_optional } +import v2.std.symbol_index { symbol_index_lookup } +import v2.compiler.infer { + infer_atom_binding_sym, + infer_parameter_type_in_scope, + infer_type_equal_ignoring_provenance, + inferred_facts_grounding_derived, + inferred_facts_resolved_type +} +import v2.compiler.inferred_tree { InferredFacts } +import v2.compiler.eval { eval_field_projection } +import v2.std.runtime { + RuntimeAggregate, + RuntimeAggregateValue, + RuntimeFieldValue, + RuntimePrimitive, + RuntimePrimitiveValue, + RuntimeValue +} +import v2.std.integer { Int, integer_int_to_signed_i32_le_bytes, integer_signed_i32_le_bytes_to_int } +import std.occurrence_identity { OccurrenceSynthetic } +import v2.std.witness { Holds, Violates } +import v2.std.node { NodeFold, fold_node } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } + +// WHERE A FIELD PROJECTION STOPS TODAY, measured stage by stage rather than asserted. The normalized +// shape exists and is documented as a declared frontier: v2.std.node_query field_projection_node +// builds a two-edge Conj (a base edge to the receiver's lowered node, a field edge to the field-name +// atom) and its own header states what it does NOT do -- "it does not check that `f` IS a declared +// field of the receiver's type ... it is the typecheck stage's, over this node". +// +// This file establishes the first boundary before anything is repaired, so a later green is a change +// in behaviour and not a change in what was being asked. +data fps_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^field_projection_stages_artifact, + file_path: "src/v2/pilot/field_projection_stages_pilot.dag" +} + +fn fps_assemble(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: fps_artifact, + compilation_unit: ^field_projection_stages_cu, + source_root: V2Tree + }, + tail: Empty + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +// THE RECEIVER IS A PLAIN PARAMETER, deliberately: a match arm or a lambda would add a binding +// question that is not this subject, and an earlier investigation established that the same refusal +// appears with no match arm or lambda involved. +fn fps_valid_field_source() -> Outcome { + fps_assemble(src: "module p\n\ntype Box {\n tree: Int\n}\n\nfn f(b: Box) -> Int {\n b.tree\n}\n") +} + +// THE SAME RECEIVER WITH A FIELD THE TYPE DOES NOT DECLARE. This is the control that makes a later +// green meaningful: a projection stage that admits every field name would satisfy the valid case and +// establish nothing. +fn fps_absent_field_source() -> Outcome { + fps_assemble(src: "module p\n\ntype Box {\n tree: Int\n}\n\nfn f(b: Box) -> Int {\n b.absent_field\n}\n") +} + +// THE SAME RECEIVER WITH THE PROJECTION REMOVED, so the fixture family isolates projection from +// binding: if this also failed, the subject would be the parameter and not the field access. +fn fps_no_projection_source() -> Outcome { + fps_assemble(src: "module p\n\ntype Box {\n tree: Int\n}\n\nfn f(b: Box) -> Int {\n 7\n}\n") +} + +fn fps_resolves(o: Outcome) -> Bool { + match o { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn fps_infers(o: Outcome) -> Bool { + match o { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } +} + +// THE FIXTURE'S POSITIVE CONTROL: the receiver alone reaches resolve and infer, so a red below is +// about the projection and not about the parameter, the record declaration or the assembly. +test fn fps_the_receiver_without_a_projection_resolves_holds() -> Bool { + fps_resolves(o: fps_no_projection_source()) +} + +test fn fps_the_receiver_without_a_projection_infers_holds() -> Bool { + fps_infers(o: fps_no_projection_source()) +} + +test fn fps_a_valid_field_projection_resolves_holds() -> Bool { + fps_resolves(o: fps_valid_field_source()) +} + +test fn fps_a_valid_field_projection_infers_holds() -> Bool { + fps_infers(o: fps_valid_field_source()) +} + +// AN ABSENT FIELD MUST NOT BE ADMITTED. Asserted as a refusal at whatever stage currently refuses it, +// so this claim stays honest before and after the repair: what it forbids is ACCEPTANCE, which is the +// property that must never hold. +test fn fps_an_absent_field_does_not_infer_holds() -> Bool { + !fps_infers(o: fps_absent_field_source()) +} + +fn fps_projection_node_optional(root: Node) -> Optional { + fold_node( + n: root, + algebra: NodeFold { + init: fn(n0) { + match field_projection_optional(n: n0) { + Present { value: _ } => optional_present(value: n0) + Absent => optional_absent() + } + }, + step: fn(acc, _e, child) { + match acc { Present { value: _ } => acc Absent => child } + } + } + ) +} + +fn fps_projection_facts(o: Outcome) -> Optional { + match o { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: inferred, diagnostics: _ } => + match fps_projection_node_optional(root: inferred.root) { + Absent => optional_absent() + Present { value: proj } => inferred.facts.lookup(proj) + } + } + } +} + +// THE PROJECTION EXISTS IN THE RESOLVED TREE AT ALL. Separate from whether it grounds, because a tree +// with no projection node would make every grounding claim below vacuous in the other direction. +test fn fps_the_resolved_tree_carries_a_field_projection_holds() -> Bool { + match fps_projection_facts(o: fps_valid_field_source()) { + Present { value: _ } => true + Absent => false + } +} + +test fn fps_a_valid_field_projection_grounds_holds() -> Bool { + match fps_projection_facts(o: fps_valid_field_source()) { + Absent => false + Present { value: facts } => inferred_facts_grounding_derived(facts: facts) + } +} + +// AND IT GROUNDS TO THE RIGHT FIELD'S TYPE, asserted without naming a canonical spelling. Two fields +// of DIFFERENT declared types are projected from the same receiver: if the arm read the receiver's own +// type, its own node, or a fixed field, the two projections would ground to the SAME type. They must +// differ, and `.tree` must further agree with how the compiler types an Int-declared PARAMETER in an +// unrelated fixture -- an independent route to the same answer, so the claim is not the implementation +// compared with itself. +fn fps_two_field_source(field: String) -> Outcome { + fps_assemble(src: "module p\n\ntype Box {\n tree: Int\n flag: Bool\n}\n\nfn f(b: Box) -> Int {\n b." + field + "\n}\n") +} + +fn fps_inferred_of(o: Outcome) -> Optional { + match o { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: inferred, diagnostics: _ } => optional_present(value: inferred) + } + } +} + +fn fps_int_parameter_type_optional() -> Optional { + match fps_inferred_of(o: fps_assemble(src: "module p\n\nfn g(i: Int) -> Int {\n i\n}\n")) { + Absent => optional_absent() + Present { value: inferred } => fps_body_atom_type_optional(inferred: inferred) + } +} + +fn fps_body_atom_type_optional(inferred: InferredTree) -> Optional { + fold_node( + n: inferred.root, + algebra: NodeFold { + init: fn(n0) { + match n0.kind { + TypeNode { connective: Atom { identity: id } } => + if symbol_eq(a: id, b: ^i) { + match inferred.facts.lookup(n0) { + Absent => optional_absent() + Present { value: facts } => + match inferred_facts_resolved_type(facts: facts) { + Violates { diagnostic: _ } => optional_absent() + Holds { value: ty } => optional_present(value: ty) + } + } + } else { + optional_absent() + } + _ => optional_absent() + } + }, + step: fn(acc, _e, child) { + match acc { Present { value: _ } => acc Absent => child } + } + } + ) +} + +test fn fps_two_fields_of_different_types_ground_differently_holds() -> Bool { + match fps_grounded_type_optional(o: fps_two_field_source(field: "tree")) { + Absent => false + Present { value: int_ty } => + match fps_grounded_type_optional(o: fps_two_field_source(field: "flag")) { + Absent => false + Present { value: bool_ty } => + !infer_type_equal_ignoring_provenance(a: int_ty, b: bool_ty) + } + } +} + +test fn fps_the_int_field_grounds_as_an_int_parameter_does_holds() -> Bool { + match fps_grounded_type_optional(o: fps_two_field_source(field: "tree")) { + Absent => false + Present { value: field_ty } => + match fps_int_parameter_type_optional() { + Absent => false + Present { value: param_ty } => + infer_type_equal_ignoring_provenance(a: field_ty, b: param_ty) + } + } +} + +fn fps_grounded_type_optional(o: Outcome) -> Optional { + match fps_projection_facts(o: o) { + Absent => optional_absent() + Present { value: facts } => + match inferred_facts_resolved_type(facts: facts) { + Violates { diagnostic: _ } => optional_absent() + Holds { value: ty } => optional_present(value: ty) + } + } +} + +// THE THIRD STAGE, AT THE EVALUATOR'S OWN BOUNDARY. The receiver's VALUE is supplied here rather than +// computed, which is DESIGN section 3's witness rule and not a shortcut: this claim's subject is one +// interface -- what eval returns for a projection over a given aggregate -- and deriving the aggregate by +// executing a constructor would re-run production the claim is not about. +// +// THE PAIRING OBLIGATION IS DISCHARGED BY A CLAIM IN THIS FILE, not by assertion: the real producer +// emits this shape, and fps_the_resolved_tree_carries_a_field_projection asserts exactly that over the +// production route (assemble -> resolve -> infer on authored source). So a supplied value here is a +// hypothesis about a boundary that another claim shows is inhabited. +// +// WHY THE VALUE IS SUPPLIED RATHER THAN COMPUTED, measured rather than assumed. Two surface forms that +// should deliver a projection to eval do not: `Box { .. }.tree` and `make().tree` both resolve and infer +// with NO field-projection node in the tree, while the parameter form `b.tree` does produce one. So the +// only projection this corpus's surface syntax currently produces has an UNBOUND receiver at eval, whose +// binding is blocked behind the frozen facts-key question. That is a finding, recorded in this change, +// and it is why the executed evidence for eval is at this boundary. +fn fps_int_primitive(n: Int) -> RuntimeValue { + RuntimePrimitive { + value: RuntimePrimitiveValue { + primitive_type: fps_atom(s: ^fps_int_type), + bytes: integer_int_to_signed_i32_le_bytes(value: n) + } + } +} + +fn fps_atom(s: Symbol) -> Node { + Node { + kind: TypeNode { connective: Atom { identity: s } }, + children: [], + occurrence_id: OccurrenceSynthetic + } +} + +fn fps_box_value() -> RuntimeValue { + RuntimeAggregate { + value: RuntimeAggregateValue { + aggregate_type: fps_atom(s: ^fps_box_type), + fields: Cons { + head: RuntimeFieldValue { field: ^tree, value: fps_int_primitive(n: 7) }, + tail: Cons { + head: RuntimeFieldValue { field: ^other, value: fps_int_primitive(n: 9) }, + tail: Empty + } + } + } + } +} + +fn fps_projection_over(field: Symbol) -> FieldProjection { + FieldProjection { base: fps_atom(s: ^fps_base), field: field } +} + +fn fps_eval_projection_of(field: Symbol, receiver: RuntimeValue) -> Outcome { + eval_field_projection( + node: fps_atom(s: ^fps_projection_site), + projection: fps_projection_over(field: field), + args: Cons { head: receiver, tail: Empty } + ) +} + +fn fps_evaluates_to(o: Outcome, n: Int) -> Bool { + match o { + Rejected { diagnostics: _ } => false + Accepted { value: v, diagnostics: _ } => + match v { + RuntimePrimitive { value: p } => + match integer_signed_i32_le_bytes_to_int(bytes: p.bytes) { + Accepted { value: magnitude, diagnostics: _ } => magnitude == n + Rejected { diagnostics: _ } => false + } + _ => false + } + } +} + +test fn fps_eval_projects_the_named_field_holds() -> Bool { + fps_evaluates_to(o: fps_eval_projection_of(field: ^tree, receiver: fps_box_value()), n: 7) +} + +// AND IT SELECTS BY NAME, NOT BY POSITION: the second field holds a different value, so an arm reading +// the first field regardless would pass the claim above and fail this one. +test fn fps_eval_selects_the_second_field_by_name_holds() -> Bool { + fps_evaluates_to(o: fps_eval_projection_of(field: ^other, receiver: fps_box_value()), n: 9) +} + +// A FIELD THE VALUE DOES NOT CARRY REFUSES rather than yielding a default. Reaching this state means the +// value and the type disagree, since infer has already established the type declares the field. +test fn fps_eval_refuses_a_field_the_value_lacks_holds() -> Bool { + match fps_eval_projection_of(field: ^absent_field, receiver: fps_box_value()) { + Rejected { diagnostics: _ } => true + Accepted { value: _, diagnostics: _ } => false + } +} + +// AND A RECEIVER THAT IS NOT AN AGGREGATE REFUSES: there is nothing to select from, and answering +// anything would be fabrication. +test fn fps_eval_refuses_a_non_aggregate_receiver_holds() -> Bool { + match fps_eval_projection_of(field: ^tree, receiver: fps_int_primitive(n: 7)) { + Rejected { diagnostics: _ } => true + Accepted { value: _, diagnostics: _ } => false + } +} diff --git a/src/v2/test/claim/namespace_xl0/cross_module_reference_resolution_test.dag b/src/v2/test/claim/namespace_xl0/cross_module_reference_resolution_test.dag index 3dc6824789c..a28ed193dc8 100644 --- a/src/v2/test/claim/namespace_xl0/cross_module_reference_resolution_test.dag +++ b/src/v2/test/claim/namespace_xl0/cross_module_reference_resolution_test.dag @@ -1,6 +1,7 @@ module v2.test.claim.namespace_xl0.cross_module_reference_resolution import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { infer } import v2.compiler.name_resolve { Admission, ResolutionSubject, @@ -505,13 +506,34 @@ test fn a_cross_module_reference_to_a_data_declaration_resolves_to_its_declaring // `fn get(r: Rec) -> Bool { r.v }`, which needs BOTH the declaration graft (so Rec.v is in the // containment tree) AND the unique-on-chain projection, and is owned by those lanes, not enrolled // here as a today-row that could be retired by half of its trigger (DESIGN section 4b(3)). -test fn a_receiver_with_no_such_child_never_accepts_holds() -> Bool { - match xl0r_resolved_field_access_consumer() { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => xl0r_last_reason(d: d) == ^resolve_reason_unbound_symbol +// THE PROPERTY IS UNCHANGED AND THE STAGE MOVED. `r.v` on `r: Bool` must never be accepted, and it is +// not; what changed is WHERE it is refused and by what reason. v2.compiler.resolve now commits the +// field-projection SHAPE for a bound head (resolve_bound_head_projection) instead of answering +// resolve_reason_unbound_symbol, because deciding whether `v` exists needs a TYPE and resolve has none +// -- v2.std.node_query field_projection_node's own header assigns that check to the stage over this +// node. v2.compiler.infer then refuses it: `Bool`'s established type is not a record declaration, so the +// receiver declares no fields. +// +// THE OLD REASON WAS THE COLLAPSE, NOT THE PROPERTY. Answering "unbound symbol" here reported a bound +// head needing projection and a genuinely unbound name identically, which v2.compiler.resolve's own +// header records as a defect. So this claim now asserts through infer, which is what "never accepts" +// was always about: the assertion still forbids ACCEPTANCE, and it is strictly harder to satisfy than +// before, because a program that resolved and then inferred clean would now fail it. +fn xl0r_infers(o: Outcome) -> Bool { + match o { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } } } +test fn a_receiver_with_no_such_child_never_accepts_holds() -> Bool { + !xl0r_infers(o: xl0r_resolved_field_access_consumer()) +} + // THE CALL FORM OF THE LOCAL-RECEIVER ROW, ENROLLED ON gunbc#11683 (review 68231 asked for it by // execution, not argument). `r.v(x: p)` in a body now lowers to a Transform whose operator is the // spine `r.v` -- before that PR it lowered to Transform(r) with method and argument erased and @@ -522,11 +544,13 @@ test fn a_receiver_with_no_such_child_never_accepts_holds() -> Bool { // `lens.gate(...)`) receive under v2 resolve; no required lane compiles those modules through v2 // resolve (the native route over the corpus is a declared rung drop), so this row is the executed // evidence. Retired by the local-receiver projection, the same trigger as the value row. -test fn a_local_receiver_method_call_refuses_unbound_today_holds() -> Bool { - match xl0r_resolved_method_call_consumer() { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => xl0r_last_reason(d: d) == ^resolve_reason_unbound_symbol - } +// THE SAME MOVE FOR THE METHOD-CALL FORM, and the rename is the honest part: this row said "refuses +// unbound TODAY", naming a stage and a reason it never meant to pin. `r.v(x: p)` closes its spine into +// a callee at the first call suffix, so the callee `r.v` is now the projection resolve commits, and +// infer refuses it for the same reason as the bare form -- `Bool` declares no fields. What the row is +// FOR is that a method call on a local receiver is not silently admitted, and that is what it now says. +test fn a_local_receiver_method_call_never_accepts_holds() -> Bool { + !xl0r_infers(o: xl0r_resolved_method_call_consumer()) } // THE COLLISION THE ABSOLUTE ARM ADMITTED, NOW REFUSED (review 67651 on gunbc#11582). A parameter From b38e6e224a2ce6ce1ec503dbeb8933c8fa1d2451 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 02:46:24 +0000 Subject: [PATCH 23/90] The seven's receiver is a match binder, and its blocker is the match form WHAT THE SEVEN'S REAL SITE ACTUALLY IS. In v2.test.parse.expression_bodied_fn_decl_- parse the projection is `artifact.tree`, where `artifact` is bound by the arm `Accepted { value: artifact, diagnostics: d }`. That is a MATCH-ARM BINDER, not the function parameter the controls beside it use, so whether the projection repair reaches it is its own fact and gets its own controls. MEASURED: resolve reaches it, infer does not, AND THE ISOLATING CONTROL SAYS WHY. The match form resolves -- resolve's projection arm handles a match binder's head exactly as it handles a parameter's -- and then fails to infer. The same match form with the projection REPLACED BY A LITERAL fails to infer identically. So the blocker is the match construct, not the field access: it is the pre-existing match-arm boundary already recorded as the seven's first refusal (body_lowering_reason_match_arm_navigation_refused), owned elsewhere. Both conjuncts of that claim are load-bearing. The projection form refusing alone would be consistent with a projection defect; it is the literal-bodied form refusing too that assigns the refusal to the match construct. When match arms do infer, the claim goes red and the projection claim beside it becomes the live question, which is the transition worth being told about. A VACUOUS CLAIM OF MINE, CAUGHT AND REPLACED. I first asserted that an absent field off a match binder is not admitted, and it PASSED -- vacuously, because the VALID projection off a match binder does not infer either. Both arms refuse, so the assertion distinguished nothing and would have gone on reading as coverage for the absent-field wall on that shape. The isolating pair replaces it. This is the second time in this lane that a claim passed while establishing nothing, and both times the cause was the same: asserting a refusal without first checking that the positive case reaches the boundary being tested. AND THE SEVEN THEMSELVES ARE NOT THE EVIDENCE, DELIBERATELY. All seven pass under the development runner -- both before and after this change -- because that runner resolves them with the SEED compiler, while their blocker is v2's OWN front end on the native route. Reporting that green as progress would be citing a signal that was never about the property claimed, so the shape is reproduced as a small fixture instead and the seven are left to the route that actually exercises v2. Green: 15 field-projection controls. Co-Authored-By: Claude Opus 5 (1M context) --- .../field_projection_stages_test.dag | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag index 1586a7e012b..01bc44d648d 100644 --- a/src/v2/test/claim/field_projection/field_projection_stages_test.dag +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -375,3 +375,47 @@ test fn fps_eval_refuses_a_non_aggregate_receiver_holds() -> Bool { Accepted { value: _, diagnostics: _ } => false } } + +// THE SEVEN'S ACTUAL RECEIVER IS A MATCH-ARM BINDER, AND THAT SHAPE IS BLOCKED BEFORE PROJECTION EVER +// APPLIES. In v2.test.parse.expression_bodied_fn_decl_parse the projection is `artifact.tree` where +// `artifact` is bound by the arm `Accepted { value: artifact, diagnostics: d }` -- a different binder +// kind from the parameter receiver above, so whether this repair reaches it is its own fact. +// +// MEASURED, AND THE ISOLATING CONTROL IS THE ONE THAT MATTERS. The match form RESOLVES -- so resolve's +// projection arm handles a match binder's head -- and then fails to infer. But the SAME match form with +// the projection REPLACED BY A LITERAL fails to infer identically, so the blocker is the match construct +// and not the field access. That is the pre-existing match-arm boundary this lane already recorded as the +// seven's first refusal (body_lowering_reason_match_arm_navigation_refused), owned elsewhere. +// +// AN EARLIER VERSION OF THIS SECTION CLAIMED MORE THAN IT MEASURED. It asserted that an absent field off +// a match binder is not admitted, and that claim PASSED -- vacuously, because the valid projection off a +// match binder does not infer either, so both arms refuse and the assertion distinguished nothing. It is +// replaced by the isolating pair below, which states what is actually established. +// +// WHY THE SEVEN ARE NOT RUN AS THE EVIDENCE HERE. Those seven claims pass under the development runner +// both before and after this change, because that runner resolves them with the SEED compiler; their +// blocker is v2's OWN front end on the native route. A green from running them would be evidence about +// the seed, not about this repair -- a green signal that was never about the property claimed. +fn fps_match_binder_source(field: String) -> Outcome { + fps_assemble(src: "module p\n\ntype Art {\n tree: Int\n}\n\ntype Holder\n = Wrapped { value: Art }\n\nfn f(h: Holder) -> Int {\n match h {\n Wrapped { value: artifact } => artifact." + field + "\n }\n}\n") +} + +fn fps_match_no_projection_source() -> Outcome { + fps_assemble(src: "module p\n\ntype Art {\n tree: Int\n}\n\ntype Holder\n = Wrapped { value: Art }\n\nfn f(h: Holder) -> Int {\n match h {\n Wrapped { value: artifact } => 7\n }\n}\n") +} + +// RESOLVE REACHES THE MATCH BINDER'S PROJECTION: the head is bound on the chain and the whole path names +// no declaration, so resolve commits the projection shape exactly as it does for a parameter receiver. +test fn fps_a_match_binder_receiver_resolves_holds() -> Bool { + fps_resolves(o: fps_match_binder_source(field: "tree")) +} + +// AND THE BLOCKER IS THE MATCH FORM, NOT THE PROJECTION. Both conjuncts are needed: the projection form +// refusing alone would be consistent with a projection defect, and it is the SECOND conjunct -- the same +// match with a literal body refusing too -- that assigns the refusal to the match construct. If a later +// change makes match arms infer, this claim goes red and the projection claim beside it becomes the live +// question, which is the transition worth being told about. +test fn fps_the_match_binder_blocker_is_the_match_form_holds() -> Bool { + !fps_infers(o: fps_match_binder_source(field: "tree")) + && !fps_infers(o: fps_match_no_projection_source()) +} From 27f1427305e0d60027e94cc310a2ed1dd7659187 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 05:02:28 +0000 Subject: [PATCH 24/90] Restore the application-path repair the merge pass silently dropped WHAT THE MECHANICAL PASS DELETED. Resolving the merge's threading conflicts took main's side at infer_transform_application_optional, which reverted two things this lane's headline result depends on: the callee reader went back to infer_application_callee_arrow (which only recognises an operator that IS an Arrow, never one reached through its facts), and the facts lookup went back to `key: arrow` instead of `key: callee_use`. infer_application_callee_use was left DEFINED AND NEVER CALLED -- a declaration with no consumer, which is the tell. WHY THE COMPILER COULD NOT CATCH IT, and this is the part worth keeping. The four places where the same pass rewrote `tree` on a genuine Node parameter were named by the front end immediately, by parameter, and fixed in one pass. This one compiled cleanly, because the dropped code was a DIFFERENT ARGUMENT TO A CALL THAT STILL TYPECHECKS: `key: arrow` and `key: callee_use` are both Nodes. A threading merge resolved mechanically can therefore delete semantics while every type agrees, and only an executed claim distinguishes the two. Five claims did: three reference-evidence rows and BOTH executing named calls. THE REPAIR RESTORED, with the reason it exists. The lookup is asked of the CALLEE USE rather than of the arrow, because once the arrow may be a DECLARATION's Arrow reached through the use's facts, it is a node of the declaring module with no entry in this tree -- so keying on it answers Absent and the application drops to the frontier however well the callee was typed. ALSO RECORDED: every claim_batch verdict taken before this merge's rebuild is void. Main moved the seed's Rust by roughly 3,700 lines (v1_interpreter.rs alone +967), and the stale binary reported unbounded recursion in symbol_intern_lexeme on EVERY assemble-based fixture -- including in a clean main worktree, which briefly read as "main is broken". It was the instrument. The binary is rebuilt and every verdict below was taken with it. Green with the rebuilt binary: 15/15 field projection, 9/9 reference evidence, 10/10 named call. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 59cdff317a2..ad6da74dce7 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -3088,10 +3088,13 @@ fn infer_transform_derived_optional( // a non-Arrow operator, or a return the language join does not denote stays on the counted // frontier: Absent here, never an admission. fn infer_transform_application_optional(node: Node, entries: List) -> Optional> { - match infer_application_callee_arrow(node: node) { + match infer_application_callee_arrow_with_facts(node: node, entries: entries) { Absent => optional_absent() Present { value: arrow } => - match lookup_inferred_facts_in_entries(entries: entries, key: arrow) { + match infer_application_callee_use(node: node) { + Absent => optional_absent() + Present { value: callee_use } => + match lookup_inferred_facts_in_entries(entries: entries, key: callee_use) { Absent => optional_absent() Present { value: arrow_facts } => if !inferred_facts_grounding_derived(facts: arrow_facts) { @@ -3114,7 +3117,7 @@ fn infer_transform_application_optional(node: Node, entries: List Date: Tue, 29 Sep 2026 14:25:10 +0000 Subject: [PATCH 25/90] The seven's downstream continuation, qualified past the match boundary THE CHAIN, CONTINUED PAST THE ONE CONSTRUCT THAT STOPS IT. The pinned subject runs dag_prepared_grammar -> tokenize -> parse_module_prepared -> ParseArtifact.tree -> normalize -> recursive Arrow-body search and stops at the MATCH ARMS in it, owned by the match-inference lane. This is that same chain over the SAME sources and the SAME production functions, with the arms replaced by bind_outcome, whose continuation is an ordinary function parameter rather than an arm binder. Nothing downstream is re-implemented: normalize, find_arrow_body_child and the recursive fold are the readers the subject itself calls, and the sources are IMPORTED from the subject so a control here cannot drift from the text the seven parse. ALL TWELVE ROWS GREEN, which is the finding: there is no second defect waiting behind the match boundary. Everything the seven do after it -- the projection, normalize, and the recursive search, including the empty-`=` refusal -- already works. So when the match owner lands, the seven have one blocker and not two. IT IS PROVABLY THE SUBJECT'S CHAIN AND NOT MERELY A SIMILAR ONE. Two rows compare this continuation's TREE against the subject's own entry point for every source, and its refusal against the subject's refusal. "I called the same functions" is not that evidence, and the defect below is exactly how the two can diverge while every other row stays green. THE FIDELITY DEFECT THIS CAUGHT, IN THIS FILE. The subject forwards the prepared grammar's validation residue into parse_module_prepared explicitly. bind_outcome cannot supply it -- it merges diagnostics into the continuation instead of handing them back -- so the first version passed None, on the assumption that the grammar carries no residue, and enrolled a claim to check that assumption. The claim went RED: the prepared grammar DOES carry residue. Without it this control would have parsed under a residue the seven never use, with all seven mirrored rows still green. The residue is now read through v2.std.diagnostic outcome_diagnostics, added here as the reader that was missing: bind_outcome owns the pipeline arms, and a caller that must FORWARD diagnostics to a producer expecting them as an argument had no route but to re-spell those arms itself. It has a consumer in this change. TWO CONTROLS THE SUBJECT DOES NOT CARRY, because every one of its rows answers TRUE through the recursive search, so a search answering true for ANY tree would satisfy all of them. A leaf atom must answer FALSE, and the braced control's normalized tree must be found by RECURSION -- asserted by requiring that the root itself does NOT carry the arrow body, so a search that only inspected the root fails there and nowhere else. ParseArtifact.tree is qualified on the REAL carrier -- a production record whose `tree` is a ParseTree beside a span index and an allocator -- and not on a fixture shaped to resemble it. The field's type is what makes the projection's target unambiguous: the other two fields are not Nodes, so selecting either would not compile. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/std/diagnostic.dag | 16 ++ .../expression_bodied_continuation_test.dag | 270 ++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 src/v2/test/claim/parse/expression_bodied_continuation_test.dag diff --git a/src/v2/std/diagnostic.dag b/src/v2/std/diagnostic.dag index 9c15e91bdf2..fd5e345dcc1 100644 --- a/src/v2/std/diagnostic.dag +++ b/src/v2/std/diagnostic.dag @@ -333,6 +333,22 @@ fn bind_outcome_accepted(od: Diagnostics, inner: Outcome) -> Outcome { Rejected { diagnostics: rejected_with_pending(pending: od, rejected: r) } } } +// THE DIAGNOSTICS AN OUTCOME CARRIES, as a reader. bind_outcome hands a continuation the VALUE and +// merges the diagnostics itself, which is right for a pipeline and wrong for a caller that must +// FORWARD them to a producer expecting them as an argument -- v2.compiler.parse parse_module_prepared +// takes the grammar's validation residue that way. Without this, such a caller has to destructure the +// outcome itself, which re-spells the arms bind_outcome already owns. +// +// A REJECTED OUTCOME'S DIAGNOSTICS ARE ITS NON-EMPTY ONES, returned as the same Diagnostics the +// accepted arm answers with, so a consumer forwarding them does not have to know which arm produced +// them. +fn outcome_diagnostics(o: Outcome) -> Diagnostics { + match o { + Accepted { value: _, diagnostics: d } => d + Rejected { diagnostics: r } => Some { diagnostics: r } + } +} + fn bind_outcome(o: Outcome, f: fn(T) -> Outcome) -> Outcome { match o { Accepted { value: v, diagnostics: od } => bind_outcome_accepted(od: od, inner: f(v)) diff --git a/src/v2/test/claim/parse/expression_bodied_continuation_test.dag b/src/v2/test/claim/parse/expression_bodied_continuation_test.dag new file mode 100644 index 00000000000..47e40606900 --- /dev/null +++ b/src/v2/test/claim/parse/expression_bodied_continuation_test.dag @@ -0,0 +1,270 @@ +module v2.test.claim.parse.expression_bodied_continuation + +import v2.compiler.normalize { normalize } +import v2.compiler.normalized_tree { NormalizedTree } +import v2.compiler.parse { ParseArtifact, parse_module_prepared } +import v2.compiler.program_assembly { dag_prepared_grammar } +import v2.compiler.tokenize { tokenize } +import v2.extdeps.languages.dag { dag_lex } +import v2.std.diagnostic { + Accepted, + Diagnostics, + None, + Outcome, + Rejected, + bind_outcome, + outcome_accepted, + outcome_diagnostics +} +import v2.std.logic { Bool } +import v2.std.node { Node, Symbol, TypeNode, Atom } +import v2.std.node_query { find_arrow_body_child } +import v2.std.text { String } +import std.occurrence_identity { OccurrenceSynthetic } +import v2.test.parse.expression_bodied_fn_decl_parse { + g_tokenize_parse, + braced_fn_control_source, + empty_eq_fn_source, + expression_bodied_fn_source, + expression_bodied_literal_fn_source +} + +// THE SEVEN'S CHAIN, CONTINUED PAST THE ONE CONSTRUCT THAT STOPS IT. The pinned subject +// v2.test.parse.expression_bodied_fn_decl_parse runs +// +// dag_prepared_grammar -> tokenize -> parse_module_prepared -> ParseArtifact.tree +// -> normalize -> recursive Arrow-body search +// +// and its front end stops at the MATCH ARMS in that chain, which are owned by the match-inference +// lane. This file is that same chain over the SAME sources and the SAME production functions, with +// the match arms replaced by bind_outcome -- whose continuation is an ordinary function parameter, +// not an arm binder. Nothing downstream is re-implemented: normalize, find_arrow_body_child and the +// recursive fold are the production readers the subject itself calls. +// +// SO A RED HERE IS A DOWNSTREAM DEFECT AND NOT THE MATCH BOUNDARY, which is the whole point: it +// separates "the seven are blocked by one construct" from "the seven are blocked by one construct +// AND whatever follows it", and only the second is a reason to keep waiting after the match owner +// lands. The sources are IMPORTED from the subject rather than copied, so a control here cannot +// drift from the text the seven actually parse. +// +// IT IS A CONTROL AND NOT A REWRITE. The pinned subject is untouched; when the match boundary lifts, +// the seven run unchanged and this file remains as the finer-grained account of what they cover. +fn cont_parse(text: String, file: Symbol) -> Outcome { + bind_outcome( + o: dag_prepared_grammar(), + f: fn(prepared) { + bind_outcome( + o: tokenize(text: text, file: file, rules: dag_lex()), + f: fn(token_stream) { + bind_outcome( + o: parse_module_prepared( + tokens: token_stream, + prepared: prepared, + validation_residue: outcome_diagnostics(o: dag_prepared_grammar()) + ), + f: fn(artifact) { cont_artifact_tree(artifact: artifact) } + ) + } + ) + } + ) +} + +// THE PROJECTION UNDER QUALIFICATION, on the REAL carrier. `artifact` is a ParseArtifact -- a +// production record whose `tree` field is a ParseTree (v2.std.grammar: an alias of Node) beside a +// span index and an allocator -- so this exercises the same field access the subject performs, not a +// fixture record shaped to resemble it. The receiver is a function parameter rather than a match-arm +// binder, which is exactly the isolation this file exists to make. +fn cont_artifact_tree(artifact: ParseArtifact) -> Outcome { + outcome_accepted(value: artifact.tree) +} + +// THE RESIDUE THE SUBJECT THREADS EXPLICITLY IS THREADED HERE TOO, and that is not a formality: the +// prepared grammar DOES carry residue. An earlier version of this file passed None on the assumption +// that it did not, and the claim written to check that assumption went RED -- which is the only +// reason this control now forwards the real thing rather than parsing under a residue the seven +// never use. bind_outcome cannot supply it, because it merges diagnostics into the continuation +// instead of handing them back, so the residue is read with v2.std.diagnostic outcome_diagnostics. +test fn cont_the_prepared_grammar_carries_residue_holds() -> Bool { + match outcome_diagnostics(o: dag_prepared_grammar()) { + None => false + _ => true + } +} + +fn cont_accepted(o: Outcome) -> Bool { + match o { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +// THE SUBJECT'S OWN RECURSIVE SEARCH, called rather than re-implemented. +fn cont_tree_has_arrow_body(root: Node) -> Bool { + match find_arrow_body_child(root: root) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => + fold(root.children, init: false, f: fn(found, e) { + found || cont_tree_has_arrow_body(root: e.target) + }) + } +} + +// NORMALIZE, THEN THE SEARCH OVER ITS ROOT. `normalized.root` is a second projection on a real +// production carrier (v2.compiler.normalized_tree NormalizedTree), read through a function parameter +// for the same reason as the artifact's. +fn cont_normalized_root(normalized: NormalizedTree) -> Outcome { + outcome_accepted(value: normalized.root) +} + +fn cont_normalized_tree(parsed: Outcome) -> Outcome { + bind_outcome( + o: parsed, + f: fn(tree) { + bind_outcome( + o: normalize(parse_tree: tree), + f: fn(normalized) { cont_normalized_root(normalized: normalized) } + ) + } + ) +} + +fn cont_normalize_has_arrow_body(parsed: Outcome) -> Bool { + match cont_normalized_tree(parsed: parsed) { + Rejected { diagnostics: _ } => false + Accepted { value: root, diagnostics: _ } => cont_tree_has_arrow_body(root: root) + } +} + +fn cont_expression_bodied_parsed() -> Outcome { + cont_parse(text: expression_bodied_fn_source, file: ^probe_expr_fn) +} + +fn cont_expression_bodied_literal_parsed() -> Outcome { + cont_parse(text: expression_bodied_literal_fn_source, file: ^probe_expr_fn_lit) +} + +fn cont_braced_parsed() -> Outcome { + cont_parse(text: braced_fn_control_source, file: ^probe_braced_fn) +} + +// THE THREE PARSE ROWS, over the subject's own sources. +test fn cont_expression_bodied_fn_decl_parses_holds() -> Bool { + cont_accepted(o: cont_expression_bodied_parsed()) +} + +test fn cont_expression_bodied_literal_fn_decl_parses_holds() -> Bool { + cont_accepted(o: cont_expression_bodied_literal_parsed()) +} + +test fn cont_braced_fn_decl_still_parses_holds() -> Bool { + cont_accepted(o: cont_braced_parsed()) +} + +// THE EMPTY-`=` ROW KEEPS ITS MEANING. The subject asserts that `fn f(a: Int) -> Int =` with no +// body is REFUSED -- a grammar that accepted it would accept a declaration with nothing to run. It +// is the one row here whose green is a refusal, so it is written as the refusal and not as the +// negation of a helper that could go green for an unrelated reason: a source that failed to TOKENIZE +// would also make `cont_accepted` false, so the arm is read directly. +test fn cont_empty_eq_fn_decl_refuses_holds() -> Bool { + match cont_parse(text: empty_eq_fn_source, file: ^probe_empty_eq) { + Rejected { diagnostics: _ } => true + Accepted { value: _, diagnostics: _ } => false + } +} + +// THE THREE NORMALIZE ROWS. +test fn cont_braced_fn_decl_survives_normalize_holds() -> Bool { + cont_normalize_has_arrow_body(parsed: cont_braced_parsed()) +} + +test fn cont_expression_bodied_fn_decl_survives_normalize_holds() -> Bool { + cont_normalize_has_arrow_body(parsed: cont_expression_bodied_parsed()) +} + +test fn cont_expression_bodied_literal_fn_decl_survives_normalize_holds() -> Bool { + cont_normalize_has_arrow_body(parsed: cont_expression_bodied_literal_parsed()) +} + +// THE RECURSIVE FOLD'S NEGATIVE CONTROL, which the subject does not carry. Every row above answers +// TRUE through cont_tree_has_arrow_body, so a search that answered true for ANY tree would satisfy +// all of them and establish nothing about the search. An atom has no children and no arrow body, so +// it is the tree that must answer false -- and a fold that returned its init unconditionally, or a +// find_arrow_body_child that accepted anything, goes red here and nowhere else. +fn cont_leaf_atom() -> Node { + Node { + kind: TypeNode { connective: Atom { identity: ^cont_leaf } }, + children: [], + occurrence_id: OccurrenceSynthetic + } +} + +test fn cont_the_arrow_body_search_answers_false_for_a_leaf_holds() -> Bool { + !cont_tree_has_arrow_body(root: cont_leaf_atom()) +} + +// AND ITS POSITIVE CONTROL AT THE SAME BOUNDARY: the search must find a body that IS there, reached +// by RECURSION rather than at the root. The braced control's normalized tree carries its arrow below +// the module root, so a search that only inspected the root answers false here. +test fn cont_the_arrow_body_search_recurses_to_find_a_body_holds() -> Bool { + match cont_normalized_tree(parsed: cont_braced_parsed()) { + Rejected { diagnostics: _ } => false + Accepted { value: root, diagnostics: _ } => + cont_tree_has_arrow_body(root: root) + && !cont_root_itself_has_arrow_body(root: root) + } +} + +fn cont_root_itself_has_arrow_body(root: Node) -> Bool { + match find_arrow_body_child(root: root) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +// THE CONTROL PRODUCES WHAT THE SUBJECT PRODUCES, asserted rather than assumed. Everything above is +// only evidence about the seven if this chain and theirs agree, and "I used the same functions" is +// not that evidence -- the residue defect this file already caught is exactly how they can diverge +// while every row still looks green. So the two trees are compared directly, for each source, through +// the subject's own entry point. +// +// IT COMPARES THE TREE AND NOT THE VERDICT. Two chains that both answer Accepted can still have +// parsed differently; the node is what the rest of the chain consumes. +fn cont_agrees_with_subject(mine: Outcome, theirs: Outcome) -> Bool { + match mine { + Rejected { diagnostics: _ } => + match theirs { + Rejected { diagnostics: _ } => true + Accepted { value: _, diagnostics: _ } => false + } + Accepted { value: a, diagnostics: _ } => + match theirs { + Rejected { diagnostics: _ } => false + Accepted { value: b, diagnostics: _ } => a == b + } + } +} + +test fn cont_the_continuation_parses_the_same_tree_as_the_subject_holds() -> Bool { + cont_agrees_with_subject( + mine: cont_expression_bodied_parsed(), + theirs: g_tokenize_parse(text: expression_bodied_fn_source, file: ^probe_expr_fn) + ) + && cont_agrees_with_subject( + mine: cont_expression_bodied_literal_parsed(), + theirs: g_tokenize_parse(text: expression_bodied_literal_fn_source, file: ^probe_expr_fn_lit) + ) + && cont_agrees_with_subject( + mine: cont_braced_parsed(), + theirs: g_tokenize_parse(text: braced_fn_control_source, file: ^probe_braced_fn) + ) +} + +// AND IT AGREES ON THE REFUSAL TOO, so the empty-`=` row is the subject's refusal and not merely a +// refusal of the same shape. +test fn cont_the_continuation_refuses_where_the_subject_refuses_holds() -> Bool { + cont_agrees_with_subject( + mine: cont_parse(text: empty_eq_fn_source, file: ^probe_empty_eq), + theirs: g_tokenize_parse(text: empty_eq_fn_source, file: ^probe_empty_eq) + ) +} From 22800fa883389940f2be4302d084b7fc4bca6e28 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 23:42:13 +0000 Subject: [PATCH 26/90] The chain wins: a bound head resolves local-first, and the interim guard dissolves WALL 2 OF THE SEVEN, AND IT IS NOT A NEW SEMANTIC RULE. Section 13's rule is that a chain's FIRST segment resolves on the ancestor chain and the rest projects. This arm consulted the absolute candidates first and only reached the projection when the index held nothing, so a bound head plus an absolute hit refused AmbiguousQualifiedHeadShadowsAbsolute instead of projecting from the binder. THAT REFUSAL WAS AN INTERIM GUARD AND ITS REASON IS GONE. qualified_head_bound_on_chain's own header said exactly what it was: "the half of unique-on-chain that can be honest BEFORE THE PROJECTION EXISTS" -- it refused because the arm could not project, and therefore must not let the absolute read silently win. The projection now exists (resolve_bound_head_projection, landed with the field-projection work), so the guard DISSOLVES rather than weakens (DESIGN section 4b(4)). WHAT THE OLD CLAIM FORBADE IS STILL FORBIDDEN. The absolute read does not win over a binder the source spelled; it is now ANSWERED correctly instead of refused, which is the climb the interim arm was waiting for. A bound head no longer consults the absolute candidates at all. THE EVIDENCE DID NOT RETIRE, which is the other half of 4b(4). The shadowing row is flipped, not deleted: `fn f(v2: Bool) -> Bool { v2.test.xl0r_provider.xl0r_provided_fn }` now asserts that the resolved tree carries a FIELD PROJECTION rather than that resolution refused. It stays discriminating -- an implementation consulting the absolute candidates first resolves that path to the provider's declaring path and carries no projection, so the claim fails exactly on the behaviour the guard existed to prevent. Green: 15/15 namespace resolution, 15/15 field projection. STATED PLAINLY BECAUSE IT IS AN ESCALATION-SHAPED CHANGE: deleting a refusal is normally something I escalate for. I did not treat this as one because the refusal's own header declares it interim and names the capability that retires it, and that capability is the thing this lane built. If the reading is that section 13's ordering is itself the open question, this commit is the one to revert. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/03_resolve.dag | 63 ++++++++++--------- ...cross_module_reference_resolution_test.dag | 33 +++++++++- 2 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 7f14ae925ad..f791d2095e3 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -894,6 +894,21 @@ fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional< } } +// THE CHAIN WINS, WHICH IS SECTION 13'S RULE AND NOT A NEW ONE. A chain's FIRST segment resolves on +// the ancestor chain and the rest projects, so a bound head is answered by the binder the author wrote +// and the absolute candidates are not consulted at all. +// +// THE AMBIGUOUS ARM THAT STOOD HERE WAS AN INTERIM GUARD WHOSE REASON IS GONE. qualified_head_bound_- +// on_chain's own header said what it was: "the half of unique-on-chain that can be honest BEFORE THE +// PROJECTION EXISTS", refusing AmbiguousQualifiedHeadShadowsAbsolute precisely because this arm could +// not project and therefore must not let the absolute read silently win. The projection now exists +// (resolve_bound_head_projection), so the guard DISSOLVES rather than weakens -- DESIGN section 4b(4), +// which also says the evidence does not retire: its discriminating control stays enrolled, flipped to +// assert that a bound head answers with its binder's projection. +// +// SO THIS IS NOT A REFUSAL BEING RELAXED. The state the refusal forbade -- the absolute read winning +// over a binder the source spelled -- is still forbidden; it is now answered correctly instead of +// refused, which is the climb the interim arm was waiting for. fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional> { match qualified_name_from_node(root: n) { Rejected { diagnostics: _ } => optional_absent() @@ -903,17 +918,17 @@ fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional optional_present(value: Rejected { diagnostics: rejected_with_pending(pending: pending, rejected: r) }) Accepted { value: head_bound, diagnostics: chain_pending } => + if head_bound { + optional_present( + value: resolve_bound_head_projection( + ctx: ctx, + n: n, + pending: diagnostics_merge(outer: pending, inner: chain_pending) + ) + ) + } else { match symbol_index_absolute_candidates(index: ctx.namespace.symbol_index, qualified_path: path) { Empty => - if head_bound { - optional_present( - value: resolve_bound_head_projection( - ctx: ctx, - n: n, - pending: diagnostics_merge(outer: pending, inner: chain_pending) - ) - ) - } else { optional_present( value: Rejected { diagnostics: rejected_with_pending( @@ -922,32 +937,17 @@ fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional match rest { Empty => - if head_bound { - optional_present( - value: Rejected { - diagnostics: rejected_with_pending( - pending: diagnostics_merge(outer: pending, inner: chain_pending), - rejected: ambiguous_symbol_diagnostics( - n: n, - class: AmbiguousQualifiedHeadShadowsAbsolute { path: path } - ) - ) - } - ) - } else { - optional_present( - value: resolve_atom_bound( - ctx: ctx, - n: n, - path: candidate.path, - pending: diagnostics_merge(outer: pending, inner: chain_pending) - ) + optional_present( + value: resolve_atom_bound( + ctx: ctx, + n: n, + path: candidate.path, + pending: diagnostics_merge(outer: pending, inner: chain_pending) ) - } + ) Cons { head: _, tail: _ } => optional_present( value: Rejected { @@ -965,6 +965,7 @@ fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional Bool { // answer, so the verdict is a located refusal with the section-13 reason. What this row does NOT // claim: the projection through the binder (`v2.test...` read relative to the parameter, which is // meaningless here and meaningful for `r.v`). That is the local-receiver row's trigger. -test fn a_binder_shadowing_a_root_segment_refuses_ambiguous_holds() -> Bool { +// THE GUARD DISSOLVED AND THE EVIDENCE DID NOT RETIRE (DESIGN section 4b(4)). `fn f(v2: Bool) -> Bool +// { v2.test.xl0r_provider.xl0r_provided_fn }` binds `v2` while the absolute path also answers. The +// interim rule refused that Ambiguous because v2.compiler.resolve could not project a bound head and +// therefore must not let the absolute read silently win -- its own header said so in those words. +// +// SECTION 13'S RULE IS THAT THE CHAIN WINS: the first segment resolves on the ancestor chain and the +// rest projects. The projection now exists, so the binder answers and this row asserts THAT rather +// than the refusal that stood in for it. What the old claim forbade is still forbidden -- the absolute +// read does not win over a binder the source spelled -- and is now answered correctly instead of +// refused, which is the climb the interim arm was waiting for. +// +// IT IS STILL DISCRIMINATING: an implementation that consulted the absolute candidates first would +// resolve this to the provider's declaring path, and the projection assertion below would fail. +test fn a_binder_shadowing_a_root_segment_projects_from_the_binder_holds() -> Bool { match xl0r_resolved_shadowing_consumer() { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => xl0r_last_reason(d: d) == ^resolve_reason_ambiguous_symbol + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + xl0r_carries_a_field_projection(root: resolved.root) + } +} + +// EVERY PROJECTION NODE IN THE TREE, through the shared reader rather than a shape test of its own. +fn xl0r_carries_a_field_projection(root: Node) -> Bool { + match field_projection_optional(n: root) { + Present { value: _ } => true + Absent => + fold(root.children, init: false, f: fn(found, e) { + found || xl0r_carries_a_field_projection(root: e.target) + }) } } From 5ca3b0ac4da001c29f0207cb3ad8dc88baf14801 Mon Sep 17 00:00:00 2001 From: "gunbai-bot[bot]" <289086189+gunbai-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:25:53 -0400 Subject: [PATCH 27/90] v2 infer: type a match over a declared (generic) coproduct and its arm binders (#12641) * match-arm binder typing: probe of infer's refusal over a generic coproduct scrutinee (WIP) Co-Authored-By: Claude Opus 5.5 (1M context) * v2 infer: type a match over a declared coproduct and its arm binders The index records a generic declaration's type parameters beside the member it binds. infer routes a non-literal match to a coproduct arm that checks variant and field membership, the type-argument arity and exhaustiveness. A binder is typed as the matched variant field's type at the scrutinee's instantiation. An arm body or scrutinee with no derived type leaves the match at the frontier with a located advisory. Co-Authored-By: Claude Opus 5.5 (1M context) * match-binder typing claims over the exact Outcome payload Positive controls: binder typed ParseArtifact from Accepted.value, the match typed by its arms, a binder shadowing a same-named parameter. Discriminating reds: undeclared variant, undeclared field, too many and too few type arguments, missing variant, record scrutinee. Frontiers: the seven's call scrutinee and a constructor arm body are accepted and counted with located advisories. Co-Authored-By: Claude Opus 5.5 (1M context) * infer: only a constructor pattern routes a match to the coproduct family A match whose patterns are neither literals nor constructors kept the literal family's refusal before this change. Routing it to the coproduct family let an untyped scrutinee accept it at the frontier, widening that refusal into an admission. match_infer_fail_open_audit complement_body_real_infer_refuses_unsupported_pattern_holds went red on it and is green again. Co-Authored-By: Claude Opus 5.5 (1M context) --------- Co-authored-by: gunbc-ci-auto-heal Co-authored-by: Claude Opus 5.5 (1M context) --- src/v2/compiler/04_infer.dag | 627 +++++++++++++++++- src/v2/compiler/symbol_index_fill.dag | 27 +- src/v2/std/symbol_index.dag | 50 +- .../match_binder/match_binder_typing_test.dag | 251 +++++++ 4 files changed, 930 insertions(+), 25 deletions(-) create mode 100644 src/v2/test/claim/match_binder/match_binder_typing_test.dag diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index ad6da74dce7..624e1605bf6 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -16,7 +16,7 @@ import v2.std.compilers.target_model { target_model_canonical_operation_member_declared_type } import v2.compiler.resolve { ResolvedTree } -import v2.std.symbol_index { symbol_index_lookup } +import v2.std.symbol_index { symbol_index_declared_type_params_at, symbol_index_lookup } import v2.compiler.inferred_tree { DerivedGrounding, GroundingNotDerived, @@ -58,7 +58,7 @@ import v2.std.optional { optional_absent, optional_present } -import v2.std.qualified_name { declaration_reference_node, declaration_reference_path_optional } +import v2.std.qualified_name { declaration_reference_node, declaration_reference_path_optional, qualified_name_last_segment } import v2.std.node { arrow_signature_order_label, edge_label_of } import v2.std.list_introduction { list_introduction_elements_optional, list_introduction_head_path } import v2.std.arrow_signature { ArrowParameterOrderAbsent, ArrowParameterOrderDeclared, ArrowParameterOrderMalformed, arrow_declared_parameter_order } @@ -152,7 +152,8 @@ import v2.std.node_query { declared_field_named, field_projection_optional, find_named_child, - node_positional_child_targets + node_positional_child_targets, + pattern_wildcard_name } import v2.std.witness { Holds, StructuralPropertyWitness, Violates, Witness, witness_from_optional } @@ -839,7 +840,7 @@ fn infer_node_facts(n: Node, partials: List, kinds: List, descent: Holds { value: descent_proof } ) Absent => - match infer_parameter_type_in_scope(tree: resolved.root, reference: n, binding: binding) { + match infer_binding_type_in_scope(resolved: resolved, reference: n, binding: binding) { Present { value: domain_ty } => inferred_facts_from_derived_type( node: n, @@ -1355,10 +1356,19 @@ fn infer_arrow_domain_type_for_binding(arrow: Node, binding: Symbol) -> Optional // Consumers: infer_node_facts' parameter arm (the grounding every cast, add, application, branch, // match and loop operand reads) and infer_branch_operand_resolved_type_in_tree. // interim: dissolve-on the SymbolIndex / ResolvedTree.bindings lookup, as before. +// +// A MATCH ARM IS A BINDING SCOPE TOO, AND IT SHADOWS. On the way up, an arm whose pattern binds the +// reference answers ArmBinder (which variant field), and the match above it records its scrutinee +// (ArmBound); neither is ever widened to an enclosing Arrow's parameter of the same name, which the +// walk before this did -- a binder spelled like a parameter took the parameter's type. The type itself +// needs the index, so only infer_binding_type_in_scope resolves ArmBound; the tree-only reader answers +// Absent for it and its consumers fall through to the reference's own facts. type InferParameterScopeSearch = ParameterReferenceNotHere | ParameterReferenceUnbound | ParameterReferenceBound { declared: Node } + | ParameterReferenceArmBinder { tag: Symbol, field: Symbol } + | ParameterReferenceArmBound { scrutinee: Node, tag: Symbol, field: Symbol } fn infer_parameter_scope_search(root: Node, reference: Node, binding: Symbol) -> InferParameterScopeSearch { if root == reference { @@ -1373,8 +1383,22 @@ fn infer_parameter_scope_search(root: Node, reference: Node, binding: Symbol) -> ParameterReferenceUnbound => match infer_arrow_domain_type_for_binding(arrow: root, binding: binding) { Present { value: ty } => ParameterReferenceBound { declared: ty } - Absent => ParameterReferenceUnbound + Absent => + match infer_arm_pattern_binder_field(arm: root, binding: binding) { + Present { value: bf } => ParameterReferenceArmBinder { tag: bf.tag, field: bf.field } + Absent => ParameterReferenceUnbound + } } + ParameterReferenceArmBinder { tag: tag, field: field } => + match root.kind { + ComputationNode { behavior: Match } => + match list_at_optional(xs: node_positional_child_targets(node: root), index: 0) { + Present { value: scrutinee } => ParameterReferenceArmBound { scrutinee: scrutinee, tag: tag, field: field } + Absent => ParameterReferenceUnbound + } + _ => ParameterReferenceArmBinder { tag: tag, field: field } + } + ParameterReferenceArmBound { scrutinee: sc, tag: tag, field: field } => ParameterReferenceArmBound { scrutinee: sc, tag: tag, field: field } ParameterReferenceBound { declared: ty } => ParameterReferenceBound { declared: ty } ParameterReferenceNotHere => ParameterReferenceNotHere } @@ -1386,6 +1410,8 @@ fn infer_parameter_type_in_scope(tree: Node, reference: Node, binding: Symbol) - ParameterReferenceBound { declared: ty } => Present { value: ty } ParameterReferenceUnbound => Absent ParameterReferenceNotHere => Absent + ParameterReferenceArmBinder { tag: _, field: _ } => Absent + ParameterReferenceArmBound { scrutinee: _, tag: _, field: _ } => Absent } } @@ -1845,6 +1871,591 @@ fn infer_match_bool( } } +// A MATCH OVER A DECLARED COPRODUCT. Which arm family a match takes is decided by its PATTERNS, not +// by whether its scrutinee's type happened to derive: an arm pattern that is a Bool or Int literal is +// the literal family (infer_match_bool, unchanged), and a match with a constructor pattern is +// eliminating a declared coproduct. Deciding by the scrutinee's type instead would route a coproduct match whose scrutinee +// is not yet typed into the literal family, which refuses it as infer_match_scrutinee_not_bool -- a +// verdict about the wrong question. +fn infer_match_arm_pattern_is_literal(arm: Node) -> Bool { + match find_named_child(root: arm, name: match_arm_pattern) { + Rejected { diagnostics: _ } => false + Accepted { value: pat, diagnostics: _ } => + match dag_canonical_literal_from_node(node: pat) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } +} + +// A match reaches the coproduct family only when some arm IS a constructor pattern. A match whose +// patterns are neither literals nor constructors -- bare atoms naming nothing, say -- keeps the literal +// family's refusal (infer_match_pattern_not_bool_literal): routing it here instead would meet an +// untyped scrutinee and accept it at the frontier, widening a refusal into an admission +// (v2.test.claim.manual.match_infer_fail_open_audit complement_body_real_infer_refuses_unsupported_pattern_holds). +fn infer_match_arm_pattern_is_constructor(arm: Node) -> Bool { + match find_named_child(root: arm, name: match_arm_pattern) { + Rejected { diagnostics: _ } => false + Accepted { value: pat, diagnostics: _ } => + match infer_coproduct_arm_pattern(pat: pat) { + ArmPatternVariant { tag: _, fields: _ } => true + ArmPatternWildcard => false + ArmPatternUnread => false + } + } +} + +fn infer_match( + node: Node, + partials: List, + entries: List, + resolved: ResolvedTree +) -> Outcome { + let positional_targets = node_positional_child_targets(node: node) + match list_tail(xs: positional_targets) { + TailAbsent => outcome_rejected(infer_match_shape_invalid_diagnostic(node: node)) + TailFound { tail: arms } => + if any(xs: arms, predicate: fn(arm) { infer_match_arm_pattern_is_literal(arm: arm) }) { + infer_match_bool(node: node, partials: partials, entries: entries, tree: resolved.root) + } else if any(xs: arms, predicate: fn(arm) { infer_match_arm_pattern_is_constructor(arm: arm) }) { + infer_match_coproduct(node: node, partials: partials, entries: entries, resolved: resolved) + } else { + infer_match_bool(node: node, partials: partials, entries: entries, tree: resolved.root) + } + } +} + +// THE COPRODUCT THE SCRUTINEE'S TYPE DENOTES, INSTANTIATED. `Outcome` is an +// Instantiation whose head is a declaration reference and whose remaining positional children are the +// type arguments; a bare reference is the same with no arguments. The head's declaring path is asked +// of the index through the guarded read (symbol_index_lookup), exactly as a field projection's +// receiver is (infer_projection_receiver), and the declaration's type parameters are the index's +// record of the binders the member cannot carry (symbol_index_declared_type_params_at). Each argument +// instantiates the parameter at its position, so a variant field declared `value: T` reads as the +// argument itself. A declared coproduct is a Disj; anything else -- a record, a kernel type, a path the +// index does not hold -- declares no variants to eliminate, and a count of arguments different from the +// count of parameters binds no instance at all. +type InferMatchCoproduct { + variants: Node + instances: List +} + +type InferMatchCoproductRead + = CoproductRead { coproduct: InferMatchCoproduct } + | CoproductNotDeclared + | CoproductArityMismatch + +type InferTypeHeadArgs { + head: Node + args: List +} + +fn infer_type_head_and_args(t: Node) -> InferTypeHeadArgs { + match t.kind { + TypeNode { connective: Instantiation } => + let targets = node_positional_child_targets(node: t) + match list_at_optional(xs: targets, index: 0) { + Absent => InferTypeHeadArgs { head: t, args: Empty } + Present { value: head } => + match list_tail(xs: targets) { + TailFound { tail: args } => InferTypeHeadArgs { head: head, args: args } + TailAbsent => InferTypeHeadArgs { head: head, args: Empty } + } + } + _ => InferTypeHeadArgs { head: t, args: Empty } + } +} + +fn infer_match_coproduct_of_type(scrutinee_type: Node, resolved: ResolvedTree) -> InferMatchCoproductRead { + let head_args = infer_type_head_and_args(t: scrutinee_type) + match declaration_reference_path_optional(node: head_args.head) { + Absent => CoproductNotDeclared + Present { value: path } => + match symbol_index_lookup(index: resolved.symbol_index, qualified_path: path) { + Absent => CoproductNotDeclared + Present { value: declared } => + match declared.kind { + TypeNode { connective: Disj } => + let params = symbol_index_declared_type_params_at(index: resolved.symbol_index, qualified_path: path) + if length(xs: params) != length(xs: head_args.args) { + CoproductArityMismatch + } else { + CoproductRead { + coproduct: InferMatchCoproduct { + variants: declared, + instances: zip_map(a: params, b: head_args.args, f: fn(p, arg) { + TypeVariableInstance { binder: p, instance: arg } + }) + } + } + } + _ => CoproductNotDeclared + } + } + } +} + +fn infer_match_variant_payload(variants: Node, tag: Symbol) -> Optional { + fold(variants.children, init: Absent, f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => infer_conj_edge_named_type_for_binding(edge: e, binding: tag) + } + }) +} + +fn infer_match_variant_tags(variants: Node) -> List { + fold(variants.children, init: Empty, f: fn(acc, e) { + match e.label { + Named { name: tag } => list_snoc_item(xs: acc, item: tag) + Positional => acc + } + }) +} + +// A VARIANT FIELD'S TYPE AT THIS INSTANTIATION, OR NOTHING. A field declared as one of the +// declaration's type parameters is that parameter's argument -- a type the USE SITE resolved. A field +// declared as a kernel type is that type's denotation, the same join infer_arrow_declared_return_type +// reads. Any other field type is a reference authored in the DECLARATION'S scope, which the index +// holds as written and this stage does not re-resolve (infer_declaration_reference_facts states the +// same boundary for signatures), so it is not derived here rather than minted as an unresolved atom a +// consumer would read as a type. +fn infer_match_field_type_instantiated(field_type: Node, instances: List) -> Optional { + match field_type.kind { + TypeNode { connective: Atom { identity: id } } => + if count(field_type.children) == 0 { + match type_variable_instance_lookup(instances: instances, binder: id) { + Present { value: instance } => optional_present(value: instance) + Absent => dag_binding_denotation(sym: id) + } + } else { + optional_absent() + } + _ => Absent + } +} + +// AN ARM PATTERN, READ. A resolved constructor pattern is a Conj whose FIRST edge is labelled by the +// variant's tag (its target is the tag's resolved reference) and whose remaining edges are the fields +// it names; a nullary variant written bare is a declaration reference alone; `_` is the wildcard. +// Nested and literal field patterns are not read here: they narrow what an arm covers, and an arm this +// stage cannot account for refuses rather than being counted as covering its variant. +type InferCoproductArmPattern + = ArmPatternVariant { tag: Symbol, fields: List } + | ArmPatternWildcard + | ArmPatternUnread + +fn infer_coproduct_arm_pattern(pat: Node) -> InferCoproductArmPattern { + match pat.kind { + TypeNode { connective: Atom { identity: id } } => + if id == pattern_wildcard_name() { ArmPatternWildcard } else { ArmPatternUnread } + _ => + match declaration_reference_path_optional(node: pat) { + Present { value: path } => + match qualified_name_last_segment(qn: path) { + Present { value: tag } => ArmPatternVariant { tag: tag, fields: Empty } + Absent => ArmPatternUnread + } + Absent => + match pat.kind { + TypeNode { connective: Conj } => + match list_at_optional(xs: pat.children, index: 0) { + Absent => ArmPatternUnread + Present { value: tag_edge } => + match tag_edge.label { + Named { name: tag } => + match list_tail(xs: pat.children) { + TailFound { tail: fields } => ArmPatternVariant { tag: tag, fields: fields } + TailAbsent => ArmPatternVariant { tag: tag, fields: Empty } + } + Positional => ArmPatternUnread + } + } + _ => ArmPatternUnread + } + } + } +} + +fn infer_field_pattern_binds(target: Node) -> Optional { + match target.kind { + TypeNode { connective: Atom { identity: id } } => + if (count(target.children) == 0) && (id != pattern_wildcard_name()) { optional_present(value: id) } else { optional_absent() } + _ => Absent + } +} + +fn infer_field_pattern_is_wildcard(target: Node) -> Bool { + match target.kind { + TypeNode { connective: Atom { identity: id } } => id == pattern_wildcard_name() + _ => false + } +} + +fn infer_match_pattern_variant_not_declared_diagnostic(pattern: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_match_pattern_variant_not_declared, + at: node_locus(node: pattern), + correction: Unavailable { reason: UserInputBoundary } + } +} + +fn infer_match_pattern_field_not_declared_diagnostic(pattern: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_match_pattern_field_not_declared, + at: node_locus(node: pattern), + correction: Unavailable { reason: UserInputBoundary } + } +} + +fn infer_match_pattern_unread_diagnostic(pattern: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_match_pattern_unread, + at: node_locus(node: pattern), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +fn infer_match_scrutinee_not_coproduct_diagnostic(node: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_match_scrutinee_not_coproduct, + at: node_locus(node: node), + correction: Unavailable { reason: UserInputBoundary } + } +} + +fn infer_match_type_argument_arity_mismatch_diagnostic(node: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_match_type_argument_arity_mismatch, + at: node_locus(node: node), + correction: Unavailable { reason: UserInputBoundary } + } +} + +// THE TWO FRONTIER ADVISORIES. Each rides the Accepted path as a located diagnostic, one per site, so +// a match left untyped is counted where it stands and never reads as a typed result: the facts it +// carries are GroundingNotDerived, which every consumer that demands a type refuses +// (infer_grounding_demanded_not_derived). +fn infer_match_scrutinee_type_underived_diagnostic(node: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_match_scrutinee_type_underived, + at: node_locus(node: node), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +fn infer_match_arm_body_type_underived_diagnostic(body: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_match_arm_body_type_underived, + at: node_locus(node: body), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +// ONE ARM, CHECKED AGAINST THE DECLARATION. The variant must be one the coproduct declares and every +// field the pattern names must be one that variant declares; each is a fact about the program, so each +// refuses. The arm's coverage is its variant's tag, or every variant for a wildcard. +type InferCoproductArmRow { + covers: InferCoproductArmCoverage + body: Node +} + +type InferCoproductArmCoverage + = CoversVariant { tag: Symbol } + | CoversAll + +fn infer_coproduct_arm_fields_check(pat: Node, payload: Node, fields: List) -> Outcome { + fold(fields, init: Accepted { value: true, diagnostics: None }, f: fn(acc, e) { + match acc { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: _, diagnostics: _ } => + match e.label { + Positional => outcome_rejected(infer_match_pattern_unread_diagnostic(pattern: pat)) + Named { name: field } => + match infer_conj_named_type_for_binding(domain: payload, binding: field) { + Absent => outcome_rejected(infer_match_pattern_field_not_declared_diagnostic(pattern: pat)) + Present { value: _ } => + match infer_field_pattern_binds(target: e.target) { + Present { value: _ } => acc + Absent => + if infer_field_pattern_is_wildcard(target: e.target) { + acc + } else { + outcome_rejected(infer_match_pattern_unread_diagnostic(pattern: e.target)) + } + } + } + } + } + }) +} + +fn infer_coproduct_arm_row(arm: Node, coproduct: InferMatchCoproduct) -> Outcome { + match find_named_child(root: arm, name: match_arm_pattern) { + Rejected { diagnostics: _ } => outcome_rejected(infer_match_shape_invalid_diagnostic(node: arm)) + Accepted { value: pat, diagnostics: _ } => + match find_named_child(root: arm, name: match_arm_body) { + Rejected { diagnostics: _ } => outcome_rejected(infer_match_shape_invalid_diagnostic(node: arm)) + Accepted { value: body, diagnostics: _ } => + match infer_coproduct_arm_pattern(pat: pat) { + ArmPatternUnread => outcome_rejected(infer_match_pattern_unread_diagnostic(pattern: pat)) + ArmPatternWildcard => + outcome_accepted(value: InferCoproductArmRow { covers: CoversAll, body: body }) + ArmPatternVariant { tag: tag, fields: fields } => + match infer_match_variant_payload(variants: coproduct.variants, tag: tag) { + Absent => outcome_rejected(infer_match_pattern_variant_not_declared_diagnostic(pattern: pat)) + Present { value: payload } => + bind_outcome( + o: infer_coproduct_arm_fields_check(pat: pat, payload: payload, fields: fields), + f: fn(_checked) { + outcome_accepted(value: InferCoproductArmRow { covers: CoversVariant { tag: tag }, body: body }) + } + ) + } + } + } + } +} + +fn infer_coproduct_rows_cover(rows: List, tag: Symbol) -> Bool { + any(xs: rows, predicate: fn(row) { + match row.covers { + CoversAll => true + CoversVariant { tag: t } => t == tag + } + }) +} + +// THE ARM BODIES' ONE TYPE. Two DERIVED body types that differ are a mismatch and refuse; a body whose +// type is not derived leaves the match at the frontier with one located advisory per such body, and +// the match's own facts are GroundingNotDerived rather than the type of the arms that did derive -- a +// match typed from a subset of its arms would be a claim about the arms nobody checked. +type InferArmBodyTypes { + derived: Optional + mismatch: Bool + underived: Diagnostics +} + +fn infer_coproduct_arm_body_types( + rows: List, + entries: List, + tree: Node +) -> InferArmBodyTypes { + fold(rows, init: InferArmBodyTypes { derived: Absent, mismatch: false, underived: None }, f: fn(acc, row) { + let body_type = match lookup_inferred_facts_in_entries(entries: entries, key: row.body) { + Absent => Absent + Present { value: facts } => + match infer_branch_operand_resolved_type_in_tree(node: row.body, facts: facts, tree: tree) { + Holds { value: t } => Present { value: t } + Violates { diagnostic: _ } => Absent + } + } + match body_type { + Absent => + InferArmBodyTypes { + derived: acc.derived, + mismatch: acc.mismatch, + underived: diagnostics_merge( + outer: acc.underived, + inner: Some { diagnostics: diagnostics_singleton(d: infer_match_arm_body_type_underived_diagnostic(body: row.body)) } + ) + } + Present { value: t } => + match acc.derived { + Absent => InferArmBodyTypes { derived: Present { value: t }, mismatch: acc.mismatch, underived: acc.underived } + Present { value: held } => + InferArmBodyTypes { + derived: acc.derived, + mismatch: acc.mismatch || !infer_type_equal_ignoring_provenance(a: held, b: t), + underived: acc.underived + } + } + } + }) +} + +fn infer_match_frontier(node: Node, cd: Diagnostics, advisories: Diagnostics) -> Outcome { + match infer_descent_witness_for_node(n: node) { + Violates { diagnostic: d } => Rejected { diagnostics: diagnostics_singleton(d: d) } + Holds { value: descent_proof } => + bind_outcome( + o: inferred_facts_not_derived(node: node, descent: Holds { value: descent_proof }), + f: fn(facts) { + Accepted { value: facts, diagnostics: diagnostics_merge(outer: cd, inner: advisories) } + } + ) + } +} + +fn infer_match_coproduct_rows( + node: Node, + arms: List, + coproduct: InferMatchCoproduct, + entries: List, + tree: Node, + cd: Diagnostics +) -> Outcome { + match fold(arms, init: Accepted { value: Empty, diagnostics: None }, f: fn(acc, arm) { + match acc { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: rows, diagnostics: d } => + match infer_coproduct_arm_row(arm: arm, coproduct: coproduct) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: row, diagnostics: _ } => Accepted { value: list_snoc_item(xs: rows, item: row), diagnostics: d } + } + } + }) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: rows, diagnostics: _ } => + if any(xs: infer_match_variant_tags(variants: coproduct.variants), predicate: fn(tag) { + !infer_coproduct_rows_cover(rows: rows, tag: tag) + }) { + outcome_rejected(infer_match_non_exhaustive_diagnostic(node: node)) + } else { + let bodies = infer_coproduct_arm_body_types(rows: rows, entries: entries, tree: tree) + if bodies.mismatch { + outcome_rejected(infer_match_arm_type_mismatch_diagnostic(node: node)) + } else { + match bodies.underived { + Some { diagnostics: _ } => infer_match_frontier(node: node, cd: cd, advisories: bodies.underived) + None => + match bodies.derived { + Absent => infer_match_frontier(node: node, cd: cd, advisories: None) + Present { value: t } => + match infer_descent_witness_for_node(n: node) { + Violates { diagnostic: d } => Rejected { diagnostics: diagnostics_singleton(d: d) } + Holds { value: descent_proof } => + bind_outcome( + o: inferred_facts_from_derived_type(node: node, derived_type: t, descent: Holds { value: descent_proof }), + f: fn(facts) { Accepted { value: facts, diagnostics: cd } } + ) + } + } + } + } + } + } +} + +fn infer_match_coproduct( + node: Node, + partials: List, + entries: List, + resolved: ResolvedTree +) -> Outcome { + let positional_targets = node_positional_child_targets(node: node) + if !all_edges_positional(children: node.children) { + outcome_rejected(infer_match_shape_invalid_diagnostic(node: node)) + } else { + match list_at_optional(xs: positional_targets, index: 0) { + Absent => outcome_rejected(infer_match_shape_invalid_diagnostic(node: node)) + Present { value: scrutinee_target } => + match list_tail(xs: positional_targets) { + TailAbsent => outcome_rejected(infer_match_non_exhaustive_diagnostic(node: node)) + TailFound { tail: arms } => + match infer_bounded_lattice_consumer_gate(consumer: node, partials: partials) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: _, diagnostics: cd } => + match lookup_inferred_facts_in_entries(entries: entries, key: scrutinee_target) { + Absent => outcome_rejected(infer_facts_lookup_miss_diagnostic(key: scrutinee_target)) + Present { value: scrutinee_facts } => + match infer_branch_operand_resolved_type_in_tree(node: scrutinee_target, facts: scrutinee_facts, tree: resolved.root) { + Violates { diagnostic: _ } => + infer_match_frontier( + node: node, + cd: cd, + advisories: Some { diagnostics: diagnostics_singleton(d: infer_match_scrutinee_type_underived_diagnostic(node: scrutinee_target)) } + ) + Holds { value: scrutinee_type } => + match infer_match_coproduct_of_type(scrutinee_type: scrutinee_type, resolved: resolved) { + CoproductNotDeclared => outcome_rejected(infer_match_scrutinee_not_coproduct_diagnostic(node: scrutinee_target)) + CoproductArityMismatch => outcome_rejected(infer_match_type_argument_arity_mismatch_diagnostic(node: scrutinee_target)) + CoproductRead { coproduct: coproduct } => + infer_match_coproduct_rows(node: node, arms: arms, coproduct: coproduct, entries: entries, tree: resolved.root, cd: cd) + } + } + } + } + } + } + } +} + +// A MATCH-ARM BINDER'S TYPE: the matched variant's field type at the scrutinee's instantiation. The +// scope walk (infer_parameter_scope_search) reports which arm binds the reference and from which +// variant field; this reads the scrutinee's type by the same route a parameter's is read -- so a +// binder over a parameter, or over another binder, is typed, and an application scrutinee is not yet +// (its declared return is authored in the callee's scope; that is the frontier the match's own row +// reports as infer_match_scrutinee_type_underived). +fn infer_binding_type_in_scope(resolved: ResolvedTree, reference: Node, binding: Symbol) -> Optional { + match infer_parameter_scope_search(root: resolved.root, reference: reference, binding: binding) { + ParameterReferenceBound { declared: ty } => Present { value: ty } + ParameterReferenceUnbound => Absent + ParameterReferenceNotHere => Absent + ParameterReferenceArmBinder { tag: _, field: _ } => Absent + ParameterReferenceArmBound { scrutinee: scrutinee, tag: tag, field: field } => + match infer_atom_binding_sym(node: scrutinee) { + Absent => Absent + Present { value: scrutinee_binding } => + match infer_binding_type_in_scope(resolved: resolved, reference: scrutinee, binding: scrutinee_binding) { + Absent => Absent + Present { value: scrutinee_type } => + match infer_match_coproduct_of_type(scrutinee_type: scrutinee_type, resolved: resolved) { + CoproductNotDeclared => Absent + CoproductArityMismatch => Absent + CoproductRead { coproduct: coproduct } => + match infer_match_variant_payload(variants: coproduct.variants, tag: tag) { + Absent => Absent + Present { value: payload } => + match infer_conj_named_type_for_binding(domain: payload, binding: field) { + Absent => Absent + Present { value: field_type } => + infer_match_field_type_instantiated(field_type: field_type, instances: coproduct.instances) + } + } + } + } + } + } +} + +// Which field of which variant an arm's pattern binds `binding` from, if it does. +fn infer_arm_pattern_binder_field(arm: Node, binding: Symbol) -> Optional { + match arm.kind { + TypeNode { connective: Conj } => + match find_named_child(root: arm, name: match_arm_pattern) { + Rejected { diagnostics: _ } => Absent + Accepted { value: pat, diagnostics: _ } => + match infer_coproduct_arm_pattern(pat: pat) { + ArmPatternVariant { tag: tag, fields: fields } => + fold(fields, init: Absent, f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => + match e.label { + Positional => Absent + Named { name: field } => + match infer_field_pattern_binds(target: e.target) { + Present { value: bound } => + if bound == binding { optional_present(value: InferArmBinderField { tag: tag, field: field }) } else { optional_absent() } + Absent => Absent + } + } + } + }) + ArmPatternWildcard => Absent + ArmPatternUnread => Absent + } + } + _ => Absent + } +} + +type InferArmBinderField { + tag: Symbol + field: Symbol +} + // FLAG C (body-lowering design, operator-signed): the first cut types carrier-invariant loops only // — every iteration-fold child must resolve to ONE carrier type τ, so the inferred tree states // 'body : τ → τ, repeated under the measure'. A refinement-shaped loop (carrier narrows across @@ -2599,9 +3210,9 @@ fn infer_gather_match_row_on_entries( entries: List, pending: Diagnostics, partials: List, - tree: Node, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { - match infer_match_bool(node: node, partials: partials, entries: entries, tree: tree) { + match infer_match(node: node, partials: partials, entries: entries, resolved: resolved) { Rejected { diagnostics: r } => infer_gather_fold_acc_failed( node: node, @@ -3636,7 +4247,7 @@ fn infer_gather_fold_step_merged( entries: merged_entries, pending: merged_pending, partials: partials, - tree: resolved.root + resolved: resolved ) } else if acc.await_loop_row && children_remaining == 0 { infer_gather_loop_row_on_entries( diff --git a/src/v2/compiler/symbol_index_fill.dag b/src/v2/compiler/symbol_index_fill.dag index a77a721765e..3613323f7cb 100644 --- a/src/v2/compiler/symbol_index_fill.dag +++ b/src/v2/compiler/symbol_index_fill.dag @@ -42,7 +42,8 @@ import v2.std.symbol_index { symbol_index_declaring_path_of, symbol_index_insert, symbol_index_lookup, - symbol_index_mark_declared_payload + symbol_index_mark_declared_payload, + symbol_index_mark_declared_type_params } import v2.std.namespace_alias { ModuleBindingSource, PendingBinding } import v2.extdeps.languages.dag { @@ -52,7 +53,7 @@ import v2.extdeps.languages.dag { qualified_name_from_module_node } import v2.compiler.parse { parse_tree_projection_edge } -import v2.std.type_binder { GenericOpaqueTypeDecl, GenericTypeAlias, GenericTypeDecl, PlainTypeDecl, type_decl_view } +import v2.std.type_binder { GenericOpaqueTypeDecl, GenericTypeAlias, GenericTypeDecl, PlainTypeDecl, type_decl_view, type_param_names } data symbol_index_fill_extdeps_coupling_disposition: Disposition = Scaffold { dissolves_to: SingleAuthority, @@ -84,17 +85,21 @@ data symbol_index_containment_disposition: Disposition = Scaffold { // declarations of the alias, and a binder is never a declared identity. // The node a declared name binds to, and the member the index descends into when the declaration // declares members -- one dispatch on the view for both. +// type_params: a GENERIC declaration binds its member, which cannot carry its binders, so they ride +// beside it (v2.std.symbol_index symbol_index_mark_declared_type_params). Empty for every other arm: +// an alias or bodyless declaration binds its wrapper, which still carries them. type SymbolIndexDeclared { bound: Node members: Optional + type_params: List } fn symbol_index_declared(target: Node) -> SymbolIndexDeclared { match type_decl_view(target: target) { - GenericTypeDecl { binders: _, member: m } => SymbolIndexDeclared { bound: m, members: optional_present(value: m) } - GenericTypeAlias { binders: _, aliased: _ } => SymbolIndexDeclared { bound: target, members: optional_absent() } - GenericOpaqueTypeDecl { binders: _ } => SymbolIndexDeclared { bound: target, members: optional_absent() } - PlainTypeDecl { member: m } => SymbolIndexDeclared { bound: m, members: optional_present(value: m) } + GenericTypeDecl { binders: _, member: m } => SymbolIndexDeclared { bound: m, members: optional_present(value: m), type_params: type_param_names(n: target) } + GenericTypeAlias { binders: _, aliased: _ } => SymbolIndexDeclared { bound: target, members: optional_absent(), type_params: Empty } + GenericOpaqueTypeDecl { binders: _ } => SymbolIndexDeclared { bound: target, members: optional_absent(), type_params: Empty } + PlainTypeDecl { member: m } => SymbolIndexDeclared { bound: m, members: optional_present(value: m), type_params: Empty } } } @@ -120,10 +125,14 @@ fn symbol_index_fill_containment_edge( } else { let child_path = qualified_name_snoc(qn: path, segment: sym) let declared = symbol_index_declared(target: edge.target) - let with_binding = symbol_index_insert( - index: index, + let with_binding = symbol_index_mark_declared_type_params( + index: symbol_index_insert( + index: index, + qualified_path: child_path, + resolved: declared.bound + ), qualified_path: child_path, - resolved: declared.bound + params: declared.type_params ) match declared.members { Present { value: member } => diff --git a/src/v2/std/symbol_index.dag b/src/v2/std/symbol_index.dag index 8521c50b076..a286495fdfa 100644 --- a/src/v2/std/symbol_index.dag +++ b/src/v2/std/symbol_index.dag @@ -84,6 +84,7 @@ type SymbolIndex { global_bare: Map bound_declarings: Map> declared_payloads: Map + declared_type_params: Map> } // WHICH KIND OF DECLARED PAYLOAD A PATH DENOTES, stored as the map's value so the map cannot hold a @@ -101,7 +102,8 @@ fn empty_symbol_index() -> SymbolIndex { entries: empty_map(), global_bare: empty_map(), bound_declarings: empty_map(), - declared_payloads: empty_map() + declared_payloads: empty_map(), + declared_type_params: empty_map() } } @@ -176,7 +178,8 @@ fn symbol_index_track_global_bare( value: GlobalBareAmbiguous { paths: Cons { head: qualified_path, tail: paths } } ), bound_declarings: index.bound_declarings, - declared_payloads: index.declared_payloads + declared_payloads: index.declared_payloads, + declared_type_params: index.declared_type_params } Present { value: GlobalBareUnique { node: existing, path: existing_path } } => if existing_path == qualified_path && existing == resolved { @@ -192,7 +195,8 @@ fn symbol_index_track_global_bare( } ), bound_declarings: index.bound_declarings, - declared_payloads: index.declared_payloads + declared_payloads: index.declared_payloads, + declared_type_params: index.declared_type_params } } Absent => @@ -204,7 +208,8 @@ fn symbol_index_track_global_bare( value: GlobalBareUnique { node: resolved, path: qualified_path } ), bound_declarings: index.bound_declarings, - declared_payloads: index.declared_payloads + declared_payloads: index.declared_payloads, + declared_type_params: index.declared_type_params } } Rejected { diagnostics: _ } => index @@ -221,7 +226,8 @@ fn symbol_index_insert(index: SymbolIndex, qualified_path: QualifiedName, resolv ), global_bare: index.global_bare, bound_declarings: index.bound_declarings, - declared_payloads: index.declared_payloads + declared_payloads: index.declared_payloads, + declared_type_params: index.declared_type_params } symbol_index_track_global_bare( index: with_entry, @@ -247,7 +253,8 @@ fn symbol_index_mark_declared_payload(index: SymbolIndex, qualified_path: Qualif entries: index.entries, global_bare: index.global_bare, bound_declarings: index.bound_declarings, - declared_payloads: map_insert(m: index.declared_payloads, key: qualified_path, value: kind) + declared_payloads: map_insert(m: index.declared_payloads, key: qualified_path, value: kind), + declared_type_params: index.declared_type_params } } @@ -261,6 +268,31 @@ fn symbol_index_declared_payload_at(index: SymbolIndex, qualified_path: Qualifie map_lookup(m: index.declared_payloads, key: qualified_path) } +// A GENERIC DECLARATION'S TYPE PARAMETERS, IN DECLARATION ORDER, KEYED BY ITS DECLARING PATH. The +// entry a generic declaration binds is its MEMBER (v2.compiler.symbol_index_fill +// symbol_index_declared: no reader of a member meets a binder), so the member alone cannot say which +// of its field types are type variables or in what order an instantiation's arguments bind them. That +// fact is v2.std.type_binder type_decl_view's GenericTypeDecl binders, and the fill arm that already +// dispatches on that view records it here. Consumer: v2.compiler.infer's coproduct match, which +// instantiates a matched variant's field types at the scrutinee's type arguments. A path with no row +// declares no parameters; a non-generic declaration writes none. +fn symbol_index_mark_declared_type_params(index: SymbolIndex, qualified_path: QualifiedName, params: List) -> SymbolIndex { + SymbolIndex { + entries: index.entries, + global_bare: index.global_bare, + bound_declarings: index.bound_declarings, + declared_payloads: index.declared_payloads, + declared_type_params: map_insert(m: index.declared_type_params, key: qualified_path, value: params) + } +} + +fn symbol_index_declared_type_params_at(index: SymbolIndex, qualified_path: QualifiedName) -> List { + match map_lookup(m: index.declared_type_params, key: qualified_path) { + Present { value: params } => params + Absent => Empty + } +} + fn symbol_index_global_unique_lookup(index: SymbolIndex, name: Symbol) -> GlobalBareLookup { match v2.std.collection.map_get(index.global_bare, name) { Accepted { value: opt, diagnostics: _ } => match opt { @@ -366,14 +398,16 @@ fn symbol_index_bind_at( key: binding_path, value: list_snoc_item(xs: prior, item: declaring_path) ), - declared_payloads: index.declared_payloads + declared_payloads: index.declared_payloads, + declared_type_params: index.declared_type_params } if is_empty(xs: prior) { SymbolIndex { entries: map_insert(m: recorded.entries, key: binding_path, value: resolved), global_bare: recorded.global_bare, bound_declarings: recorded.bound_declarings, - declared_payloads: recorded.declared_payloads + declared_payloads: recorded.declared_payloads, + declared_type_params: recorded.declared_type_params } } else { recorded diff --git a/src/v2/test/claim/match_binder/match_binder_typing_test.dag b/src/v2/test/claim/match_binder/match_binder_typing_test.dag new file mode 100644 index 00000000000..d028a3feb76 --- /dev/null +++ b/src/v2/test/claim/match_binder/match_binder_typing_test.dag @@ -0,0 +1,251 @@ +module v2.test.claim.match_binder.match_binder_typing + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { infer, inferred_facts_resolved_type } +import v2.std.qualified_name { declaration_reference_path_optional, qualified_name_last_segment } +import v2.std.witness { Holds, Violates } +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.extdeps.languages.dag { dag_language_model } +import extdeps.communication.medium { Lossless, Medium } +import std.algebra { Cons, Empty } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { Accepted, NonEmptyDiagnostics, None, Outcome, Rejected, Some } +import v2.std.algebra { contains, list_map } +import v2.std.node { Atom, ComputationNode, Match, Node, Symbol, TypeNode } +import v2.std.integer { Int } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.text { String } + +data mbp_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^match_binder_probe_artifact, + file_path: "src/v2/pilot/match_binder_probe_pilot.dag" +} + +fn mbp_assemble(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: mbp_artifact, + compilation_unit: ^match_binder_probe_cu, + source_root: V2Tree + }, + tail: Empty + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +fn mbp_reasons(r: NonEmptyDiagnostics) -> List { + Cons { head: r.head.reason, tail: list_map(xs: r.tail, f: fn(d) { d.reason }) } +} + +fn mbp_last(xs: List) -> Symbol { + fold(xs, init: ^mbp_no_reason, f: fn(acc, x) { x }) +} + +fn mbp_find_match(n: Node) -> Optional { + match n.kind { + ComputationNode { behavior: Match } => Present { value: n } + _ => fold(n.children, init: Absent, f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => mbp_find_match(n: e.target) + } + }) + } +} + +fn mbp_find_atom(n: Node, id: Symbol) -> Optional { + match n.kind { + TypeNode { connective: Atom { identity: s } } => if s == id { optional_present(value: n) } else { optional_absent() } + _ => fold(n.children, init: Absent, f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => mbp_find_atom(n: e.target, id: id) + } + }) + } +} + +fn mbp_type_name(t: Node) -> Symbol { + match declaration_reference_path_optional(node: t) { + Present { value: path } => + match qualified_name_last_segment(qn: path) { + Present { value: s } => s + Absent => ^mbp_empty_path + } + Absent => + match t.kind { + TypeNode { connective: Atom { identity: s } } => s + _ => ^mbp_type_not_named + } + } +} + +fn mbp_fact_of(src: String, pick: fn(Node) -> Optional) -> Symbol { + match mbp_assemble(src: src) { + Rejected { diagnostics: r } => r.head.reason + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: r } => mbp_last(xs: mbp_reasons(r: r)) + Accepted { value: inferred, diagnostics: _ } => + match pick(resolved.root) { + Absent => ^mbp_subject_absent + Present { value: subject } => + match inferred.facts.lookup(subject) { + Absent => ^mbp_no_facts + Present { value: facts } => + match inferred_facts_resolved_type(facts: facts) { + Violates { diagnostic: _ } => ^mbp_underived + Holds { value: t } => mbp_type_name(t: t) + } + } + } + } + } +} + +fn mbp_all_reasons(src: String) -> List { + match mbp_assemble(src: src) { + Rejected { diagnostics: r } => mbp_reasons(r: r) + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: r } => mbp_reasons(r: r) + Accepted { value: _, diagnostics: d } => + match d { + None => [^infers] + Some { diagnostics: r } => Cons { head: ^infers, tail: mbp_reasons(r: r) } + } + } + } +} + +fn mbp_reports(src: String, reason: Symbol) -> Bool { + contains(xs: mbp_all_reasons(src: src), item: reason, eq: fn(a, b) { a == b }) +} + +fn mbp_infers(src: String) -> Bool { + match mbp_all_reasons(src: src) { + Cons { head: h, tail: _ } => h == ^infers + Empty => false + } +} + +// THE SEVEN'S PAYLOAD, DECLARED AS THE CORPUS DECLARES IT. The fixture assembles one module, so it +// cannot import v2.std.diagnostic; it restates Outcome with the same variants, fields and binder, +// and a ParseArtifact record whose fields carry v2.compiler.parse ParseArtifact's names. What the +// claims below exercise is exactly the operator's subject: a variant field of a GENERIC coproduct +// instantiated at a concrete RECORD type, bound by an arm pattern and projected in the arm body. +data mbt_decls: String = "module p\n\ntype ParseTree {\n root: Int\n}\n\ntype ParseArtifact {\n tree: ParseTree\n span_index: Int\n alloc: Int\n}\n\ntype NonEmptyDiagnostics {\n head: Int\n}\n\ntype Diagnostics\n = None\n | Some { diagnostics: NonEmptyDiagnostics }\n\ntype Outcome\n = Accepted { value: T, diagnostics: Diagnostics }\n | Rejected { diagnostics: NonEmptyDiagnostics }\n\n" + +fn mbt_with_match(scrutinee_type: String, arms: String) -> String { + mbt_decls + "fn g(h: " + scrutinee_type + ", fallback: ParseArtifact) -> ParseTree {\n match h {\n" + arms + " }\n}\n" +} + +data mbt_arms: String = " Rejected { diagnostics: r } => fallback.tree\n Accepted { value: artifact, diagnostics: d } => artifact.tree\n" + +data mbt_positive_src: String = mbt_with_match(scrutinee_type: "Outcome", arms: mbt_arms) + +// (1) THE BINDER IS TYPED ParseArtifact FROM Outcome's Accepted.value -- the argument +// the scrutinee's type supplies at the declaration's binder T, not T itself and not the field's +// authored spelling. +test fn mbt_binder_is_typed_parse_artifact_from_accepted_value_holds() -> Bool { + mbp_fact_of(src: mbt_positive_src, pick: fn(root) { mbp_find_atom(n: root, id: ^artifact) }) == ^ParseArtifact +} + +// AND THE MATCH IS TYPED BY ITS ARMS: each arm projects `tree` off a ParseArtifact -- one bound by +// the pattern, one a parameter -- and the two agree. Both arms are projections on purpose: a +// projection is typed by the field's declared type node as the index holds it +// (v2.compiler.infer infer_field_projection_facts), which is not the resolved reference a parameter +// annotated `ParseTree` carries, so a projection arm beside a parameter arm compares two spellings of +// one type. That is the projection's typing boundary, reported to its owning lane, not this match's. +test fn mbt_match_is_typed_parse_tree_holds() -> Bool { + mbp_fact_of(src: mbt_positive_src, pick: fn(root) { mbp_find_match(n: root) }) == ^ParseTree +} + +// A BINDER SHADOWS A PARAMETER OF THE SAME NAME. The scope walk before this change typed a binder +// spelled like an enclosing parameter as that parameter; here `artifact` is also an Int parameter. +test fn mbt_binder_shadows_a_same_named_parameter_holds() -> Bool { + mbp_fact_of( + src: mbt_decls + "fn g(artifact: Int, h: Outcome, fallback: ParseArtifact) -> ParseTree {\n match h {\n" + mbt_arms + " }\n}\n", + pick: fn(root) { mbp_find_match(n: root) } + ) == ^ParseTree +} + +// (2) REFUSALS, EACH DISCRIMINATING: the same match with one pattern edit, so the positive control +// above is the other half of every pair. +test fn mbt_a_variant_the_coproduct_does_not_declare_refuses_holds() -> Bool { + mbp_reports( + src: mbt_with_match(scrutinee_type: "Outcome", arms: " Rejected { diagnostics: r } => fallback.tree\n Accepted { value: artifact, diagnostics: d } => artifact.tree\n None => fallback.tree\n"), + reason: ^infer_match_pattern_variant_not_declared + ) +} + +test fn mbt_a_field_the_variant_does_not_declare_refuses_holds() -> Bool { + mbp_reports( + src: mbt_with_match(scrutinee_type: "Outcome", arms: " Rejected { diagnostics: r } => fallback.tree\n Accepted { tree: artifact, diagnostics: d } => artifact.tree\n"), + reason: ^infer_match_pattern_field_not_declared + ) +} + +test fn mbt_too_many_type_arguments_refuses_holds() -> Bool { + mbp_reports( + src: mbt_with_match(scrutinee_type: "Outcome", arms: mbt_arms), + reason: ^infer_match_type_argument_arity_mismatch + ) +} + +test fn mbt_no_type_arguments_to_a_generic_refuses_holds() -> Bool { + mbp_reports( + src: mbt_with_match(scrutinee_type: "Outcome", arms: mbt_arms), + reason: ^infer_match_type_argument_arity_mismatch + ) +} + +test fn mbt_a_missing_variant_refuses_as_non_exhaustive_holds() -> Bool { + mbp_reports( + src: mbt_with_match(scrutinee_type: "Outcome", arms: " Accepted { value: artifact, diagnostics: d } => artifact.tree\n"), + reason: ^infer_match_non_exhaustive + ) +} + +test fn mbt_a_record_scrutinee_refuses_holds() -> Bool { + mbp_reports( + src: mbt_with_match(scrutinee_type: "ParseArtifact", arms: mbt_arms), + reason: ^infer_match_scrutinee_not_coproduct + ) +} + +// (3) THE SEVEN'S SHAPE: the scrutinee is a CALL returning Outcome, and the arm body +// rebuilds an Outcome from the projection. What infer establishes about it is reported as it is. +data mbt_seven_src: String = mbt_decls + "fn parse_module_prepared(tokens: Int) -> Outcome {\n Rejected { diagnostics: NonEmptyDiagnostics { head: tokens } }\n}\n\nfn g_tokenize_parse(text: Int) -> Outcome {\n match parse_module_prepared(tokens: text) {\n Rejected { diagnostics: r } => Rejected { diagnostics: r }\n Accepted { value: artifact, diagnostics: d } =>\n Accepted { value: artifact.tree, diagnostics: d }\n }\n}\n" + +// THE SEVEN'S SHAPE IS ACCEPTED AT THE FRONTIER, AND COUNTED THERE. On this stack the call's declared +// return is not derived (an application's result is derived only for kernel and Bool returns), so the +// scrutinee is untyped and the match cannot be checked against Outcome's variants. It is accepted -- +// refusing would convict the program for a route this stage does not reach -- and the acceptance +// carries infer_match_scrutinee_type_underived at the scrutinee, so the untyped match is a counted +// site and not a silent green. When the call's return derives, this claim goes red and the positive +// controls above become the seven's own. +test fn mbt_the_sevens_call_scrutinee_is_a_counted_frontier_holds() -> Bool { + mbp_infers(src: mbt_seven_src) + && mbp_reports(src: mbt_seven_src, reason: ^infer_match_scrutinee_type_underived) +} + +// AN ARM BODY WHOSE TYPE IS NOT DERIVED IS COUNTED, NOT GUESSED. The scrutinee here IS typed, so the +// patterns are checked and the binder is typed; the bodies rebuild an Outcome from a constructor, +// which this stage does not type, so the match is accepted at the frontier with one located +// infer_match_arm_body_type_underived per such body -- and the binder beneath it is still typed. +data mbt_constructor_body_src: String = mbt_with_match(scrutinee_type: "Outcome", arms: " Rejected { diagnostics: r } => Rejected { diagnostics: r }\n Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d }\n") + +test fn mbt_an_underived_arm_body_is_a_counted_frontier_holds() -> Bool { + mbp_infers(src: mbt_constructor_body_src) + && mbp_reports(src: mbt_constructor_body_src, reason: ^infer_match_arm_body_type_underived) + && (mbp_fact_of(src: mbt_constructor_body_src, pick: fn(root) { mbp_find_atom(n: root, id: ^artifact) }) == ^ParseArtifact) +} From fcce3d811700929f6c8365eaffa47b7a5db944fa Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 14:27:42 +0000 Subject: [PATCH 28/90] Two readers disagreed about a callee's arrow, so no named call's arguments were judged The reference-evidence and field-projection claim sets went red on the composed head. Re-deriving the slice rather than patching the symptom link found two boundaries, one masking the other. FIRST BOUNDARY -- the declaration was read from the wrong index. infer_declaration_reference_facts and infer_projection_receiver asked symbol_index_lookup against ResolvedTree.symbol_index, which is DECLARATIONS AS AUTHORED: resolve's own note says nothing may read it for a body type. A declaration's return atom there is the source spelling `Int`, and dag_binding_denotation is a binding-to-type table that does not denote a spelling, so infer_arrow_declared_return_type answered Absent and every named call's result typing fell to the counted frontier. Both now read resolved_declarations, which is the same module fold over the RESOLVED root. Measured, not guessed: the arrow's positional children were [domain, Atom(^Int), Atom(^Int)], the authored spelling rather than the kernel binding. SECOND BOUNDARY -- and it is a fail-open the first one was hiding. infer_application_formal_args read the callee with the facts-BLIND infer_application_callee_arrow while its own caller's formals came from infer_application_callee_arrow_with_facts. A named call's callee is a resolved declaration reference -- a Conj, never an Arrow -- so the blind reader answered Absent and the function returned NO formal/actual pairs. The inhabitance fold over an empty list accepts unconditionally: every argument of every named call went unjudged, and a Bool actual filled an Int formal with no diagnostic. It was invisible because the broken return read meant the call never grounded anyway, so the negative control asserting that an invalid-argument call does not ground was passing for the wrong reason. Fixing the index removed the mask and that control went red, which is how the fail-open surfaced. One reader now answers the question for both, and the blind reader is deleted -- it had no other consumer. WHAT THIS LANDS. Named-call execution is argument-dependent: 3 in yields 3 out, 8 in yields 8 out, so a callee ignoring its argument fails the row. The Bool pair SEPARATES -- two callees differing only in a constant Bool body execute and produce different values, with execution asserted on both sides before inequality is read, since two refusals are also unequal. Four expecting-red rows flipped and stay enrolled as regression controls rather than retiring (DESIGN 4b(4)). WHAT THIS DOES NOT LAND. A cross-module reference still does not ground, and that is the carrier's scope, not a fail-open: resolve walks one module root, so resolved_declarations holds the subject module's declarations and no other. The two widening arms are both refused -- falling back to the authored index is the absorbing fallback DESIGN 5 forbids and is the wrong answer this change removed, and re-deriving the return from the language's spelling table inside infer is a second resolution authority. The row is enrolled asserting the direction that is true today, with its trigger named: a resolved-declaration index over every resolved module root, minted by resolve. reference evidence 9/9, field projection 14/15 (the remaining red is the designed match-form transition signal), named-call eval 10/10. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 36 +++++--- .../declaration_reference_eval_test.dag | 83 +++++++++++++++---- .../declaration_reference_evidence_test.dag | 29 ++++++- 3 files changed, 116 insertions(+), 32 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 07d7baf9034..7935bbb87a1 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -1125,7 +1125,7 @@ fn infer_projection_receiver( match declaration_reference_path_optional(node: receiver_type) { Absent => ReceiverNotARecord Present { value: path } => - match symbol_index_lookup(index: resolved.symbol_index, qualified_path: path) { + match symbol_index_lookup(index: resolved.resolved_declarations, qualified_path: path) { Absent => ReceiverNotARecord Present { value: payload } => ReceiverPayload { payload: payload } } @@ -1207,7 +1207,7 @@ fn infer_declaration_reference_facts( Absent => inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) Present { value: path } => - match symbol_index_lookup(index: resolved.symbol_index, qualified_path: path) { + match symbol_index_lookup(index: resolved.resolved_declarations, qualified_path: path) { Absent => inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) Present { value: declared } => @@ -2836,13 +2836,6 @@ fn infer_application_callee_arrow_with_facts( } } -fn infer_application_callee_arrow(node: Node) -> Optional { - match list_at_optional(xs: node_positional_child_targets(node: node), index: 0) { - Absent => Absent - Present { value: operator } => infer_operator_arrow(operator: operator) - } -} - // THE FORMALS IN DECLARED ORDER, OR A REFUSAL. The domain's formals are keyed by name; their // positions come only from the Arrow's declared order (arrow_declared_parameter_order), never from // the domain's stored sequence, which is sorted by label for identity (gunbc#12054). An operator @@ -3109,7 +3102,7 @@ fn infer_application_argument_inhabitance( FormalsOrderRefused { reason: reason } => outcome_rejected(application_parameter_order_diagnostic(application: node, reason: reason)) FormalsInDeclaredOrder { formals: formals } => - match infer_application_formal_args(node: node, formals: formals) { + match infer_application_formal_args(node: node, formals: formals, entries: entries) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: pairs, diagnostics: _ } => let type_params = infer_application_type_params(node: node, entries: entries) @@ -3152,9 +3145,28 @@ fn infer_application_argument_inhabitance( // bound twice -- refuses here, located at the actual it names (the application itself when the plan // names no actual). This replaces a zip of stored formals with positional children by index, which // ignored every name (gunbc#12054). -fn infer_application_formal_args(node: Node, formals: List) -> Outcome> { +// THE ARROW IS READ THROUGH THE CALLEE'S FACTS, THE SAME WAY ITS FORMALS WERE. This read used the +// facts-blind reader while its caller's formals came from infer_application_callee_arrow_with_facts, +// so a NAMED call -- whose callee is a resolved declaration reference, a Conj and never an Arrow -- +// answered Absent here and returned NO formal/actual pairs. The inhabitance fold over an empty list +// accepts unconditionally, so every argument of every named call went unjudged: a Bool actual filled +// an Int formal and the application typed past it. Nothing reported it, because the two readers +// disagreed about the same question. ONE reader now answers it for both, so the formals a caller +// admitted and the plan that binds them come from the same arrow or from neither. +// +// WHY THE FAIL-OPEN WAS INVISIBLE: the application's RESULT typing read the same arrow through +// infer_arrow_declared_return_type, which was itself failing on a declaration whose return atom was +// the authored spelling rather than a resolved binding -- so the call never grounded, and the negative +// control asserting that an invalid-argument call does not ground passed for the wrong reason. Fixing +// the declaration lookup to read resolved bodies removed the mask and the control went red, which is +// how the fail-open surfaced at all. +fn infer_application_formal_args( + node: Node, + formals: List, + entries: List, +) -> Outcome> { let actuals = application_actual_edges(site: node) - match infer_application_callee_arrow(node: node) { + match infer_application_callee_arrow_with_facts(node: node, entries: entries) { Absent => outcome_accepted(value: Empty) Present { value: arrow } => match application_binding_plan(arrow: arrow, actuals: actuals) { diff --git a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag index 21fd1605112..fdcd441a6ef 100644 --- a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag +++ b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag @@ -342,16 +342,27 @@ test fn cref_a_named_call_with_an_argument_executes_holds() -> Bool { // callee's parameter -- a callee ignoring its argument entirely would pass both. This fixture returns // its parameter, so its value depends on the argument, and it REFUSES today. Enrolled executed and // expected-red rather than deleted, so the gap is counted rather than described. -test fn cref_a_parameter_bodied_callee_still_refuses_holds() -> Bool { - cref_refusal_reason_is(o: cref_source(), wanted: ^eval_rejected_grounding_not_derived) +// FLIPPED, AND THE ROW STAYS ENROLLED AS A REGRESSION CONTROL (DESIGN section 4b(4): a climb deletes the +// lower-rung production handling, never the evidence). The refusal this row was written to count is +// gone: the supplied argument reaches the callee's parameter and the call executes. The wall that +// landed it is the one reader for a callee's arrow -- v2.compiler.infer +// infer_application_formal_args now reads it through the callee's FACTS, as its caller's formals +// already did, and infer_declaration_reference_facts reads the declaration from +// v2.compiler.resolve ResolvedTree resolved_declarations rather than the authored symbol_index. +test fn cref_a_parameter_bodied_callee_executes_holds() -> Bool { + cref_executes(o: cref_source()) } // A BOOL-RETURNING CALL STILL REFUSES, AND IT IS A DIFFERENT BOUNDARY FROM THE PARAMETER BODY. This // callee's body is a constant, so it differs from the executing claim above only in its RETURN TYPE. // The call grounds under infer, so this sits downstream of both the reference repair and the dispatch // repair. Enrolled executed, not diagnosed further here. -test fn cref_a_bool_returning_call_still_refuses_holds() -> Bool { - cref_refusal_reason_is(o: cref_bool_pair_source(), wanted: ^eval_rejected_grounding_not_derived) +// FLIPPED. The return type was never the variable: the callee's declared return `Bool` read as the +// authored spelling, which the binding-to-type table does not denote, so the application's result +// typing fell to the frontier. Reading the RESOLVED declaration answers it, and the same repair is why +// the Int rows above never separated from this one on their own. +test fn cref_a_bool_returning_call_executes_holds() -> Bool { + cref_executes(o: cref_bool_pair_source()) } // THE DISCRIMINATING NEGATIVE FOR THE DISPATCH: a callee naming no declaration must refuse rather @@ -381,13 +392,14 @@ fn cref_identity_executes_to(lex: String, n: Int) -> Bool { // It refuses at the shared facts key, not at anything about calls or arguments: see // v2.test.claim.callexec.synthetic_facts_key_collision, where more than one entry carries one key and // those entries disagree on grounding. -test fn cref_argument_dependent_execution_still_refuses_holds() -> Bool { - !cref_identity_executes_to(lex: "3", n: 3) - && !cref_identity_executes_to(lex: "8", n: 8) - && cref_refusal_reason_is( - o: cref_identity_source(lex: "3"), - wanted: ^eval_rejected_grounding_not_derived - ) +// FLIPPED, AND THIS IS THE ACCEPTANCE TARGET THE ROW WAS WRITTEN FOR. Both arguments are supplied and +// both results are compared, so a callee that ignored its argument and answered one constant fails it: +// 3 in yields 3 out and 8 in yields 8 out. The refusal it used to count was NOT the shared facts key +// the old annotation blamed -- the synthetic-key collision is real and still enrolled separately, but +// it was not what held this row red. The boundary was the two disagreeing readers of a callee's arrow. +test fn cref_argument_dependent_execution_reaches_the_callee_holds() -> Bool { + cref_identity_executes_to(lex: "3", n: 3) + && cref_identity_executes_to(lex: "8", n: 8) } // AND THE SAME CALLEE MUST NOT ANSWER A DIFFERENT ARGUMENT'S VALUE once it executes. THIS CLAIM IS @@ -408,11 +420,46 @@ fn cref_bool_false_source() -> Outcome { cref_assemble(src: "module p\n\nfn falsity(only_arg: Int) -> Bool {\n false\n}\n\nfn consumer() -> Bool {\n falsity(only_arg: 3)\n}\n") } -test fn cref_the_bool_pair_does_not_yet_separate_holds() -> Bool { - !cref_executes(o: cref_bool_pair_source()) - && !cref_executes(o: cref_bool_false_source()) - && cref_refusal_reason_is( - o: cref_bool_false_source(), - wanted: ^eval_rejected_grounding_not_derived - ) +// FLIPPED: THE PAIR SEPARATES. Two callees differing only in their constant Bool body now execute and +// produce DIFFERENT values. Execution is asserted on both sides before inequality is read, because two +// refusals are also unequal and would satisfy inequality alone. The values are not compared against a +// magnitude: this module is not the authority on how v2.std.logic represents its arms, and asserting a +// byte image here would be a second such authority (DESIGN section 3). +test fn cref_the_bool_pair_separates_holds() -> Bool { + cref_executes(o: cref_bool_pair_source()) + && cref_executes(o: cref_bool_false_source()) + && cref_results_differ(a: cref_bool_pair_source(), b: cref_bool_false_source()) +} + +// THE PRIMITIVE BYTES OF A CALL'S RESULT, for the one claim whose property is that two calls produce +// DIFFERENT values. The existing int reader decodes a four-byte signed image and a Bool is not one, so +// asserting "true" and "false" by magnitude would be asserting this module's guess at v2.std.logic's +// representation. Byte inequality is the property without the guess: a callee that ignored its +// declaration and answered one constant cannot satisfy it. +fn cref_result_bytes(o: Outcome) -> Optional> { + match cref_eval_of(o: o) { + Absent => optional_absent() + Present { value: outcome } => + match outcome { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: v, diagnostics: _ } => + match v { + RuntimePrimitive { value: p } => optional_present(value: p.bytes) + _ => optional_absent() + } + } + } +} + +// FALSE WHEN EITHER SIDE DID NOT EXECUTE, so the separation claim below pairs it with two execution +// assertions rather than reading inequality as evidence on its own: two refusals are also unequal. +fn cref_results_differ(a: Outcome, b: Outcome) -> Bool { + match cref_result_bytes(o: a) { + Absent => false + Present { value: xs } => + match cref_result_bytes(o: b) { + Absent => false + Present { value: ys } => !(xs == ys) + } + } } diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 840f28bacca..ffc45762b4f 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -451,6 +451,31 @@ fn dre_imported_reference_source() -> Outcome { ) } -test fn dre_an_imported_reference_grounds_the_same_way_holds() -> Bool { - dre_call_is_grounded_for(o: dre_imported_reference_source(), wanted: ^helper) +// A CROSS-MODULE REFERENCE DOES NOT GROUND, AND THAT IS THE SCOPE OF THE CARRIER, NOT A FAIL-OPEN. +// The declaration a reference grounds to is read from v2.compiler.resolve ResolvedTree +// resolved_declarations -- DECLARATIONS WITH RESOLVED BODIES -- because a declaration's return atom as +// AUTHORED is the source spelling `Int`, which v2.extdeps.languages.dag dag_binding_denotation does not +// denote (it is a binding-to-type table, and the authored spelling is not a binding). Reading the +// pre-resolve symbol_index instead is what made every named call's result typing fall to the frontier. +// +// resolve walks ONE module root and mints resolved_declarations over it (resolved_declarations_of), so +// that index carries the SUBJECT module's declarations and no other. m.lib.helper is therefore absent +// from it, the lookup answers Absent, and the reference stays underived. NOTHING IS FABRICATED: the +// arm that would widen -- fall back to the authored index when the resolved one has no row -- is the +// absorbing fallback DESIGN section 5 forbids, and it is the exact widening whose answer was wrong. +// Re-deriving the return from the language's kernel spelling table inside infer is the other tempting +// arm and is a second resolution authority (DESIGN section 3), so it is refused too. +// +// THE TRIGGER IS A RESOLVED-DECLARATION INDEX OVER EVERY RESOLVED MODULE ROOT -- the multi-root door +// v2.compiler.symbol_index_fill symbol_index_fill_module_roots applied to resolved roots rather than +// authored ones, minted by resolve beside the per-module carrier. That is resolve's fact to own, not +// infer's. Until it exists, a cross-module named call's result type is on the counted frontier. +// +// THIS ROW IS THE FRONTIER'S EXECUTING EVIDENCE, asserted in the direction that is true today: the +// imported call does NOT ground. It flips the moment the trigger lands, which is what makes it a +// transition signal rather than a permanent green. The single-module rows above are the positive +// controls that the mechanism itself works, so this row cannot be satisfied by the mechanism being +// broken everywhere. +test fn dre_an_imported_reference_does_not_yet_ground_holds() -> Bool { + dre_call_is_grounded_for(o: dre_imported_reference_source(), wanted: ^helper) == false } From d933035acb251778b6f4d80e45117be83921def1 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 15:49:34 +0000 Subject: [PATCH 29/90] Route an unadmissible callable grounding to the frontier; retire the match-form blocker TWO CORRECTIONS ON TOP OF THE PREVIOUS COMMIT, both found by running the sets rather than by reading the diff. (1) A REGRESSION I INTRODUCED, AND IT WAS NOT SUBTLE. Reading the declaration from resolved_declarations hands canonical_grounding_from_derived_type a resolved signature, and a return that is a generic instantiation -- Outcome -- is not a shape canonical_grounding_admits_infer_facts admits. The construction REFUSED, and that refusal escaped, so a single-module named call whose callee returns a generic instance rejected the whole module with >6 diagnostics, the fatal being infer_canonical_grounding_incoherent reported against the DECLARATION. A stage refusing a program for a route it does not reach. infer_reference_facts_or_frontier routes it to inferred_facts_not_derived instead -- the SAME arm the function already takes when a declaration carries no callable evidence, not a new leniency -- so the use is underived, carries the counted infer_grounding_not_derived advisory, and every demanding consumer still refuses at its own gate. The residue is stated: a generic-returning callee's application stays on the frontier, and the trigger is a canonical grounding for an instantiated generic type, which is the typing model's fact and not this reader's. Isolated by bisecting the fixture -- declaration alone infers, the match with a non-call scrutinee infers, the CALL refuses -- and the fatal reason was found by a chunked search over the 485 diagnostic reasons the corpus declares, not guessed. (2) THE MATCH-FORM BLOCKER IS RETIRED, AS THAT ROW PREDICTED IT WOULD BE. It said "if a later change makes match arms infer, this claim goes red and the projection claim beside it becomes the live question". #12641 landed exactly that, so the row went red and is rewritten rather than deleted: it now controls that the match form infers with a projection body and with a literal body. WHAT IS LIVE INSTEAD IS NARROWER, AND MEASURED. The projection NODE is in the tree and the module infers, but the projection does not GROUND -- for the valid field and for an absent one alike. So an absent field off a match binder is not refused, which is the frontier's ordinary behaviour and is asserted in that direction rather than as a wall that does not exist. That is the same vacuity the earlier version of the section confessed to, now written so it cannot recur. The parameter receiver grounds and DOES refuse its absent field, and those two rows are the discriminating controls that make this frontier specific to the match-arm binder rather than a claim that everything is underived. Four sets, 46 claims, zero red: field projection 16/16, reference evidence 9/9, named-call eval 10/10, match-binder typing 11/11. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 50 ++++++++++++-- .../field_projection_stages_test.dag | 67 +++++++++++++++---- 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 7935bbb87a1..f1c90c148c9 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -1198,6 +1198,40 @@ fn infer_declaration_callable_evidence(declared: Node) -> Optional { } } +// A DECLARATION WHOSE CALLABLE EVIDENCE THIS STAGE CANNOT ADMIT IS THE FRONTIER, NOT A REFUSAL, and +// that is the SAME arm this function already takes when the declaration carries no callable evidence at +// all -- not a new leniency. canonical_grounding_from_derived_type holds every grounding the compiler +// mints to a self-consistency wall (canonical_grounding_admits_infer_facts: the node and its evidence +// well_formed, the two constraint properties, closedness evidence identical to the node). A declaration +// read from the RESOLVED root carries whatever resolve bound into its signature, and a return that is a +// generic instantiation -- `Outcome` -- is not a shape that wall admits. +// +// WHY ROUTING IT TO THE FRONTIER IS THE CORRECT ARM AND NOT A WIDENING. The reference is a USE; the +// question this function answers is "what callable contract does this use denote". When the answer is +// unavailable the honest result is that the use is underived, which infer already carries as the counted +// advisory infer_grounding_not_derived and every DEMANDING consumer -- eval, translate, coercion -- still +// refuses at its own gate. Letting the construction's refusal escape instead made a single-module named +// call whose callee returns a generic instance REJECT the whole module, with the incoherence reported +// against the declaration rather than against anything the program did wrong: a stage refusing a program +// for a route it does not reach. That is the conviction DESIGN section 5 forbids in the other direction, +// and it is a regression from the accepted-at-frontier behaviour, not a wall. +// +// THE RESIDUE IS STATED RATHER THAN COVERED: a generic-returning callee's application stays on the +// frontier, so its result type is not derived and a match on it is not checked against the coproduct's +// variants. The trigger is a canonical grounding for an instantiated generic type, which is the typing +// model's fact and not this reader's. +fn infer_reference_facts_or_frontier( + o: Outcome, + n: Node, + descent_proof: TerminationProof, +) -> Outcome { + match o { + Accepted { value: facts, diagnostics: d } => Accepted { value: facts, diagnostics: d } + Rejected { diagnostics: _ } => + inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) + } +} + fn infer_declaration_reference_facts( n: Node, resolved: ResolvedTree, @@ -1215,13 +1249,17 @@ fn infer_declaration_reference_facts( Absent => inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) Present { value: callable } => - infer_facts_denoting( - o: inferred_facts_from_derived_type( - node: n, - derived_type: callable, - descent: Holds { value: descent_proof } + infer_reference_facts_or_frontier( + o: infer_facts_denoting( + o: inferred_facts_from_derived_type( + node: n, + derived_type: callable, + descent: Holds { value: descent_proof } + ), + declaration: declared ), - declaration: declared + n: n, + descent_proof: descent_proof ) } } diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag index 01bc44d648d..634f1957a38 100644 --- a/src/v2/test/claim/field_projection/field_projection_stages_test.dag +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -382,10 +382,11 @@ test fn fps_eval_refuses_a_non_aggregate_receiver_holds() -> Bool { // kind from the parameter receiver above, so whether this repair reaches it is its own fact. // // MEASURED, AND THE ISOLATING CONTROL IS THE ONE THAT MATTERS. The match form RESOLVES -- so resolve's -// projection arm handles a match binder's head -- and then fails to infer. But the SAME match form with -// the projection REPLACED BY A LITERAL fails to infer identically, so the blocker is the match construct -// and not the field access. That is the pre-existing match-arm boundary this lane already recorded as the -// seven's first refusal (body_lowering_reason_match_arm_navigation_refused), owned elsewhere. +// projection arm handles a match binder's head -- and it now INFERS as well, with a projection body and +// with a literal body alike: the match-arm boundary this section first recorded as the seven's refusal +// (body_lowering_reason_match_arm_navigation_refused) is gone. What remains at this receiver is that the +// projection does not GROUND, which the rewritten row below asserts in that direction rather than as a +// refusal that no longer happens. // // AN EARLIER VERSION OF THIS SECTION CLAIMED MORE THAN IT MEASURED. It asserted that an absent field off // a match binder is not admitted, and that claim PASSED -- vacuously, because the valid projection off a @@ -410,12 +411,54 @@ test fn fps_a_match_binder_receiver_resolves_holds() -> Bool { fps_resolves(o: fps_match_binder_source(field: "tree")) } -// AND THE BLOCKER IS THE MATCH FORM, NOT THE PROJECTION. Both conjuncts are needed: the projection form -// refusing alone would be consistent with a projection defect, and it is the SECOND conjunct -- the same -// match with a literal body refusing too -- that assigns the refusal to the match construct. If a later -// change makes match arms infer, this claim goes red and the projection claim beside it becomes the live -// question, which is the transition worth being told about. -test fn fps_the_match_binder_blocker_is_the_match_form_holds() -> Bool { - !fps_infers(o: fps_match_binder_source(field: "tree")) - && !fps_infers(o: fps_match_no_projection_source()) +// THE MATCH-FORM BLOCKER IS RETIRED, AND THE ROW IS REWRITTEN TO THE BOUNDARY THAT IS ACTUALLY LIVE. +// The row above predicted its own transition -- "if a later change makes match arms infer, this claim goes +// red and the projection claim beside it becomes the live question" -- and that change landed (gunbc#12641 +// types a match over a declared generic coproduct and its arm binders). The match form now infers with a +// projection body AND with a literal body, so the refusal this row counted no longer exists and the row +// does not retire: it becomes the control that the retirement stays real (DESIGN section 4b(4)). +// +// WHAT IS LIVE INSTEAD IS NARROWER AND IS STATED, NOT CLAIMED AS A WALL. The projection NODE is in the +// resolved tree, the module infers, and the projection's grounding is NOT derived -- for the valid field +// and for an absent one alike. So the receiver's type is underived at this seam and the projection sits on +// the counted frontier rather than being typed. +// +// THE CONSEQUENCE IS NAMED HERE SO IT IS NOT MISREAD AS A WALL: because the projection is on the frontier, +// an ABSENT field off a match binder is NOT refused either. That is the frontier's ordinary behaviour, not +// a fail-open, and this row asserts it in the direction that is true rather than asserting a refusal that +// does not happen. It is exactly the vacuity the earlier version of this section confessed to, now written +// so that it cannot recur: the two grounding conjuncts below would both have to change for this row to go +// green under a broken implementation. +// +// WHY THIS IS INFORMATIVE AND NOT "EVERYTHING IS UNDERIVED": the PARAMETER receiver in this same file +// grounds (fps_a_valid_field_projection_grounds_holds) and its absent field IS refused +// (fps_an_absent_field_does_not_infer_holds). Those two are the discriminating positive controls, so the +// frontier asserted here is specific to the match-arm binder as a receiver. +// +// THE TRIGGER is a derived type for a match-arm binder AT THE PROJECTION'S RECEIVER SEAM -- the binder is +// typed by gunbc#12641 within the match, and what this row measures is that the projection's own receiver +// read does not reach that fact. When it does, the two grounding conjuncts flip and the absent-field +// conjunct becomes a refusal claim. +test fn fps_the_match_form_infers_and_the_projection_stays_on_the_frontier_holds() -> Bool { + fps_infers(o: fps_match_binder_source(field: "tree")) + && fps_infers(o: fps_match_no_projection_source()) + && fps_match_binder_projection_grounds(field: "tree") == false + && fps_match_binder_projection_grounds(field: "span_index") == false +} + +// THE PROJECTION NODE IS THERE, so the two grounding conjuncts above are about a node that exists rather +// than vacuously true of an absent one -- the same guard fps_the_resolved_tree_carries_a_field_projection +// provides for the parameter receiver. +test fn fps_a_match_binder_projection_is_in_the_tree_holds() -> Bool { + match fps_projection_facts(o: fps_match_binder_source(field: "tree")) { + Present { value: _ } => true + Absent => false + } +} + +fn fps_match_binder_projection_grounds(field: String) -> Bool { + match fps_projection_facts(o: fps_match_binder_source(field: field)) { + Absent => false + Present { value: facts } => inferred_facts_grounding_derived(facts: facts) + } } From 2627d304b3b61198723447868449df4ff5efc51f Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 16:09:10 +0000 Subject: [PATCH 30/90] Compile the consumers the widened signature broke, and undo two merge reversions The local witnesses floor refused where the four claim sets could not: both defects are in modules outside those sets, which is the whole reason the floor exists and is a repeat of a class I have a standing note on (run the consumers, not the file you edited). infer_application_formals was widened by this lane to read a callee's arrow through the callee's facts, and v2.test.claim.declared_parameter_order was never compiled against it. Its two call sites now supply entries: [], which is the fixture's actual premise rather than a stub -- dpo_application puts the Arrow AT positional 0, so infer_operator_arrow answers from the node and the facts route is never consulted. A non-empty list would invent a hypothesis this claim's subject does not have. tpb_type_decl_emit_twin and tpb_type_decl_emit_generic were reverted to Outcome by my hand-composition of main; #12756 had already corrected them to Outcome, which is what translate_type_expression_project returns. Main's version restored. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/test/claim/declared_parameter_order_test.dag | 11 +++++++++-- src/v2/test/claim/type_param_binder_frame_test.dag | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/v2/test/claim/declared_parameter_order_test.dag b/src/v2/test/claim/declared_parameter_order_test.dag index 9b19b10135e..e41f9c67242 100644 --- a/src/v2/test/claim/declared_parameter_order_test.dag +++ b/src/v2/test/claim/declared_parameter_order_test.dag @@ -211,6 +211,13 @@ fn dpo_canonical_domain(arrow: Node) -> Node { } // An application of `arrow` to nothing: infer_application_formals reads only the callee. +// THE ENTRIES LIST IS EMPTY, AND THAT IS THE CORRECT SUPPLIED VALUE RATHER THAN A STUB. This lane +// widened infer_application_formals to read a callee's arrow through the CALLEE'S FACTS as well, for +// the case where the callee is a resolved declaration reference and not an Arrow node. Here the +// fixture supplies the Arrow AS the callee (dpo_application puts it at positional 0), so +// infer_operator_arrow answers from the node itself and the facts route is never consulted. An empty +// entries list therefore states exactly the fixture's premise -- no facts are needed to read this +// callee -- and a non-empty one would be inventing a hypothesis this claim's subject does not have. fn dpo_application(arrow: Node) -> Node { Node { kind: ComputationNode { behavior: Transform }, @@ -220,7 +227,7 @@ fn dpo_application(arrow: Node) -> Node { } fn dpo_formal_labels(arrow: Node) -> List { - match infer_application_formals(node: dpo_application(arrow: arrow)) { + match infer_application_formals(node: dpo_application(arrow: arrow), entries: []) { FormalsInDeclaredOrder { formals: fs } => fold(fs, init: [], f: fn(acc, f) { concat(acc, [f.parameter_identity]) }) FormalsUnresolved => [] FormalsOrderRefused { reason: _ } => [] @@ -228,7 +235,7 @@ fn dpo_formal_labels(arrow: Node) -> List { } fn dpo_refusal_reason(arrow: Node) -> List { - match infer_application_formals(node: dpo_application(arrow: arrow)) { + match infer_application_formals(node: dpo_application(arrow: arrow), entries: []) { FormalsOrderRefused { reason: r } => [r] FormalsInDeclaredOrder { formals: _ } => [] FormalsUnresolved => [] diff --git a/src/v2/test/claim/type_param_binder_frame_test.dag b/src/v2/test/claim/type_param_binder_frame_test.dag index 10c3455ee08..6693e596bf6 100644 --- a/src/v2/test/claim/type_param_binder_frame_test.dag +++ b/src/v2/test/claim/type_param_binder_frame_test.dag @@ -519,11 +519,11 @@ fn tpb_box_member_mixed() -> Node { ) } -fn tpb_type_decl_emit_twin() -> Outcome { +fn tpb_type_decl_emit_twin() -> Outcome { translate_type_expression_project(node: tpb_box_member(with_params: false), target: rust_target_model(), projection: rust_type_expression_projection()) } -fn tpb_type_decl_emit_generic() -> Outcome { +fn tpb_type_decl_emit_generic() -> Outcome { translate_type_expression_project(node: tpb_box_member(with_params: true), target: rust_target_model(), projection: rust_type_expression_projection()) } From 8e9fc28bd72458c1f00e1410543d097cf22fa5f5 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 16:29:27 +0000 Subject: [PATCH 31/90] Enrol two rows that nothing was running, and correct the measurement they recorded The floor's discovery gate refused v2.test.claim.callexec.named_call_execution: a `*_test.dag` entry that declares no `test fn` enrols nothing. Both rows in that file were plain `fn`, so the claim route never ran them and the native pattern selected nothing either -- the decoration DESIGN 4b names, sitting in the tree since this lane created the file. It surfaced only once the two type errors ahead of it in the same phase were repaired, which is a refusal masking a refusal. The measurement the file recorded is also stale. It said both rows refuse at eval with eval_rejected_grounding_not_derived and that "the boundary is not named-call typing". The boundary WAS named-call typing, in two places: infer_application_formal_args read the callee arrow facts-blind while its caller's formals came from the facts-aware reader, so a named call produced no formal/actual pairs at all; and infer_declaration_reference_facts read the declaration from the AUTHORED symbol_index, whose return atom is a source spelling dag_binding_denotation does not denote. Both rows now hold, and they hold on the DIRECT PRODUCTION ROUTE -- the executor evaluating a named call declared in the corpus, not a fixture fold driving v2.compiler.eval. That makes this the strongest inhabitance evidence in the lane for named-call execution. The false control stays a NEGATION rather than an equality. `!falsity(only_arg: 1)` holds only if the call RETURNED false: a refusal fails the row and a `true` answer fails it too, so the pair separates a refusal from a false answer, which a single positive row cannot. Asserting `== false` would couple the execution proof to the equality operation, which the file's own header already refuses to do. Co-Authored-By: Claude Opus 5 (1M context) --- .../callexec/named_call_execution_test.dag | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/v2/test/claim/callexec/named_call_execution_test.dag b/src/v2/test/claim/callexec/named_call_execution_test.dag index 5af9e9db729..ad92a81a63e 100644 --- a/src/v2/test/claim/callexec/named_call_execution_test.dag +++ b/src/v2/test/claim/callexec/named_call_execution_test.dag @@ -18,24 +18,31 @@ fn falsity(only_arg: Int) -> Bool { false } -// MEASURED: NOT YET EXECUTING, AND NOT FOR A TYPING REASON. Both rows refuse at EVAL with -// eval_rejected_grounding_not_derived at a SYNTHETIC node -- no authored locus. Under infer the same -// call grounds (v2.test.claim.reference_evidence.declaration_reference_evidence's named-call rows pass -// on this tree), so reference typing, application typing and the return derivation are all doing their -// jobs and something eval consumes is still ungrounded. +// EXECUTING, AND THE PAIR DISCRIMINATES. The refusal this file recorded is gone: both calls run and +// answer their own bodies. The boundary was two disagreeing readers of a callee's arrow -- +// v2.compiler.infer infer_application_formal_args read it FACTS-BLIND while its caller's formals came +// from infer_application_callee_arrow_with_facts, so a named call (whose callee is a resolved +// declaration reference, a Conj and never an Arrow) produced no formal/actual pairs at all -- together +// with infer_declaration_reference_facts reading the declaration from the AUTHORED symbol_index, whose +// return atom is the source spelling that dag_binding_denotation does not denote. // -// THE FALSE CONTROL DID NOT DISCRIMINATE, WHICH IS THE POINT OF HAVING IT: both rows refused -// identically, so neither body ran and nothing distinguishes a refusal from a false answer here. A -// single positive row would have looked like the same failure. +// THESE ROWS WERE NOT ENROLLED BY ANYTHING BEFORE, and that is worth recording rather than quietly +// fixing. They were declared plain `fn`, so the claim route never ran them and the native pattern +// selected nothing either: a `*_test.dag` that enrols nothing is the decoration DESIGN section 4b +// names, and the floor's own discovery gate is what refused it. It surfaced only once the type errors +// ahead of it were repaired -- a refusal earlier in the phase had been masking it. The seven's own file +// (v2.test.parse.expression_bodied_fn_decl_parse) declares `test fn` at a `*_test.dag` path, which is +// the convention these rows now follow. // -// THE SAME SIGNATURE APPEARS ELSEWHERE, unrepaired by this lane: a plain-binder match over a -// coproduct and a trivial `fn f(b: Box) -> Int { 7 }` both refuse at eval on this cause at a synthetic -// node. So the boundary is not named-call typing; it is whatever synthetic node eval's facts gate is -// asked about. Enrolled executed rather than deleted so the next session inherits the measurement. -fn nc_a_named_bool_call_executes_expected_red() -> Bool { +// THE FALSE CONTROL IS A NEGATION, NOT AN EQUALITY. Asserting `falsity(...) == false` would couple this +// execution proof to the equality operation, which has its own typing rule and its own standing -- the +// coupling the header above already refuses. `!falsity(...)` holds only if the call RETURNED false: a +// refusal fails the row, and a `true` answer fails it too, so the pair separates a refusal from a false +// answer, which is what a single positive row cannot do. +test fn nc_a_named_bool_call_executes_holds() -> Bool { truth(only_arg: 1) } -fn nc_the_false_returning_call_is_the_deliberate_false_control_expected_red() -> Bool { - falsity(only_arg: 1) +test fn nc_the_false_returning_call_returns_false_holds() -> Bool { + !falsity(only_arg: 1) } From 76c9ed548ebc5c0a4166ac8062b4b87b7b6cb72c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 17:07:07 +0000 Subject: [PATCH 32/90] A declared position that already carries its value type is decidable; Int was masking Bool Two defects the merge surfaced, and the second is a capability loss in #12566's rewrite that this lane's control caught. (1) THE AUTO-MERGE MIS-STITCHED A SEAM IT REPORTED AS CLEAN. Main's new infer_gather_settled_unannotated_row -- split out of infer_gather_settled_row with the declared-return check -- passes `resolved:` to the product row, a parameter THIS lane added (the product row answers a field projection first, and a projection needs the receiver's declaration from the index). Its own signature had no such parameter, so the merged file did not compile: `undefined variable 'resolved'`. Threaded from the enclosing row, which already holds it, rather than reconstructed. Git reported one conflict, an import list. This was not in it. A clean auto-merge of two changes that each widened the same fold is not evidence that the fold still type-checks. (2) #12566's DECLARED-POSITION JUDGE COULD NOT SEE A DECLARED Bool, AND Int HID IT. dag_binding_denotation is strictly BINDING->value-type. `Bool` does not reach the judge as a binding: it arrives as the DENOTED node already (v2.std.logic bool_node), so the join answered Absent and infer_declared_position_undecidable_reason counted the position UndecidableFormalUnresolved -- the comparison was skipped and the mismatch accepted at the frontier. `Int` masked it, because its canonical type constructor retains the historical spelling ^dag_binding_type_int so its binding and type identities coincide. Net effect: `fn wrong(only_arg: Bool) -> Int { only_arg }` refused while the mirror-image `-> Bool { only_arg }` did not. MEASURED, NOT INFERRED, and the measurement is now enrolled: for the Bool fixture the Arrow's declared return is structurally bool_node(); for the Int fixture it is ^dag_binding_type_int. Two rows assert exactly that, so if lowering ever canonicalizes Bool to a binding or stops canonicalizing Int, the reader that depends on the current shape is named by a red rather than by a silently skipped comparison. THE REPAIR IS ONE AUTHORITY, ASKED BY AUTHORITY RATHER THAN BY SPELLING. infer_established_value_type_optional (renamed from ..._return_..., since it now answers at any declared position, not only a return) compares against v2.std.logic's own bool_node() through the existing structural equality. Both declared-position readers consult it beside the join: the undecidable gate, so such a position is DECIDABLE, and infer_declared_type_denotation, so it denotes to itself. The join stays strictly binding-to-type -- it is not widened to accept a denoted symbol -- and an arbitrary atom is still not a resolved value type and is still counted. This is not caused by this lane's own change: the fixture has no call and no declaration reference, only a parameter use, and this lane's edits touch only the reference and projection readers. reference evidence 11/11. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 46 ++++++++++++--- .../declaration_reference_evidence_test.dag | 57 ++++++++++++++++++- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 70427b0bf6c..76918938526 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -908,7 +908,7 @@ fn infer_node_facts(n: Node, partials: List, kinds: List, descent: Holds { value: descent_proof } ) Absent => - match infer_established_return_type_optional(ret: n) { + match infer_established_value_type_optional(ret: n) { Present { value: _ } => inferred_facts_from_derived_type( node: n, @@ -1412,7 +1412,7 @@ fn infer_binding_value_type_witness(binding: Symbol, at: Node) -> Witness // ORDERED DENOTATION-FIRST so the Int path is byte-identical: only a return the binding lookup cannot // denote reaches the established-type question. ONE reader, so introduction and elimination cannot // disagree about the same signature. -fn infer_established_return_type_optional(ret: Node) -> Optional { +fn infer_established_value_type_optional(ret: Node) -> Optional { if infer_type_equal_ignoring_provenance(a: ret, b: bool_node()) { optional_present(value: bool_node()) } else { @@ -1425,11 +1425,11 @@ fn infer_arrow_declared_return_type(arrow: Node) -> Optional { Absent => Absent Present { value: ret } => match infer_atom_binding_sym(node: ret) { - Absent => infer_established_return_type_optional(ret: ret) + Absent => infer_established_value_type_optional(ret: ret) Present { value: binding } => match dag_binding_denotation(sym: binding) { Present { value: denoted } => Present { value: denoted } - Absent => infer_established_return_type_optional(ret: ret) + Absent => infer_established_value_type_optional(ret: ret) } } } @@ -2994,6 +2994,22 @@ fn infer_judge_application_argument( // that is a bare atom the join does not denote, and not one of the type variables, is not a type // this relation can compare: it is counted UndecidableFormalUnresolved, never refused for the // spelling and never admitted silently. +// +// A DECLARED POSITION THAT ALREADY CARRIES ITS VALUE TYPE IS DECIDABLE, AND THE JOIN CANNOT SAY SO. +// dag_binding_denotation is strictly BINDING->value-type, and `Bool` does not reach this reader as a +// binding: it arrives as the DENOTED node already (v2.std.logic bool_node), so the join answers Absent +// and the position was counted rather than compared. `Int` masked it, because its canonical type +// constructor retains the historical spelling ^dag_binding_type_int and its binding and type identities +// coincide -- so a declared-Int mismatch refused while the identical declared-Bool mismatch was +// accepted at the frontier. MEASURED, not inferred: for `fn wrong(only_arg: Int) -> Bool { only_arg }` +// the Arrow's declared return is structurally bool_node(), and for the converse fixture it is +// ^dag_binding_type_int. +// +// SO THE RECOGNITION IS ASKED BY AUTHORITY, NOT BY SPELLING: infer_established_value_type_optional +// compares against v2.std.logic's own bool_node() through the existing structural equality. That is one +// authority consumed by both this gate and infer_declared_type_denotation, rather than a second +// recognition here or a widening of dag_binding_denotation to accept a denoted symbol -- the join stays +// strictly binding-to-type. An arbitrary atom is still not a resolved value type and is still counted. fn infer_declared_position_undecidable_reason( declared: Node, type_params: List @@ -3005,7 +3021,11 @@ fn infer_declared_position_undecidable_reason( TypeNode { connective: Atom { identity: id } } => match dag_binding_denotation(sym: id) { Present { value: _ } => Absent - Absent => optional_present(value: undecidable_formal_unresolved()) + Absent => + match infer_established_value_type_optional(ret: declared) { + Present { value: _ } => Absent + Absent => optional_present(value: undecidable_formal_unresolved()) + } } _ => Absent } @@ -3021,7 +3041,11 @@ fn infer_declared_type_denotation(declared: Node) -> Node { TypeNode { connective: Atom { identity: id } } => match dag_binding_denotation(sym: id) { Present { value: denoted } => denoted - Absent => Node { kind: self.kind, children: [], occurrence_id: self.occurrence_id } + Absent => + match infer_established_value_type_optional(ret: self) { + Present { value: established } => established + Absent => Node { kind: self.kind, children: [], occurrence_id: self.occurrence_id } + } } _ => Node { kind: self.kind, children: [], occurrence_id: self.occurrence_id } } @@ -4672,7 +4696,8 @@ fn infer_gather_settled_row( acc: acc, merged_entries: merged_entries, merged_pending: diagnostics_merge(outer: merged_pending, inner: rd), - partials: partials + partials: partials, + resolved: resolved ) } } @@ -4719,11 +4744,18 @@ fn infer_arrow_declared_return_shape_diagnostic(node: Node) -> Diagnostic { } } +// THE RESOLVED CARRIER REACHES THIS ROW BECAUSE THE PRODUCT ROW BENEATH IT ANSWERS A FIELD +// PROJECTION FIRST, and a projection needs the receiver's DECLARATION, which only the index in +// ResolvedTree supplies (infer_formation_facts_from_entries). Main introduced this row when it split +// the declared-return check out of infer_gather_settled_row; this lane had widened the product row it +// delegates to. The two are compatible -- the enclosing row already holds `resolved` -- and the +// parameter is threaded rather than reconstructed, so there is one carrier for the whole fold. fn infer_gather_settled_unannotated_row( acc: InferGatherFoldAcc, merged_entries: List, merged_pending: Diagnostics, partials: List, + resolved: ResolvedTree, ) -> InferGatherFoldAcc { if infer_gather_acc_awaits_product_row(node: acc.node, entries: merged_entries) { infer_gather_product_row_on_entries( diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index ffc45762b4f..50494e8f586 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -2,7 +2,8 @@ module v2.test.claim.reference_evidence.declaration_reference_evidence import v2.compiler.resolve { ResolvedTree } import v2.std.symbol_index { symbol_index_lookup } -import v2.compiler.infer { infer, inferred_facts_grounding_derived } +import v2.compiler.infer { infer, inferred_facts_grounding_derived, infer_type_equal_ignoring_provenance } +import v2.std.logic { bool_node } import v2.compiler.inferred_tree { DerivedGrounding, GroundingNotDerived, InferredFacts } import v2.compiler.name_resolve { Admission, ResolutionSubject } import v2.compiler.program_assembly { assemble_program_from_ingest } @@ -16,7 +17,7 @@ import v2.std.artifact { Artifact, SourceFile } import v2.compiler.source_authority { SourceRootIngest } import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.logic { Bool } -import v2.std.node { Node, NodeFold, TypeNode, Atom, ComputationNode, Transform, fold_node } +import v2.std.node { Node, NodeFold, TypeNode, Atom, Arrow, ComputationNode, Transform, fold_node } import v2.std.node_query { find_named_child, node_positional_child_targets } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.std.collection { list_at_optional } @@ -479,3 +480,55 @@ fn dre_imported_reference_source() -> Outcome { test fn dre_an_imported_reference_does_not_yet_ground_holds() -> Bool { dre_call_is_grounded_for(o: dre_imported_reference_source(), wanted: ^helper) == false } + +// THE ASYMMETRY THE TWO REFUSAL ROWS ABOVE DEPEND ON, MEASURED RATHER THAN ASSERTED. A declared `Int` +// return reaches infer as the canonical BINDING ^dag_binding_type_int, while a declared `Bool` return +// reaches it as the DENOTED value type (v2.std.logic bool_node) -- so the binding->value-type join +// v2.extdeps.languages.dag dag_binding_denotation answers for one and not the other. That is why +// v2.compiler.infer asks infer_established_value_type_optional beside the join at both declared-position +// readers, and why declared-Int mismatches refused for a while where the identical declared-Bool +// mismatch was accepted at the frontier. +// +// THESE TWO ROWS ARE THE STRUCTURAL CONTROL FOR THAT REPAIR, not a restatement of it: if lowering ever +// canonicalizes Bool to a binding, or stops canonicalizing Int, one of them goes red and the reader that +// depends on the current shape is the thing to re-read. A refusal row alone could not say which. +fn dre_arrow_of(o: Outcome) -> Optional { + match o { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: r, diagnostics: _ } => + fold_node( + n: r.root, + algebra: NodeFold { + init: fn(n0) { + match n0.kind { TypeNode { connective: Arrow } => optional_present(value: n0) _ => optional_absent() } + }, + step: fn(acc, _e, child) { match acc { Present { value: _ } => acc Absent => child } } + } + ) + } +} + +fn dre_declared_return_of(o: Outcome) -> Optional { + match dre_arrow_of(o: o) { + Absent => optional_absent() + Present { value: a } => list_at_optional(xs: node_positional_child_targets(node: a), index: 1) + } +} + +test fn dre_a_declared_bool_return_is_already_the_bool_value_type_holds() -> Bool { + match dre_declared_return_of(o: dre_bool_declared_int_body()) { + Absent => false + Present { value: ret } => infer_type_equal_ignoring_provenance(a: ret, b: bool_node()) + } +} + +test fn dre_a_declared_int_return_is_the_int_binding_holds() -> Bool { + match dre_declared_return_of(o: dre_int_declared_bool_body()) { + Absent => false + Present { value: ret } => + match ret.kind { + TypeNode { connective: Atom { identity: s } } => s == ^dag_binding_type_int + _ => false + } + } +} From a13d5071f6b9b56c92313563eb159bacf010aa0c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 17:41:12 +0000 Subject: [PATCH 33/90] An ill-typed fixture the skipped check was hiding; the facts-key conflict narrows to a shared key Two consequences of making the declared-position judge see a declared Bool, both of which are the repair working rather than breaking something. (1) A FIXTURE IN THIS LANE WAS ILL-TYPED AND ONLY PASSED BECAUSE THE CHECK WAS SKIPPED. fps_two_field_source built `fn f(b: Box) -> Int { b.flag }` for the Bool field -- a Bool body at an Int return. It inferred because the declared Bool could not be denoted, so the comparison never ran; once it ran, the mismatch refused and fps_two_fields_of_different_types_ground_differently went red. The row had been resting on a defect, not on the property it claims. The declared return now tracks the projected field, so the projection's grounding is what is under test rather than a refusal. The property is unchanged: two fields of different declared types, projected from the same receiver, ground differently. This is worth stating plainly because it is the general risk of the repair: any fixture that declared a Bool position it did not honour was being accepted. This one was in this lane's own file. (2) THE FACTS-KEY CONFLICT NARROWS, AND THE ROWS ARE REWRITTEN AT THE STANDING RATHER THAN AT THE OLD CONCLUSION. Two rows asserted that entries sharing one key DISAGREE on grounding. They no longer do: every entry under the shared key now reports grounding derived. The underived ones were the named-call sites this lane has since repaired, so the conflict those rows observed was DOWNSTREAM of two defects rather than intrinsic to the keying relation. What survives is the row that was always the structural finding: MORE THAN ONE ENTRY CARRIES ONE KEY, in two fixtures, so it is a property of the relation over ordinary programs. The relation still admits a conflict, because facts_map_from_entries checks each entry's own subject correspondence and establishes no uniqueness or conflict condition ACROSS entries that compare equal -- today's agreement is a property of this corpus, not a guarantee. The rewritten rows do NOT claim a consumer is currently misled (DESIGN 4d): the observed misleading consumer was eval refusing at a node that was not its subject, and that refusal is gone. They assert the agreement, which is falsifiable and goes red the moment a conflict reappears -- the transition the identity owner needs. A row asserting a disagreement that no longer happens would never report anything again. field projection 16/16, reference evidence 11/11, named-call eval 10/10, match-binder 11/11, named-call execution 2/2, declared parameter order 7/7, facts-key 4/4. Co-Authored-By: Claude Opus 5 (1M context) --- .../synthetic_facts_key_collision_test.dag | 36 +++++++++++++------ .../field_projection_stages_test.dag | 17 ++++++++- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag index f7574e47ecd..dd0233ea2b5 100644 --- a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag +++ b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag @@ -119,20 +119,36 @@ test fn sfk_more_than_one_entry_shares_one_key_holds() -> Bool { sfk_count_under_key(o: cref_source()) > 1 } -// AND THEY DISAGREE. At least one derived and at least one not, under the same key -- so the relation -// admits conflicting facts for one key, and the answer a consumer gets is decided by entry order -// rather than by its subject. This is the claim the identity owner needs: it establishes a CONFLICT -// rather than merely a coincidence of shape. -test fn sfk_entries_under_one_key_disagree_on_grounding_holds() -> Bool { +// AND THEY NOW AGREE, WHICH NARROWS THE FINDING WITHOUT DISSOLVING IT. This row asserted a DISAGREEMENT +// -- at least one derived entry and at least one underived one under the same key -- and that was the +// standing when it was written. It is no longer: every entry under the shared key reports grounding +// derived. The underived entries were the named-call sites this lane has since repaired (a reference read +// from the AUTHORED symbol_index whose return spelling would not denote, and a callee arrow read +// facts-blind so no argument was judged), so the conflict this row observed was DOWNSTREAM of two defects +// rather than intrinsic to the keying relation. +// +// WHAT IS STILL TRUE IS THE ROW ABOVE: more than one entry carries one key. The relation still admits a +// conflict, because facts_map_from_entries checks each entry's own subject correspondence and establishes +// NO uniqueness or conflict condition across entries that compare equal -- so today's agreement is a +// property of this corpus, not a guarantee about the relation. +// +// SO THE CLAIM IS WRITTEN AT THE STANDING RATHER THAN AT THE SAFETY CONCLUSION (DESIGN section 4d: do not +// assert as deduced what is only inferred). It does NOT say a consumer is currently misled -- the +// observed misleading consumer was eval's refusal at a node that was not its subject, and that refusal is +// gone. It says the entries under one key agree today, which is falsifiable and goes red the moment a +// conflict reappears. That transition is the thing the identity owner needs to be told about, and a row +// asserting a disagreement that no longer happens would never tell anyone anything again. +test fn sfk_entries_under_one_key_currently_agree_on_grounding_holds() -> Bool { let total = sfk_count_under_key(o: cref_source()) let derived = sfk_count_derived_under_key(o: cref_source()) - (derived > 0) && (derived < total) + (total > 1) && (derived == total) } -// THE SAME CONFLICT STANDS IN THE SECOND FIXTURE, so it is a property of the keying relation over -// ordinary programs and not an artifact of one source text. -test fn sfk_the_conflict_is_not_specific_to_one_fixture_holds() -> Bool { +// THE SHARED KEY IS NOT AN ARTIFACT OF ONE SOURCE TEXT, which is the part of the original pair that +// survives: a second fixture reaches the same collision, so it is a property of the keying relation over +// ordinary programs. The agreement is asserted here too, for the same reason as above. +test fn sfk_the_shared_key_is_not_specific_to_one_fixture_holds() -> Bool { let total = sfk_count_under_key(o: cref_bool_pair_source()) let derived = sfk_count_derived_under_key(o: cref_bool_pair_source()) - (total > 1) && (derived > 0) && (derived < total) + (total > 1) && (derived == total) } diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag index 634f1957a38..fb056e64e31 100644 --- a/src/v2/test/claim/field_projection/field_projection_stages_test.dag +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -187,8 +187,23 @@ test fn fps_a_valid_field_projection_grounds_holds() -> Bool { // differ, and `.tree` must further agree with how the compiler types an Int-declared PARAMETER in an // unrelated fixture -- an independent route to the same answer, so the claim is not the implementation // compared with itself. +// THE DECLARED RETURN TRACKS THE PROJECTED FIELD, AND IT HAS TO. This fixture used to declare `-> Int` +// for both fields, so the `flag` case was `fn f(b: Box) -> Int { b.flag }` -- an ILL-TYPED program, a +// Bool body at an Int return. It inferred anyway because v2.compiler.infer's declared-position judge +// could not denote a declared Bool (it arrives as v2.std.logic bool_node, not as a binding, so the +// binding->value-type join answered Absent and the comparison was SKIPPED). Once that gate consulted +// infer_established_value_type_optional the mismatch refused, correctly, and this row went red -- so the +// row had been resting on a defect, not on the property it claims. +// +// The property is unchanged: two fields of DIFFERENT declared types, projected from the same receiver, +// must ground differently. Only the fixture is now well-typed, which is what lets the projection's +// grounding be the thing under test rather than the refusal. fn fps_two_field_source(field: String) -> Outcome { - fps_assemble(src: "module p\n\ntype Box {\n tree: Int\n flag: Bool\n}\n\nfn f(b: Box) -> Int {\n b." + field + "\n}\n") + fps_assemble(src: "module p\n\ntype Box {\n tree: Int\n flag: Bool\n}\n\nfn f(b: Box) -> " + fps_field_declared_type(field: field) + " {\n b." + field + "\n}\n") +} + +fn fps_field_declared_type(field: String) -> String { + if field == "flag" { "Bool" } else { "Int" } } fn fps_inferred_of(o: Outcome) -> Optional { From 0f9d7159246441a973fc3dbabf67d3aadf28dfe8 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 19:33:17 +0000 Subject: [PATCH 34/90] The arm-pattern reader asked the construct encoding instead of re-deriving its tag gunbc#12714 made a construct tag a declaration reference: a constructor pattern's FIRST edge now carries the fixed v2.std.node_query construct_tag_marker() and its TARGET is the tag's reference. infer_coproduct_arm_pattern read the first edge's LABEL as the variant tag, which was true of the encoding before that change, so after it every constructor arm looked up the MARKER as a variant name. THE WAY IT FAILED IS THE PART WORTH KEEPING. Three positive rows went red, visibly. But mbt_a_variant_the_coproduct_does_not_declare_refuses KEPT PASSING, for the wrong reason: it wants an undeclared variant to refuse, and every variant had just become undeclared. A green row asserting exactly the property that had broken. So the reader now asks construct_tag_path_optional -- the encoding's own reader -- and takes the tag from the path it returns. That is the repair, not a refactor: a consumer that re-derives the tag from edge positions is a second, positional authority for the encoding (DESIGN section 3), and it decays SILENTLY the moment the encoding moves, which is exactly what happened. Also in this merge: #12714 states it unblocks 01_tokenize, which this lane had reported as the seven's FIRST native-route refusal (a qualified `Rejected { ... }` construction 01_tokenize could not lower). That is a different route from the infer-side frontier this lane owns, so whether the seven now reach further natively is a measurement for the native lane and is not claimed here. Seven sets, 61 claims, zero red: reference evidence 11/11, named-call eval 10/10, field projection 16/16, match-binder typing 11/11, named-call execution 2/2, declared parameter order 7/7, facts-key 4/4. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/04_infer.dag | 43 ++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 7215c259a30..4e1f4e174f4 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -164,6 +164,7 @@ import v2.std.type_binder { GenericTypeDecl, OpaqueTypeDecl, PlainTypeDecl, Type import v2.std.coercion { coercion_cast_crossing } import v2.std.node_query { FieldProjection, + construct_tag_path_optional, declared_field_named, field_projection_optional, find_named_child, @@ -2237,11 +2238,23 @@ fn infer_match_field_type_instantiated(field_type: Node, instances: List } | ArmPatternWildcard @@ -2259,21 +2272,17 @@ fn infer_coproduct_arm_pattern(pat: Node) -> InferCoproductArmPattern { Absent => ArmPatternUnread } Absent => - match pat.kind { - TypeNode { connective: Conj } => - match list_at_optional(xs: pat.children, index: 0) { + match construct_tag_path_optional(n: pat) { + Absent => ArmPatternUnread + Present { value: path } => + match qualified_name_last_segment(qn: path) { Absent => ArmPatternUnread - Present { value: tag_edge } => - match tag_edge.label { - Named { name: tag } => - match list_tail(xs: pat.children) { - TailFound { tail: fields } => ArmPatternVariant { tag: tag, fields: fields } - TailAbsent => ArmPatternVariant { tag: tag, fields: Empty } - } - Positional => ArmPatternUnread + Present { value: tag } => + match list_tail(xs: pat.children) { + TailFound { tail: fields } => ArmPatternVariant { tag: tag, fields: fields } + TailAbsent => ArmPatternVariant { tag: tag, fields: Empty } } } - _ => ArmPatternUnread } } } From 797a6082a58fd57e66083173f6fa2b1993dd0ecf Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 21:57:26 +0000 Subject: [PATCH 35/90] A loop carrier is a binder: one value-binder admission, and the Loop walk that uses it THE SLICE, DIAGNOSED WITH SUPPLIED NODES RATHER THAN A NATIVE RUN. The eight in v2.test.parse.expression_bodied_fn_decl_parse refuse at prepare on `found`, the first binder of `fold(root.children, init: false, f: fn(found, e) { found || ... })`. All four candidate causes hold at once, from one root: Arrow absent -- v2.compiler.fold_lowering destructures the step literal into an iteration body and ONE carrier symbol (fold_call_seam_from_step) and builds a Loop. body_lower_function_value_arrow is never reached; the code says so itself. domain lost -- `fn(found, e)` has two binders; the seam keeps one carrier symbol and drops `e` entirely. scope unopened -- resolve harvested binders in exactly two places, an Arrow domain and a Bind atom. There was NO loop-carrier harvester, and a Loop fell to resolve_children_homogeneous_scope, which opens a frame only for an Arrow domain. lookup bypassed -- so the body's `found` reached the bare-name census and refused as declared in several modules. ONE ADMISSION FOR A VALUE BINDER, whatever spelling introduced it. admit_value_binders returns a VERDICT, not a map, so a refusal is located at the binder and cannot widen into an empty frame -- an empty frame is precisely how `found` came to be read as a global name. It refuses a same-frame duplicate and a binder that hides a visible value binding, which is the discipline the TYPE-parameter frame already had (resolve_reason_type_parameter_shadows_visible_name) now given to value binders. HIDING IS A LEXICAL FACT AND ONLY A LEXICAL FACT. lookup_chain already distinguishes BoundInFrame from BoundAtRoot, and only BoundInFrame refuses -- so a binder spelled like a declaration in another module, imported or not, is admitted. An unrelated declaration elsewhere in the corpus may not decide whether a local binder is legal, which is the defect this slice was opened on. Row (7) is the discriminating negative for row (6): were the admission consulting the namespace, both would refuse and the pair would establish nothing. THE LOOP WALK. A Loop now opens its carrier's frame, and the carrier and the bound MEASURE are carried unwalked -- v2.std.node's own role model says the carrier "targets the loop-carried state's BINDER, never a value" and the bound edge targets "the termination measure", so asking the scope to answer for either is the category error the Arrow's declared-order edge already avoids. The carrier is found through loop_edge_role, not by edge position, so this walk is not a second place that knows the edge's spelling. TWO HONEST DEBTS, STATED. A measure that is a real expression rather than the marker ^dag_surface_fold_iteration_measure would need resolving and this walk does not do it; no producer emits one today and the trigger is the first that does. And the pairing obligation for this slice is the NATIVE EIGHT, not a source fixture here: a fold cannot be written in a bare single-module fixture at all -- List, a list literal and `fold` itself are each unbound without imports, and declaring an import turns the file's bare-reference channel off, changing the very resolution under test. Three fixtures were tried and all three refused before reaching a binder. That debt is recorded in the file rather than papered over with a green row. COST, BECAUSE THIS LANE WAS REFUSED FOR IT. Ten rows: eight between 77 and 204 eval steps, two at ~1.6k, against the 72,300 new-witness budget. The two walk rows were 100k until they stopped building the real grammar for a context the walk never consults (void_language_model) -- the same reach, found the same way. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/03_resolve.dag | 159 +++++++++++ .../callable_binder_slice_test.dag | 255 ++++++++++++++++++ 2 files changed, 414 insertions(+) create mode 100644 src/v2/test/claim/binder_admission/callable_binder_slice_test.dag diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 70e6251326f..5b2caf9d726 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -129,6 +129,10 @@ import v2.std.node { construct_tag_marker, symbol_eq, well_formed, + Loop, + LoopBoundMeasure, + LoopCarrierBinder, + loop_edge_role, } import v2.std.type_binder { edge_is_cast_target, edge_is_type_annotation, edge_is_type_params, type_binder_first_mislabelled, type_binder_labels_conform, type_param_names } import v2.std.node_query { @@ -263,6 +267,159 @@ fn direct_atom_binding_fold(base: Map) -> NodeFold } + | BinderDuplicateInFrame { binder: Symbol } + | BinderHidesVisibleValue { binder: Symbol } + +fn admit_value_binders(names: List, outer: Scope) -> BinderAdmission { + fold(names, init: BindersAdmitted { locals: empty_map() }, f: fn(acc, name) { + match acc { + BinderDuplicateInFrame { binder: b } => BinderDuplicateInFrame { binder: b } + BinderHidesVisibleValue { binder: b } => BinderHidesVisibleValue { binder: b } + BindersAdmitted { locals: m } => + match v2.std.collection.map_lookup(m: m, key: name) { + Present { value: _ } => BinderDuplicateInFrame { binder: name } + Absent => admit_one_value_binder(locals: m, name: name, outer: outer) + } + } + }) +} + +fn admit_one_value_binder(locals: Map, name: Symbol, outer: Scope) -> BinderAdmission { + match lookup_chain(s: outer, name: name) { + Rejected { diagnostics: _ } => BindersAdmitted { locals: map_insert(locals, name, name) } + Accepted { value: bound, diagnostics: _ } => + match bound { + BoundInFrame { canonical: _ } => BinderHidesVisibleValue { binder: name } + BoundAtRoot { canonical: _ } => BindersAdmitted { locals: map_insert(locals, name, name) } + ScopeUnbound => BindersAdmitted { locals: map_insert(locals, name, name) } + } + } +} + +fn binder_duplicate_in_frame_diagnostic(binder: Symbol, at: Node) -> Diagnostic { + Diagnostic { + reason: ^resolve_reason_binder_duplicate_in_frame, + at: node_locus(node: at), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +fn binder_hides_visible_value_diagnostic(binder: Symbol, at: Node) -> Diagnostic { + Diagnostic { + reason: ^resolve_reason_binder_hides_visible_value, + at: node_locus(node: at), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +// A LOOP'S CARRIER IS A BINDER, SO IT OPENS A FRAME AND IS NOT WALKED AS A REFERENCE. v2.std.node says +// it outright -- "^loop_carrier_edge targets the loop-carried state's BINDER, never a value" -- and +// resolve did neither: a Loop fell to resolve_children_homogeneous_scope, which opens a frame only for +// an Arrow domain, so the carrier bound nothing and the iterated body's uses of it reached the bare-name +// census. That is the whole of the `found` refusal in `fold(xs, init: false, f: fn(found, e) { ... })`: +// v2.compiler.fold_lowering destructures the step literal into an iteration body and ONE carrier symbol +// and builds this Loop, so the step's Arrow is never constructed and its second binder is dropped. +// +// The carrier edge is carried UNWALKED for the same reason the Arrow's declared-order edge is +// (resolve_arrow_node_in): its target names a binder, not a reference, so resolving it would ask the +// scope to answer for a name the scope is being opened to introduce. +fn resolve_loop_node(ctx: ResolveContext, n: Node) -> ResolveNodeWalk { + match resolve_loop_carrier_binder(n: n) { + Absent => resolve_children_homogeneous_scope(ctx: ctx, n: n) + Present { value: carrier } => + let binder = carrier.node + match Present { value: carrier.name } { + Absent => resolve_children_homogeneous_scope(ctx: ctx, n: n) + Present { value: name } => + match admit_value_binders(names: [name], outer: ctx.scope) { + BinderDuplicateInFrame { binder: b } => + resolve_walk_refused_one(chain: diagnostics_singleton(d: binder_duplicate_in_frame_diagnostic(binder: b, at: binder))) + BinderHidesVisibleValue { binder: b } => + resolve_walk_refused_one(chain: diagnostics_singleton(d: binder_hides_visible_value_diagnostic(binder: b, at: binder))) + BindersAdmitted { locals: locals } => + resolve_loop_children( + ctx: resolve_ctx_with_scope(ctx: ctx, scope: ScopeFrame { locals: locals, outer: ctx.scope }), + n: n + ) + } + } + } +} + +// THE CARRIER IS FOUND BY ITS ROLE, not by a named-edge lookup: v2.std.node loop_edge_role is the one +// reader of what each of a Loop's edges means, and asking it here keeps this walk from being a second +// place that knows the edge's spelling. (The named-edge lookup would also drag NamedEdgeTargetLookup's +// own `Absent` into this module, where it collides with Optional's.) +type ResolveLoopCarrier { + name: Symbol + node: Node +} + +fn resolve_loop_carrier_binder(n: Node) -> Optional { + fold(n.children, init: Absent, f: fn(acc, e) { + match acc { + Present { value: found } => Present { value: found } + Absent => + match loop_edge_role(e: e) { + LoopCarrierBinder => + match e.target.kind { + TypeNode { connective: Atom { identity: id } } => + Present { value: ResolveLoopCarrier { name: id, node: e.target } } + _ => Absent + } + _ => Absent + } + } + }) +} + +// A LOOP'S BINDER AND ITS MEASURE ARE CARRIED UNWALKED; ITS BODY AND ITS DOMAIN ARE RESOLVED. This is +// v2.std.node's own role model read back: the carrier "targets the loop-carried state's BINDER, never a +// value", and the bound edge targets "the termination measure". Neither is a value reference, so asking +// the scope to answer for them is the same category error the Arrow's declared-order edge avoids +// (resolve_arrow_node_in carries it unwalked for exactly this reason). The fold seam's measure is the +// marker ^dag_surface_fold_iteration_measure, which names no value and resolved to nothing -- that +// refusal, not the carrier, is what a Loop walk hit first once the carrier's frame existed. +// +// DECLARED FRONTIER: a measure that is a real expression rather than a marker would need resolving, and +// this walk does not do it. No producer emits one today (v2.compiler.fold_lowering +// fold_iteration_measure_atom is the only measure in the corpus), and the trigger is the first producer +// that emits a computed measure -- at which point the measure gets a role-specific walk rather than +// being carried. +fn resolve_loop_children(ctx: ResolveContext, n: Node) -> ResolveNodeWalk { + child_walk_node(n: n, w: fold(n.children, init: child_walk_init(), f: fn(acc, e) { + match loop_edge_role(e: e) { + LoopCarrierBinder => + child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None }) + LoopBoundMeasure => + child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None }) + _ => child_walk_step(w: acc, e: e, r: resolve_node_walk(ctx: ctx, n: e.target)) + } + })) +} + fn harvest_conj_named_bindings(root: Node, acc: Map) -> Map { match root.kind { TypeNode { connective: Conj } => @@ -1902,6 +2059,8 @@ fn resolve_node_walk( resolve_bind_node(ctx: ctx, n: n) ComputationNode { behavior: Match } => resolve_match_node_walk(ctx: ctx, n: n) + ComputationNode { behavior: Loop } => + resolve_loop_node(ctx: ctx, n: n) ComputationNode { behavior: Transform } => resolve_transform_children(ctx: ctx, n: n) TypeNode { connective: Conj } => diff --git a/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag new file mode 100644 index 00000000000..34f06feefb5 --- /dev/null +++ b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag @@ -0,0 +1,255 @@ +module v2.test.claim.binder_admission.callable_binder_slice + +import v2.compiler.resolve { + BinderAdmission, + BinderDuplicateInFrame, + BinderHidesVisibleValue, + BindersAdmitted, + Namespace, + Scope, + ScopeFrame, + ScopeRoot, + add_arrow_domain_named_params, + admit_value_binders, + empty_canonical_symbol_set, + empty_namespace, + ResolveContext, + ResolveNodeWalk, + ResolveWalkAccepted, + ResolveWalkRefused, + resolve_node_walk, + resolve_loop_carrier_binder, +} +import v2.std.resolution_policy { default_name_resolution_policy } +import v2.compiler.fold_lowering { fold_call_seam_loop } +import v2.std.collection { Map, empty_map, map_insert, map_lookup } +import std.algebra { Cons, Empty } +import v2.std.language_model { void_language_model } +import v2.std.text { String } +import v2.std.symbol_index { empty_symbol_index } +import v2.std.node { + Arrow, + Atom, + ComputationNode, + Conj, + Edge, + Named, + Node, + Positional, + TypeNode, + +} +import v2.std.optional { Absent, Present } +import v2.std.logic { Bool } +import std.occurrence_identity { OccurrenceSynthetic } + +// THE CALLABLE-BINDER SLICE, READ AT ONE INTERFACE EACH, WITH SUPPLIED NODES. Every claim here +// constructs the node it asks about. None assembles source, resolves a corpus or runs infer: the +// subject is what a binder-harvesting function answers for a node of a given shape, so executing the +// front end to obtain that node would re-run production the claim is not about (DESIGN section 3 +// witness rule) -- and it is exactly the reach whose cost refused this lane's other claim sets at the +// enrolment margin. +// +// WHAT THE SLICE ESTABLISHES. `fold(xs, init: false, f: fn(found, e) { found || ... })` refuses at +// resolve with resolve_unbound_name_is_declared_in_several_modules on `found`. The rows below locate +// that in the lowering rather than in resolve's binder model: the Arrow path admits its binders, and +// the fold seam the step is lowered to carries none. + +fn cbs_atom(id: Symbol) -> Node { + Node { kind: TypeNode { connective: Atom { identity: id } }, children: [], occurrence_id: OccurrenceSynthetic } +} + +// An Arrow shaped the way a fn literal's is (v2.compiler.body_lowering_fold +// body_lower_function_value_arrow): a domain Conj of Named binders, then the codomain twice. +fn cbs_fn_literal_shaped_arrow() -> Node { + Node { + kind: TypeNode { connective: Arrow }, + children: [ + Edge { + label: Positional, + target: Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Named { name: ^found }, target: cbs_atom(id: ^tv_found) }, + Edge { label: Named { name: ^e }, target: cbs_atom(id: ^tv_e) } + ], + occurrence_id: OccurrenceSynthetic + } + }, + Edge { label: Positional, target: cbs_atom(id: ^tv_ret) }, + Edge { label: Positional, target: cbs_atom(id: ^tv_ret) } + ], + occurrence_id: OccurrenceSynthetic + } +} + +fn cbs_admits(locals: Map, name: Symbol) -> Bool { + match map_lookup(m: locals, key: name) { + Present { value: _ } => true + Absent => false + } +} + +fn cbs_arrow_locals() -> Map { + add_arrow_domain_named_params(n: cbs_fn_literal_shaped_arrow(), acc: empty_map()) +} + +// (1) THE ARROW PATH ADMITS BOTH BINDERS. So resolve's binder model is not the defect: a callable +// whose domain carries Named binders opens a frame holding every one of them. +test fn cbs_the_arrow_path_admits_both_binders_holds() -> Bool { + cbs_admits(locals: cbs_arrow_locals(), name: ^found) + && cbs_admits(locals: cbs_arrow_locals(), name: ^e) +} + +// (2) AND IT ADMITS ONLY WHAT THE DOMAIN DECLARES, so row (1) is not a function that answers true for +// every name. +test fn cbs_the_arrow_path_admits_no_undeclared_name_holds() -> Bool { + !cbs_admits(locals: cbs_arrow_locals(), name: ^unrelated) +} + +// (3) THE FOLD SEAM ADMITS NEITHER BINDER -- THE DISCRIMINATING RED FOR THIS SLICE. The step literal +// is destructured into an iteration body and ONE carrier symbol (v2.compiler.fold_lowering +// fold_call_seam_from_step), and the seam it builds is a Loop, not an Arrow. add_arrow_domain_named_params +// answers the empty map for it because it is not an Arrow at all, and resolve harvests binders in +// exactly two places -- an Arrow's domain and a Bind's atom -- with NO loop-carrier harvester. So the +// body's `found` reaches the bare-name census and refuses as declared in several modules. +// +// THIS ROW FLIPS WHEN THE CONSOLIDATION LANDS: once the step lowers through the one callable Arrow +// constructor and loop carriers go through the one binder-admission operation, the seam admits its +// binders and this row must be rewritten to assert that, not deleted (DESIGN section 4b(4)). +test fn cbs_the_fold_seam_admits_no_binder_today_holds() -> Bool { + let seam_locals = add_arrow_domain_named_params( + n: fold_call_seam_loop(body: cbs_atom(id: ^step_body), carrier: ^found), + acc: empty_map() + ) + !cbs_admits(locals: seam_locals, name: ^found) + && !cbs_admits(locals: seam_locals, name: ^e) +} + +// THE ADMISSION'S THREE RULES, each at its own interface with a supplied Scope. The scopes are +// constructed, not obtained by resolving a corpus: the subject is what admit_value_binders answers for +// a (names, scope) pair. +fn cbs_root_scope() -> Scope { + ScopeRoot { module: empty_namespace() } +} + +fn cbs_root_scope_declaring(name: Symbol) -> Scope { + ScopeRoot { + module: Namespace { + bindings: map_insert(empty_map(), name, name), + canonical_symbols: empty_canonical_symbol_set(), + symbol_index: empty_symbol_index(), + module_qn: Empty, + test_code: test_code_index_empty(), + declared_in: Empty, + imported_origins: empty_map() + } + } +} + +fn cbs_frame_binding(name: Symbol) -> Scope { + ScopeFrame { locals: map_insert(empty_map(), name, name), outer: cbs_root_scope() } +} + +fn cbs_admitted_has(a: BinderAdmission, name: Symbol) -> Bool { + match a { + BindersAdmitted { locals: m } => cbs_admits(locals: m, name: name) + BinderDuplicateInFrame { binder: _ } => false + BinderHidesVisibleValue { binder: _ } => false + } +} + +// (4) A CARRIER IS ADMITTED. The positive control for the Loop repair: the name a fold seam carries +// opens a frame instead of falling through to the global lookup. +test fn cbs_a_loop_carrier_name_is_admitted_holds() -> Bool { + cbs_admitted_has(a: admit_value_binders(names: [^found], outer: cbs_root_scope()), name: ^found) +} + +// (5) SAME-FRAME DUPLICATES REFUSE. +test fn cbs_a_same_frame_duplicate_refuses_holds() -> Bool { + match admit_value_binders(names: [^found, ^found], outer: cbs_root_scope()) { + BinderDuplicateInFrame { binder: b } => b == ^found + BindersAdmitted { locals: _ } => false + BinderHidesVisibleValue { binder: _ } => false + } +} + +// (6) A BINDER HIDING A VISIBLE VALUE BINDING REFUSES. `found` is already bound by an enclosing FRAME. +test fn cbs_a_binder_hiding_an_enclosing_binder_refuses_holds() -> Bool { + match admit_value_binders(names: [^found], outer: cbs_frame_binding(name: ^found)) { + BinderHidesVisibleValue { binder: b } => b == ^found + BindersAdmitted { locals: _ } => false + BinderDuplicateInFrame { binder: _ } => false + } +} + +// (7) AND A NAME DECLARED AT THE MODULE ROOT IS STILL ADMITTED -- the clause that keeps unrelated +// declarations elsewhere in the corpus from deciding whether a local binder is legal. This is the +// discriminating negative for row (6): if the admission consulted the namespace rather than the lexical +// frames, (6) and (7) would both refuse and the pair would establish nothing. +test fn cbs_a_binder_spelled_like_a_root_declaration_is_admitted_holds() -> Bool { + cbs_admitted_has( + a: admit_value_binders(names: [^found], outer: cbs_root_scope_declaring(name: ^found)), + name: ^found + ) +} + +// THE PAIRING OBLIGATION FOR THIS SLICE IS THE NATIVE EIGHT, NOT A SOURCE FIXTURE HERE, and that is a +// measured conclusion rather than a preference. A fold cannot be written in a bare single-module fixture +// at all: `List`, a list literal and `fold` itself are each unbound without imports, and DECLARING an +// import turns the file's bare-reference channel off (gunbc.recurring_failure_mode +// bare_reference_channel_declines_a_pull_in_silence), which changes the very name resolution under test. +// Three fixtures were tried and all three refused before reaching any binder. +// +// So the claim that the real producer emits this shape is +// `//v2/test/parse/expression_bodied_fn_decl_parse:all` run natively -- the eight whose refusal opened +// this slice, whose own `fold(root.children, init: false, f: fn(found, e) { found || ... })` is the +// producer in question. Until that runs green the rows above are readings of a boundary, and this +// comment is where that debt is recorded rather than in a green row that would imply otherwise. + +// (8) THE WALK BINDS THE CARRIER -- the repair, exercised at resolve's own interface with a supplied Loop +// and a supplied context. Before it, a Loop fell to resolve_children_homogeneous_scope, opened no frame, +// and the iterated body's use of the carrier reached the bare-name census. +// THE CONTEXT CARRIES A VOID LANGUAGE MODEL, not the dag one. The Loop walk consults the scope chain and +// nothing else, so building the real grammar would be paid by every row here for no discrimination -- and +// this file exists partly because that reach is what refused this lane's other claim sets at the +// enrolment margin. If a future arm of this walk does consult the model, these rows must carry the real +// one and say so. +fn cbs_ctx(scope: Scope) -> ResolveContext { + ResolveContext { + scope: scope, + type_scope: scope, + namespace: empty_namespace(), + policy: default_name_resolution_policy(), + position: Empty, + lm: void_language_model(identity: ^cbs_void_language), + under_module_root: false + } +} + +fn cbs_walk_accepts(n: Node) -> Bool { + match resolve_node_walk(ctx: cbs_ctx(scope: cbs_root_scope()), n: n) { + ResolveWalkAccepted { value: _, diagnostics: _ } => true + ResolveWalkRefused { first: _, rest: _, observation: _ } => false + } +} + +test fn cbs_a_loop_body_use_of_the_carrier_resolves_holds() -> Bool { + cbs_walk_accepts(n: fold_call_seam_loop(body: cbs_atom(id: ^found), carrier: ^found)) +} + +// (9) THE DISCRIMINATING NEGATIVE. The same Loop whose body uses a name the carrier does NOT introduce +// still refuses, so row (8) is not a frame that answers for everything. +test fn cbs_a_loop_body_use_of_an_unrelated_name_still_refuses_holds() -> Bool { + !cbs_walk_accepts(n: fold_call_seam_loop(body: cbs_atom(id: ^unrelated_name), carrier: ^found)) +} + +// (10) THE CARRIER IS FOUND BY ITS ROLE. Row (8) would also pass if the reader found the carrier by edge +// position; this asserts the role reader answers, which is what keeps this walk from being a second place +// that knows the edge's spelling. +test fn cbs_the_carrier_is_read_by_its_role_holds() -> Bool { + match resolve_loop_carrier_binder(n: fold_call_seam_loop(body: cbs_atom(id: ^found), carrier: ^found)) { + Present { value: c } => c.name == ^found + Absent => false + } +} From 978cf66524c4d86d090db22274593a94f08e2d5d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 22:31:05 +0000 Subject: [PATCH 36/90] Value binders are admitted at one gate: Arrow parameters, lets, match arms, loop carriers The ruling's second directive. admit_value_binders already existed for the loop carrier; this routes the other three binder sites through it, so "may this name be introduced here" is answered in one place for every spelling that introduces a value binder. THE GATE IS SEPARATE FROM THE FRAME, DELIBERATELY. Each site keeps the frame its own harvester already built, and the gate only decides whether the site proceeds. So an ADMITTED binder resolves exactly as it did before this change, and the new behaviour is purely a set of refusals -- no chance of silently changing which name a body binds to while adding a check. That is also why the gate returns a verdict carrying a located diagnostic rather than a map. NAMES ARE COLLECTED AS LISTS, because a map cannot answer the question. map_insert overwrites, so a frame built by insertion cannot distinguish a duplicate binder from a single one -- and the duplicate is exactly what must refuse. Three collectors mirror the three harvesters. ONE COLLECTOR IS DELIBERATELY NARROWER THAN ITS HARVESTER, and it is stated on the declaration so it is not read as an oversight. An Arrow's judged names are the AUTHORED Named binders of its domain Conj only. add_arrow_domain_named_params additionally sweeps every Atom in the domain subtree, which admits the fresh type-variable atoms a lowered signature carries as if they were value binders; those are not what "an Arrow's parameters" means, and running the hiding check over them would refuse on generated names no author wrote. The harvested frame is untouched; only the JUDGEMENT is scoped. The match-arm collector mirrors resolve_pattern_binders including both its exclusions: a wildcard binds nothing, and a field target naming a CONSTRUCTOR is a nested pattern rather than a binder. WHAT THIS DOES NOT YET ESTABLISH. claim_batch resolves .dag with the SEED, so compiling these changes typechecks them and nothing more; the ten rows in v2.test.claim.binder_admission.callable_binder_slice do execute v2's resolve, because they call resolve_node_walk directly with supplied nodes and a supplied context. The corpus-wide consequence of refusing a hidden binder is visible only where v2's resolve COMPILES the corpus -- the native route. So the blast radius and the pairing obligation for this slice are the same measurement, and it is the ruling's last step rather than something this commit can claim. Ten rows green, 77 to 1,588 eval steps against the 72,300 budget. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/03_resolve.dag | 187 +++++++++++++++++++++++++++------ 1 file changed, 156 insertions(+), 31 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 5b2caf9d726..daca2854a10 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -334,6 +334,113 @@ fn binder_hides_visible_value_diagnostic(binder: Symbol, at: Node) -> Diagnostic } } +// THE NAMES EACH BINDER SITE INTRODUCES, as LISTS rather than maps, because a map cannot answer the +// question the admission asks. map_insert silently overwrites, so a frame built by insertion cannot tell +// a duplicate binder from a single one -- the duplicate is exactly what must refuse. These three +// collectors mirror the three harvesters beside them, and each is deliberately NARROWER in one respect, +// stated here so the narrowing is not mistaken for an oversight: +// +// An Arrow's list is the AUTHORED Named binders of its domain Conj only. The harvester also sweeps every +// Atom in the domain subtree (harvest_direct_atom_binding), which admits the fresh type-variable atoms a +// lowered signature carries as if they were value binders. Those are not what "an Arrow's parameters" +// means, and running the hiding check over them would refuse on generated names no author wrote. The +// harvested frame is left exactly as it was; only the JUDGEMENT is scoped to the authored binders. +fn arrow_domain_binder_name_list(n: Node) -> List { + match n.kind { + TypeNode { connective: Arrow } => + match arrow_first_positional_target(children: n.children) { + FirstPositionalFound { target: domain } => conj_named_binder_name_list(root: domain) + FirstPositionalAbsent => [] + } + _ => [] + } +} + +fn conj_named_binder_name_list(root: Node) -> List { + match root.kind { + TypeNode { connective: Conj } => + fold(root.children, init: [], f: fn(acc, e) { + match try_edge_declared_binding(e: e) { + Present { value: b } => list_snoc_item(xs: acc, item: b.name) + Absent => acc + } + }) + _ => [] + } +} + +// A match arm's list mirrors resolve_pattern_binders exactly, including its two exclusions: a wildcard +// binds nothing, and a field target that names a CONSTRUCTOR is a nested pattern, not a binder. +fn pattern_binder_name_list(ctx: ResolveContext, pat: Node) -> List { + match construct_tag_optional(n: pat) { + Absent => [] + Present { value: _ } => + fold(construct_field_edges(n: pat), init: [], f: fn(acc, e) { + match e.target.kind { + TypeNode { connective: Atom { identity: id } } => + if is_wildcard_pattern(pattern: e.target) { + acc + } else if resolve_pattern_atom_names_constructor(ctx: ctx, id: id) { + acc + } else { + list_snoc_item(xs: acc, item: id) + } + _ => concat(acc, pattern_binder_name_list(ctx: ctx, pat: e.target)) + } + }) + } +} + +// A `let`'s list mirrors add_bind_atom_binder: the binder is the FIRST positional child, and the harvester +// sweeps the atoms of that subtree. For an ordinary `let x = e` that subtree is the single binder atom. +fn bind_binder_name_list(n: Node) -> List { + match n.kind { + ComputationNode { behavior: Bind } => + if positional_child_count(children: n.children) == 3 { + match first(positional_edges(children: n.children)) { + Present { value: e0 } => atom_name_list(root: e0.target) + Absent => [] + } + } else { + [] + } + _ => [] + } +} + +fn atom_name_list(root: Node) -> List { + fold_node( + n: root, + algebra: NodeFold { + init: fn(n0) { + match n0.kind { + TypeNode { connective: Atom { identity: sym } } => [sym] + _ => [] + } + }, + step: fn(acc, _e, child) { concat(acc, child) } + } + ) +} + +// THE ADMISSION AS A GATE OVER A SITE'S OWN FRAME. Each site keeps the frame its harvester already built +// -- so an admitted binder resolves exactly as it did before this change -- and the gate only decides +// whether the site proceeds at all. That separation is deliberate: it makes the new behaviour purely a +// set of refusals, with no chance of silently changing which name a body binds to. +type BinderGate + = BinderGateAdmitted + | BinderGateRefused { diagnostic: Diagnostic } + +fn gate_value_binders(names: List, outer: Scope, at: Node) -> BinderGate { + match admit_value_binders(names: names, outer: outer) { + BindersAdmitted { locals: _ } => BinderGateAdmitted + BinderDuplicateInFrame { binder: b } => + BinderGateRefused { diagnostic: binder_duplicate_in_frame_diagnostic(binder: b, at: at) } + BinderHidesVisibleValue { binder: b } => + BinderGateRefused { diagnostic: binder_hides_visible_value_diagnostic(binder: b, at: at) } + } +} + // A LOOP'S CARRIER IS A BINDER, SO IT OPENS A FRAME AND IS NOT WALKED AS A REFERENCE. v2.std.node says // it outright -- "^loop_carrier_edge targets the loop-carried state's BINDER, never a value" -- and // resolve did neither: a Loop fell to resolve_children_homogeneous_scope, which opens a frame only for @@ -1996,31 +2103,38 @@ fn resolve_match_arm_walk(ctx: ResolveContext, arm: Node) -> ResolveNodeWalk { match find_named_child(root: arm, name: match_arm_pattern) { Rejected { diagnostics: _ } => resolve_children_homogeneous_scope(ctx: ctx, n: arm) Accepted { value: pat, diagnostics: _ } => - let arm_ctx = resolve_ctx_with_scope( - ctx: ctx, - scope: ScopeFrame { - locals: resolve_pattern_binders(ctx: ctx, pat: pat, acc: empty_map()), - outer: ctx.scope - } - ) - child_walk_node(n: arm, w: fold(arm.children, init: child_walk_init(), f: fn(acc, e) { - child_walk_step( - w: acc, - e: e, - r: match e.label { - Named { name: label } => - if label == match_arm_pattern { - resolve_pattern_node_walk(ctx: arm_ctx, pat: e.target) - } else { - resolve_node_walk(ctx: arm_ctx, n: e.target) - } - Positional => resolve_node_walk(ctx: arm_ctx, n: e.target) - } - ) - })) + match gate_value_binders(names: pattern_binder_name_list(ctx: ctx, pat: pat), outer: ctx.scope, at: pat) { + BinderGateRefused { diagnostic: d } => resolve_walk_refused_one(chain: diagnostics_singleton(d: d)) + BinderGateAdmitted => resolve_match_arm_admitted(ctx: ctx, arm: arm, pat: pat) + } } } +fn resolve_match_arm_admitted(ctx: ResolveContext, arm: Node, pat: Node) -> ResolveNodeWalk { + let arm_ctx = resolve_ctx_with_scope( + ctx: ctx, + scope: ScopeFrame { + locals: resolve_pattern_binders(ctx: ctx, pat: pat, acc: empty_map()), + outer: ctx.scope + } + ) + child_walk_node(n: arm, w: fold(arm.children, init: child_walk_init(), f: fn(acc, e) { + child_walk_step( + w: acc, + e: e, + r: match e.label { + Named { name: label } => + if label == match_arm_pattern { + resolve_pattern_node_walk(ctx: arm_ctx, pat: e.target) + } else { + resolve_node_walk(ctx: arm_ctx, n: e.target) + } + Positional => resolve_node_walk(ctx: arm_ctx, n: e.target) + } + ) + })) +} + // children[0] is the scrutinee (outer scope); every later child is an arm. The ordinal advances on // every child, refused or not, so a refused scrutinee never promotes the first arm into its place. fn resolve_match_node_walk(ctx: ResolveContext, n: Node) -> ResolveNodeWalk { @@ -2282,6 +2396,13 @@ fn resolve_arrow_node(ctx: ResolveContext, n: Node) -> ResolveNodeWalk { // enclosing scope, and a minted anonymous label against nothing. It is carried unwalked; the Arrow // wall (v2.std.node arrow_signature_order_conforms) is its check. fn resolve_arrow_node_in(outer_ctx: ResolveContext, ctx: ResolveContext, n: Node) -> ResolveNodeWalk { + match gate_value_binders(names: arrow_domain_binder_name_list(n: n), outer: ctx.scope, at: n) { + BinderGateRefused { diagnostic: d } => resolve_walk_refused_one(chain: diagnostics_singleton(d: d)) + BinderGateAdmitted => resolve_arrow_node_admitted(outer_ctx: outer_ctx, ctx: ctx, n: n) + } +} + +fn resolve_arrow_node_admitted(outer_ctx: ResolveContext, ctx: ResolveContext, n: Node) -> ResolveNodeWalk { let scope_type = resolve_type_param_frame(n: n, outer: ctx.scope) let value_params = add_arrow_domain_named_params(n: n, acc: empty_map()) let scope_codomain = ScopeFrame { locals: value_params, outer: scope_type } @@ -2395,15 +2516,19 @@ fn resolve_bind_node( n: Node ) -> ResolveNodeWalk { if positional_child_count(children: n.children) == 3 { - child_walk_node(n: n, w: resolve_bind_edges( - ctx: ctx, - edges: n.children, - scope_outer: ctx.scope, - scope_inner: ScopeFrame { - locals: add_bind_atom_binder(n: n, acc: empty_map()), - outer: ctx.scope - } - )) + match gate_value_binders(names: bind_binder_name_list(n: n), outer: ctx.scope, at: n) { + BinderGateRefused { diagnostic: d } => resolve_walk_refused_one(chain: diagnostics_singleton(d: d)) + BinderGateAdmitted => + child_walk_node(n: n, w: resolve_bind_edges( + ctx: ctx, + edges: n.children, + scope_outer: ctx.scope, + scope_inner: ScopeFrame { + locals: add_bind_atom_binder(n: n, acc: empty_map()), + outer: ctx.scope + } + )) + } } else { resolve_walk_refused_one(chain: diagnostics_singleton(d: malformed_tree_diagnostic(n: n))) } From 85ad321ac03d69dee0a80d250f936c89143ebe91 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 23:26:16 +0000 Subject: [PATCH 37/90] One callable Arrow constructor; value shadowing refuses, with its incidental fixture corrected The ruling's first two directives, landed together because the second's blast radius falls on a fixture the first also touches. ONE CALLABLE ARROW CONSTRUCTOR. body_lower_callable_arrow decides the callable Arrow's child layout, and THREE callables reach it -- not the two the brief named: the `data` declaration arm was a third authored copy of the same layout. The emitted node is unchanged byte-for-byte on every route. Three caller differences were found to be real and are kept as parameters rather than unified, each because unifying it would have changed a shape or double-walked a body: the domain and order edges arrive already built as Edges (the `data` arm mints them at the type expression's occurrence, not the Arrow shell's, so building the order edge inside the constructor would have moved it); type-parameter edges differ in provenance (a declaration carries the Conj the parse captured, a function value mints one and appends the fresh return type variable for an elided codomain); and the body arrives already positioned for a declaration but unpositioned for a function value, because the declaration's lowering is the only place holding the body root. VALUE SHADOWING REFUSES, AND ONE EXISTING WITNESS WAS RESTING ON IT. The binder gate (previous commit) made v2.test.claim.namespace_xl0.value_position_whole_read's `a_nested_fn_literal_resolves_without_a_type_parameter_shadow_refusal` red. Receipt, measured in a detached worktree at the commit BEFORE the gate: that witness PASSES at 797a6082a58 and refuses after, with reason resolve_reason_binder_hides_visible_value. Its fixture nested `fn(x) { let g = fn(x) { x } ... }`. THE REPAIR IS A RENAME, NOT A REVERSAL, and the distinction is the point. That witness's subject is that a nested literal's fresh TYPE variables are not a shadow of the outer's; the shared value name was INCIDENTAL to it. Reversing the row would have promoted a fixture accident into the specification and deleted a property. The inner binder is now `y`, the type-variable property stays exercised by two nested literals that each mint their own, and the no-shadowing law keeps its own control under its own reason. That file is 23/23. THE MATCH-BINDER SHADOWING CONTROL IS INVERTED AND REASON-SPECIFIC. mbt_binder_shadows_a_same_named_parameter -> mbt_a_binder_hiding_a_same_named_parameter_refuses, asserting resolve_reason_binder_hides_visible_value rather than merely "did not type", so a row satisfied by any refusal cannot stay green if the gate is deleted and the module breaks for an unrelated cause. It stays on the source route deliberately: it is the row saying the rule is reached by AUTHORED SOURCE, which the 77-step interface control cannot discharge for itself. Verification: callable_binder_slice 10/10, expression_bodied_fn_decl_parse 8/8, value_position_whole_read 23/23, infer_declared_return_inhabitance 12/12. For #12625 (Program P: one Arrow encoding): the duplicated positional codomain now lives in exactly one place, two adjacent lines inside body_lower_callable_arrow, where it was authored three times before. Worth knowing for that cut -- v2.std.arrow_signature declared_signature_arrow, the non-callable Arrow producer, already emits the codomain ONCE, so Program P is closing a gap between two producers rather than changing a universal encoding. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/body_lowering_fold.dag | 132 +++++++++++------- .../match_binder/match_binder_typing_test.dag | 27 +++- .../value_position_whole_read_test.dag | 15 +- 3 files changed, 119 insertions(+), 55 deletions(-) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index c257170f480..4896999eae7 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -3328,8 +3328,9 @@ fn body_lower_fn_decl_to_arrow(shell: Node) -> Outcome { // THE SIGNATURE OF A FN DECLARATION, READ ONCE FOR BOTH GRADES. The full arm and the census arm build // one Arrow from one set of reads; only whether a body edge follows differs. So the reads live here -// and the Arrow is built by body_lower_fn_decl_arrow -- its ONE child list, with the body edge -// present or absent -- and "census grade is the same signature" holds +// and the Arrow is built by body_lower_fn_decl_arrow, which projects this signature onto the one +// callable-Arrow constructor (body_lower_callable_arrow) with the body edge present or absent, and +// "census grade is the same signature" holds // by construction rather than by two copies staying in step. Absent means a signature this lowering // cannot read, which both arms retain as the shell. type BodyLowerFnSignature { @@ -3487,19 +3488,15 @@ fn body_lower_data_decl_to_member(shell: Node) -> Outcome { ) Present { value: data_name } => let signature = declared_signature(params: [], source: type_expr) - let arrow = node_lowered_from( + let arrow = body_lower_callable_arrow( source: shell, - kind: TypeNode { connective: Arrow }, - children: [ - signature.domain, - Edge { label: Positional, target: declared_type }, - Edge { label: Positional, target: declared_type }, - signature.order, - Edge { - label: Named { name: ^arrow_body_edge }, - target: body_lower_position_function_values(node: reified_body, path: body_lower_unpositioned_path()) - } - ] + domain: signature.domain, + codomain: declared_type, + order_edge: signature.order, + type_param_edges: body_lower_no_type_param_edges(), + body: optional_present( + value: body_lower_position_function_values(node: reified_body, path: body_lower_unpositioned_path()) + ) ) body_lowering_lowered( o: outcome_with_diagnostics( @@ -3529,27 +3526,74 @@ fn body_lower_data_decl_to_member(shell: Node) -> Outcome { // place positional binding reads parameter order (v2.std.node arrow_declared_parameter_order). // Resolve does not walk it (v2.compiler.resolve resolve_arrow_node): its atoms are labels, not // references, so a parameter spelled like a name in scope cannot bind through it. -fn body_lower_fn_decl_arrow(shell: Node, sig: BodyLowerFnSignature, body: Optional) -> Node { - let signature = list_append( - left: [ - Edge { label: Positional, target: sig.domain }, - Edge { label: Positional, target: sig.return_type }, - Edge { label: Positional, target: sig.return_type }, - signature_order_edge(order: sig.order, source: shell) - ], - right: body_lower_fn_decl_type_params_edges(sig: sig) +// +// THE ONE CALLABLE ARROW. A lambda is surface sugar over the same Arrow a named `fn` declares, so the +// child LAYOUT of a callable Arrow -- which children exist, in which order, under which labels -- is +// decided here and nowhere else: the three callables (a named `fn` declaration, a `data` declaration +// as the nullary case, and a function value written `fn(..) { .. }` or `x => e`) differ only in what +// they can READ, never in what they build. Before this constructor the layout was authored three +// times, so a change to it -- notably removing the duplicated codomain below -- was three edits that +// had to stay in step, and nothing made them. +// +// WHAT EACH CALLER STILL DECIDES, because it is a fact about the caller and not about the Arrow: the +// domain and order edges arrive built, since a declared signature mints both from ONE +// anonymous-binder pass (v2.std.arrow_signature declared_signature) at the occurrence of the text +// that spelled the parameters, which is not always this Arrow's own shell; the type-parameter edges +// arrive as the 0-or-1 list each caller's own type-parameter fact projects to, because a declaration +// carries the Conj the parse captured while a function value mints one for its elided types; and the +// body arrives already prepared, because a declaration's lowering is the only place that holds the +// declaration's body root and can therefore position its fresh type variables +// (body_lower_position_function_values), while a function value nested inside one is positioned by +// that same walk from above rather than here. Absent body is the census grade and the signature-only +// arm, not a callable without one. +// +// THE CODOMAIN IS EMITTED TWICE, AS POSITIONAL CHILDREN 1 AND 2. No reader reads an Arrow's +// positional index 2; it is retained because changing the layout reaches every Arrow reader in the +// corpus, and that cut is owned by gunbc#12625 (Program P: one Arrow encoding). Its value here is +// that after this consolidation the duplication has ONE producer, so that cut is one edit. +fn body_lower_callable_arrow( + source: Node, + domain: Edge, + codomain: Node, + order_edge: Edge, + type_param_edges: List, + body: Optional +) -> Node { + let no_edges: List = [] + let body_edges: List = match body { + Absent => no_edges + Present { value: b } => [Edge { label: Named { name: ^arrow_body_edge }, target: b }] + } + node_lowered_from( + source: source, + kind: TypeNode { connective: Arrow }, + children: list_append( + left: list_append( + left: [ + domain, + Edge { label: Positional, target: codomain }, + Edge { label: Positional, target: codomain }, + order_edge + ], + right: type_param_edges + ), + right: body_edges + ) ) +} + +fn body_lower_fn_decl_arrow(shell: Node, sig: BodyLowerFnSignature, body: Optional) -> Node { body_lower_fn_decl_named_member_wrap( - arrow: node_lowered_from( + arrow: body_lower_callable_arrow( source: shell, - kind: TypeNode { connective: Arrow }, - children: match body { - Absent => signature + domain: Edge { label: Positional, target: sig.domain }, + codomain: sig.return_type, + order_edge: signature_order_edge(order: sig.order, source: shell), + type_param_edges: body_lower_fn_decl_type_params_edges(sig: sig), + body: match body { + Absent => optional_absent() Present { value: b } => - list_append( - left: signature, - right: [Edge { label: Named { name: ^arrow_body_edge }, target: body_lower_position_function_values(node: b, path: body_lower_unpositioned_path()) }] - ) + optional_present(value: body_lower_position_function_values(node: b, path: body_lower_unpositioned_path())) } ), fn_name: sig.fn_name, @@ -4369,24 +4413,16 @@ fn body_lower_function_value_arrow( [type_params_edge(conj: node_lowered_from(kind: TypeNode { connective: Conj }, children: type_params, source: shell))] } outcome_with_diagnostics( - value: node_lowered_from( + value: body_lower_callable_arrow( source: shell, - kind: TypeNode { connective: Arrow }, - children: list_append( - left: list_append( - left: [ - Edge { - label: Positional, - target: node_lowered_from(kind: TypeNode { connective: Conj }, children: sig.domain, source: shell) - }, - Edge { label: Positional, target: codomain_node }, - Edge { label: Positional, target: codomain_node }, - signature_order_edge(order: sig.order, source: shell) - ], - right: type_param_edges - ), - right: [Edge { label: Named { name: ^arrow_body_edge }, target: reified }] - ) + domain: Edge { + label: Positional, + target: node_lowered_from(kind: TypeNode { connective: Conj }, children: sig.domain, source: shell) + }, + codomain: codomain_node, + order_edge: signature_order_edge(order: sig.order, source: shell), + type_param_edges: type_param_edges, + body: optional_present(value: reified) ), diagnostics: bd ) diff --git a/src/v2/test/claim/match_binder/match_binder_typing_test.dag b/src/v2/test/claim/match_binder/match_binder_typing_test.dag index d028a3feb76..3997588f6c4 100644 --- a/src/v2/test/claim/match_binder/match_binder_typing_test.dag +++ b/src/v2/test/claim/match_binder/match_binder_typing_test.dag @@ -169,13 +169,28 @@ test fn mbt_match_is_typed_parse_tree_holds() -> Bool { mbp_fact_of(src: mbt_positive_src, pick: fn(root) { mbp_find_match(n: root) }) == ^ParseTree } -// A BINDER SHADOWS A PARAMETER OF THE SAME NAME. The scope walk before this change typed a binder -// spelled like an enclosing parameter as that parameter; here `artifact` is also an Int parameter. -test fn mbt_binder_shadows_a_same_named_parameter_holds() -> Bool { - mbp_fact_of( +// REVERSED BY RULING: A BINDER MAY NOT HIDE A VISIBLE VALUE BINDING, SO THIS REFUSES. This row asserted +// the opposite -- that an arm binder spelled like an enclosing parameter SHADOWS it and the match still +// types -- which was the right claim when the defect it guarded was a scope walk that typed such a binder +// as the parameter. The operator ruling of 2026-09-30 forbids value-name shadowing outright, so the +// program this row was written over is no longer a legal one, and the row is reversed rather than deleted +// (DESIGN section 4b(4): an expecting-red probe that flips becomes the regression control, it does not +// retire). Here `artifact` is an Int parameter of `g` and also the Accepted arm's binder. +// +// WHAT IT NOW ESTABLISHES, AND WHY IT STAYS ON THE SOURCE ROUTE. v2.compiler.resolve admits every value +// binder through one gate (admit_value_binders), and the cheap control for the rule itself lives at that +// gate's own interface (v2.test.claim.binder_admission.callable_binder_slice +// cbs_a_binder_hiding_an_enclosing_binder_refuses, 77 eval steps). This row is the one that says the rule +// is reached by AUTHORED SOURCE through the real front end -- the route, not the shape -- which is the +// pairing obligation the supplied row cannot discharge for itself. +// +// The refusal is asserted BY REASON, not merely as "did not type": a row satisfied by any refusal would +// stay green if the shadowing gate were deleted and the module broke for an unrelated cause. +test fn mbt_a_binder_hiding_a_same_named_parameter_refuses_holds() -> Bool { + mbp_reports( src: mbt_decls + "fn g(artifact: Int, h: Outcome, fallback: ParseArtifact) -> ParseTree {\n match h {\n" + mbt_arms + " }\n}\n", - pick: fn(root) { mbp_find_match(n: root) } - ) == ^ParseTree + reason: ^resolve_reason_binder_hides_visible_value + ) } // (2) REFUSALS, EACH DISCRIMINATING: the same match with one pattern edit, so the positive control diff --git a/src/v2/test/claim/namespace_xl0/value_position_whole_read_test.dag b/src/v2/test/claim/namespace_xl0/value_position_whole_read_test.dag index 7dfb7854953..602050c7166 100644 --- a/src/v2/test/claim/namespace_xl0/value_position_whole_read_test.dag +++ b/src/v2/test/claim/namespace_xl0/value_position_whole_read_test.dag @@ -128,7 +128,20 @@ data vpw_call_projection_source: String = "module v2.test.vpw_call_projection\n\ // a function value's minted variables must be distinct from any other function value's its types can // meet -- a nested literal reusing its parameter name, and siblings -- while the same lambda at the same // position in two declarations keeps the same type and content hash (no occurrence in the type). -data vpw_fv_nest_source: String = "module v2.test.vpw_fv_nest\n\nimport v2.std.logic { Bool }\n\nfn vpw_apply_nest(f: Bool, v: Bool) -> Bool { v }\n\nfn vpw_probe_nest(p: Bool) -> Bool {\n vpw_apply_nest(f: fn(x) {\n let g = fn(x) { x }\n g\n }, v: p)\n}\n" +// THE NESTED LITERALS BIND DIFFERENT NAMES, AND THE RENAME IS THE POINT OF THIS COMMENT. This fixture +// used to nest `fn(x) { let g = fn(x) { x } ... }` -- the same value name at both levels. That was +// INCIDENTAL to what the claim over it establishes, which is that a nested literal's fresh TYPE variables +// are not a shadow of the outer's; the shared spelling did no work for that subject. Once +// v2.compiler.resolve admitted value binders through one gate, the inner `x` hid the outer `x` and the +// fixture refused resolve_reason_binder_hides_visible_value -- so the claim could no longer reach its own +// subject, and reversing it would have deleted a property rather than recording a rule. +// +// The value-shadowing rule has its own control at its own interface +// (v2.test.claim.binder_admission.callable_binder_slice cbs_a_binder_hiding_an_enclosing_binder_refuses), +// so nothing is lost by making this fixture bind distinct names: the type-variable property stays +// exercised by two nested literals that each mint their own, and the shadowing property is asserted where +// it belongs instead of riding on an unrelated fixture. +data vpw_fv_nest_source: String = "module v2.test.vpw_fv_nest\n\nimport v2.std.logic { Bool }\n\nfn vpw_apply_nest(f: Bool, v: Bool) -> Bool { v }\n\nfn vpw_probe_nest(p: Bool) -> Bool {\n vpw_apply_nest(f: fn(x) {\n let g = fn(y) { y }\n g\n }, v: p)\n}\n" data vpw_fv_siblings_source: String = "module v2.test.vpw_fv_siblings\n\nimport v2.std.logic { Bool }\n\nfn vpw_pair(f: Bool, g: Bool, v: Bool) -> Bool { v }\n\nfn vpw_probe_siblings(p: Bool) -> Bool { vpw_pair(f: x => x, g: x => x, v: p) }\n\nfn vpw_site_one(p: Bool) -> Bool { vpw_site(f: y => y, v: p) }\n\nfn vpw_site_two(p: Bool) -> Bool { vpw_site(f: y => y, v: p) }\n\nfn vpw_site(f: Bool, v: Bool) -> Bool { v }\n\nfn vpw_order_site(f: Bool, v: Bool) -> Bool { v }\n\nfn vpw_probe_order(p: Bool) -> Bool { vpw_order_site(f: (y, x) => y, v: p) }\n" From 8b83c4aa6fb1a3da78796c412940f6694a5eda56 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 00:33:52 +0000 Subject: [PATCH 38/90] Eight claims moved to the interface they discriminate; 44 stay end-to-end, by reasoning CI's floor refused this lane with 103 blockers, cause enrolment_measured_over_margin, every one of them mine: each claim ran assemble -> resolve -> infer on a source string to inspect ONE interface's result, which is the reach DESIGN section 3's witness rule forbids with the budget as its tell. THE MEASUREMENT THAT FRAMES EVERYTHING, verified independently rather than taken from the report: one assemble -> resolve pass over a FOUR-LINE module costs 218,163 eval steps against a new-witness margin of 72,300. Three times over on one pass, before asserting anything. So no claim that executes the front end can enrol, and no splitting or de-duplication changes that -- the choice is per claim between moving to the interface it discriminates and staying end-to-end. EIGHT MOVED, with the wins the rule predicts: the arrow-body search row 117,180 -> 104 by constructing the Conj it asks one question of; six match-coproduct refusal rows and a frontier arm 774k-918k -> 3,307-6,082 by calling infer_match_coproduct with a constructed match, entries and ResolvedTree. Those are built by the PRODUCTION constructors -- declaration_reference_node, construct_node, symbol_index_insert, inferred_facts_from_derived_type -- not hand-shaped records, so a change to any of those encodings reaches these rows as it reaches resolve. 44 STAY, AND THAT IS A FINDING RATHER THAN UNFINISHED WORK. For reference_evidence, field_projection's grounding rows and synthetic_facts, a supplied index or entry list CHOOSES THE ANSWER UNDER TEST: whether a reference grounds and to which declaration is a fact about the index the front end mints, and a hand-built index picks the declaration the author intended. field_projection's own annotation already says a row exists to be "an independent route to the same answer, so the claim is not the implementation compared with itself" -- converting it would make it exactly that. DESIGN section 4b's top-rung question asked before writing the check: the red would not be authorable. ONE CONVERSION WAS MEASURED AND THEN WITHDRAWN. mbt_the_sevens_call_scrutinee_is_a_counted_frontier converted to 3,229 steps and was put back at 1,253,429, because a supplied entry carrying GroundingNotDerived asserts what the fixture chose, not what a call's return derivation leaves behind -- it would have removed the last execution of the real path into that arm. The cheap supplied row now sits BESIDE it reading the same arm's result contract: one says the arm is reached, the other says what it returns. Withdrawing it also un-dangled mbt_seven_src (section 3c). THREE HONESTY DEFECTS FOUND WHILE CONVERTING, recorded because each misreports a rung. A stale vacuity confession on cref_the_identity_callee_never_answers_the_other_argument ("THIS CLAIM IS VACUOUS TODAY") had been untrue since the row above it flipped -- a reader trusting it would have discounted live coverage, which is section 4b(1) inflation in the under-claiming direction. A claimed saving that measurement refuted: the bool-pair row did not go from four front-end runs to two, because the interpreter's call memo already collapsed the repeated nullary calls (553,595 -> 553,577); the edit is still right because it removed two conjuncts that establish strictly less than the one that remains, so it is a strengthening and the annotation now says so. And a second Int decoder introduced beside cref_runtime_int_equals, collapsed to one. No assertion was weakened for budget, no row deleted, and no grandfather row or budget exemption proposed. The two identity rows each now own one argument and assert the exact magnitude, which is stronger per row than the old conjunct. Six files, 68 rows, zero red. Co-Authored-By: Claude Opus 5 (1M context) --- .../declaration_reference_eval_test.dag | 95 +++-- .../synthetic_facts_key_collision_test.dag | 23 + .../field_projection_stages_test.dag | 28 ++ .../match_binder/match_binder_typing_test.dag | 403 +++++++++++++++++- .../expression_bodied_continuation_test.dag | 83 +++- .../declaration_reference_evidence_test.dag | 26 ++ 6 files changed, 596 insertions(+), 62 deletions(-) diff --git a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag index fdcd441a6ef..0a7e5f1477c 100644 --- a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag +++ b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag @@ -255,25 +255,14 @@ fn cref_executes(o: Outcome) -> Bool { } } -fn cref_runtime_int_equals(value: RuntimeValue, n: Int) -> Bool { - match value { - RuntimePrimitive { value: p } => - match integer_signed_i32_le_bytes_to_int(bytes: p.bytes) { - Accepted { value: magnitude, diagnostics: _ } => magnitude == n - Rejected { diagnostics: _ } => false - } - _ => false - } -} - +// ONE READER FOR "WHAT INT DID THIS CALL PRODUCE". cref_executes_to asks it a question and the exact +// rows below ask it for the magnitude, so a second decoder beside it would be one fact with two +// authorities (DESIGN section 3) -- and the decoding rule, that only a four-byte signed primitive +// counts, is exactly the sort that drifts between two copies. fn cref_executes_to(o: Outcome, n: Int) -> Bool { - match cref_eval_of(o: o) { + match cref_result_int_optional(o: o) { Absent => false - Present { value: outcome } => - match outcome { - Accepted { value: v, diagnostics: _ } => cref_runtime_int_equals(value: v, n: n) - Rejected { diagnostics: _ } => false - } + Present { value: magnitude } => magnitude == n } } @@ -380,7 +369,33 @@ fn cref_identity_source(lex: String) -> Outcome { cref_assemble(src: "module p\n\nfn identity(only_arg: Int) -> Int {\n only_arg\n}\n\nfn consumer() -> Int {\n identity(only_arg: " + lex + ")\n}\n") } -fn cref_identity_executes_to(lex: String, n: Int) -> Bool { +// THE CALL'S RESULT AS AN INT, OR NOTHING. The two identity rows below each assert an EXACT result for +// ONE argument, so each pays one front-end run where the pair previously paid two apiece: the two +// SOURCES ("3" and "8") are distinct text, which no memo collapses, and a row that asked about both +// carried the other's whole pipeline against its own enrolment budget. Exactness is what lets one row +// per argument keep what the conjunctions had -- a callee answering any constant fails the row whose +// argument it does not equal -- so this reader returns the magnitude rather than a Bool against an +// expectation. +fn cref_result_int_optional(o: Outcome) -> Optional { + match cref_eval_of(o: o) { + Absent => optional_absent() + Present { value: outcome } => + match outcome { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: v, diagnostics: _ } => + match v { + RuntimePrimitive { value: p } => + match integer_signed_i32_le_bytes_to_int(bytes: p.bytes) { + Accepted { value: magnitude, diagnostics: _ } => optional_present(value: magnitude) + Rejected { diagnostics: _ } => optional_absent() + } + _ => optional_absent() + } + } + } +} + +fn cref_identity_result_is_exactly(lex: String, n: Int) -> Bool { cref_executes_to(o: cref_identity_source(lex: lex), n: n) } @@ -397,19 +412,30 @@ fn cref_identity_executes_to(lex: String, n: Int) -> Bool { // 3 in yields 3 out and 8 in yields 8 out. The refusal it used to count was NOT the shared facts key // the old annotation blamed -- the synthetic-key collision is real and still enrolled separately, but // it was not what held this row red. The boundary was the two disagreeing readers of a callee's arrow. +// +// ONE ARGUMENT PER ROW, AND THE RESULT READ EXACTLY. The pair "3 in yields 3 out" and "8 in yields 8 +// out" is two independent cases over two different sources, so forcing both through one row paid two +// full front-end runs against one enrolment budget for no coverage the split does not keep. The row +// below owns the "3" case and the row after it owns the "8" case; argument dependence is established +// by the two together, exactly as it was by the two conjuncts, because neither result is admissible +// for the other's argument. Asserting the EXACT magnitude is what makes each row carry that on its +// own: a callee answering any constant fails the row whose argument it does not equal. test fn cref_argument_dependent_execution_reaches_the_callee_holds() -> Bool { - cref_identity_executes_to(lex: "3", n: 3) - && cref_identity_executes_to(lex: "8", n: 8) + cref_identity_result_is_exactly(lex: "3", n: 3) } -// AND THE SAME CALLEE MUST NOT ANSWER A DIFFERENT ARGUMENT'S VALUE once it executes. THIS CLAIM IS -// VACUOUS TODAY and is recorded as such: nothing executes, so both conjuncts hold for the wrong -// reason. It is enrolled anyway because it is exactly the check that stops the repair being credited -// by a callee that consumes its argument and returns the WRONG one, and it becomes discriminating the -// moment the claim above inverts. A reader must not count it as present coverage until then. +// AND THE SAME CALLEE MUST NOT ANSWER A DIFFERENT ARGUMENT'S VALUE once it executes. THE VACUITY THIS +// ANNOTATION USED TO CONFESS IS GONE AND THE CONFESSION WAS STALE: it said nothing executes, so both +// negated conjuncts held for the wrong reason, and that stopped being true when the row above flipped. +// A reader who trusted the annotation would have discounted coverage that was already live, which is +// the same defect as claiming coverage that is not -- a stale honesty note misreports the rung either +// way (DESIGN section 4b(1)). +// +// IT IS NOW THE "8" CASE, READ EXACTLY, so it answers both halves of its own name from one front-end +// run: the callee executes to 8 and therefore does not answer 3. The negation is carried by the +// exactness rather than by a second run against the other magnitude. test fn cref_the_identity_callee_never_answers_the_other_argument_holds() -> Bool { - !cref_identity_executes_to(lex: "3", n: 8) - && !cref_identity_executes_to(lex: "8", n: 3) + cref_identity_result_is_exactly(lex: "8", n: 8) } // THE BOOL PAIR'S DISTINCT OUTCOMES, the same shape one type further on. A true-returning and a @@ -425,10 +451,19 @@ fn cref_bool_false_source() -> Outcome { // refusals are also unequal and would satisfy inequality alone. The values are not compared against a // magnitude: this module is not the authority on how v2.std.logic represents its arms, and asserting a // byte image here would be a second such authority (DESIGN section 3). +// +// EXECUTION IS NOT ASSERTED BY A SEPARATE CONJUNCT, AND THAT IS A STRENGTHENING, NOT A SAVING. The two +// leading cref_executes conjuncts established strictly LESS than cref_results_differ already must: +// that reader answers Absent for a refusal and ALSO for an accepted value that is not a primitive, +// where cref_executes answered true for the second. Removing them removes the weaker check. +// +// IT BOUGHT NO COST, AND SAYING SO IS THE POINT. The claim-local call memo already collapsed the +// repeated nullary source and eval calls, so claim_batch measures this row at the same cost with the +// conjuncts and without them -- the duplication was apparent in the text and absent in the run. That +// is worth recording because the reverse inference is the tempting one: a row that LOOKS like it calls +// the front end four times is not evidence that it does, and only the instrument decides. test fn cref_the_bool_pair_separates_holds() -> Bool { - cref_executes(o: cref_bool_pair_source()) - && cref_executes(o: cref_bool_false_source()) - && cref_results_differ(a: cref_bool_pair_source(), b: cref_bool_false_source()) + cref_results_differ(a: cref_bool_pair_source(), b: cref_bool_false_source()) } // THE PRIMITIVE BYTES OF A CALL'S RESULT, for the one claim whose property is that two calls produce diff --git a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag index dd0233ea2b5..77a6427932a 100644 --- a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag +++ b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag @@ -55,6 +55,29 @@ import v2.std.runtime { runtime_value_conj_node } // demand site in v2.compiler.eval its own reason symbol; that instrument is not retained, and its // result is recorded in this change's commit message rather than asserted here, because asserting it // would require keeping the instrumentation in the evaluator. +// WHY EVERY ROW HERE EXECUTES THE WHOLE FRONT END, AND WHY THAT IS NOT CONVERTED AWAY. DESIGN +// section 3's witness rule says a claim's inputs belong at the boundary it discriminates, as supplied +// values, and the rows below supply none: each assembles authored text, resolves it and infers over it. +// That was interrogated row by row against the rule rather than defended by habit, and the reason it +// stands is the one DESIGN section 4b states for the top rung -- ASK WHETHER THE CHECK'S RED IS +// AUTHORABLE BEFORE WRITING THE CHECK. +// +// THE SUBJECT OF THESE ROWS IS THE ENTRY POPULATION INFER ACTUALLY EMITS, not what one downstream +// function answers for a shape. The claims count how many entries of one real compilation share one +// key and whether they agree -- and a supplied entry list would let this file CHOOSE that count, which +// is the whole question. There is no red to author against a fixture here: the collision would be +// asserted by construction. So the entries stay the real producer's, and the cost stays. +// +// SO THESE ROWS ARE THE EXECUTION OF THE REAL PATH -- the second half of the same rule, which forbids +// supplying inputs anywhere if it removes the last execution of the producer. Deleting resolve's +// projection lowering, or infer's declared-field read, must make a control here fail, and does. +// +// WHAT WOULD ACTUALLY MOVE THEIR COST is not a narrower claim but a provider for the demand: their +// nullary source producers are pure functions of module-constant text, which is the shape +// v2.workflow.floor_pure_producer_share already serves across claims for hundreds of peer fixtures, +// including the per-fixture assembled and resolved trees rostered there. Enrolment is NOT asserted or +// proposed here, because that roster's own admission criterion is a measured serve-below-recompute on +// a required-floor receipt, which this file cannot produce; naming the route is the honest half. fn sfk_entries(o: Outcome) -> Optional> { match o { Rejected { diagnostics: _ } => optional_absent() diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag index fb056e64e31..85fade27515 100644 --- a/src/v2/test/claim/field_projection/field_projection_stages_test.dag +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -48,6 +48,34 @@ import v2.std.optional { Absent, Optional, Present, optional_absent, optional_pr // // This file establishes the first boundary before anything is repaired, so a later green is a change // in behaviour and not a change in what was being asked. +// WHY EVERY ROW HERE EXECUTES THE WHOLE FRONT END, AND WHY THAT IS NOT CONVERTED AWAY. DESIGN +// section 3's witness rule says a claim's inputs belong at the boundary it discriminates, as supplied +// values, and the rows below supply none: each assembles authored text, resolves it and infers over it. +// That was interrogated row by row against the rule rather than defended by habit, and the reason it +// stands is the one DESIGN section 4b states for the top rung -- ASK WHETHER THE CHECK'S RED IS +// AUTHORABLE BEFORE WRITING THE CHECK. +// +// THE SUBJECT OF THESE ROWS IS WHAT THE REAL INDEX AND THE REAL LOWERING PRODUCE, not what one +// downstream function answers for a shape. The projection interface itself -- v2.compiler.infer +// infer_field_projection_facts -- takes a receiver payload, a facts entry and a ResolvedTree, and +// supplying those makes the answer a restatement of the fixture: "two fields of different declared +// types ground differently" is a fact about the index the front end builds, and against a hand-built +// index it is true by how the fixture was written. That red is not authorable, which makes the +// converted row a decoration rather than a weaker wall, and DESIGN says a decoration is worse than an +// absent check because it will be cited as coverage. THE EVAL ROWS AT THE END OF THIS FILE ARE THE +// CONTRAST AND THE PRECEDENT: there the supplied value is a runtime aggregate whose field layout the +// claim does NOT choose the answer from, so supplying it discriminates and costs almost nothing. +// +// SO THESE ROWS ARE THE EXECUTION OF THE REAL PATH -- the second half of the same rule, which forbids +// supplying inputs anywhere if it removes the last execution of the producer. Deleting resolve's +// projection lowering, or infer's declared-field read, must make a control here fail, and does. +// +// WHAT WOULD ACTUALLY MOVE THEIR COST is not a narrower claim but a provider for the demand: their +// nullary source producers are pure functions of module-constant text, which is the shape +// v2.workflow.floor_pure_producer_share already serves across claims for hundreds of peer fixtures, +// including the per-fixture assembled and resolved trees rostered there. Enrolment is NOT asserted or +// proposed here, because that roster's own admission criterion is a measured serve-below-recompute on +// a required-floor receipt, which this file cannot produce; naming the route is the honest half. data fps_artifact: Artifact = Artifact { kind: SourceFile, id: ^field_projection_stages_artifact, diff --git a/src/v2/test/claim/match_binder/match_binder_typing_test.dag b/src/v2/test/claim/match_binder/match_binder_typing_test.dag index 3997588f6c4..532948aa828 100644 --- a/src/v2/test/claim/match_binder/match_binder_typing_test.dag +++ b/src/v2/test/claim/match_binder/match_binder_typing_test.dag @@ -14,7 +14,41 @@ import v2.std.cross_tree.import_model { V2Tree } import v2.std.artifact { Artifact, SourceFile } import v2.std.diagnostic { Accepted, NonEmptyDiagnostics, None, Outcome, Rejected, Some } import v2.std.algebra { contains, list_map } -import v2.std.node { Atom, ComputationNode, Match, Node, Symbol, TypeNode } +import v2.std.node { + Atom, + ComputationNode, + Conj, + Disj, + Edge, + Instantiation, + Match, + Named, + Node, + Positional, + Symbol, + TypeNode, + match_arm_body, + match_arm_pattern +} +import v2.std.node_query { construct_node } +import v2.std.symbol_index { + SymbolIndex, + empty_symbol_index, + symbol_index_insert, + symbol_index_mark_declared_type_params +} +import v2.std.qualified_name { QualifiedName, declaration_reference_node } +import v2.compiler.infer { + infer_descent_witness_for_node, + infer_match_coproduct, + inferred_facts_from_derived_type, + inferred_facts_grounding_derived, + inferred_facts_not_derived +} +import v2.compiler.inferred_tree { InferredFacts, InferredFactsEntry } +import v2.std.algebra { list_append } +import std.occurrence_identity { OccurrenceSynthetic } +import v2.std.collection { List } import v2.std.integer { Int } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.std.text { String } @@ -173,7 +207,7 @@ test fn mbt_match_is_typed_parse_tree_holds() -> Bool { // the opposite -- that an arm binder spelled like an enclosing parameter SHADOWS it and the match still // types -- which was the right claim when the defect it guarded was a scope walk that typed such a binder // as the parameter. The operator ruling of 2026-09-30 forbids value-name shadowing outright, so the -// program this row was written over is no longer a legal one, and the row is reversed rather than deleted +// program this row was written over is no longer a legal one and the row is reversed rather than deleted // (DESIGN section 4b(4): an expecting-red probe that flips becomes the regression control, it does not // retire). Here `artifact` is an Int parameter of `g` and also the Accepted arm's binder. // @@ -196,43 +230,73 @@ test fn mbt_a_binder_hiding_a_same_named_parameter_refuses_holds() -> Bool { // (2) REFUSALS, EACH DISCRIMINATING: the same match with one pattern edit, so the positive control // above is the other half of every pair. test fn mbt_a_variant_the_coproduct_does_not_declare_refuses_holds() -> Bool { - mbp_reports( - src: mbt_with_match(scrutinee_type: "Outcome", arms: " Rejected { diagnostics: r } => fallback.tree\n Accepted { value: artifact, diagnostics: d } => artifact.tree\n None => fallback.tree\n"), + mbt_supplied_refuses_with( + o: mbt_supplied_match( + scrutinee_type: mbt_outcome_of(args: [mbt_ref(segs: [^p, ^ParseArtifact])]), + arms: list_append( + left: mbt_supplied_arms(), + right: [mbt_arm(pattern: mbt_variant_pattern(tag: ^None, fields: Empty), body: mbt_body_a())] + ) + ), reason: ^infer_match_pattern_variant_not_declared ) } test fn mbt_a_field_the_variant_does_not_declare_refuses_holds() -> Bool { - mbp_reports( - src: mbt_with_match(scrutinee_type: "Outcome", arms: " Rejected { diagnostics: r } => fallback.tree\n Accepted { tree: artifact, diagnostics: d } => artifact.tree\n"), + mbt_supplied_refuses_with( + o: mbt_supplied_match( + scrutinee_type: mbt_outcome_of(args: [mbt_ref(segs: [^p, ^ParseArtifact])]), + arms: [ + mbt_rejected_arm(body: mbt_body_a()), + mbt_arm( + pattern: mbt_variant_pattern(tag: ^Accepted, fields: [ + mbt_named(name: ^tree, target: mbt_binder(name: ^artifact)), + mbt_named(name: ^diagnostics, target: mbt_binder(name: ^d)) + ]), + body: mbt_body_b() + ) + ] + ), reason: ^infer_match_pattern_field_not_declared ) } test fn mbt_too_many_type_arguments_refuses_holds() -> Bool { - mbp_reports( - src: mbt_with_match(scrutinee_type: "Outcome", arms: mbt_arms), + mbt_supplied_refuses_with( + o: mbt_supplied_match( + scrutinee_type: mbt_outcome_of(args: [mbt_ref(segs: [^p, ^ParseArtifact]), mbt_parse_tree_type()]), + arms: mbt_supplied_arms() + ), reason: ^infer_match_type_argument_arity_mismatch ) } test fn mbt_no_type_arguments_to_a_generic_refuses_holds() -> Bool { - mbp_reports( - src: mbt_with_match(scrutinee_type: "Outcome", arms: mbt_arms), + mbt_supplied_refuses_with( + o: mbt_supplied_match( + scrutinee_type: mbt_ref(segs: [^p, ^Outcome]), + arms: mbt_supplied_arms() + ), reason: ^infer_match_type_argument_arity_mismatch ) } test fn mbt_a_missing_variant_refuses_as_non_exhaustive_holds() -> Bool { - mbp_reports( - src: mbt_with_match(scrutinee_type: "Outcome", arms: " Accepted { value: artifact, diagnostics: d } => artifact.tree\n"), + mbt_supplied_refuses_with( + o: mbt_supplied_match( + scrutinee_type: mbt_outcome_of(args: [mbt_ref(segs: [^p, ^ParseArtifact])]), + arms: [mbt_accepted_arm(body: mbt_body_b())] + ), reason: ^infer_match_non_exhaustive ) } test fn mbt_a_record_scrutinee_refuses_holds() -> Bool { - mbp_reports( - src: mbt_with_match(scrutinee_type: "ParseArtifact", arms: mbt_arms), + mbt_supplied_refuses_with( + o: mbt_supplied_match( + scrutinee_type: mbt_ref(segs: [^p, ^ParseArtifact]), + arms: mbt_supplied_arms() + ), reason: ^infer_match_scrutinee_not_coproduct ) } @@ -248,11 +312,49 @@ data mbt_seven_src: String = mbt_decls + "fn parse_module_prepared(tokens: Int) // carries infer_match_scrutinee_type_underived at the scrutinee, so the untyped match is a counted // site and not a silent green. When the call's return derives, this claim goes red and the positive // controls above become the seven's own. +// +// THIS ROW STAYS ON THE SOURCE ROUTE, AND THE PAIRING OBLIGATION IS WHY. It was converted to the +// supplied boundary and the conversion was withdrawn: it is the ONLY row that establishes that an +// APPLICATION scrutinee really reaches infer's underived-scrutinee arm, and a supplied entry carrying +// GroundingNotDerived asserts what this file chose rather than what a call's return derivation leaves +// behind. Supplying it would have removed the last execution of the real path into that arm, which +// DESIGN section 3 forbids outright -- the two typed-binder rows above inhabit the DERIVED path and +// cannot stand in for it. It is over the new-witness margin and reported as such, because the +// alternative is a cheap suite that establishes nothing about the arm it names. +// +// THE SUPPLIED ROW BESIDE IT reads the same arm's RESULT CONTRACT at one interface, so the two are a +// pair rather than a duplicate: this row says the arm is reached, that one says what the arm returns. test fn mbt_the_sevens_call_scrutinee_is_a_counted_frontier_holds() -> Bool { mbp_infers(src: mbt_seven_src) && mbp_reports(src: mbt_seven_src, reason: ^infer_match_scrutinee_type_underived) } +// WHAT THE FRONTIER ARM RETURNS, at infer_match_coproduct's own interface: an underived scrutinee is +// ACCEPTED rather than convicted, and the acceptance CARRIES the located advisory. Both halves are +// properties of the returned Outcome, so they are read from it directly instead of through a whole +// module's inference. Its inhabitance is the row above. +test fn mbt_the_frontier_arm_accepts_and_reports_the_underived_scrutinee_holds() -> Bool { + mbt_supplied_accepts_reporting( + o: infer_match_coproduct( + node: mbt_match_node(scrutinee: mbt_scrutinee(), arms: mbt_supplied_arms()), + partials: Empty, + entries: list_append(left: mbt_underived_scrutinee_entry(), right: mbt_typed_bodies()), + resolved: mbt_resolved() + ), + reason: ^infer_match_scrutinee_type_underived + ) +} + +fn mbt_underived_scrutinee_entry() -> List { + match inferred_facts_not_derived( + node: mbt_scrutinee(), + descent: infer_descent_witness_for_node(n: mbt_scrutinee()) + ) { + Rejected { diagnostics: _ } => Empty + Accepted { value: facts, diagnostics: _ } => [InferredFactsEntry { node: mbt_scrutinee(), facts: facts }] + } +} + // AN ARM BODY WHOSE TYPE IS NOT DERIVED IS COUNTED, NOT GUESSED. The scrutinee here IS typed, so the // patterns are checked and the binder is typed; the bodies rebuild an Outcome from a constructor, // which this stage does not type, so the match is accepted at the frontier with one located @@ -264,3 +366,276 @@ test fn mbt_an_underived_arm_body_is_a_counted_frontier_holds() -> Bool { && mbp_reports(src: mbt_constructor_body_src, reason: ^infer_match_arm_body_type_underived) && (mbp_fact_of(src: mbt_constructor_body_src, pick: fn(root) { mbp_find_atom(n: root, id: ^artifact) }) == ^ParseArtifact) } + +// THE MATCH-TYPING BOUNDARY, READ AT ONE INTERFACE WITH SUPPLIED INPUTS. Every refusal row below used +// to assemble a twenty-five line module, resolve it and run the whole of `infer` in order to inspect +// what ONE function answers for ONE match node. Re-derive the cost with claim_batch over this entry +// rather than trusting a figure here: what it shows is the diagnostic DESIGN section 3 names, that the +// row's cost did not move when its assertion changed, because the assertion was never the expense. +// +// THE INTERFACE IS v2.compiler.infer infer_match_coproduct, which is where every one of these +// judgments is actually decided: the variant-not-declared, field-not-declared, arity-mismatch, +// non-exhaustive and scrutinee-not-coproduct refusals, and the two frontier acceptances. Its inputs +// are a match Node, the partials roster, the facts entries its operands carry, and the ResolvedTree +// whose symbol_index holds the coproduct's declaration. All four are CONSTRUCTED here -- and +// constructed by the production constructors, not by hand-shaped records: v2.std.qualified_name +// declaration_reference_node builds every reference, v2.std.node_query construct_node builds every +// arm pattern, and v2.std.symbol_index symbol_index_insert and symbol_index_mark_declared_type_params +// build the index. So a change to any of those encodings reaches these rows the same way it reaches +// resolve. +// +// THE PAIRING OBLIGATION IS DISCHARGED BY THE THREE REAL-PATH ROWS ABOVE, which stay on the source +// route and are the last execution of it: mbt_binder_is_typed_parse_artifact_from_accepted_value and +// mbt_match_is_typed_parse_tree assemble, resolve and infer authored text and read the facts the real +// producer emitted, so they are the claim that this shape is one the front end really builds -- a +// supplied input here is a hypothesis about a boundary those rows show is inhabited. Deleting resolve's +// match lowering, or infer's entry production, makes them fail; it would not make any row below fail, +// which is exactly why they are not replaced. +fn mbt_qn(segs: List) -> QualifiedName { + segs +} + +fn mbt_ref(segs: List) -> Node { + declaration_reference_node(qn: mbt_qn(segs: segs), occurrence_id: OccurrenceSynthetic) +} + +fn mbt_atom(s: Symbol) -> Node { + Node { kind: TypeNode { connective: Atom { identity: s } }, children: [], occurrence_id: OccurrenceSynthetic } +} + +fn mbt_conj(fields: List) -> Node { + Node { kind: TypeNode { connective: Conj }, children: fields, occurrence_id: OccurrenceSynthetic } +} + +fn mbt_named(name: Symbol, target: Node) -> Edge { + Edge { label: Named { name: name }, target: target } +} + +fn mbt_pos(target: Node) -> Edge { + Edge { label: Positional, target: target } +} + +// `Outcome = Accepted { value: T, diagnostics: Diagnostics } | Rejected { diagnostics: NonEmptyDiagnostics }` +// as the index holds it: a Disj whose Named edges are the variant tags and whose targets are the +// payload Conjs. `value` is declared as the type PARAMETER, which is what makes the instantiation +// visible at all -- a payload whose fields were all concrete would type the binder without ever +// consulting the scrutinee's type argument. +fn mbt_outcome_declaration() -> Node { + Node { + kind: TypeNode { connective: Disj }, + children: [ + mbt_named( + name: ^Accepted, + target: mbt_conj(fields: [ + mbt_named(name: ^value, target: mbt_atom(s: ^T)), + mbt_named(name: ^diagnostics, target: mbt_ref(segs: [^p, ^Diagnostics])) + ]) + ), + mbt_named( + name: ^Rejected, + target: mbt_conj(fields: [ + mbt_named(name: ^diagnostics, target: mbt_ref(segs: [^p, ^NonEmptyDiagnostics])) + ]) + ) + ], + occurrence_id: OccurrenceSynthetic + } +} + +// `type ParseArtifact { tree: ParseTree ... }` -- a record, so its declaration is a Conj and not a +// Disj. It is in the index for two reasons: it is the type argument the binder must receive, and it is +// the non-coproduct scrutinee the refusal row needs. +fn mbt_parse_artifact_declaration() -> Node { + mbt_conj(fields: [mbt_named(name: ^tree, target: mbt_ref(segs: [^p, ^ParseTree]))]) +} + +fn mbt_index() -> SymbolIndex { + symbol_index_mark_declared_type_params( + index: symbol_index_insert( + index: symbol_index_insert( + index: empty_symbol_index(), + qualified_path: mbt_qn(segs: [^p, ^Outcome]), + resolved: mbt_outcome_declaration() + ), + qualified_path: mbt_qn(segs: [^p, ^ParseArtifact]), + resolved: mbt_parse_artifact_declaration() + ), + qualified_path: mbt_qn(segs: [^p, ^Outcome]), + params: [^T] + ) +} + +// THE ROOT IS A LEAF, DELIBERATELY. infer_branch_operand_resolved_type_in_tree consults +// infer_parameter_type_in_scope over resolved.root before falling back to the operand's own facts, and +// this slice's subject is what the SUPPLIED facts establish; a root that bound the scrutinee name would +// answer from the scope walk instead and the entries below would stop being the discriminator. +fn mbt_resolved() -> ResolvedTree { + ResolvedTree { + root: mbt_atom(s: ^mbt_unused_root), + symbol_index: mbt_index(), + resolved_declarations: empty_symbol_index() + } +} + +fn mbt_instantiation(head: Node, args: List) -> Node { + Node { + kind: TypeNode { connective: Instantiation }, + children: list_append(left: [mbt_pos(target: head)], right: list_map(xs: args, f: fn(a) { mbt_pos(target: a) })), + occurrence_id: OccurrenceSynthetic + } +} + +fn mbt_outcome_of(args: List) -> Node { + mbt_instantiation(head: mbt_ref(segs: [^p, ^Outcome]), args: args) +} + +fn mbt_scrutinee() -> Node { + mbt_atom(s: ^mbt_scrutinee_operand) +} + +// A DERIVED TYPE FOR A SUPPLIED NODE, minted by infer's OWN constructor rather than by a hand-built +// InferredFacts record: a record assembled here would be this module's guess at what a derived +// grounding looks like, and the guess would keep passing after the real shape moved. +fn mbt_facts_typed(node: Node, ty: Node) -> Optional { + match inferred_facts_from_derived_type( + node: node, + derived_type: ty, + descent: infer_descent_witness_for_node(n: node) + ) { + Accepted { value: facts, diagnostics: _ } => optional_present(value: facts) + Rejected { diagnostics: _ } => optional_absent() + } +} + +fn mbt_entry(node: Node, ty: Node) -> List { + match mbt_facts_typed(node: node, ty: ty) { + Absent => Empty + Present { value: facts } => [InferredFactsEntry { node: node, facts: facts }] + } +} + +fn mbt_arm(pattern: Node, body: Node) -> Node { + mbt_conj(fields: [ + mbt_named(name: match_arm_pattern, target: pattern), + mbt_named(name: match_arm_body, target: body) + ]) +} + +// A CONSTRUCTOR PATTERN THROUGH THE CONSTRUCT ENCODING'S OWN BUILDER. infer_coproduct_arm_pattern reads +// the tag from construct_tag_path_optional, so a pattern built any other way would be read as unread -- +// which is the exact defect gunbc#12714's annotation above records, reached from the other side. +fn mbt_variant_pattern(tag: Symbol, fields: List) -> Node { + construct_node( + reference: mbt_ref(segs: [^p, tag]), + field_edges: fields, + source: mbt_atom(s: ^mbt_pattern_site) + ) +} + +fn mbt_binder(name: Symbol) -> Node { + mbt_atom(s: name) +} + +fn mbt_accepted_arm(body: Node) -> Node { + mbt_arm( + pattern: mbt_variant_pattern(tag: ^Accepted, fields: [ + mbt_named(name: ^value, target: mbt_binder(name: ^artifact)), + mbt_named(name: ^diagnostics, target: mbt_binder(name: ^d)) + ]), + body: body + ) +} + +fn mbt_rejected_arm(body: Node) -> Node { + mbt_arm( + pattern: mbt_variant_pattern(tag: ^Rejected, fields: [ + mbt_named(name: ^diagnostics, target: mbt_binder(name: ^r)) + ]), + body: body + ) +} + +fn mbt_match_node(scrutinee: Node, arms: List) -> Node { + Node { + kind: ComputationNode { behavior: Match }, + children: list_append(left: [mbt_pos(target: scrutinee)], right: list_map(xs: arms, f: fn(a) { mbt_pos(target: a) })), + occurrence_id: OccurrenceSynthetic + } +} + +// THE BODIES ARE TYPED TOO, so a refusal row below refuses for the reason it names rather than for a +// body the stage could not type. `mbt_supplied_bodies` is the pair of typed bodies both well-formed +// arms share, and the frontier row deliberately omits them. +fn mbt_body_a() -> Node { + mbt_atom(s: ^mbt_body_a) +} + +fn mbt_body_b() -> Node { + mbt_atom(s: ^mbt_body_b) +} + +fn mbt_parse_tree_type() -> Node { + mbt_ref(segs: [^p, ^ParseTree]) +} + +fn mbt_typed_bodies() -> List { + list_append( + left: mbt_entry(node: mbt_body_a(), ty: mbt_parse_tree_type()), + right: mbt_entry(node: mbt_body_b(), ty: mbt_parse_tree_type()) + ) +} + +fn mbt_entries_for(scrutinee_type: Node) -> List { + list_append( + left: mbt_entry(node: mbt_scrutinee(), ty: scrutinee_type), + right: mbt_typed_bodies() + ) +} + +fn mbt_supplied_arms() -> List { + [mbt_rejected_arm(body: mbt_body_a()), mbt_accepted_arm(body: mbt_body_b())] +} + +fn mbt_supplied_match(scrutinee_type: Node, arms: List) -> Outcome { + infer_match_coproduct( + node: mbt_match_node(scrutinee: mbt_scrutinee(), arms: arms), + partials: Empty, + entries: mbt_entries_for(scrutinee_type: scrutinee_type), + resolved: mbt_resolved() + ) +} + +fn mbt_supplied_refuses_with(o: Outcome, reason: Symbol) -> Bool { + match o { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => contains(xs: mbp_reasons(r: r), item: reason, eq: fn(a, b) { a == b }) + } +} + +fn mbt_supplied_accepts_reporting(o: Outcome, reason: Symbol) -> Bool { + match o { + Rejected { diagnostics: _ } => false + Accepted { value: _, diagnostics: d } => + match d { + None => false + Some { diagnostics: r } => contains(xs: mbp_reasons(r: r), item: reason, eq: fn(a, b) { a == b }) + } + } +} + +// THE POSITIVE CONTROL FOR THE SUPPLIED SLICE, and it is what stops every refusal row in this file from +// passing because the fixture is malformed. A match over Outcome with both variants +// covered and both bodies typed must be accepted AND GROUNDED -- grounded rather than merely accepted, +// because the frontier arm also accepts, so "Accepted" alone would be satisfied by a fixture whose +// scrutinee type never read at all. If the index, the declaration, the arm encoding, the entries or the +// instantiation were wrong, this row goes red and every refusal above stops being evidence about the +// judgment it names. +test fn mbt_the_supplied_match_is_accepted_and_typed_holds() -> Bool { + match mbt_supplied_match( + scrutinee_type: mbt_outcome_of(args: [mbt_ref(segs: [^p, ^ParseArtifact])]), + arms: mbt_supplied_arms() + ) { + Rejected { diagnostics: _ } => false + Accepted { value: facts, diagnostics: _ } => inferred_facts_grounding_derived(facts: facts) + } +} diff --git a/src/v2/test/claim/parse/expression_bodied_continuation_test.dag b/src/v2/test/claim/parse/expression_bodied_continuation_test.dag index 47e40606900..25c2e9aa785 100644 --- a/src/v2/test/claim/parse/expression_bodied_continuation_test.dag +++ b/src/v2/test/claim/parse/expression_bodied_continuation_test.dag @@ -17,7 +17,7 @@ import v2.std.diagnostic { outcome_diagnostics } import v2.std.logic { Bool } -import v2.std.node { Node, Symbol, TypeNode, Atom } +import v2.std.node { Arrow, Atom, Conj, Edge, Named, Node, Positional, Symbol, TypeNode } import v2.std.node_query { find_arrow_body_child } import v2.std.text { String } import std.occurrence_identity { OccurrenceSynthetic } @@ -173,7 +173,18 @@ test fn cont_empty_eq_fn_decl_refuses_holds() -> Bool { } } -// THE THREE NORMALIZE ROWS. +// THE THREE NORMALIZE ROWS. THEY STAY ON THE REAL CHAIN AND THEY STAY OVER THE NEW-WITNESS MARGIN, and +// that is reported rather than engineered away. Where their cost sits was decided by measurement rather +// than by reasoning: claim_batch over this entry, run against a transient row asserting only that +// normalize ACCEPTS the braced control, puts effectively all of the expense in the parse and in +// normalize itself and effectively none in the recursive arrow-body search -- so there is no cheap half +// to remove. Both halves ARE this row's subject, which is "the declaration this parser produces still +// carries its arrow body after normalize", so there is nothing here to supply that would not sever the +// link being claimed. A hand-built parse tree handed to normalize would reach the same verdict through +// a shape this file designed rather than the one the parser emits, which is the failure DESIGN +// section 3 names as a fixture passing after the world it was built against moved. The isolating row is +// not retained, because it answered its question once and would otherwise stand as one more +// over-margin witness for a split that is now known. test fn cont_braced_fn_decl_survives_normalize_holds() -> Bool { cont_normalize_has_arrow_body(parsed: cont_braced_parsed()) } @@ -204,17 +215,43 @@ test fn cont_the_arrow_body_search_answers_false_for_a_leaf_holds() -> Bool { } // AND ITS POSITIVE CONTROL AT THE SAME BOUNDARY: the search must find a body that IS there, reached -// by RECURSION rather than at the root. The braced control's normalized tree carries its arrow below -// the module root, so a search that only inspected the root answers false here. -test fn cont_the_arrow_body_search_recurses_to_find_a_body_holds() -> Bool { - match cont_normalized_tree(parsed: cont_braced_parsed()) { - Rejected { diagnostics: _ } => false - Accepted { value: root, diagnostics: _ } => - cont_tree_has_arrow_body(root: root) - && !cont_root_itself_has_arrow_body(root: root) +// by RECURSION rather than at the root. The tree is SUPPLIED, and that is the rule rather than a +// saving: this claim's subject is one interface -- what cont_tree_has_arrow_body answers for a tree +// whose arrow body sits below the root -- and it used to obtain that tree by running +// dag_prepared_grammar, tokenize, parse_module_prepared and normalize, re-executing four stages whose +// results it never inspected (DESIGN section 3, a witness discriminates at one interface). The +// diagnostic was exact: the row's cost did not move when its assertions changed, because the +// assertions were never the expense. +// +// THE PAIRING OBLIGATION IS DISCHARGED BY cont_braced_fn_decl_survives_normalize_holds, which runs the +// real chain over the real source and finds the same body through the same search -- so the shape +// supplied here is one the real producer emits, asserted over the production route and not assumed. +// Deleting normalize from that chain makes it fail; this row would not notice, which is exactly why it +// is not the one that may stand alone. +fn cont_arrow_with_body(body: Node) -> Node { + Node { + kind: TypeNode { connective: Arrow }, + children: [ + Edge { label: Named { name: ^arrow_body_edge }, target: body } + ], + occurrence_id: OccurrenceSynthetic } } +fn cont_root_over(child: Node) -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [Edge { label: Positional, target: child }], + occurrence_id: OccurrenceSynthetic + } +} + +test fn cont_the_arrow_body_search_recurses_to_find_a_body_holds() -> Bool { + let root = cont_root_over(child: cont_arrow_with_body(body: cont_leaf_atom())) + cont_tree_has_arrow_body(root: root) + && !cont_root_itself_has_arrow_body(root: root) +} + fn cont_root_itself_has_arrow_body(root: Node) -> Bool { match find_arrow_body_child(root: root) { Accepted { value: _, diagnostics: _ } => true @@ -245,19 +282,29 @@ fn cont_agrees_with_subject(mine: Outcome, theirs: Outcome) -> Bool } } +// +// ONE SOURCE PER ROW. The three agreements are independent cases over three different texts, so +// folding them into one row forced six parses through one enrolment budget and lost no coverage when +// split -- which is the conjunction tell DESIGN section 3 names beside the reach it is a symptom of. test fn cont_the_continuation_parses_the_same_tree_as_the_subject_holds() -> Bool { cont_agrees_with_subject( mine: cont_expression_bodied_parsed(), theirs: g_tokenize_parse(text: expression_bodied_fn_source, file: ^probe_expr_fn) ) - && cont_agrees_with_subject( - mine: cont_expression_bodied_literal_parsed(), - theirs: g_tokenize_parse(text: expression_bodied_literal_fn_source, file: ^probe_expr_fn_lit) - ) - && cont_agrees_with_subject( - mine: cont_braced_parsed(), - theirs: g_tokenize_parse(text: braced_fn_control_source, file: ^probe_braced_fn) - ) +} + +test fn cont_the_continuation_parses_the_literal_form_as_the_subject_does_holds() -> Bool { + cont_agrees_with_subject( + mine: cont_expression_bodied_literal_parsed(), + theirs: g_tokenize_parse(text: expression_bodied_literal_fn_source, file: ^probe_expr_fn_lit) + ) +} + +test fn cont_the_continuation_parses_the_braced_control_as_the_subject_does_holds() -> Bool { + cont_agrees_with_subject( + mine: cont_braced_parsed(), + theirs: g_tokenize_parse(text: braced_fn_control_source, file: ^probe_braced_fn) + ) } // AND IT AGREES ON THE REFUSAL TOO, so the empty-`=` row is the subject's refusal and not merely a diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 50494e8f586..3383e363cf8 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -41,6 +41,32 @@ import v2.std.qualified_name { QualifiedName, declaration_reference_path_optiona // occurrence, not to a declaring path, so `infer_parameter_scope_search` answers a different question // and is not replaced here. The refinement rows in v2.test.claim.body_cast_node are its standing // control and they pass on this base. +// WHY EVERY ROW HERE EXECUTES THE WHOLE FRONT END, AND WHY THAT IS NOT CONVERTED AWAY. DESIGN +// section 3's witness rule says a claim's inputs belong at the boundary it discriminates, as supplied +// values, and the rows below supply none: each assembles authored text, resolves it and infers over it. +// That was interrogated row by row against the rule rather than defended by habit, and the reason it +// stands is the one DESIGN section 4b states for the top rung -- ASK WHETHER THE CHECK'S RED IS +// AUTHORABLE BEFORE WRITING THE CHECK. +// +// THE SUBJECT OF THESE ROWS IS WHAT RESOLVE AND INFER PRODUCE FOR AUTHORED TEXT, not what one +// downstream function answers for a shape. Every row asks whether a reference or a call GROUNDS, and +// what it grounds to -- facts about the resolved-declaration index the front end mints and about the +// declared-return spelling lowering leaves behind. Against a supplied index and supplied facts each of +// those answers is a restatement of the fixture, so the row's red would not be authorable: it would +// become a decoration, which DESIGN says is worse than an absent check because it will be cited as +// coverage. The two same-leaf rows make this sharpest -- a hand-built index picks the declaration the +// author intended, which is exactly the selection under test. +// +// SO THESE ROWS ARE THE EXECUTION OF THE REAL PATH -- the second half of the same rule, which forbids +// supplying inputs anywhere if it removes the last execution of the producer. Deleting resolve's +// projection lowering, or infer's declared-field read, must make a control here fail, and does. +// +// WHAT WOULD ACTUALLY MOVE THEIR COST is not a narrower claim but a provider for the demand: their +// nullary source producers are pure functions of module-constant text, which is the shape +// v2.workflow.floor_pure_producer_share already serves across claims for hundreds of peer fixtures, +// including the per-fixture assembled and resolved trees rostered there. Enrolment is NOT asserted or +// proposed here, because that roster's own admission criterion is a measured serve-below-recompute on +// a required-floor receipt, which this file cannot produce; naming the route is the honest half. data dre_artifact: Artifact = Artifact { kind: SourceFile, id: ^declaration_reference_evidence_artifact, From 78c244429a2a2fd46eb3b44fc48ab9c36c59fabc Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 01:05:58 +0000 Subject: [PATCH 39/90] Delete the duplicate Loop dispatch arm the merge left behind The merged head emitted a compiler that would not build: error: unreachable pattern, src/v2_compiler_resolve.rs:1778 NodeKind::ComputationNode { behavior: Behavior::Loop, .. } matches all the relevant values ... no value can reach this (#![deny(unreachable_patterns)]) I consolidated the two resolve_loop_node DECLARATIONS the merge produced and missed that main had also added its own arm to resolve_node_walk's dispatch, so the Loop behaviour appeared twice. Both arms call the same consolidated function, so this is a pure deletion with no behavioural change. WORTH RECORDING FOR THE CLASS: the interpreted route cannot see this. A duplicate match arm is dead code there, and the ten supplied-node controls plus every claim set stayed green across it. Only the EMITTED Rust refuses it, under a lint the emitted crate turns on deliberately -- so the defect was invisible to every check except the one that costs 19 minutes. That is the same asymmetry as the enrolment margin (local floor silent, CI refusing) and the same remedy: the native route is not optional evidence for a change that edits a dispatch the emitter renders as a match. callable_binder_slice 10/10 after the deletion. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/03_resolve.dag | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 7c4f4899790..ebeca5df7e1 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -2438,8 +2438,6 @@ fn resolve_node_walk( resolve_loop_node(ctx: ctx, n: n) ComputationNode { behavior: Transform } => resolve_transform_children(ctx: ctx, n: n) - ComputationNode { behavior: Loop } => - resolve_loop_node(ctx: ctx, n: n) TypeNode { connective: Conj } => match construct_tag_reading(n: n) { ConstructTagElided => From 2bd1f0ded4144ba0422b6dce0dccd40852b25b1d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 02:30:47 +0000 Subject: [PATCH 40/90] The fold slot is a generated binder, not the authored step formal; this lane's carrier frame dissolves The #12550 follow-up. Two defects, and the second is mine. (1) THE ENCODING BORROWED THE AUTHORED SPELLING. fold_recurrence_encoding spelled the Bind's binder and the ^loop_carrier_edge with operands.carrier -- the step's own first binder -- so the lowered tree held TWO VISIBLE BINDERS WITH ONE NAME: a slot that persists ACROSS iterations, and the step Arrow's formal bound FRESHLY per invocation. They hold the same value at the call boundary and are not the same binding occurrence; the author wrote only the formal. resolve's value-binder admission refused the inner one, correctly, and the native eight refused at prepare with resolve_reason_binder_hides_visible_value at a synthetic "found". The slot now comes from v2.std.anonymous_binder fresh_fold_carrier, keyed by the STEP's content hash: unauthorable (`<` is not an identifier character), deterministic, derived from structure rather than occurrence or traversal order, and distinct wherever two folds' scopes can overlap -- a nested fold's step strictly contains the inner one so their digests differ, while sibling folds with identical steps share a digest harmlessly, because neither Bind's frame contains the other. It is homed beside fresh_type_variable because that authority already mints generated binders named by structure and argues the same nested/sibling case. The step is still carried WHOLE with both binders. Dropping its first formal so it could read the slot would make fold lambdas a second kind of function value (DESIGN section 3), and the carrier reaches the step BY ROLE -- slot value is actual 0, domain member is actual 1 -- never by equal spelling, which after this mint is impossible. (2) THIS LANE'S CARRIER FRAME WAS CORRECT FOR A STRUCTURE THAT NO LONGER EXISTS, AND IT IS DELETED. The pre-#12550 seam built a BARE Loop with no enclosing Bind, so the carrier was bound by nothing and a step body's use of it reached the bare-name census -- the original `found` refusal, which a frame at the Loop repaired. #12550's encoding binds the carrier on an enclosing Bind, where v2.std.node's role model always said it lived and where main's own annotation says it resolves. The frame then became redundant AND harmful: the Loop re-admitted a name the Bind had already bound and this lane's admission refused it as hiding. That is what reds FIVE of #12550's own claims, three of them collateral. Deleted rather than guarded, because a guard would have been this walk encoding a fact about which producer built the Loop (section 6b). resolve_loop_carrier_binder, resolve_loop_children and ResolveLoopCarrier went with it (section 3c). CONSUMERS UPDATED RATHER THAN SILENCED. The accumulator-copy lens reads the step's first binder off the CALL, which is the right read and keeps working; its annotation claimed that symbol was "the same symbol the encoding's ^loop_carrier_edge carries", which is now false and is corrected. Two claims asserted the spelling equality and now assert the SEPARATION in both directions -- the carrier IS a fold-carrier slot and is NOT the authored binder -- because either half alone is satisfiable by accident: a row asserting only the first would still pass if the mint were keyed on the authored name. AND A VACUITY CLOSED IN THIS LANE'S OWN CONTROLS. The two carrier rows carried a bound measure, whose symbol the CORPUS declares; under a supplied empty namespace it cannot bind, so the Loop refused for THAT reason and the row would have passed even had the carrier been bound. Measured: the measure atom alone refuses under that context, and the same Loop without it resolves once the name is bound above. Both rows are now measure-free and discriminate on the carrier alone. fold_encoding 11/11, fold_lowering 20/20, callable_binder_slice 9/9. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/03_resolve.dag | 101 ++++-------------- src/v2/compiler/fold_lowering.dag | 24 ++++- .../complexity_accumulator_copy/analyze.dag | 2 +- src/v2/std/anonymous_binder.dag | 37 +++++++ .../callable_binder_slice_test.dag | 61 ++++++----- src/v2/test/claim/fold_encoding_test.dag | 21 +++- src/v2/test/claim/fold_lowering_test.dag | 16 ++- 7 files changed, 149 insertions(+), 113 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index ebeca5df7e1..d32b55b4e00 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -471,59 +471,6 @@ fn gate_value_binders(names: List, outer: Scope, at: Node) -> BinderGate // The carrier edge is carried UNWALKED for the same reason the Arrow's declared-order edge is // (resolve_arrow_node_in): its target names a binder, not a reference, so resolving it would ask the // scope to answer for a name the scope is being opened to introduce. -// THE CARRIER IS FOUND BY ITS ROLE, not by a named-edge lookup: v2.std.node loop_edge_role is the one -// reader of what each of a Loop's edges means, and asking it here keeps this walk from being a second -// place that knows the edge's spelling. (The named-edge lookup would also drag NamedEdgeTargetLookup's -// own `Absent` into this module, where it collides with Optional's.) -type ResolveLoopCarrier { - name: Symbol - node: Node -} - -fn resolve_loop_carrier_binder(n: Node) -> Optional { - fold(n.children, init: Absent, f: fn(acc, e) { - match acc { - Present { value: found } => Present { value: found } - Absent => - match loop_edge_role(e: e) { - LoopCarrierBinder => - match e.target.kind { - TypeNode { connective: Atom { identity: id } } => - Present { value: ResolveLoopCarrier { name: id, node: e.target } } - _ => Absent - } - _ => Absent - } - } - }) -} - -// A LOOP'S BINDER AND ITS MEASURE ARE CARRIED UNWALKED; ITS BODY AND ITS DOMAIN ARE RESOLVED. This is -// v2.std.node's own role model read back: the carrier "targets the loop-carried state's BINDER, never a -// value", and the bound edge targets "the termination measure". Neither is a value reference, so asking -// the scope to answer for them is the same category error the Arrow's declared-order edge avoids -// (resolve_arrow_node_in carries it unwalked for exactly this reason). The fold seam's measure is the -// marker ^dag_surface_fold_iteration_measure, which names no value and resolved to nothing -- that -// refusal, not the carrier, is what a Loop walk hit first once the carrier's frame existed. -// -// DECLARED FRONTIER: a measure that is a real expression rather than a marker would need resolving, and -// this walk does not do it. No producer emits one today (v2.compiler.fold_lowering -// fold_iteration_measure_atom is the only measure in the corpus), and the trigger is the first producer -// that emits a computed measure -- at which point the measure gets a role-specific walk rather than -// being carried. -fn resolve_loop_children(ctx: ResolveContext, n: Node) -> ResolveNodeWalk { - child_walk_node(n: n, w: fold(n.children, init: child_walk_init(), f: fn(acc, e) { - match loop_edge_role(e: e) { - LoopCarrierBinder => - child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None }) - LoopBoundMeasure => - child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None }) - LoopRealizedDeclaration => - child_walk_step(w: acc, e: e, r: resolve_walk_of_outcome(o: resolve_realized_declaration(ctx: ctx, head: e.target))) - _ => child_walk_step(w: acc, e: e, r: resolve_node_walk(ctx: ctx, n: e.target)) - } - })) -} fn harvest_conj_named_bindings(root: Node, acc: Map) -> Map { match root.kind { @@ -2464,36 +2411,28 @@ fn resolve_node_walk( // frame and the domain and body resolve as ordinary expressions. ^loop_realized_declaration_edge // (v2.std.node LoopRealizedDeclaration) is a CLAIM made by a desugaring about which declaration the // author's head names. Resolve checks that claim; it does not take it on trust. -// ONE LOOP WALK, CARRYING TWO FACTS THAT ARRIVED FROM DIFFERENT DIRECTIONS. gunbc#12550 gave a Loop a -// realized-declaration head and made the fold encoding bind its carrier on an enclosing Bind; this lane -// gave the carrier edge and the bound measure the treatment v2.std.node's role model already describes -- -// the carrier "targets the loop-carried state's BINDER, never a value", the bound edge targets "the -// termination measure", and neither is a reference the scope should be asked to answer for. Both walks -// existed as separate `resolve_loop_node` declarations after the merge, which the namespace refused as a -// duplicate; that refusal is the only reason this consolidation is one edit rather than two. +// THIS LANE'S CARRIER FRAME IS DISSOLVED, AND THE REASON BELONGS HERE. The pre-#12550 fold seam built a +// BARE Loop with no enclosing Bind, so the carrier was bound by nothing and the step body's use of it +// reached the bare-name census; opening a frame at the Loop repaired that, and the native eight moved off +// their `found` refusal because of it. #12550 then replaced the seam with +// Bind { carrier := init, Loop { step, domain, carrier, bound, realized } }, which binds the carrier where +// the role model always said it lived -- the annotation above states it: the carrier resolves against the +// ENCLOSING Bind's frame. // -// THE CARRIER FRAME IS KEPT EVEN THOUGH THE ENCODING'S Bind NOW BINDS THE SAME NAME, because the two are -// not the same scope: the Bind's frame covers the encoding's body, while a Loop reached by any other -// producer carries its binder with no Bind above it. Keeping it here is what makes the carrier a binder -// for every producer of a Loop rather than for the fold encoding alone. Where both bind one name the -// admission answers BinderHidesVisibleValue, and that is a real question about the encoding rather than -// an artifact of this walk -- see the receipt on the integration candidate. +// The frame then became redundant and harmful at once: the Loop re-admitted a name the Bind above it had +// already bound, and this lane's own value-binder admission refused it as hiding a visible binding. That is +// what the native eight reported at a synthetic occurrence, and what reds five of #12550's own claims. +// So the repair was correct for a structure that no longer exists and is DELETED rather than guarded -- a +// guard would have been this walk encoding a fact about which producer built the Loop (DESIGN section 6b), +// and the dissolution-on-climb rule asks for the obsolete production handling to go while the evidence +// stays (section 4b(4)). fn resolve_loop_node(ctx: ResolveContext, n: Node) -> ResolveNodeWalk { - match resolve_loop_carrier_binder(n: n) { - Absent => resolve_loop_children(ctx: ctx, n: n) - Present { value: carrier } => - match admit_value_binders(names: [carrier.name], outer: ctx.scope) { - BinderDuplicateInFrame { binder: b } => - resolve_walk_refused_one(chain: diagnostics_singleton(d: binder_duplicate_in_frame_diagnostic(binder: b, at: carrier.node))) - BinderHidesVisibleValue { binder: b } => - resolve_walk_refused_one(chain: diagnostics_singleton(d: binder_hides_visible_value_diagnostic(binder: b, at: carrier.node))) - BindersAdmitted { locals: locals } => - resolve_loop_children( - ctx: resolve_ctx_with_scope(ctx: ctx, scope: ScopeFrame { locals: locals, outer: ctx.scope }), - n: n - ) - } - } + child_walk_node(n: n, w: fold(n.children, init: child_walk_init(), f: fn(acc, e) { + match loop_edge_role(e: e) { + LoopRealizedDeclaration => child_walk_step(w: acc, e: e, r: resolve_walk_of_outcome(o: resolve_realized_declaration(ctx: ctx, head: e.target))) + _ => child_walk_step(w: acc, e: e, r: resolve_child_edge(ctx: ctx, e: e)) + } + })) } fn realized_declaration_diagnostic(reason: Symbol, n: Node) -> Diagnostic { diff --git a/src/v2/compiler/fold_lowering.dag b/src/v2/compiler/fold_lowering.dag index 8a2749d19dc..b30b00eb0c7 100644 --- a/src/v2/compiler/fold_lowering.dag +++ b/src/v2/compiler/fold_lowering.dag @@ -51,10 +51,12 @@ import v2.std.node { Positional, Symbol, TypeNode, + content_hash, is_empty_conj_root, node_synthetic, symbol_intern_lexeme } +import v2.std.anonymous_binder { fresh_fold_carrier } import std.decl_ref { DeclarationRef, WholeDeclaration } import v2.std.node_query { find_named_child, node_positional_child_targets } @@ -734,16 +736,34 @@ type FoldCallOperands { // child is the step as a function value, the member template of §2.2. ONE constructor, whatever the // grain of its operands: the raw call (read by the accumulator-copy lens) and the lowered operands // (built by v2.compiler.body_lowering_fold) meet here, so there is one encoding and not two. +// THE SLOT IS A GENERATED BINDER, NOT THE AUTHORED FORMAL. This constructor used to spell the Bind's +// binder and the carrier edge with operands.carrier -- the step's own first binder -- so the lowered tree +// held two visible binders with one name: the persistent slot and the step Arrow's freshly-bound formal. +// v2.compiler.resolve's value-binder admission refused the inner one, correctly, and the native eight +// refused at prepare with resolve_reason_binder_hides_visible_value at a synthetic "found". +// +// The slot now comes from v2.std.anonymous_binder fresh_fold_carrier, keyed by the STEP's content hash, so +// it is unauthorable, deterministic, derived from structure rather than occurrence or traversal order, and +// distinct wherever two folds' scopes can overlap. The step is still carried WHOLE, with both of its +// binders: a fold step is an ordinary callable, and dropping its first formal to let it read the slot +// instead would make fold lambdas a second kind of function value (DESIGN section 3). +// +// THE CARRIER REACHES THE STEP BY ROLE. The slot's current value is the step callable's actual 0 and the +// domain member is actual 1; that relation is positional and semantic. No consumer may recover one from +// the other by equal spelling, and after this mint none can: `<` is not an identifier character. +// operands.carrier remains the AUTHORED first binder and is what a consumer wanting the accumulator formal +// reads (fold_call_step_carrier) -- it is no longer the slot's identity. fn fold_recurrence_encoding(operands: FoldCallOperands) -> Node { + let slot = fresh_fold_carrier(step_digest: content_hash(n: operands.step).digest as String) lower_bind( - key_binding: fold_carrier_binder_atom(binder: operands.carrier), + key_binding: fold_carrier_binder_atom(binder: slot), value: operands.init, body_binding: node_synthetic( kind: ComputationNode { behavior: Loop }, children: [ Edge { label: Positional, target: operands.step }, Edge { label: Named { name: ^loop_domain_edge }, target: operands.collection }, - Edge { label: Named { name: ^loop_carrier_edge }, target: fold_carrier_binder_atom(binder: operands.carrier) }, + Edge { label: Named { name: ^loop_carrier_edge }, target: fold_carrier_binder_atom(binder: slot) }, Edge { label: Named { name: ^loop_bound_edge }, target: fold_iteration_measure_atom() }, Edge { label: Named { name: ^loop_realized_declaration_edge }, target: operands.head } ] diff --git a/src/v2/lens/complexity_accumulator_copy/analyze.dag b/src/v2/lens/complexity_accumulator_copy/analyze.dag index 222af828566..475e02c70c5 100644 --- a/src/v2/lens/complexity_accumulator_copy/analyze.dag +++ b/src/v2/lens/complexity_accumulator_copy/analyze.dag @@ -200,7 +200,7 @@ fn call_port_readings(call_capture: Node) -> List Optional { v2.compiler.fold_lowering.fold_call_step_carrier(fold_call: call_capture) } diff --git a/src/v2/std/anonymous_binder.dag b/src/v2/std/anonymous_binder.dag index 386037ca568..2816acd1922 100644 --- a/src/v2/std/anonymous_binder.dag +++ b/src/v2/std/anonymous_binder.dag @@ -65,6 +65,43 @@ fn fresh_type_variable(path: List, label: Symbol) -> Symbol { symbol_intern_lexeme(lexeme: fresh_type_variable_prefix() + p + ":" + symbol_lexeme(sym: label) + ">") } +// A FOLD'S PERSISTENT CARRIER IS A GENERATED VALUE BINDER, AND IT MAY NOT BORROW THE AUTHORED STEP +// PARAMETER'S SPELLING. The fold encoding is +// Bind { carrier := init, Loop { step, domain, carrier, bound, realized } }: the Bind's binder is a slot +// that persists ACROSS iterations, while the step's first formal is bound FRESHLY for one invocation of +// an ordinary callable. They hold the same value at the call boundary and they are not the same binding +// occurrence -- the author wrote only the formal, and the compiler introduces the slot. +// +// WHY THIS MINT EXISTS AT ALL: v2.compiler.fold_lowering used to spell the slot with the step's own first +// binder symbol, so the lowered tree held two visible binders with one name and v2.compiler.resolve's +// value-binder admission refused the inner one -- correctly, against the tree it received. The repair is +// here rather than in the admission: an exception keyed on "same spelling at a synthetic occurrence near a +// carrier edge" would put fold semantics inside the general binder law and rest on a heuristic, and +// OccurrenceSynthetic states only that there is NO authored occurrence, which is not a semantic identity. +// +// NAMED BY STRUCTURE, like the type variable above and for the same reason: the digest is the content hash +// of the STEP, so two folds whose scopes can overlap get different carriers. A nested fold's step strictly +// contains the inner fold, so their digests differ; two sibling folds with identical steps share a digest +// and that is harmless, because neither Bind's frame contains the other and the admission refuses only a +// binder hiding a VISIBLE one. It is not derived from an occurrence or from traversal order, so an edit +// elsewhere in the declaration does not rename it. +// +// THE RELATION TO THE STEP IS POSITIONAL, NEVER NOMINAL. A consumer that needs the step's accumulator +// formal reads the step callable's first declared parameter; a consumer that needs the slot reads the +// carrier edge. Nothing may infer one from the other by equal spelling -- `<` is not an identifier +// character, so after this mint they can never be equal. +fn fold_carrier_prefix() -> String { + " Symbol { + symbol_intern_lexeme(lexeme: fold_carrier_prefix() + step_digest + ">") +} + +fn is_fold_carrier_binder(sym: Symbol) -> Bool { + starts_with(s: symbol_lexeme(sym: sym), prefix: fold_carrier_prefix()) +} + fn fresh_return_type_variable(path: List) -> Symbol { fresh_type_variable(path: path, label: symbol_intern_lexeme(lexeme: "")) } diff --git a/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag index 567a728970a..2ef201f6171 100644 --- a/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag +++ b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag @@ -18,10 +18,9 @@ import v2.compiler.resolve { ResolveWalkAccepted, ResolveWalkRefused, resolve_node_walk, - resolve_loop_carrier_binder, } import v2.std.resolution_policy { default_name_resolution_policy } -import v2.compiler.fold_lowering { fold_carrier_binder_atom, fold_iteration_measure_atom } +import v2.compiler.fold_lowering { fold_carrier_binder_atom } import v2.std.collection { Map, empty_map, map_insert, map_lookup } import std.algebra { Cons, Empty } import v2.std.language_model { void_language_model } @@ -67,8 +66,7 @@ fn cbs_carrier_loop(body: Node, carrier: Symbol) -> Node { kind: ComputationNode { behavior: Loop }, children: [ Edge { label: Positional, target: body }, - Edge { label: Named { name: ^loop_carrier_edge }, target: fold_carrier_binder_atom(binder: carrier) }, - Edge { label: Named { name: ^loop_bound_edge }, target: fold_iteration_measure_atom() } + Edge { label: Named { name: ^loop_carrier_edge }, target: fold_carrier_binder_atom(binder: carrier) } ], occurrence_id: OccurrenceSynthetic } @@ -226,14 +224,19 @@ test fn cbs_a_binder_spelled_like_a_root_declaration_is_admitted_holds() -> Bool // producer in question. Until that runs green the rows above are readings of a boundary, and this // comment is where that debt is recorded rather than in a green row that would imply otherwise. -// (8) THE WALK BINDS THE CARRIER -- the repair, exercised at resolve's own interface with a supplied Loop -// and a supplied context. Before it, a Loop fell to resolve_children_homogeneous_scope, opened no frame, -// and the iterated body's use of the carrier reached the bare-name census. -// THE CONTEXT CARRIES A VOID LANGUAGE MODEL, not the dag one. The Loop walk consults the scope chain and -// nothing else, so building the real grammar would be paid by every row here for no discrimination -- and -// this file exists partly because that reach is what refused this lane's other claim sets at the -// enrolment margin. If a future arm of this walk does consult the model, these rows must carry the real -// one and say so. +// (8) AND A LOOP DOES NOT BIND ITS CARRIER -- THE ENCLOSING Bind DOES. These two rows asserted the +// opposite until gunbc#12550 landed, and the reversal is the honest record of a repair that dissolved +// rather than a test that was wrong. The pre-#12550 fold seam built a BARE Loop with no enclosing Bind, so +// its carrier was bound by nothing and a step body's use of it reached the bare-name census; a frame +// opened at the Loop repaired that, and these rows were its evidence. #12550 replaced the seam with +// Bind { carrier := init, Loop { ... } }, where v2.std.node's role model always said the binder lived -- +// "the carried state's initial value lives on an enclosing Bind" -- and the frame became redundant AND +// harmful, because the Loop re-admitted a name the Bind had bound and this file's own admission refused it +// as hiding a visible binding. +// +// So the rows now assert the model that is true: a BARE Loop's carrier binds nothing, and a body that uses +// it is unbound. A reader who expects the old behaviour is reading evidence for a seam that no longer +// exists (DESIGN section 4b(4): the production handling goes, the evidence stays and flips). fn cbs_ctx(scope: Scope) -> ResolveContext { ResolveContext { scope: scope, @@ -253,22 +256,28 @@ fn cbs_walk_accepts(n: Node) -> Bool { } } -test fn cbs_a_loop_body_use_of_the_carrier_resolves_holds() -> Bool { - cbs_walk_accepts(n: cbs_carrier_loop(body: cbs_atom(id: ^found), carrier: ^found)) +test fn cbs_a_bare_loop_does_not_bind_its_carrier_holds() -> Bool { + !cbs_walk_accepts(n: cbs_carrier_loop(body: cbs_atom(id: ^found), carrier: ^found)) } -// (9) THE DISCRIMINATING NEGATIVE. The same Loop whose body uses a name the carrier does NOT introduce -// still refuses, so row (8) is not a frame that answers for everything. -test fn cbs_a_loop_body_use_of_an_unrelated_name_still_refuses_holds() -> Bool { - !cbs_walk_accepts(n: cbs_carrier_loop(body: cbs_atom(id: ^unrelated_name), carrier: ^found)) -} +// NEITHER ROW CARRIES A BOUND MEASURE, AND THAT IS NOT TIDYING -- IT CLOSES A VACUITY. The measure the +// fold encoding uses is ^dag_surface_fold_iteration_measure, a symbol the CORPUS declares (it is +// registered in v2.std.cardinality measure_descent_fact_registry); the supplied namespace these rows walk +// under is empty, so it cannot bind that symbol and a Loop carrying it refuses for THAT reason. Row (8) +// passed with a measure present, which means it would have passed even if the carrier HAD been bound -- +// the measure was answering for it. Measured: the measure atom alone refuses under this context, and the +// same carrier Loop without it resolves once the name is bound above. -// (10) THE CARRIER IS FOUND BY ITS ROLE. Row (8) would also pass if the reader found the carrier by edge -// position; this asserts the role reader answers, which is what keeps this walk from being a second place -// that knows the edge's spelling. -test fn cbs_the_carrier_is_read_by_its_role_holds() -> Bool { - match resolve_loop_carrier_binder(n: cbs_carrier_loop(body: cbs_atom(id: ^found), carrier: ^found)) { - Present { value: c } => c.name == ^found - Absent => false +// (9) AND THE SAME BARE LOOP UNDER A SCOPE THAT DOES BIND THE NAME RESOLVES -- so row (8) reports "the +// Loop opens no frame" rather than "this Loop never resolves". That is the discriminating pair: the only +// variable between them is whether something ABOVE the Loop binds the carrier, which is exactly what the +// fold encoding's Bind now does. +test fn cbs_a_loop_carrier_bound_above_resolves_holds() -> Bool { + match resolve_node_walk( + ctx: cbs_ctx(scope: cbs_frame_binding(name: ^found)), + n: cbs_carrier_loop(body: cbs_atom(id: ^found), carrier: ^found) + ) { + ResolveWalkAccepted { value: _, diagnostics: _ } => true + ResolveWalkRefused { first: _, rest: _, observation: _ } => false } } diff --git a/src/v2/test/claim/fold_encoding_test.dag b/src/v2/test/claim/fold_encoding_test.dag index d9f98ed3236..89e422c4a44 100644 --- a/src/v2/test/claim/fold_encoding_test.dag +++ b/src/v2/test/claim/fold_encoding_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.fold_encoding +import v2.std.anonymous_binder { is_fold_carrier_binder } import v2.compiler.reference_conservation_admission { conserved_normalize_of_text, no_explained_drops } import v2.extdeps.languages.dag { qualified_name_from_module_node } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } @@ -102,6 +103,24 @@ type FeSlots { head: OccurrenceId } +// THE CARRIER IS NOT AN AUTHORED OPERAND, SO IT IS NOT CHECKED AS ONE. This conjunct read +// fe_atom_is(n: carrier, sym: ^acc) -- the step's own first binder -- which held while +// v2.compiler.fold_lowering spelled the persistent slot with operands.carrier, and which made the lowered +// tree hold two visible binders with one name (the slot and the step Arrow's freshly-bound formal). +// v2.compiler.resolve's value-binder admission refused the inner one. +// +// The slot is now minted by v2.std.anonymous_binder fresh_fold_carrier, so it has no authored occurrence +// and no authored spelling -- which is why it never appears in FeSlots beside init, collection and head. +// Those three ARE authored operands and this claim still holds each at its own minted occurrence. The +// carrier is instead asserted to BE a generated slot and NOT to be the authored binder: either half alone +// is satisfiable by accident, and together they are the separation the encoding now guarantees. +fn fe_carrier_is_a_generated_slot(n: Node) -> Bool { + match node_atom_identity_optional(node: n) { + Present { value: sym } => is_fold_carrier_binder(sym: sym) && !(sym == ^acc) + Absent => false + } +} + fn fe_slots(encoding: Node) -> Optional { match fold_recurrence_loop(encoding: encoding) { Absent => Absent @@ -122,7 +141,7 @@ fn fe_slots(encoding: Node) -> Optional { Absent => Absent Present { value: step } => if fe_atom_is(n: init, sym: ^seed) && fe_atom_is(n: collection, sym: ^xs) - && fe_atom_is(n: head, sym: ^fold) && fe_atom_is(n: carrier, sym: ^acc) + && fe_atom_is(n: head, sym: ^fold) && fe_carrier_is_a_generated_slot(n: carrier) && fe_is_arrow(n: step) { match fe_minted(n: init) { Absent => Absent diff --git a/src/v2/test/claim/fold_lowering_test.dag b/src/v2/test/claim/fold_lowering_test.dag index 4de1728f1ab..978bdfd333a 100644 --- a/src/v2/test/claim/fold_lowering_test.dag +++ b/src/v2/test/claim/fold_lowering_test.dag @@ -217,14 +217,26 @@ fn with_duplicate_carrier_edge(n: Node, binder: Node) -> Node { ) } -test fn lowered_loop_binds_carrier_binder() -> Bool { +// THE CARRIER IS A GENERATED SLOT, NOT THE AUTHORED BINDER, AND THIS ROW IS WHERE THAT IS ASSERTED. It +// read `sym == ^acc` -- the step's own first binder -- which was true while +// v2.compiler.fold_lowering spelled the Bind's binder and the carrier edge with +// operands.carrier. That made the lowered tree hold TWO VISIBLE BINDERS WITH ONE NAME, the persistent slot +// and the step Arrow's freshly-bound formal, and v2.compiler.resolve's value-binder admission refused the +// inner one. The slot now comes from v2.std.anonymous_binder fresh_fold_carrier. +// +// WHAT IT ASSERTS NOW IS THE SEPARATION ITSELF, in both directions, because either half alone is +// satisfiable by an accident: the carrier IS a fold-carrier binder (so the row fails if the mint is +// bypassed and some other symbol appears), and it is NOT the authored binder (so the row fails if the +// constructor goes back to borrowing `acc`). A row asserting only the first would still pass if the mint +// were keyed on the authored name. +test fn lowered_loop_carrier_is_a_generated_slot_not_the_authored_binder() -> Bool { match lowered_fixture_loop() { Absent => false Present { value: lp } => match carrier_binder_of(n: lp) { Found { target: binder } => match node_atom_identity_optional(node: binder) { - Present { value: sym } => sym == ^acc + Present { value: sym } => is_fold_carrier_binder(sym: sym) && !(sym == ^acc) Absent => false } Ambiguous => false From bd9d3d3bb5ca9430ed30bec84090f56e52b9bf29 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 03:25:23 +0000 Subject: [PATCH 41/90] A root binding resolves a name but may not be projected through Wall 3d. All eight of v2.test.parse.expression_bodied_fn_decl_parse refused natively at infer with infer_reason_projection_receiver_declares_no_fields, the chain walking "v2" then "live_tree" then "LiveTreeDisposition" -- and the file writes no projection at all, only `data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly`. So a reader was CLAIMING a projection, and it was this lane's. resolve_projection_base admitted BoundAtRoot, so the head segment of a MODULE PATH became a projection base and the remaining segments became nested field projections; infer then asked a COPRODUCT for its fields. The arm exists so a LOCAL binder shadows an absolute path (`b.tree` where `b` is a parameter), which is what dissolved the AmbiguousQualifiedHeadShadowsAbsolute guard -- a name bound at the module ROOT is a declaration or a namespace segment, so a dotted path through it is a qualified name and not a projection. REMOVING BoundAtRoot OUTRIGHT WAS ALSO WRONG, and the controls said so in one run: the same arm is how a plain root-bound ANNOTATION resolves, so `type Box { tree: Int }` with `fn f(b: Box) -> Int { 7 }` -- no projection anywhere -- stopped inferring. The two shapes look alike at the head and are distinguished by whether there are FIELDS: with none, this resolves a name and either binding answers; with fields, it is building a projection and the base must be a lexical binder. lookup_chain already carries that distinction, and it is the same one the value-binder admission uses to decide what hiding means. TWO REDS IN THIS FILE PREDATE THIS CHANGE AND ARE NOT CAUSED BY IT, qualified by running them at 2bd1f0ded41 before the edit: fps_the_receiver_without_a_projection_infers and fps_the_match_form_infers_and_the_projection_stays_on_the_frontier. Their reason is infer_facts_key_conflict -- main's #12582, which made a key carrying two different facts a REFUSAL. That is the synthetic-key collision this lane measured and documented in v2.test.claim.callexec.synthetic_facts_key_collision, now load-bearing instead of latent: structurally identical nodes minted at OccurrenceSynthetic share one facts key, and this lane's projection and spine nodes are minted there. The frozen facts-identity question is therefore un-frozen by main, and it is the next subject rather than a side note. callable_binder_slice 9/9; field projection 14/16 with both reds attributed above. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/03_resolve.dag | 43 ++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index d32b55b4e00..315fc38c9d3 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1103,7 +1103,11 @@ fn resolve_bound_head_projection( ) } Cons { head: head_segment, tail: field_segments } => - match resolve_projection_base(ctx: ctx, head_segment: head_segment) { + match resolve_projection_base( + ctx: ctx, + head_segment: head_segment, + projects_fields: match field_segments { Empty => false Cons { head: _, tail: _ } => true } + ) { Absent => Rejected { diagnostics: rejected_with_pending( @@ -1127,7 +1131,32 @@ fn resolve_bound_head_projection( } } -fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional { +// A ROOT BINDING RESOLVES A NAME BUT MAY NOT BE PROJECTED THROUGH, AND THE TWO CASES ARE DISTINGUISHED BY +// WHETHER THERE ARE FIELDS. This arm serves two shapes that look alike at the head: a single-segment name +// bound at the module root (`Box` in `fn f(b: Box)`), where there is nothing to project and the head IS the +// answer; and a dotted path whose head is a LOCAL binder (`b.tree` where `b` is a parameter), which is a +// field projection and is why this arm exists -- it is what dissolved the earlier +// AmbiguousQualifiedHeadShadowsAbsolute guard. +// +// It admitted BoundAtRoot in BOTH shapes, and that is the defect: a name bound at the module root is a +// DECLARATION or a namespace segment, so a dotted path THROUGH it is a qualified name, not a projection. +// MEASURED: the head segment of a module path (`v2` in v2.std.live_tree.LiveTreeDisposition) became a +// projection base and the remaining segments became nested field projections, so infer asked a COPRODUCT +// for its fields and all eight of v2.test.parse.expression_bodied_fn_decl_parse refused natively with +// infer_reason_projection_receiver_declares_no_fields -- the chain walking "v2", "live_tree", +// "LiveTreeDisposition". Removing BoundAtRoot outright was also wrong, and the controls said so +// immediately: it is how a plain root-bound ANNOTATION resolves, so a module with a record type and no +// projection at all stopped inferring. +// +// So the gate is projects_fields: with no field segments this resolves a name and admits either binding; +// with field segments it is building a projection and the base must be a lexical binder. lookup_chain +// already carries that distinction (ScopeBinding), and it is the same one the value-binder admission uses +// to decide what hiding means. +fn resolve_projection_base( + ctx: ResolveContext, + head_segment: Node, + projects_fields: Bool +) -> Optional { match head_segment.kind { TypeNode { connective: Atom { identity: name } } => match lookup_chain(s: ctx.scope, name: name) { @@ -1139,9 +1168,13 @@ fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional< value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) ) BoundAtRoot { canonical: canonical } => - optional_present( - value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) - ) + if projects_fields { + optional_absent() + } else { + optional_present( + value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) + ) + } ScopeUnbound => optional_absent() } } From 6df74f8a61be66be6586532883319620d92b9460 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 04:18:29 +0000 Subject: [PATCH 42/90] A root-bound data value's projection: control added, and it is not yet supported The 3d repair says a namespace segment may not be projected through. Read carelessly that becomes "only a lexical binder may have fields", which would be an over-prohibition (DESIGN section 4d), so the case that distinguishes them gets a control: `cfg.tree` where `cfg` is a module-level `data` value names a VALUE, not a namespace segment. MEASURED: it refuses at resolve with resolve_reason_unbound_symbol, and it did so BEFORE the gate as well -- the same reason at 2bd1f0ded41 and at bd9d3d3bb5c, taken in a detached worktree so the head is the only variable. The gate therefore removed no working capability: the head segment is not bound in the scope chain at all, upstream of the arm that decides projection bases, so it was Absent before and is Absent after. The row is written AT THE REFUSAL rather than at the capability, so it cannot sit green while the thing it names stays broken, and it flips when a root-bound value becomes projectable. It asserts the REASON, not merely that something refused -- a row satisfied by any refusal would stay green if the gate over-prohibited and broke this for a different cause, which is the failure this control exists to catch. Next trigger: a module-level value binding the scope chain answers for. field projection 15/17; the two reds are the pre-existing infer_facts_key_conflict pair (main's #12582), unchanged by this commit and attributed in the previous one. Co-Authored-By: Claude Opus 5 (1M context) --- .../field_projection_stages_test.dag | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag index 85fade27515..1e78e145e4a 100644 --- a/src/v2/test/claim/field_projection/field_projection_stages_test.dag +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -505,3 +505,40 @@ fn fps_match_binder_projection_grounds(field: String) -> Bool { Present { value: facts } => inferred_facts_grounding_derived(facts: facts) } } + +// A MODULE-LEVEL `data` VALUE IS STILL PROJECTABLE, AND THIS ROW EXISTS SO THE 3d REPAIR CANNOT QUIETLY +// BECOME "ONLY LEXICAL VALUES MAY HAVE FIELDS". resolve_projection_base admits a BoundAtRoot head only +// where there are no field segments, because a dotted path through a module-root DECLARATION is a +// qualified name rather than a projection. A root-bound `data` value is the case that reading could +// over-prohibit: `cfg` names a value, not a namespace segment, so `cfg.tree` is an ordinary projection and +// must keep working. +// +// THE ROW REPORTS WHAT IS TRUE RATHER THAN WHAT WOULD BE TIDY (DESIGN section 4d: do not forbid more of the +// world than the evidence supports, and do not under-assert either). If this holds, the repair is scoped to +// namespace segments as intended. If it does not, the repair HAS over-prohibited and that is a defect to +// fix rather than a frontier to declare -- the row's reason, not merely its verdict, is what says which. +fn fps_root_data_projection_source() -> Outcome { + fps_assemble(src: "module p\n\ntype Box {\n tree: Int\n}\n\ndata cfg: Box = Box { tree: 1 }\n\nfn f() -> Int {\n cfg.tree\n}\n") +} + +// MEASURED, AND IT IS NOT YET SUPPORTED -- BY A ROUTE THIS REPAIR NEITHER CAUSED NOR FIXED. `cfg.tree` +// where `cfg` is a module-level `data` value refuses at resolve with resolve_reason_unbound_symbol. +// Receipt, taken in a detached worktree so the two heads are the only variable: the SAME reason at +// 2bd1f0ded41 (before the BoundAtRoot gate) and at bd9d3d3bb5c (after it). So the gate did not remove a +// working capability -- the head segment is not bound in the scope chain at all, upstream of the arm that +// decides projection bases, so it was Absent before the gate and is Absent after it. +// +// THE ROW IS WRITTEN AT THE REFUSAL, NOT AT THE CAPABILITY, so it cannot sit green while the thing it names +// stays broken, and it flips the moment a root-bound value becomes projectable. It is asserted BY REASON: +// a row satisfied by any refusal would stay green if the gate over-prohibited and broke this for a +// different cause, which is exactly the failure this control exists to catch. +// +// NEXT TRIGGER: a module-level value binding that the scope chain answers for. Until then the 3d repair's +// scope claim is "a namespace segment may not be projected through", and this row is what keeps it from +// being read as "only a lexical binder may have fields". +test fn fps_a_root_bound_data_value_is_not_yet_projectable_holds() -> Bool { + match fps_root_data_projection_source() { + Rejected { diagnostics: r } => r.head.reason == ^resolve_reason_unbound_symbol + Accepted { value: _, diagnostics: _ } => false + } +} From 8ea7415dc422068061b5cde7c2902cb9e6dfdb94 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 10:49:36 +0000 Subject: [PATCH 43/90] The cref locator asks the production application reader and selects by full declaring path Two things, and the second corrects two wrong readings of mine from the same hour. THE LOCATOR WAS A SECOND AUTHORITY FOR THE APPLICATION ENCODING. cref_callee_reference_optional matched ComputationNode { behavior: Transform } and read positional child 0 by hand -- the same positional re-derivation DESIGN section 3 forbids, and the same defect repaired earlier today in the arm-pattern reader. It now asks v2.compiler.infer infer_application_callee_use, the reader the application's own formals and type parameters come from, so a change to the encoding reaches this file as it reaches infer. AND IT SELECTS BY THE WHOLE PATH. The old annotation claimed it keyed on "a declaration path ending in `callee`"; the code checked NO name, taking the first node with any declaration-reference callee -- so an annotation or an unrelated reference could have been selected and the row would still have looked green. Each fixture declares one callee at module `p`, every value helper now carries that name, and the locator requires EXACTLY ONE call to the full path; two matches are Absent, so a fixture that grows a second call to one declaration refuses rather than silently picking. THE NINE RED cref ROWS ARE NOT EXPLAINED BY THIS, AND I SAID TWICE THAT THEY WERE. I reported first that they were a separate cause from the two field-projection reds, then that they were a stale locator rather than a compiler regression. Both wrong. Measured: infer REFUSES cref_source with infer_facts_key_conflict -- main's #12582 -- which is the SAME cause as the field-projection pair. What misled me was probing with a hand-written fixture that passed the callee a PARAMETER while cref_source passes an Int LITERAL; the parameter form infers and the literal form does not, because the literal mints a further synthetic node that collides on the shared key. So this commit is a section 3 repair that stands on its own and fixes none of the reds. The rows stay red and the named-call claims are NOT restored: the ruling admits them only if the fresh rows pass, and they do not. EVERY claim_batch RESULT I REPORTED SINCE THE MAIN MERGE IS VOID. The binary was built 19:53 with six seed commits after it, including #12550's merge; gunbc was rebuilt but claim_batch was not. Fresh binary, fresh numbers from here. cref 1/10 (the unresolved-callee negative), all nine reds attributed above. Co-Authored-By: Claude Opus 5 (1M context) --- .../declaration_reference_eval_test.dag | 128 +++++++++++------- 1 file changed, 81 insertions(+), 47 deletions(-) diff --git a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag index 0a7e5f1477c..77661afd5e9 100644 --- a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag +++ b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag @@ -44,7 +44,7 @@ import v2.std.integer { Int, integer_signed_i32_le_bytes_to_int } import v2.std.runtime { RuntimePrimitive, RuntimeValue } import v2.std.algebra { fold_list } -import v2.std.qualified_name { +import v2.std.qualified_name { qualified_name_last_segment, declaration_reference_path_optional, declaration_reference_spine_optional } @@ -106,41 +106,68 @@ fn cref_inferred() -> Optional { } } -// THE CALL IS THE TRANSFORM WHOSE CHILD 0 DECODES TO A DECLARATION PATH ENDING IN `callee`. Keyed on -// the decoded path rather than on a spelling, so an annotation or an unrelated reference cannot be -// selected instead. -fn cref_callee_reference_optional(n: Node) -> Optional { - match n.kind { - ComputationNode { behavior: Transform } => - match list_at_optional(xs: node_positional_child_targets(node: n), index: 0) { - Absent => optional_absent() - Present { value: child } => - match declaration_reference_path_optional(node: child) { - Absent => optional_absent() - Present { value: _ } => optional_present(value: child) - } - } - _ => optional_absent() +// THE CALL IS SELECTED BY ITS CALLEE'S FULL DECLARING PATH, THROUGH THE PRODUCTION READER. What stood +// here decoded the call itself: it matched ComputationNode { behavior: Transform } and read positional +// child 0 by hand. That is a second reader of the application encoding, and it decayed exactly as DESIGN +// section 3 says a positional scheme does -- main moved the call's lowered shape and this file's locator +// answered Absent for every fixture, so nine rows went red while infer ACCEPTED the same trees and eval +// was never reached. The rows were not wrong; they could not find their subject. +// +// It now asks v2.compiler.infer infer_application_callee_use, the same reader the application's own +// formals and type parameters come from, so a change to the encoding reaches this file the way it reaches +// infer. +// +// AND IT SELECTS BY THE WHOLE PATH, NOT BY SHAPE OR BY A LEAF. The old annotation claimed it keyed on "a +// declaration path ending in `callee`"; the code checked no name at all and took the FIRST node with any +// declaration-reference callee, so an annotation or an unrelated reference could have been selected and +// the claim would still have looked green. Each fixture names one callee at module `p`, and the locator +// requires EXACTLY ONE call to that full path -- two matches are Absent, so a fixture that grows a second +// call to the same declaration refuses rather than silently picking one. +fn cref_callee_path_is(callee: Node, name: Symbol) -> Bool { + match declaration_reference_path_optional(node: callee) { + Absent => false + Present { value: path } => path == Cons { head: ^p, tail: Cons { head: name, tail: Empty } } } } -fn cref_first_call_optional(root: Node) -> Optional { +fn cref_calls_to(root: Node, name: Symbol) -> List { fold_node( n: root, algebra: NodeFold { init: fn(n0) { - match cref_callee_reference_optional(n: n0) { - Present { value: _ } => optional_present(value: n0) - Absent => optional_absent() + match infer_application_callee_use(node: n0) { + Absent => [] + Present { value: callee } => + if cref_callee_path_is(callee: callee, name: name) { [n0] } else { [] } } }, - step: fn(acc, _e, child) { - match acc { Present { value: _ } => acc Absent => child } - } + step: fn(acc, _e, child) { concat(acc, child) } } ) } +fn cref_the_call_to(root: Node, name: Symbol) -> Optional { + match cref_calls_to(root: root, name: name) { + Cons { head: call, tail: rest } => + match rest { + Empty => optional_present(value: call) + Cons { head: _, tail: _ } => optional_absent() + } + Empty => optional_absent() + } +} + +fn cref_callee_reference_optional(n: Node) -> Optional { + match infer_application_callee_use(node: n) { + Absent => optional_absent() + Present { value: callee } => + match declaration_reference_path_optional(node: callee) { + Absent => optional_absent() + Present { value: _ } => optional_present(value: callee) + } + } +} + // EVERY NODE STRICTLY UNDER THE REFERENCE MARKER -- the encoded declaring path and nothing else. The // marker itself is excluded, because the marker IS the node that stands where the reference stood and // a refusal anchored there would be a different finding from a refusal anchored on path data. @@ -185,7 +212,7 @@ fn cref_eval_of_the_call() -> Optional> { match cref_inferred() { Absent => optional_absent() Present { value: inferred } => - match cref_first_call_optional(root: inferred.root) { + match cref_the_call_to(root: inferred.root, name: ^callee) { Absent => optional_absent() Present { value: call } => optional_present( @@ -227,10 +254,17 @@ fn cref_inferred_of(o: Outcome) -> Optional { } fn cref_eval_of(o: Outcome) -> Optional> { + cref_eval_of_call_to(o: o, name: ^callee) +} + +// THE CALLEE'S NAME IS A PARAMETER BECAUSE THE SELECTION IS BY FULL PATH. Each fixture declares one +// callee at module `p` and the locator requires exactly one call to it, so a helper that guessed the name +// would be back to selecting by shape. +fn cref_eval_of_call_to(o: Outcome, name: Symbol) -> Optional> { match cref_inferred_of(o: o) { Absent => optional_absent() Present { value: inferred } => - match cref_first_call_optional(root: inferred.root) { + match cref_the_call_to(root: inferred.root, name: name) { Absent => optional_absent() Present { value: call } => optional_present( @@ -244,8 +278,8 @@ fn cref_eval_of(o: Outcome) -> Optional> { } } -fn cref_executes(o: Outcome) -> Bool { - match cref_eval_of(o: o) { +fn cref_executes(o: Outcome, name: Symbol) -> Bool { + match cref_eval_of_call_to(o: o, name: name) { Absent => false Present { value: outcome } => match outcome { @@ -259,15 +293,15 @@ fn cref_executes(o: Outcome) -> Bool { // rows below ask it for the magnitude, so a second decoder beside it would be one fact with two // authorities (DESIGN section 3) -- and the decoding rule, that only a four-byte signed primitive // counts, is exactly the sort that drifts between two copies. -fn cref_executes_to(o: Outcome, n: Int) -> Bool { - match cref_result_int_optional(o: o) { +fn cref_executes_to(o: Outcome, n: Int, name: Symbol) -> Bool { + match cref_result_int_optional(o: o, name: name) { Absent => false Present { value: magnitude } => magnitude == n } } -fn cref_refusal_reason_is(o: Outcome, wanted: Symbol) -> Bool { - match cref_eval_of(o: o) { +fn cref_refusal_reason_is(o: Outcome, wanted: Symbol, name: Symbol) -> Bool { + match cref_eval_of_call_to(o: o, name: name) { Absent => false Present { value: outcome } => match outcome { @@ -283,7 +317,7 @@ test fn cref_the_fixture_yields_a_reference_callee_call_holds() -> Bool { match cref_inferred() { Absent => false Present { value: inferred } => - match cref_first_call_optional(root: inferred.root) { + match cref_the_call_to(root: inferred.root, name: ^callee) { Absent => false Present { value: _ } => true } @@ -298,7 +332,7 @@ test fn cref_the_reference_facts_carry_their_declaration_holds() -> Bool { match cref_inferred() { Absent => false Present { value: inferred } => - match cref_first_call_optional(root: inferred.root) { + match cref_the_call_to(root: inferred.root, name: ^callee) { Absent => false Present { value: call } => match cref_callee_reference_optional(n: call) { @@ -317,13 +351,13 @@ test fn cref_the_reference_facts_carry_their_declaration_holds() -> Bool { // acceptance: any Int-returning path would satisfy "Accepted" while proving nothing about WHICH // declaration ran, and 7 is written only in the callee's body. test fn cref_a_zero_argument_named_call_executes_to_its_callee_value_holds() -> Bool { - cref_executes_to(o: cref_zero_arg_source(), n: 7) + cref_executes_to(o: cref_zero_arg_source(), n: 7, name: ^callee) } // THE SAME WITH AN ARGUMENT SUPPLIED, which is a separate fact from the zero-argument case because // the argument edge is a separate child of the call. test fn cref_a_named_call_with_an_argument_executes_holds() -> Bool { - cref_executes_to(o: cref_one_arg_constant_body_source(), n: 7) + cref_executes_to(o: cref_one_arg_constant_body_source(), n: 7, name: ^callee) } // PARAMETER BINDING IS NOT YET DEMONSTRATED, AND THIS IS THE CLAIM THAT WOULD DEMONSTRATE IT. Both @@ -339,7 +373,7 @@ test fn cref_a_named_call_with_an_argument_executes_holds() -> Bool { // already did, and infer_declaration_reference_facts reads the declaration from // v2.compiler.resolve ResolvedTree resolved_declarations rather than the authored symbol_index. test fn cref_a_parameter_bodied_callee_executes_holds() -> Bool { - cref_executes(o: cref_source()) + cref_executes(o: cref_source(), name: ^callee) } // A BOOL-RETURNING CALL STILL REFUSES, AND IT IS A DIFFERENT BOUNDARY FROM THE PARAMETER BODY. This @@ -351,14 +385,14 @@ test fn cref_a_parameter_bodied_callee_executes_holds() -> Bool { // typing fell to the frontier. Reading the RESOLVED declaration answers it, and the same repair is why // the Int rows above never separated from this one on their own. test fn cref_a_bool_returning_call_executes_holds() -> Bool { - cref_executes(o: cref_bool_pair_source()) + cref_executes(o: cref_bool_pair_source(), name: ^truth) } // THE DISCRIMINATING NEGATIVE FOR THE DISPATCH: a callee naming no declaration must refuse rather // than execute, fabricate a value, or be looked up as a primitive under a name it does not have. // Without this, the repair could have admitted any Conj as a callee and still looked green. test fn cref_an_unresolved_callee_does_not_execute_holds() -> Bool { - !cref_executes(o: cref_unresolved_callee_source()) + !cref_executes(o: cref_unresolved_callee_source(), name: ^callee) } // AN ARGUMENT-DEPENDENT CALLEE: its body IS its parameter, so its result cannot be produced without @@ -376,8 +410,8 @@ fn cref_identity_source(lex: String) -> Outcome { // per argument keep what the conjunctions had -- a callee answering any constant fails the row whose // argument it does not equal -- so this reader returns the magnitude rather than a Bool against an // expectation. -fn cref_result_int_optional(o: Outcome) -> Optional { - match cref_eval_of(o: o) { +fn cref_result_int_optional(o: Outcome, name: Symbol) -> Optional { + match cref_eval_of_call_to(o: o, name: name) { Absent => optional_absent() Present { value: outcome } => match outcome { @@ -396,7 +430,7 @@ fn cref_result_int_optional(o: Outcome) -> Optional { } fn cref_identity_result_is_exactly(lex: String, n: Int) -> Bool { - cref_executes_to(o: cref_identity_source(lex: lex), n: n) + cref_executes_to(o: cref_identity_source(lex: lex), n: n, name: ^identity) } // THE ACCEPTANCE TARGET FOR THIS LANE, ENROLLED AS THE REFUSAL IT IS TODAY. The value path is written @@ -463,7 +497,7 @@ fn cref_bool_false_source() -> Outcome { // is worth recording because the reverse inference is the tempting one: a row that LOOKS like it calls // the front end four times is not evidence that it does, and only the instrument decides. test fn cref_the_bool_pair_separates_holds() -> Bool { - cref_results_differ(a: cref_bool_pair_source(), b: cref_bool_false_source()) + cref_results_differ(a: cref_bool_pair_source(), a_name: ^truth, b: cref_bool_false_source(), b_name: ^falsity) } // THE PRIMITIVE BYTES OF A CALL'S RESULT, for the one claim whose property is that two calls produce @@ -471,8 +505,8 @@ test fn cref_the_bool_pair_separates_holds() -> Bool { // asserting "true" and "false" by magnitude would be asserting this module's guess at v2.std.logic's // representation. Byte inequality is the property without the guess: a callee that ignored its // declaration and answered one constant cannot satisfy it. -fn cref_result_bytes(o: Outcome) -> Optional> { - match cref_eval_of(o: o) { +fn cref_result_bytes(o: Outcome, name: Symbol) -> Optional> { + match cref_eval_of_call_to(o: o, name: name) { Absent => optional_absent() Present { value: outcome } => match outcome { @@ -488,11 +522,11 @@ fn cref_result_bytes(o: Outcome) -> Optional> { // FALSE WHEN EITHER SIDE DID NOT EXECUTE, so the separation claim below pairs it with two execution // assertions rather than reading inequality as evidence on its own: two refusals are also unequal. -fn cref_results_differ(a: Outcome, b: Outcome) -> Bool { - match cref_result_bytes(o: a) { +fn cref_results_differ(a: Outcome, a_name: Symbol, b: Outcome, b_name: Symbol) -> Bool { + match cref_result_bytes(o: a, name: a_name) { Absent => false Present { value: xs } => - match cref_result_bytes(o: b) { + match cref_result_bytes(o: b, name: b_name) { Absent => false Present { value: ys } => !(xs == ys) } From 6b3490b26212d2ec7e7dc0e7a22c4bcdb4555e16 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 15:42:21 +0000 Subject: [PATCH 44/90] Derive the DAG canonical symbol set from the grammar instead of its serialized image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dag_canonical_symbol_map` asked `grammar_to_node` for the whole grammar and then swept the resulting tree for atom identities. That is the demand-minimization defect of DESIGN §2 in its plainest form: the only fact wanted is WHICH SYMBOLS THE GRAMMAR CARRIES, and the route to it built a complete `Node` encoding of every production expression -- a structure nothing else in the call consumed -- purely so a walker could read the symbols back out of it. The grammar already names those symbols in its own fields, so the encoding was a round trip through a representation the demand never asked for. `grammar_carried_symbol_map` reads them directly, mirroring the encoder arm for arm: a Terminal carries its token class and, under `StampBinding` or `StampLexeme`, the stamp it names; a `LiteralTerminal` carries its class and its lexeme; a Nonterminal its production; Sequence and Choice the concatenation of their sides; Optional and Repeat their element; Expect its element plus the refusal reason's symbol. `dag_language_model` now binds `dag_grammar()` and `dag_lex_rules()` once and threads those values through rather than calling the producers again underneath itself. MEASUREMENT. `cc_whole_language_model` re-derives the figure: `dag_language_model()` went from 103,518 to 47,464 eval steps, and `cc_carried_symbol_map_only` attributes 45,381 of what remains to the map itself, so the repair's subject is now the dominant term and the threading accounts for the rest. The threading was measured separately at ONE step -- the call memo had already collapsed the duplicate producer calls -- so it is committed as a correctness statement about which value the model is built from, and is NOT claimed as a latency win. WHAT WAS FALSIFIED ON THE WAY. The first reading blamed wrapper allocation for the cost and predicted the saving would come from removing the round trip's wrappers; measurement put wrappers at about 5% and located the expense in `grammar_expr_to_node` (encode plus walk), with the emitted-node walk under a thousand steps. DESIGN §6b's adversarial use of measurement is what caught it: the prediction was stated before the run and the run contradicted it, so the reading was wrong and the repair moved to the link the numbers actually named. EVIDENCE, IN BOTH DIRECTIONS. `canonical_set_equality` proves the new derivation names exactly the symbols the old one did, by structural equality against `grammar_carried_symbol_map_via_serialized_image` -- the previous route, retained as the differential oracle -- over the real `dag_grammar()`, not a fixture. A non-emptiness anchor on `^dag_surface_fn_decl` closes the vacuity where two empty maps would compare equal, and a discriminating negative establishes that a symbol neither derivation carries is absent from both. `canonical_consumer` establishes from SOURCE that resolve still asks the question and that the answer still separates. `v2.compiler.resolve` `resolved_reference_identity` consults `namespace_has_canonical_symbol` for a resolved reference's leaf and mints `ResolvedToKernelSymbol` when it is carried, `ResolvedToDeclaration` when it is not. Two four-line modules differing only in one annotation drive both arms: a parameter annotated `Int` yields no declaration-reference path, and one annotated with a type the module declares yields one. The two rows are the two signs of one question and neither is redundant -- an over-large set reds the declared-type row, an under-small set reds the kernel-spelling row -- so a derivation that drifted either way is caught rather than measured as faster. That pairing is the obligation of DESIGN §3's witness rule and the reason the equality control alone would not have been enough: a derivation that returned the empty set would satisfy equality against another empty set AND would let a module mentioning no kernel spelling assemble, which is exactly how a cost reduction can be mistaken for a success. `grammar_expr_carried_symbols` deliberately excludes `root.sync_tokens`, because `grammar_root_to_node` does not encode them either; mirroring the encoder is what makes the equality exact rather than approximately right. RESIDUE, DECLARED. The symbol accumulator is threaded through a list fold rather than composed as an endomorphism, which copies the map per production. The endomorphic form was attempted and the language refused it: `.dag` does not admit applying an fn-valued parameter or let-bound fn value, so `left(acc)` fails to resolve. That is the trigger -- application of fn-valued bindings -- and the accumulator shape is a consequence of the language's current surface, not a chosen one. BUDGET, STATED AND NOT WORKED AROUND. The three `canonical_consumer` rows cost 167,365 to 295,249 eval steps because each assembles a real module, so they are above the 72,300-step enrolment margin and are NOT proposed for the continuous floor. No grandfather row, warm-share row or raised line accompanies them: they are multi-stage integration subjects and belong on the scheduled executor route, which is a separate decision from this slice. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/extdeps/languages/dag.dag | 34 ++++-- src/v2/std/grammar.dag | 91 +++++++++++++++ .../canonical_consumer_test.dag | 105 ++++++++++++++++++ .../canonical_cost_probe_test.dag | 26 +++++ .../canonical_set_equality_test.dag | 54 +++++++++ 5 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 src/v2/test/claim/canonical_demand/canonical_consumer_test.dag create mode 100644 src/v2/test/claim/canonical_demand/canonical_cost_probe_test.dag create mode 100644 src/v2/test/claim/canonical_demand/canonical_set_equality_test.dag diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 297887feb9a..0f5b1bbc45c 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -3112,17 +3112,35 @@ fn dag_language_model_binding_canonical_map(m: Map) -> Map Map { +// THE CANONICAL SET IS DERIVED FROM THE GRAMMAR AND LEX RULES THE MODEL IS ALREADY HOLDING, NOT FROM A +// SECOND CALL TO THEIR PRODUCERS. dag_language_model binds lex: dag_lex() and grammar: dag_grammar(), and +// the nullary dag_canonical_symbol_map below then called dag_grammar() and dag_lex_rules() AGAIN -- two +// demands for a fact the constructor had in hand, which DESIGN section 2 reads as authored duplication to +// rewire rather than a repetition to cache: the values have a shared-state least common ancestor, the +// constructor itself. +// +// The `_of` forms take the prepared values, so there is ONE grammar and ONE lex-rule set per model +// construction and the producer call sites are the constructor's. The nullary forms remain for consumers +// that hold no model and are expressed through the `_of` forms, so the derivation itself has one authority. +fn dag_canonical_symbol_map_of(grammar: ParseGrammar, rules: LexRuleSet) -> Map { dag_language_model_binding_canonical_map( m: lex_rule_set_insert_token_classes( - m: grammar_carried_symbol_map(grammar: dag_grammar()), - rules: dag_lex_rules() + m: grammar_carried_symbol_map(grammar: grammar), + rules: rules ) ) } +fn dag_canonical_symbols_of(grammar: ParseGrammar, rules: LexRuleSet) -> PointwisePower { + dag_pointwise_from_symbol_map(m: dag_canonical_symbol_map_of(grammar: grammar, rules: rules)) +} + +fn dag_canonical_symbol_map() -> Map { + dag_canonical_symbol_map_of(grammar: dag_grammar(), rules: dag_lex_rules()) +} + fn dag_canonical_symbols() -> PointwisePower { - dag_pointwise_from_symbol_map(m: dag_canonical_symbol_map()) + dag_canonical_symbols_of(grammar: dag_grammar(), rules: dag_lex_rules()) } fn dag_language_model_void() -> LanguageModel { @@ -3136,11 +3154,13 @@ fn dag_language_model_void() -> LanguageModel { } fn dag_language_model() -> LanguageModel { + let rules = dag_lex_rules() + let grammar = dag_grammar() LanguageModel { - lex: dag_lex(), - grammar: dag_grammar(), + lex: ModeledLexRules { root: rules }, + grammar: grammar, language_identity: ^dag_language_model_surface_id, - canonical_symbols: dag_canonical_symbols(), + canonical_symbols: dag_canonical_symbols_of(grammar: grammar, rules: rules), core: bool_model_core() } } diff --git a/src/v2/std/grammar.dag b/src/v2/std/grammar.dag index d33415c607d..245980977c3 100644 --- a/src/v2/std/grammar.dag +++ b/src/v2/std/grammar.dag @@ -2378,7 +2378,98 @@ fn grammar_node_insert_atom_identities(m: Map, n: Node) -> Map Map` so each arm is an insertion and each +// composite a composition, copying nothing. +// +// THAT FORM DOES NOT COMPILE HERE, and the refusal is the language's, not a style objection: applying a +// fn-valued PARAMETER or a let-bound fn value is not admitted -- `left(acc)` refuses with "function 'left' +// not found in scope", as does `insert(m)` over a let-bound fold result. Only a direct call to a named +// declaration is a call. So the residue stands, and its next-rung trigger is application of fn-valued +// bindings in .dag, not a cheaper spelling of this fold. +// +// Measured so the residue is priced rather than assumed: this derivation takes dag_language_model() from +// 103,518 eval steps to 47,464, and what remains in the carried map is this fold plus its insertions. +fn grammar_expr_carried_symbols(expr: GrammarExpr) -> List { + fold_grammar_expr( + expr: expr, + algebra: GrammarExprFold { + terminal: fn(token_class, stamp) { + match stamp { + StampBinding { binding: b } => [token_class, b] + StampClass => [token_class] + StampLexeme => [token_class, ^grammar_terminal_stamp_lexeme] + } + }, + literal_terminal: fn(token_class, lexeme) { [token_class, lexeme] }, + nonterminal: fn(production) { [production] }, + sequence: fn(left, right) { concat(left, right) }, + choice: fn(left, right) { concat(left, right) }, + optional: fn(element) { element }, + repeat: fn(element) { element }, + expect: fn(element, reason) { + concat(element, [parse_refusal_reason_symbol(reason: reason)]) + } + } + ) +} + +// THE CARRIED SYMBOLS ARE FOLDED FROM THE GRAMMAR'S OWN STRUCTURE, NOT FROM A SERIALIZED IMAGE OF IT. +// This read `grammar_node_insert_atom_identities(grammar_to_node(grammar))`: it encoded the WHOLE grammar +// into a Node tree and then recovered the symbols by a generic atom walk over that tree. The encoding +// exists for the serialization direction (DESIGN section 4, one grammar read both ways); using it to +// enumerate symbols is a round trip through a representation built for a different consumer, and it is +// paid in full on every construction of a LanguageModel whether or not one membership question is asked. +// +// WHAT THE ATOMS ACTUALLY ARE, read off the encoders rather than guessed: grammar_root_to_node emits +// grammar_atom(root.start) and the productions node; grammar_production_to_node emits +// grammar_atom(production.name), the expression's encoding, and the production's emitted node. Every +// wrapper in between is a Conj with Named edges, which carries no Atom identity of its own -- so folding +// start, each name, each expression encoding and each emitted node collects exactly the same symbols and +// builds none of the wrappers. +// +// root.sync_tokens IS DELIBERATELY NOT INCLUDED, because grammar_root_to_node does not encode it. A fold +// that added it would be a different set, which is why the equality control over the real dag_grammar() +// is not a formality: v2.test.claim.canonical_demand.canonical_set_equality compares this map to the +// serialized derivation key for key, in both directions. fn grammar_carried_symbol_map(grammar: ParseGrammar) -> Map { + match grammar { + VoidGrammar => grammar_node_insert_atom_identities(m: empty_map(), n: grammar_empty_node()) + ModeledGrammar { root: root } => + fold_list( + xs: root.productions, + empty: grammar_symbol_map_put(m: empty_map(), sym: root.start), + cons: fn(acc, production) { + grammar_node_insert_atom_identities( + m: grammar_symbol_list_put( + m: grammar_symbol_map_put(m: acc, sym: production.name), + xs: grammar_expr_carried_symbols(expr: production.expression) + ), + n: production.emitted + ) + } + ) + } +} + +// THE SERIALIZED DERIVATION, KEPT AS THE EQUALITY ORACLE AND FOR NOTHING ELSE. It is what the structural +// fold above is proven equal to; no production path calls it. +fn grammar_carried_symbol_map_via_serialized_image(grammar: ParseGrammar) -> Map { grammar_node_insert_atom_identities(m: empty_map(), n: grammar_to_node(grammar: grammar)) } diff --git a/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag b/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag new file mode 100644 index 00000000000..840956f80f2 --- /dev/null +++ b/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag @@ -0,0 +1,105 @@ +module v2.test.claim.canonical_demand.canonical_consumer + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.extdeps.languages.dag { dag_language_model } +import extdeps.communication.medium { Lossless, Medium } +import std.algebra { Cons, Empty } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.logic { Bool } +import v2.std.integer { Int } +import v2.std.text { String } +import v2.std.node { Atom, Node, NodeFold, TypeNode, fold_node } +import v2.std.node_query { find_named_child } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.collection { List } +import v2.std.qualified_name { declaration_reference_path_optional } + +// THE CANONICAL SET IS CONSULTED BY AUTHORED SOURCE, NOT ONLY BY A UNIT CALL. v2.compiler.resolve +// resolved_reference_identity asks namespace_has_canonical_symbol for a resolved reference's LEAF: a +// canonical leaf becomes ResolvedToKernelSymbol and anything else becomes ResolvedToDeclaration. So the +// set's membership answer decides, for every module-level name a program mentions, whether it is a kernel +// symbol or a reference to a declaration. +// +// WHY THIS FILE EXISTS BESIDE THE EQUALITY CONTROL. The equality control proves the new derivation names +// the same symbols as the one it replaced; it cannot show that resolve still ASKS, or that the answer still +// discriminates. A derivation that silently returned the empty set would pass equality against another +// empty set and would also let a 4-line module assemble, because a module that mentions no kernel spelling +// asks nothing -- which is precisely how a cost measurement can look like a success. These rows make the +// question observable from source, in both directions. +data cc_artifact: Artifact = Artifact { + kind: SourceFile, id: ^canonical_consumer_artifact, file_path: "src/v2/pilot/canonical_consumer.dag" +} + +fn cc_assemble(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: cc_artifact, compilation_unit: ^canonical_consumer_cu, source_root: V2Tree + }, + tail: Empty + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +// A module whose parameter is annotated with the KERNEL spelling `Int`, and one annotated with a type the +// module itself declares. Same shape otherwise, so the annotation is the only variable. +fn cc_kernel_annotation() -> Outcome { + cc_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n x\n}\n") +} + +fn cc_declared_annotation() -> Outcome { + cc_assemble(src: "module p\n\ntype Box {\n tree: Int\n}\n\nfn f(x: Box) -> Box {\n x\n}\n") +} + +// Every node in the tree that carries a declaration-reference path, counted. A kernel symbol resolves to a +// canonical ATOM and carries none; a declaration reference carries its declaring path. +fn cc_declaration_reference_count(o: Outcome) -> Int { + match o { + Rejected { diagnostics: _ } => 0 - 1 + Accepted { value: t, diagnostics: _ } => + fold_node( + n: t.root, + algebra: NodeFold { + init: fn(n0) { + match declaration_reference_path_optional(node: n0) { + Present { value: _ } => 1 + Absent => 0 + } + }, + step: fn(acc, _e, child) { acc + child } + } + ) + } +} + +// (1) BOTH MODULES RESOLVE, so the rows below are about classification rather than about a refusal. +test fn cc_both_modules_resolve_holds() -> Bool { + (match cc_kernel_annotation() { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false }) + && (match cc_declared_annotation() { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false }) +} + +// (2) THE DECLARED TYPE BECOMES A DECLARATION REFERENCE. If the canonical set wrongly claimed `Box`, this +// would classify as a kernel symbol and the count would drop -- so this row goes red on a set that is too +// LARGE. +test fn cc_a_declared_type_is_a_declaration_reference_holds() -> Bool { + cc_declaration_reference_count(o: cc_declared_annotation()) > 0 +} + +// (3) AND THE KERNEL SPELLING IS NOT ONE. `Int` is carried by the language model's binding map, so resolve +// must classify it ResolvedToKernelSymbol and mint a canonical atom with no declaring path. If the +// derivation returned too SMALL a set -- the fail-open direction, since the set's absence makes resolve +// treat a kernel spelling as a corpus declaration -- this row goes red. +// +// The two rows are the two signs of one question and neither is redundant: (2) fails on an over-large set +// and (3) on an under-small one, so a derivation that drifted either way is caught by source. +test fn cc_a_kernel_spelling_is_not_a_declaration_reference_holds() -> Bool { + cc_declaration_reference_count(o: cc_kernel_annotation()) == 0 +} diff --git a/src/v2/test/claim/canonical_demand/canonical_cost_probe_test.dag b/src/v2/test/claim/canonical_demand/canonical_cost_probe_test.dag new file mode 100644 index 00000000000..8bf47ca9744 --- /dev/null +++ b/src/v2/test/claim/canonical_demand/canonical_cost_probe_test.dag @@ -0,0 +1,26 @@ +module v2.test.claim.canonical_demand.canonical_cost_probe + +import v2.extdeps.languages.dag { dag_grammar, dag_language_model, dag_lex_rules } +import v2.std.grammar { grammar_carried_symbol_map } +import v2.std.logic { Bool } + +// THE INSTRUMENT FOR THE CANONICAL-SYMBOL COST, NOT A SEMANTIC CLAIM. DESIGN §6 requires a measurement to +// be cited by naming the producer that re-derives it rather than by copying its number into prose, so the +// figure for "what dag_language_model() costs" needs an entry point. These two rows are it: read eval_steps +// from claim_batch's [witness] lines. The verdicts are deliberately trivial, and that is the honest +// description -- neither row discriminates anything, so neither is evidence for any behaviour. The +// behavioural evidence for this slice is canonical_set_equality (the derivation names the same symbols) and +// canonical_consumer (resolve still asks, and the answer still separates in both directions). +// +// TWO ROWS AND NOT TEN. The discovery partition that located the cost ran per-link over the grammar +// encoder, the emitted-node walk and the expression encodings; those rows answered their question once and +// are not kept, because a probe nothing reads is DESIGN §3c's dangling declaration whatever its shape. What +// survives is the pair that bounds the claim: the whole model, and the one link the repair changed, so a +// later reader can attribute a regression to that link instead of re-deriving the partition. +test fn cc_whole_language_model() -> Bool { + match dag_language_model() { _ => true } +} + +test fn cc_carried_symbol_map_only() -> Bool { + match grammar_carried_symbol_map(grammar: dag_grammar()) { _ => true } +} diff --git a/src/v2/test/claim/canonical_demand/canonical_set_equality_test.dag b/src/v2/test/claim/canonical_demand/canonical_set_equality_test.dag new file mode 100644 index 00000000000..6e517124415 --- /dev/null +++ b/src/v2/test/claim/canonical_demand/canonical_set_equality_test.dag @@ -0,0 +1,54 @@ +module v2.test.claim.canonical_demand.canonical_set_equality + +import v2.extdeps.languages.dag { dag_grammar } +import v2.std.grammar { + grammar_carried_symbol_map, + grammar_carried_symbol_map_via_serialized_image, +} +import v2.std.collection { List, Map, map_lookup } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.optional { Absent, Present } +import v2.std.integer { Int } + +// EXACT SET EQUALITY AGAINST THE DERIVATION IT REPLACES, OVER THE REAL GRAMMAR, IN BOTH DIRECTIONS. +// v2.std.grammar grammar_carried_symbol_map now folds the grammar's own structure; it used to recover the +// same symbols by encoding the whole grammar into a Node and walking its atoms. That image derivation is +// retained as grammar_carried_symbol_map_via_serialized_image for exactly this comparison and is called +// from no production path. +// +// BOTH DIRECTIONS, BECAUSE EITHER ALONE ADMITS A DEFECT WITH OPPOSITE SIGN. A structural fold that MISSED +// an atom would shrink the canonical set, and the set's only consumer is v2.compiler.resolve's unbound +// fallback -- so a missing symbol makes resolve admit a name it should refuse, which is a fail-open and +// the reason this control is not optional. A fold that ADDED one (root.sync_tokens is the live candidate, +// since grammar_root_to_node does not encode it) would refuse names that are legal today. +// +// THE COMPARISON IS STRUCTURAL MAP EQUALITY, NOT A COUNT. Two maps of equal size can differ in membership, +// and DESIGN section 5 rules that completeness is an identity join rather than a count equality. +fn cse_structural() -> Map { grammar_carried_symbol_map(grammar: dag_grammar()) } +fn cse_image() -> Map { grammar_carried_symbol_map_via_serialized_image(grammar: dag_grammar()) } + +test fn cse_the_two_derivations_are_the_same_map_holds() -> Bool { + cse_structural() == cse_image() +} + +// AND THE MAP IS NOT EMPTY, so the equality above is not two empty maps agreeing -- the vacuity this file +// would otherwise be wide open to. +test fn cse_the_derivation_is_not_empty_holds() -> Bool { + match map_lookup(m: cse_structural(), key: ^dag_surface_fn_decl) { + Present { value: _ } => true + Absent => false + } +} + +// A DISCRIMINATING NEGATIVE FOR THE EQUALITY ITSELF: a symbol the grammar does not carry is absent from +// both. Without it, an equality over two maps that answered Present for everything would pass. +test fn cse_an_uncarried_symbol_is_in_neither_holds() -> Bool { + (match map_lookup(m: cse_structural(), key: ^cse_definitely_not_a_grammar_symbol) { + Absent => true + Present { value: _ } => false + }) && (match map_lookup(m: cse_image(), key: ^cse_definitely_not_a_grammar_symbol) { + Absent => true + Present { value: _ } => false + }) +} From 82b5f154185b2d996fdb3c61da4d1ce87c8fd4d4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 21:50:27 +0000 Subject: [PATCH 45/90] Qualified-name resolution retains the binding kind rather than collapsing it to a Boolean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `qualified_head_bound_on_chain` answered `Bool`, mapping both `BoundInFrame` and `BoundAtRoot` to `true`, and that single Boolean was the SOLE discriminator its caller used to choose between two different readings of a dotted path: a field projection off a value, or a qualified declaration name. Those are different questions. "Is this head bound anywhere on the chain" is not "is this head a lexical value a field can be projected from", and `lookup_chain` already computes and returns the distinction the caller needs. Collapsing it here and re-deriving it downstream is DESIGN §5's validation standing where construction was available, and §6b's defect shape: a link discarding a fact its consumer must then guess at. `qualified_head_scope_binding` returns the `ScopeBinding` it was handed, and `resolve_dotted_path_reading` decides both readings in ONE place: BoundInFrame the projection, unconditionally -- §13's chain-wins rule unchanged, so a head the author bound lexically answers with that binder and the absolute candidates are not consulted BoundAtRoot the declaration path: a name bound at the module root is a declaration or a namespace segment, so a path THROUGH it is a qualified name ScopeUnbound the declaration path, there being no value to project from `resolve_declared_path_reading` REFUSES when the corpus declares no such path, and that refusal is the point rather than an incidental arm. An earlier revision of this change fell back to the projection reading when the declaration lookup found nothing, which is DESIGN §5's absorbing fallback exactly: the precise answer was unavailable and the arm widened to a superset reading, fabricating a derivation the source does not support while destroying the signal that the declaration evidence was missing. It is forbidden as a rule, independently of whether anything consumed it. A `projects_fields` gate inside `resolve_projection_base` is DELETED by the same reasoning. It refused a root-bound head whenever field segments followed, which looks like the rule above and is not: refusing THERE can only produce an unbound-symbol diagnostic, because the declaration door is in the CALLER and unreachable from that function, so it converted one wrong answer into another. Its deletion is a correctness repair on that function's own contract. RECEIPT, AND WHAT IT DOES NOT CLAIM. v2.test.claim.namespace_xl0.cross_module_reference_resolution is 15/15 on this commit alone, including `a_receiver_with_no_such_child_never_accepts` -- the control whose breakage caused the two arms to be collapsed in the first place -- and `a_binder_shadowing_a_root_segment_projects_from_the_binder`, which is the chain-wins rule this change preserves. Two new rows in v2.test.claim.field_projection.field_projection_stages qualify both readings together over a FOUR-segment cross-module path with a multi-segment module name: a resolver reading every dotted path as a declaration passes the first and reds the second, one reading every path as a projection passes the second and reds the first, so only a real discriminator passes both. A first version of that pair used a same-module two-segment `p.Box` and passed while testing a different lookup; the fixture now carries the subject's shape. NO NATIVE RECEIPT IS CLAIMED, and an earlier revision of these headers claimed one that was false. It attributed the native refusal of v2.test.parse.expression_bodied_fn_decl_parse to a fully-qualified annotation being read as nested field projections through the collapsed Boolean. That was read off a diagnostic CHAIN HEAD and was wrong twice over: the native eight measure identically before and after this change, and the actual cause entry in that chain anchors a genuine two-edge field projection off a fold-step binder -- a subject this resolution path never sees. Every trace of that account is removed rather than softened, because a false "MEASURED" paragraph in a load-bearing header is a durable source of future misdiagnosis. This commit claims no movement on the native seven or eight, does not repair wall 3d, and closes no cross-module roadmap node. A ROOT-BOUND VALUE PROJECTED THROUGH remains an explicit frontier, held at the refusal by `fps_a_root_bound_data_value_is_not_yet_projectable` so it flips the moment the form is served. The fail-closed cut does NOT legislate that module-level values may never be projected -- it declines to guess which reading a path takes when the declaration reading fails. That head is not in the scope chain at all, so closing it means making module-level value bindings answerable there rather than adding an arm to this decision. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/03_resolve.dag | 267 +++++++++++------- .../field_projection_stages_test.dag | 88 +++++- 2 files changed, 250 insertions(+), 105 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 315fc38c9d3..03372ae46c2 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1035,22 +1035,37 @@ fn lookup_symbol_index_atom_identity( // control (`Bool` has no child `v`), and the positive `Rec.v` specimen needs declaration grafting // as well as this projection and is owned by those lanes. This check only closes the fail-open // arm beside the projection. -fn qualified_head_bound_on_chain(ctx: ResolveContext, path: QualifiedName) -> Outcome { +// THE DECISION BETWEEN A DECLARATION PATH AND A VALUE PROJECTION, CARRIED AT THE GRAIN THAT DECIDES IT. +// This answered Bool, collapsing BoundInFrame and BoundAtRoot to `true`, and that Bool was the SOLE +// discriminator its caller used to choose between reading a dotted path as a field projection and reading +// it as a qualified declaration name. The two questions are different: "is this head bound anywhere on the +// chain" is not "is this head a lexical value a field can be projected from". A module-level name bound at +// the root is a DECLARATION or a namespace segment, so a path THROUGH it is a qualified name. +// +// NO NATIVE RECEIPT IS CLAIMED FOR THIS CHANGE, and an earlier revision of this header claimed one that +// was false. It attributed the native refusal of v2.test.parse.expression_bodied_fn_decl_parse to a +// fully-qualified annotation being read as nested field projections through this collapsed Bool. That was +// read off a diagnostic CHAIN HEAD and was wrong twice: the native eight measured identically before and +// after the change, and the actual cause entry in that chain anchors a genuine two-edge field projection +// (`e.target`) whose base is a fold-step binder -- a subject this function never sees. The specimen is an +// inference-side join, not a resolution one. +// +// WHAT JUSTIFIES THE CHANGE IS THE READING ITSELF, which needs no incident: one Bool cannot carry a +// discriminator its consumer must make between two readings, and a consumer forced to re-derive what it was +// handed is DESIGN section 5's validation standing where construction was available. The controls in +// v2.test.claim.field_projection.field_projection_stages establish the rule; no claim is made here about the +// eight, about wall 3d, or about any native refusal consuming this arm. +// +// WHY THE ANSWER IS ScopeBinding AND NOT A SECOND Bool. lookup_chain already computes the distinction and +// already returns it; collapsing it here and then trying to recover it downstream is what forced the +// earlier repair attempt into the wrong link -- a `projects_fields` gate inside the projection builder, +// which could only turn the case into an unbound refusal because the declaration door is in the CALLER and +// not reachable from there. Carrying the value this function was handed is the construction; re-deriving it +// later was the validation standing where construction was available. +fn qualified_head_scope_binding(ctx: ResolveContext, path: QualifiedName) -> Outcome { match path { - Cons { head: head, tail: _ } => - match lookup_chain(s: ctx.scope, name: head) { - Rejected { diagnostics: r } => Rejected { diagnostics: r } - Accepted { value: found, diagnostics: d } => - Accepted { - value: (match found { - BoundInFrame { canonical: _ } => true - BoundAtRoot { canonical: _ } => true - ScopeUnbound => false - }), - diagnostics: d - } - } - Empty => Accepted { value: false, diagnostics: None } + Cons { head: head, tail: _ } => lookup_chain(s: ctx.scope, name: head) + Empty => Accepted { value: ScopeUnbound, diagnostics: None } } } @@ -1103,11 +1118,7 @@ fn resolve_bound_head_projection( ) } Cons { head: head_segment, tail: field_segments } => - match resolve_projection_base( - ctx: ctx, - head_segment: head_segment, - projects_fields: match field_segments { Empty => false Cons { head: _, tail: _ } => true } - ) { + match resolve_projection_base(ctx: ctx, head_segment: head_segment) { Absent => Rejected { diagnostics: rejected_with_pending( @@ -1131,32 +1142,22 @@ fn resolve_bound_head_projection( } } -// A ROOT BINDING RESOLVES A NAME BUT MAY NOT BE PROJECTED THROUGH, AND THE TWO CASES ARE DISTINGUISHED BY -// WHETHER THERE ARE FIELDS. This arm serves two shapes that look alike at the head: a single-segment name -// bound at the module root (`Box` in `fn f(b: Box)`), where there is nothing to project and the head IS the -// answer; and a dotted path whose head is a LOCAL binder (`b.tree` where `b` is a parameter), which is a -// field projection and is why this arm exists -- it is what dissolved the earlier -// AmbiguousQualifiedHeadShadowsAbsolute guard. +// THE BASE OF A PROJECTION IS WHATEVER THE HEAD SEGMENT IS BOUND TO, AND THIS FUNCTION NO LONGER DECIDES +// WHETHER A PROJECTION IS THE RIGHT READING AT ALL. That decision is resolve_dotted_path_reading's, made +// from the head's ScopeBinding and the corpus's declarations before this is ever called, so by the time a +// head segment arrives here the caller has already committed to the projection reading and this function's +// only job is to produce the base node. // -// It admitted BoundAtRoot in BOTH shapes, and that is the defect: a name bound at the module root is a -// DECLARATION or a namespace segment, so a dotted path THROUGH it is a qualified name, not a projection. -// MEASURED: the head segment of a module path (`v2` in v2.std.live_tree.LiveTreeDisposition) became a -// projection base and the remaining segments became nested field projections, so infer asked a COPRODUCT -// for its fields and all eight of v2.test.parse.expression_bodied_fn_decl_parse refused natively with -// infer_reason_projection_receiver_declares_no_fields -- the chain walking "v2", "live_tree", -// "LiveTreeDisposition". Removing BoundAtRoot outright was also wrong, and the controls said so -// immediately: it is how a plain root-bound ANNOTATION resolves, so a module with a record type and no -// projection at all stopped inferring. -// -// So the gate is projects_fields: with no field segments this resolves a name and admits either binding; -// with field segments it is building a projection and the base must be a lexical binder. lookup_chain -// already carries that distinction (ScopeBinding), and it is the same one the value-binder admission uses -// to decide what hiding means. -fn resolve_projection_base( - ctx: ResolveContext, - head_segment: Node, - projects_fields: Bool -) -> Optional { +// A `projects_fields` GATE STOOD HERE AND WAS THE WRONG LINK, recorded because the reasoning that put it +// here is the reasoning worth not repeating. It refused a root-bound head whenever field segments followed, +// which looks like the same rule the caller now applies and is not: refusing HERE can only produce an +// unbound-symbol diagnostic, because the declaration-path door is in the caller and unreachable from this +// function, so it could only ever convert one wrong answer into another. The native eight measured +// identically with it and without it, which is consistent with the gate being inert on that path AND with +// the path never having been this function's subject at all; the later receipt established the second. Its +// deletion is therefore a correctness repair on this function's own contract, claiming nothing about the +// eight. +fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional { match head_segment.kind { TypeNode { connective: Atom { identity: name } } => match lookup_chain(s: ctx.scope, name: name) { @@ -1168,13 +1169,9 @@ fn resolve_projection_base( value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) ) BoundAtRoot { canonical: canonical } => - if projects_fields { - optional_absent() - } else { - optional_present( - value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) - ) - } + optional_present( + value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) + ) ScopeUnbound => optional_absent() } } @@ -1202,57 +1199,19 @@ fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional optional_absent() Accepted { value: path, diagnostics: pending } => if length(xs: path) > 1 { - match qualified_head_bound_on_chain(ctx: ctx, path: path) { + match qualified_head_scope_binding(ctx: ctx, path: path) { Rejected { diagnostics: r } => optional_present(value: Rejected { diagnostics: rejected_with_pending(pending: pending, rejected: r) }) - Accepted { value: head_bound, diagnostics: chain_pending } => - if head_bound { - optional_present( - value: resolve_bound_head_projection( - ctx: ctx, - n: n, - pending: diagnostics_merge(outer: pending, inner: chain_pending) - ) - ) - } else { - match symbol_index_absolute_candidates(index: ctx.namespace.symbol_index, qualified_path: path) { - Empty => - optional_present( - value: Rejected { - diagnostics: rejected_with_pending( - pending: diagnostics_merge(outer: pending, inner: chain_pending), - rejected: diagnostics_singleton(d: unbound_symbol_diagnostic(n: n)) - ) - } + Accepted { value: head_binding, diagnostics: chain_pending } => + optional_present( + value: resolve_dotted_path_reading( + ctx: ctx, + n: n, + path: path, + head_binding: head_binding, + pending: diagnostics_merge(outer: pending, inner: chain_pending) ) - Cons { head: candidate, tail: rest } => - match rest { - Empty => - optional_present( - value: resolve_atom_bound( - ctx: ctx, - n: n, - path: candidate.path, - pending: diagnostics_merge(outer: pending, inner: chain_pending) - ) - ) - Cons { head: _, tail: _ } => - optional_present( - value: Rejected { - diagnostics: rejected_with_pending( - pending: diagnostics_merge(outer: pending, inner: chain_pending), - rejected: ambiguous_symbol_diagnostics( - n: n, - class: AmbiguousAtBindingPosition { - candidates: Cons { head: candidate, tail: rest } - } - ) - ) - } - ) - } - } - } + ) } } else { optional_absent() @@ -1260,6 +1219,114 @@ fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional Outcome { + match head_binding { + BoundInFrame { canonical: _ } => resolve_bound_head_projection(ctx: ctx, n: n, pending: pending) + BoundAtRoot { canonical: _ } => resolve_declared_path_reading(ctx: ctx, n: n, path: path, pending: pending) + ScopeUnbound => resolve_declared_path_reading(ctx: ctx, n: n, path: path, pending: pending) + } +} + +// A DECLARATION PATH THAT NAMES NOTHING REFUSES, AND MAY NOT WIDEN TO THE PROJECTION READING. This arm +// held a fallback -- no candidate for the path, so re-read it as a field projection off the root binding -- +// and that fallback is the absorbing arm DESIGN section 5 forbids, written into the very repair that was +// meant to remove a misreading. +// +// WHY A FALLBACK IS FORBIDDEN HERE, STATED AS THE RULE AND NOT AS AN INCIDENT. When the declaration +// reading cannot be established, the tempting arm substitutes the other reading, so nothing is ever +// reported as unresolvable. That is DESIGN section 5's absorbing fallback: the precise answer was +// unavailable and the arm widened to a superset reading, which both fabricates a derivation the source does +// not support and destroys the signal that the declaration evidence was missing. An earlier revision of +// this change shipped exactly that fallback and then attributed a native refusal to it; the attribution was +// falsified by measurement, and the fallback was wrong independently of whether anything consumed it. +// +// SO THE ARM REFUSES, and BoundAtRoot and ScopeUnbound reach the same reading: a path of two or more +// segments whose head is not a LEXICAL binder is a declaration name, and if the corpus declares no such +// name that is an unbound symbol with its own locus. Only BoundInFrame projects. The single-segment +// root-bound annotation that made deleting BoundAtRoot wrong earlier never arrives here -- the caller's +// length > 1 guard keeps it out -- so this costs that form nothing. +fn resolve_declared_path_reading( + ctx: ResolveContext, + n: Node, + path: QualifiedName, + pending: Diagnostics +) -> Outcome { + match symbol_index_absolute_candidates(index: ctx.namespace.symbol_index, qualified_path: path) { + Empty => + Rejected { + diagnostics: rejected_with_pending( + pending: pending, + rejected: diagnostics_singleton(d: unbound_symbol_diagnostic(n: n)) + ) + } + Cons { head: candidate, tail: rest } => + resolve_declared_path_candidates(ctx: ctx, n: n, candidate: candidate, rest: rest, pending: pending) + } +} + +// ONE CANDIDATE IS THE ANSWER AND SEVERAL ARE AMBIGUOUS. Factored out because both arms above reach it and +// a second copy of an ambiguity refusal is the duplicated authority DESIGN section 2 prices -- the two arms +// differ in what they do with an EMPTY candidate list, not in how they read a non-empty one. +fn resolve_declared_path_candidates( + ctx: ResolveContext, + n: Node, + candidate: LexicalBindingCandidate, + rest: FreeMonoid, + pending: Diagnostics +) -> Outcome { + match rest { + Empty => resolve_atom_bound(ctx: ctx, n: n, path: candidate.path, pending: pending) + Cons { head: _, tail: _ } => + Rejected { + diagnostics: rejected_with_pending( + pending: pending, + rejected: ambiguous_symbol_diagnostics( + n: n, + class: AmbiguousAtBindingPosition { candidates: Cons { head: candidate, tail: rest } } + ) + ) + } + } +} + fn root_binding_origin(namespace: Namespace, name: Symbol) -> QualifiedName { match map_lookup(m: namespace.imported_origins, key: name) { Present { value: origin } => origin diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag index 1e78e145e4a..8706da13d1d 100644 --- a/src/v2/test/claim/field_projection/field_projection_stages_test.dag +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -521,12 +521,14 @@ fn fps_root_data_projection_source() -> Outcome { fps_assemble(src: "module p\n\ntype Box {\n tree: Int\n}\n\ndata cfg: Box = Box { tree: 1 }\n\nfn f() -> Int {\n cfg.tree\n}\n") } -// MEASURED, AND IT IS NOT YET SUPPORTED -- BY A ROUTE THIS REPAIR NEITHER CAUSED NOR FIXED. `cfg.tree` -// where `cfg` is a module-level `data` value refuses at resolve with resolve_reason_unbound_symbol. +// MEASURED, AND IT IS NOT YET SUPPORTED -- BY A ROUTE THE DOTTED-PATH DECISION NEITHER CAUSED NOR FIXED. +// `cfg.tree` where `cfg` is a module-level `data` value refuses at resolve with resolve_reason_unbound_symbol. // Receipt, taken in a detached worktree so the two heads are the only variable: the SAME reason at -// 2bd1f0ded41 (before the BoundAtRoot gate) and at bd9d3d3bb5c (after it). So the gate did not remove a -// working capability -- the head segment is not bound in the scope chain at all, upstream of the arm that -// decides projection bases, so it was Absent before the gate and is Absent after it. +// 2bd1f0ded41 and at bd9d3d3bb5c. The cause is upstream of every arm that chooses a reading -- the head +// segment is not in the scope chain AT ALL, so it answers ScopeUnbound, and the declaration reading finds no +// candidate for the path `cfg.tree`. It is therefore a THIRD form, distinct from the two +// resolve_dotted_path_reading decides between, and closing it means making module-level value bindings +// answerable on the chain rather than adding an arm here. // // THE ROW IS WRITTEN AT THE REFUSAL, NOT AT THE CAPABILITY, so it cannot sit green while the thing it names // stays broken, and it flips the moment a root-bound value becomes projectable. It is asserted BY REASON: @@ -542,3 +544,79 @@ test fn fps_a_root_bound_data_value_is_not_yet_projectable_holds() -> Bool { Accepted { value: _, diagnostics: _ } => false } } + +// BOTH READINGS OF A DOTTED PATH, QUALIFIED TOGETHER. v2.compiler.resolve resolve_dotted_path_reading +// decides between a qualified DECLARATION NAME and a value FIELD PROJECTION from the head's ScopeBinding, +// and the two forms are qualified in one place because the defect was never in either arm -- it was in a +// discriminator that could not tell them apart. A pair of rows is what holds that: either one alone is +// satisfied by a resolver that always picks its own arm. +// TWO MODULES, AND A MULTI-SEGMENT MODULE NAME, because the native subject is neither same-module nor +// two-segment: it is `v2.std.live_tree.LiveTreeDisposition`, a FOUR-segment path into ANOTHER module. A +// first version of this row used `p.Box` inside module `p` and passed, which is why it is written out here +// -- a same-module two-segment path exercises a different lookup than a cross-module one, so that row was +// green about a property it never tested. The fixture now carries the subject's actual shape. +fn fps_two_module_assemble(provider: String, consumer: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: provider, fidelity: Lossless }, + artifact: fps_provider_artifact, + compilation_unit: ^field_projection_provider_cu, + source_root: V2Tree + }, + tail: Cons { + head: DagSourceReadWitness { + source: Medium { carried: consumer, fidelity: Lossless }, + artifact: fps_artifact, + compilation_unit: ^field_projection_stages_cu, + source_root: V2Tree + }, + tail: Empty + } + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +data fps_provider_artifact: Artifact = Artifact { + kind: SourceFile, id: ^fps_provider_artifact, file_path: "src/v2/pilot/fps_provider.dag" +} + +fn fps_qualified_declaration_path_source() -> Outcome { + fps_two_module_assemble( + provider: "module a.b.c\n\ntype Box {\n tree: Int\n}\n", + consumer: "module p\n\nimport a.b.c { Box }\n\nfn f(b: a.b.c.Box) -> Int {\n b.tree\n}\n" + ) +} + +fn fps_frame_binder_projection_source() -> Outcome { + fps_assemble(src: "module p\n\ntype Box {\n tree: Int\n}\n\nfn f(b: Box) -> Int {\n b.tree\n}\n") +} + +// (1) A QUALIFIED DECLARATION PATH RESOLVES. A four-segment cross-module path names a declaration the +// corpus carries, so the head segment being bound at root must NOT make the path a projection of a field off +// that head. NO NATIVE SUBJECT IS CLAIMED FOR THIS ROW: an earlier revision described it as the native +// refusal in miniature, and that attribution was falsified -- the eight's actual cause entry anchors a real +// two-edge projection off a fold-step binder, which resolution never sees. This row stands on the rule. +test fn fps_a_qualified_declaration_path_resolves_holds() -> Bool { + match fps_qualified_declaration_path_source() { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +// (2) AND A FRAME BINDER IS STILL PROJECTED THROUGH. `b.tree` off a parameter is the form the declaration +// reading must not swallow, and it is why the decision is BoundInFrame-first rather than +// declaration-path-first for every head: Section 13's chain-wins rule says the name the author bound +// lexically answers, and the absolute candidates are not consulted. +// +// THE PAIR IS THE EVIDENCE, NOT EITHER ROW. A resolver that read every dotted path as a declaration passes +// (1) and reds (2); one that read every dotted path as a projection passes (2) and reds (1). Only a real +// discriminator passes both, which is the discriminating red the single-arm controls could not supply. +test fn fps_a_frame_binder_is_still_projected_through_holds() -> Bool { + match fps_frame_binder_projection_source() { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} From a509caabc92c59557f3a8b01947bb1df32203ec6 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 1 Oct 2026 22:30:10 +0000 Subject: [PATCH 46/90] Field-projection inference distinguishes an unretrievable declaration from a fieldless receiver, and reads the closure's resolved declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `infer_projection_receiver` collapsed two different facts into `ReceiverNotARecord`, and both emitted `projection_receiver_declares_no_fields`: the receiver's type is established and is not a declaration reference the receiver's type names a declaration THIS CONTEXT CANNOT RETRIEVE The second is not a fact about the program. A lookup miss establishes that this index does not hold the declaration, which is a statement about the evidence available to the stage. Asserting it as "declares no fields" convicts a correct program of a defect it does not have. The cause was structural rather than incidental. `resolved_declarations_of` fills the index from ONE module root, so a record an imported provider declares was absent BY CONSTRUCTION and every cross-module field read was reported as a program defect. `resolved_tree_of` now takes the closure's resolved roots and `resolved_declarations_over` folds the existing single-root door over them -- `symbol_index_fill_module_declarations` already keys each declaration at its own module's qualified name, so the fold is total and order-independent and no index-union authority needs to exist. The subject is folded last and unconditionally, so a caller passing an empty list gets exactly the previous answer. `closure_resolved_roots` assembles those roots, and the recursion is cut by passing an EMPTY closure to the per-root resolves: each provider is resolved exactly as it is resolved today and only the SUBJECT is resolved against the collected roots. N + 1 resolves, not N squared. The native per-module door reaches the same reading through `native_test_closure_resolved_roots`, which asks the shared resolution context rather than threading an accumulator through every caller; its arguments are invariant across a lane's modules, so the call memo answers all but the first. WHAT WAS REFUSED. Reading `resolved.symbol_index` instead would have made imported field typing appear to work off AUTHORED declarations rather than resolved ones, which is the fork this carrier exists to keep apart. A provider that fails to resolve contributes nothing, so a receiver typed by its records refuses with the located unavailable reason -- answering the field read anyway would have been the absorbing arm DESIGN §5 forbids. EVIDENCE, AND ONE PIECE OF IT IS NOT MINE. v2.test.claim.reference_evidence.declaration_reference_evidence carried an expecting-red probe whose header DECLARED THIS REPAIR'S TRIGGER before it was built -- "a resolved-declaration index over every resolved module root ... minted by resolve beside the per-module carrier". That is what landed, so the probe greened. Under DESIGN §4b(4) it does not retire: renamed `dre_an_imported_reference_grounds_through_the_closure_index`, same subject and fixture, assertion inverted, enrolled as the permanent regression control. 11/0. A control written as the wall's trigger, by an author who did not know how it would be built, is better evidence than any claim the implementer can make. v2.test.claim.field_projection.field_projection_stages is 22/24, the two reds being the pre-existing `infer_facts_key_conflict` rows owned by #12582. A same-module positive stands beside the cross-module subject so the repair is a gained capability rather than a widened acceptance, and a scalar receiver still refuses with `declares_no_fields` so the OTHER arm kept its meaning -- without that row the split could be satisfied by routing every projection refusal to the new reason, losing a diagnosis instead of gaining one. THE NEW ARM'S RED IS AUTHORABLE, which is checked rather than assumed. Once the closure index made the ordinary cross-module case succeed, the only remaining way to reach `ReceiverDeclarationUnavailable` is a provider whose declarations never enter the index, and `closure_resolved_roots` drops a root that fails to resolve. `fps_an_unresolvable_provider_leaves_its_declaration_unavailable` produces exactly that and asserts BOTH directions -- the unavailable reason present, declares_no_fields absent. An earlier row asserting that the cross-module case refuses was obsoleted BY SUCCESS, the same way the imported-grounding probe was; its subject moved to the condition that still reaches the arm rather than being deleted, because a permanently unreachable arm cited as coverage is the decoration §4b forbids. v2.test.claim.projection_dispatch.receiver_disposition establishes that the two representations are DISJOINT by executing both readers on both shapes: a declaration reference is not read as a projection and a projection is not read as a declaration reference. That pair is what ruled out a reader-overlap explanation for the native refusal, and it is the control that made the remaining diagnosis decidable. `cross_module_reference_resolution` is 15/15, including `a_receiver_with_no_such_child_never_accepts` -- the control whose breakage caused the arms to be collapsed in the first place. `00_compile` compiles clean: 0 blocking errors, 221 files emitted. NO MOVEMENT IS CLAIMED ON THE NATIVE SEVEN OR EIGHT, and no cross-module roadmap node is closed. The native eight still refuse, and after this split their reason is diagnostic: `declares_no_fields` fires once and the unavailable reason zero times, anchored on `field_projection_base -> e`, `field_projection_field -> target`. Since the two reasons are now disjoint arms, the absence of the second is positive evidence that `e`'s type is ESTABLISHED and is NOT a declaration reference -- not underived, not unretrievable. The split paid for itself immediately: before it those three outcomes were one string and the question could not have been asked. The remaining defect is one link above: `fold_lowering` writes `^loop_domain_edge` carrying the fold's collection, that label has a producer and NO READER anywhere in src/v2/compiler while its siblings `loop_carrier_edge` and `loop_bound_edge` are read in `03_resolve` and `std/cardinality`, and the step's element formal carries no authored annotation -- so nothing joins the domain's element type to it. That is a separate charter. RECORD PAYLOAD MARKS ARE NOT CARRIED and the limitation is inherited rather than chosen: the fill takes a normalize-time record roster, `resolved_declarations_of` passed Empty for it before this change, and a resolved `Node` does not carry that roster. `symbol_index_declared_payload_at` therefore answers Absent for a RECORD in this index, so a reader asking it would take a record for a binder. The projection path reads through `symbol_index_lookup` and is unaffected. A reader needing the payload KIND must be handed the roster; it must not infer it from a lookup hit. THE CLOSURE FOLDED IS THE SELECTED CLOSURE, not the import closure, so it does more work than the dependency relation requires. Narrowing to the exact provider set changes WHICH roots are folded and not the inference rule, so it can land later without disturbing this reading. This is the one-seat form of a dependency the demand engine owns. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/00_compile.dag | 33 ++++- src/v2/compiler/03_name_resolve.dag | 62 +++++++- src/v2/compiler/03_resolve.dag | 78 +++++++++- src/v2/compiler/04_infer.dag | 60 ++++++-- .../canonical_consumer_test.dag | 9 ++ .../field_projection_stages_test.dag | 118 ++++++++++++++- .../fold_step_receiver_test.dag | 135 ++++++++++++++++++ .../receiver_disposition_test.dag | 66 +++++++++ .../declaration_reference_evidence_test.dag | 36 ++--- 9 files changed, 563 insertions(+), 34 deletions(-) create mode 100644 src/v2/test/claim/projection_dispatch/fold_step_receiver_test.dag create mode 100644 src/v2/test/claim/projection_dispatch/receiver_disposition_test.dag diff --git a/src/v2/compiler/00_compile.dag b/src/v2/compiler/00_compile.dag index 3835201594f..59745b931cb 100644 --- a/src/v2/compiler/00_compile.dag +++ b/src/v2/compiler/00_compile.dag @@ -42,6 +42,7 @@ import v2.compiler.emit { emit } import v2.compiler.eval { eval, eval_node, eval_default_interpretation, inputs_root_only } import v2.compiler.infer { InferredTree } import v2.compiler.name_resolve { + closure_resolved_roots, Admission, ResolutionSubject, ResolutionContext, @@ -3230,6 +3231,30 @@ fn native_import_target_refusal_diagnostics(target: NativeTestFileRefusal, resol } } +// THE CLOSURE'S RESOLVED ROOTS FOR THE NATIVE PER-MODULE DOOR. This door resolves ONE module at a time and +// does not hold its siblings, so rather than thread an accumulator through every caller it asks the SHARED +// resolution context -- the same context each module is already resolved against -- for the closure, exactly +// as the assembly path does through closure_resolved_roots. One reading of the dependency, two call sites. +// +// THE ARGUMENTS ARE INVARIANT ACROSS THE LANE'S MODULES, and that is what keeps this from being quadratic: +// every module in one run passes the same context and the same shared resolution, so the call memo answers +// all but the first from cache and the closure is resolved once per run rather than once per module. That is +// a COST claim about a memo, not a correctness claim -- the reading is correct either way, and if the memo +// does not collapse it the work is N squared and must be replaced by an accumulator threaded through the +// callers. It is therefore measured rather than assumed. +fn native_test_closure_resolved_roots( + context: NativeTestContext, + shared: ResolutionContext +) -> FreeMonoid { + closure_resolved_roots( + context: context.resolution, + shared: shared, + index: context.index, + active_roots: context.active_roots, + policy: v2.std.resolution_policy.default_name_resolution_policy() + ) +} + fn native_module_resolve_verdict( context: NativeTestContext, refusal_index: Map, @@ -3252,7 +3277,13 @@ fn native_module_resolve_verdict( ResolveWalkAccepted { value: resolved, diagnostics: _ } => match context.resolution { Accepted { value: shared, diagnostics: _ } => - NativeModuleResolveAccepted { resolved: resolved_tree_of(root: resolved, symbol_index: shared.symbol_index) } + NativeModuleResolveAccepted { + resolved: resolved_tree_of( + root: resolved, + symbol_index: shared.symbol_index, + closure_roots: native_test_closure_resolved_roots(context: context, shared: shared) + ) + } Rejected { diagnostics: r } => NativeModuleResolveRefused { first: r, diff --git a/src/v2/compiler/03_name_resolve.dag b/src/v2/compiler/03_name_resolve.dag index 67704146942..aa7a6dc9c74 100644 --- a/src/v2/compiler/03_name_resolve.dag +++ b/src/v2/compiler/03_name_resolve.dag @@ -772,11 +772,71 @@ fn resolve_with_admission_context_policy( active_roots: active_roots, policy: policy ), - symbol_index: shared.symbol_index + symbol_index: shared.symbol_index, + closure_roots: closure_resolved_roots( + context: context, + shared: shared, + index: index, + active_roots: active_roots, + policy: policy + ) ) } } +// EVERY ROOT IN THE CLOSURE, RESOLVED ONCE, so inference can retrieve a declaration an imported provider +// owns. v2.compiler.resolve resolved_declarations_of walks ONE root, so the subject's index held only the +// subject's declarations and every cross-module field read refused -- measured as +// fps_a_cross_module_record_projection_infers against a passing same-module control. +// +// THE RECURSION IS CUT BY PASSING AN EMPTY CLOSURE TO THE PER-ROOT RESOLVES, and that is the whole reason +// this is N + 1 resolves rather than N squared. Each provider is resolved exactly as it is resolved today -- +// its own declarations, no closure -- and only the SUBJECT is resolved against the collected roots. A +// provider does not need the closure index to produce the declarations this fold reads from it, so nothing +// is lost by denying it one, and the alternative (every root resolved against every other) would multiply +// the dominant cost of the pipeline by the closure size. +// +// A REFUSED ROOT CONTRIBUTES NOTHING AND DOES NOT FAIL THE FOLD. That is deliberate and is NOT an absorbing +// fallback: it does not widen an answer or substitute a reading. A provider that cannot resolve simply has no +// resolved declarations to offer, so a receiver typed by one of its records refuses at +// infer_reason_projection_receiver_declaration_unavailable -- the located refusal that names missing +// evidence, which is exactly the honest outcome for a provider the compiler could not resolve. Absorbing +// would be answering the field read anyway. +// +// THIS IS THE ONE-SEAT FORM OF A DEPENDENCY THE DEMAND ENGINE OWNS. Infer(module) depends on Resolve(module) +// AND Resolve(each provider it consumes); here that is discharged by resolving the whole selected closure, +// which is broader than the import closure and therefore does more work than the relation requires. Narrowing +// it to the exact provider set is a change to WHICH roots are folded and not to the inference rule, so it can +// land later without touching this reading. +fn closure_resolved_roots( + context: Outcome, + shared: ResolutionContext, + index: SourceRootIndex, + active_roots: FreeMonoid, + policy: NameResolutionPolicy +) -> FreeMonoid { + fold_list( + xs: shared.roots.roots, + empty: [], + cons: fn(acc, nt) { + match qualified_name_from_module_node(root: nt.root) { + Rejected { diagnostics: _ } => acc + Accepted { value: module_qn, diagnostics: _ } => + match resolve_walk_with_admission_context_policy( + context: context, + admission: Admission { subject: ResolutionSubject { name: module_qn }, imports: [] }, + index: index, + active_roots: active_roots, + policy: policy + ) { + ResolveWalkAccepted { value: resolved, diagnostics: _ } => list_snoc_item(xs: acc, item: resolved) + ResolveWalkRefused { first: _, rest: _, observation: _ } => acc + } + } + } + ) +} + // THE SUBJECT'S WHOLE RESOLUTION OBSERVATION, every independent failure chain in walk order. The // Outcome entry above is its projection and answers what it always answered. A context or // namespace refusal means the walk was never entered, so none of the subject's sites were observed: diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 03372ae46c2..23fc838e405 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -197,8 +197,68 @@ fn resolved_declarations_of(root: Node) -> SymbolIndex { // THE ONE CONSTRUCTOR of an accepted resolution's carrier: the root, the index it was resolved // against, and its declarations as resolved. -fn resolved_tree_of(root: Node, symbol_index: SymbolIndex) -> ResolvedTree { - ResolvedTree { root: root, symbol_index: symbol_index, resolved_declarations: resolved_declarations_of(root: root) } +// THE INDEX INFERENCE READS SPANS THE CLOSURE, NOT ONE MODULE, and that is the capability this constructor +// previously could not supply. resolved_declarations_of walks ONE resolved root -- the subject's -- so a +// record an imported provider declares was absent from it BY CONSTRUCTION, and v2.compiler.infer +// infer_projection_receiver could not retrieve the declaration for any imported receiver type. +// +// MEASURED, one variable moved: v2.test.claim.field_projection.field_projection_stages +// fps_a_same_module_record_projection_infers passed while fps_a_cross_module_record_projection_infers failed +// over byte-identical records projected off a plain parameter. Two further rows separated the cause from its +// neighbours -- fps_dbg_same_module_in_two_root_ingest PASSES, so a multi-root ingest is not the defect and +// the subject really is cross-module retrieval. +// +// SO THE CALLER SUPPLIES THE CLOSURE'S RESOLVED DECLARATIONS, which keeps this a constructor rather than +// making it a second resolver: the facts come from the resolutions that actually happened, and whoever +// performed them owns them. The alternative considered and REFUSED was reading +// ResolutionContext.symbol_index instead, which is filled from NORMALIZED roots: it would have made +// imported field typing appear to work off AUTHORED declarations rather than resolved ones, which is the +// authored/resolved fork this carrier exists to keep apart (see the resolved_declarations header above). +fn resolved_tree_of( + root: Node, + symbol_index: SymbolIndex, + closure_roots: FreeMonoid +) -> ResolvedTree { + ResolvedTree { + root: root, + symbol_index: symbol_index, + resolved_declarations: resolved_declarations_over(roots: closure_roots, subject: root) + } +} + +// ONE FILL, FOLDED OVER EVERY RESOLVED ROOT, rather than a new multi-root door or a merge of two indexes. +// symbol_index_fill_module_declarations is already the single-root door and keys every declaration at its +// OWN module's qualified name -- its header states that no other root can write those paths -- so folding it +// is total and order-independent, and no index-union authority needs to exist. A root whose module name does +// not resolve contributes nothing, which is that function's existing behaviour and not a new drop. +// +// RECORD PAYLOAD MARKS ARE NOT CARRIED HERE, AND THAT IS INHERITED RATHER THAN CHOSEN. The fill takes a +// record_declarations carrier captured at normalize, and resolved_declarations_of passed Empty for it before +// this change; the fold passes Empty for every provider root for the same reason -- a resolved Node does not +// carry the normalize-time record roster, and inventing one here would be a second authority for a fact +// v2.compiler.normalize owns. The consequence is precise and bounded: v2.std.symbol_index +// symbol_index_declared_payload_at answers Absent for a RECORD in this index, so a reader that asks it would +// take a record for a binder, exactly as that fill's own header warns. The projection path reads through +// symbol_index_lookup and is unaffected, which is why the cross-module control passes. A reader that needs +// the payload KIND from the closure index must be given the roster first; it must not be inferred from a +// lookup hit. +// +// THE SUBJECT IS FOLDED LAST AND UNCONDITIONALLY. It is normally already a member of closure_roots, but a +// caller that holds no closure -- the per-module native door, before its accumulation is threaded -- passes +// an empty list, and this still answers exactly what it answered before: the subject's own declarations. So +// the change cannot take a capability away from a caller that has not yet been given the closure. +fn resolved_declarations_over(roots: FreeMonoid, subject: Node) -> SymbolIndex { + symbol_index_fill_module_declarations( + index: fold_list( + xs: roots, + empty: empty_symbol_index(), + cons: fn(acc, r) { + symbol_index_fill_module_declarations(index: acc, root: r, record_declarations: Empty) + } + ), + root: subject, + record_declarations: Empty + ) } // `test_code`, `declared_in` and `imported_origins` exist for one decision: whether a reference binds @@ -1709,10 +1769,17 @@ fn resolve_walk_outcome(w: ResolveNodeWalk) -> Outcome { } // The stage exit: the same projection, with the index resolution consulted minted beside the root. -fn resolved_tree_outcome(w: ResolveNodeWalk, symbol_index: SymbolIndex) -> Outcome { +fn resolved_tree_outcome( + w: ResolveNodeWalk, + symbol_index: SymbolIndex, + closure_roots: FreeMonoid +) -> Outcome { match w { ResolveWalkAccepted { value: v, diagnostics: d } => - Accepted { value: resolved_tree_of(root: v, symbol_index: symbol_index), diagnostics: d } + Accepted { + value: resolved_tree_of(root: v, symbol_index: symbol_index, closure_roots: closure_roots), + diagnostics: d + } ResolveWalkRefused { first: f, rest: _, observation: _ } => Rejected { diagnostics: f } } } @@ -3053,7 +3120,8 @@ fn resolve_with_namespace_policy( lm: lm, policy: policy ), - symbol_index: namespace.symbol_index + symbol_index: namespace.symbol_index, + closure_roots: [] ) } diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index d130fcd08b2..652bdc117fa 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -1019,8 +1019,10 @@ fn infer_field_projection_facts( descent: Holds { value: descent_proof } ) ) - ReceiverNotARecord => + ReceiverTypeNotADeclaration => outcome_rejected(infer_receiver_declares_no_fields_diagnostic(node: node)) + ReceiverDeclarationUnavailable { path: _ } => + outcome_rejected(infer_receiver_declaration_unavailable_diagnostic(node: node)) ReceiverPayload { payload: payload } => match declared_field_named(payload: payload, name: projection.field) { Present { value: declared } => @@ -1062,13 +1064,34 @@ fn infer_field_not_declared_diagnostic(node: Node) -> Diagnostic { // // SO THE DECIDED CASES REFUSE AND ONLY MISSING EVIDENCE WAITS. A receiver whose type this stage has not // derived is ReceiverTypeUnderived and stays at the frontier, because convicting a program whose -// receiver is typed by a route not yet reaching here would be a wrong answer in the other direction. A -// receiver whose type IS derived and is not a corpus declaration reference, or whose declaration the -// index does not hold, is ReceiverNotARecord: the program projects a field off something that declares -// none, and that is a fact about the program. +// receiver is typed by a route not yet reaching here would be a wrong answer in the other direction. +// +// TWO CASES WERE ONE ARM AND THEY ARE NOT ONE FACT. ReceiverNotARecord carried both "the type is +// established and is not a declaration reference" and "the type names a declaration this context cannot +// retrieve", and both emitted projection_receiver_declares_no_fields. The second is not a fact about the +// program at all: a lookup miss establishes that THIS index does not hold the declaration, which is a +// statement about the evidence available here. Asserting it as "declares no fields" convicts a correct +// program of a defect it does not have. +// +// MEASURED, with one variable moved and a green positive control beside the red: +// v2.test.claim.field_projection.field_projection_stages +// fps_a_same_module_record_projection_infers PASSES and +// fps_a_cross_module_record_projection_infers FAILS over byte-identical records projected off a plain +// parameter, differing only in whether the record is declared in the consuming module or an imported one. +// The cause is structural rather than incidental: v2.compiler.resolve resolved_declarations_of fills the +// index from ONE module root, so a declaration an imported provider owns is absent by construction and +// every cross-module field read was being reported as a program defect. +// +// WHY THE UNAVAILABLE ARM REFUSES RATHER THAN WAITING AT THE FRONTIER. Accepting it as not-derived is the +// shape that broke a_receiver_with_no_such_child_never_accepts, recorded above: a receiver with no such +// child reached the frontier and was ACCEPTED. A blocked dependency must therefore be a located refusal +// with its OWN reason -- fail-closed, and saying what is actually unknown -- and never an acceptance and +// never a claim about the receiver's fields. The arm carries the path so a reader can name the declaration +// that could not be retrieved. type ProjectionReceiver = ReceiverPayload { payload: Node } - | ReceiverNotARecord + | ReceiverTypeNotADeclaration + | ReceiverDeclarationUnavailable { path: QualifiedName } | ReceiverTypeUnderived fn infer_projection_receiver( @@ -1083,10 +1106,10 @@ fn infer_projection_receiver( Violates { diagnostic: _ } => ReceiverTypeUnderived Holds { value: receiver_type } => match declaration_reference_path_optional(node: receiver_type) { - Absent => ReceiverNotARecord + Absent => ReceiverTypeNotADeclaration Present { value: path } => match symbol_index_lookup(index: resolved.resolved_declarations, qualified_path: path) { - Absent => ReceiverNotARecord + Absent => ReceiverDeclarationUnavailable { path: path } Present { value: payload } => ReceiverPayload { payload: payload } } } @@ -1094,6 +1117,27 @@ fn infer_projection_receiver( } } +// THE REFUSAL THAT NAMES MISSING EVIDENCE RATHER THAN A PROGRAM DEFECT. Its correction is a boundary of +// this compiler's own carrier, not of the author's input: the program may be entirely correct and the +// declaration simply unreachable from the index this stage was handed. Keeping it distinct from +// declares_no_fields is what lets the capability repair be verified -- when inference consumes a +// closure-level resolved-declaration authority this reason stops firing, and a corpus that still emits it +// names exactly which declaration could not be retrieved. +// THE LOCUS IS THE PROJECTION SITE AND NOT THE MISSING DECLARATION, deliberately: the author's cursor +// belongs where the program reads the field, and the unretrievable path is carried on +// ReceiverDeclarationUnavailable for a reader that wants it. NoCorrectionReason is a closed set in a shared +// authority (v2.std.diagnostic) with no arm for a carrier gap, and widening it for this one reason would be +// a change to every consumer of that type for a fact already held on this stage's own carrier -- +// CorrectionNotModeled is the honest existing arm, since no correction IS modeled for an index that does not +// reach the declaration. +fn infer_receiver_declaration_unavailable_diagnostic(node: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_reason_projection_receiver_declaration_unavailable, + at: node_locus(node: node), + correction: Unavailable { reason: CorrectionNotModeled } + } +} + fn infer_receiver_declares_no_fields_diagnostic(node: Node) -> Diagnostic { Diagnostic { reason: ^infer_reason_projection_receiver_declares_no_fields, diff --git a/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag b/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag index 840956f80f2..d40d27b6317 100644 --- a/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag +++ b/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag @@ -25,6 +25,15 @@ import v2.std.qualified_name { declaration_reference_path_optional } // set's membership answer decides, for every module-level name a program mentions, whether it is a kernel // symbol or a reference to a declaration. // +// THE ROUTE THESE ROWS TRAVERSE, NAMED. That producer has exactly one consumer, +// v2.compiler.resolve resolve_atom_bound -- the single enforcement site every door that binds a +// module-level name accepts through, reached once the lexical frame has missed. So an annotation +// naming a kernel spelling arrives there as an unbound-in-frame name and the canonical answer is +// what classifies it. Naming the consumer rather than a line is DESIGN §3's standing rule, and it +// matters more than usual here: the rows below observe the classification INDIRECTLY, through +// whether a declaring path survives on the resolved tree, so a reader who cannot find the deciding +// call cannot tell what the rows are discriminating. +// // WHY THIS FILE EXISTS BESIDE THE EQUALITY CONTROL. The equality control proves the new derivation names // the same symbols as the one it replaced; it cannot show that resolve still ASKS, or that the answer still // discriminates. A derivation that silently returned the empty set would pass equality against another diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag index 8706da13d1d..6ef7efd8d4f 100644 --- a/src/v2/test/claim/field_projection/field_projection_stages_test.dag +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -11,7 +11,9 @@ import extdeps.communication.medium { Lossless, Medium } import std.algebra { Cons, Empty } import v2.std.cross_tree.import_model { V2Tree } import v2.std.artifact { Artifact, SourceFile } -import v2.std.diagnostic { Accepted, NodeLocus, Outcome, Rejected } +import v2.std.diagnostic { + Some, + diagnostics_has_reason, Accepted, NodeLocus, Outcome, Rejected } import v2.std.logic { Bool } import v2.std.node { Atom, Conj, Node, Symbol, TypeNode, symbol_eq } import v2.std.qualified_name { declaration_reference_path_optional, qualified_name_last_segment, qualified_name_spine_shape_present } @@ -620,3 +622,117 @@ test fn fps_a_frame_binder_is_still_projected_through_holds() -> Bool { Rejected { diagnostics: _ } => false } } + +// THE RETRIEVAL JOIN, MEASURED THROUGH INFER AND NOT ONLY RESOLVE. The rows above establish that a +// cross-module declaration path RESOLVES; they say nothing about whether inference can then retrieve that +// declaration to read a field off it, because fps_resolves stops at the resolved tree. That retrieval is +// v2.compiler.infer infer_projection_receiver asking ResolvedTree.resolved_declarations for the receiver +// type's declaring path -- and resolved_declarations is filled from the resolved root of the module being +// inferred, not from a closure-wide index of the providers it imports. +// +// ONE VARIABLE. Both sources declare the same record and project the same field off a plain parameter; they +// differ only in whether the record is declared in the consuming module or in an imported one. The fold-step +// binder of the pinned subject is deliberately NOT used here: a supplied two-module ingest cannot resolve +// `List` or `fold` without ingesting their providers, and a plain parameter reaches the same join with +// nothing else moving. +fn fps_cross_module_projection_source() -> Outcome { + fps_two_module_assemble( + provider: "module a.b.c\n\ntype Leg {\n target: Int\n}\n", + consumer: "module p\n\nimport a.b.c { Leg }\n\nfn read(l: Leg) -> Int {\n l.target\n}\n" + ) +} + +fn fps_same_module_projection_source() -> Outcome { + fps_assemble(src: "module p\n\ntype Leg {\n target: Int\n}\n\nfn read(l: Leg) -> Int {\n l.target\n}\n") +} + +// (1) POSITIVE CONTROL. Without it a red on (2) proves nothing -- the field reader and the projection rule +// must work same-module before the cross-module retrieval is the question. +test fn fps_a_same_module_record_projection_infers_holds() -> Bool { + fps_infers(o: fps_same_module_projection_source()) +} + +// (2) THE SUBJECT. A red here beside a green (1) locates the defect at the retrieval of an imported record's +// declaration, and NOT at the projection rule, the field reader or resolution. +test fn fps_a_cross_module_record_projection_infers_holds() -> Bool { + fps_infers(o: fps_cross_module_projection_source()) +} + +// ASSERTED OVER THE WHOLE CHAIN, NOT THE HEAD. An infer refusal's head is infer_grounding_not_derived -- +// the grounding that could not be completed -- and the deciding cause sits in the TAIL. A first version of +// these rows read r.head.reason and failed for that reason alone, which is the same misattribution that cost +// this lane a wrong causal account: in one measured diagnostic the head reason appeared 209 times and the +// actual cause once. v2.std.diagnostic diagnostics_has_reason is the existing authority for asking the chain, +// so these rows ask it rather than minting a second reader. +// +// (3) A PROVIDER THAT DOES NOT RESOLVE LEAVES ITS DECLARATIONS UNAVAILABLE, AND THAT IS NOT "DECLARES NO +// FIELDS". This row exists because the arm it exercises would otherwise have no authorable red. The +// honesty split gave ReceiverDeclarationUnavailable its own reason, and the closure index then made the +// ordinary cross-module case SUCCEED -- so the only remaining way to reach the arm is a provider whose +// declarations never enter the index. v2.compiler.name_resolve closure_resolved_roots drops a root that +// fails to resolve, which is exactly that condition, and this fixture produces it with a provider carrying +// an unresolvable body beside the record the consumer projects. +// +// DESIGN section 4b IS WHY THE ROW IS WRITTEN THIS WAY RATHER THAN DELETED. An earlier version asserted that +// the cross-module case refuses as unavailable; the capability repair made it infer, so that row was +// obsoleted BY SUCCESS exactly as the imported-grounding probe in +// v2.test.claim.reference_evidence.declaration_reference_evidence was. Deleting it would leave the new arm +// with no executing evidence and no discriminating red -- a permanently unreachable arm cited as coverage, +// which is the decoration section 4b forbids. So the subject moves to the condition that still reaches it. +// +// IT ASSERTS BOTH DIRECTIONS. The unavailable reason must be present AND declares_no_fields must be absent, +// because the whole defect being repaired was a correct program being told its receiver declares no fields. +fn fps_unresolvable_provider_source() -> Outcome { + fps_two_module_assemble( + provider: "module a.b.c\n\ntype Leg {\n target: Int\n}\n\nfn broken() -> Int {\n no_such_name_anywhere\n}\n", + consumer: "module p\n\nimport a.b.c { Leg }\n\nfn read(l: Leg) -> Int {\n l.target\n}\n" + ) +} + +test fn fps_an_unresolvable_provider_leaves_its_declaration_unavailable_holds() -> Bool { + match fps_unresolvable_provider_source() { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => + diagnostics_has_reason(d: Some { diagnostics: r }, reason: ^infer_reason_projection_receiver_declaration_unavailable) + && !diagnostics_has_reason(d: Some { diagnostics: r }, reason: ^infer_reason_projection_receiver_declares_no_fields) + } + } +} + +// (4) AND A RECEIVER THAT GENUINELY DECLARES NO FIELDS STILL SAYS SO. The split is only honest if the OTHER +// arm kept its meaning: an established type that is not a declaration reference at all is a fact about the +// program and must refuse with declares_no_fields. Without this row the split could be satisfied by routing +// every projection refusal to the new reason, which would lose a real diagnosis instead of gaining one. +fn fps_scalar_receiver_source() -> Outcome { + fps_assemble(src: "module p\n\nfn read(n: Int) -> Int {\n n.target\n}\n") +} + +test fn fps_a_scalar_receiver_still_declares_no_fields_holds() -> Bool { + match fps_scalar_receiver_source() { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => + diagnostics_has_reason(d: Some { diagnostics: r }, reason: ^infer_reason_projection_receiver_declares_no_fields) + } + } +} + +// DISCRIMINATOR: IS THE DEFECT "CROSS-MODULE" OR "MORE THAN ONE ROOT"? Same-module record and projection as +// the passing control, assembled through the TWO-module ingest with an unrelated provider beside it. If this +// refuses, the subject is not cross-module retrieval at all -- it is that a multi-root ingest yields an index +// that answers for nothing, which is a different and simpler defect with a different repair. +fn fps_same_module_projection_in_two_root_ingest() -> Outcome { + fps_two_module_assemble( + provider: "module a.b.c\n\ntype Unused {\n n: Int\n}\n", + consumer: "module p\n\ntype Leg {\n target: Int\n}\n\nfn read(l: Leg) -> Int {\n l.target\n}\n" + ) +} + +test fn fps_dbg_same_module_in_two_root_ingest() -> Bool { + fps_infers(o: fps_same_module_projection_in_two_root_ingest()) +} diff --git a/src/v2/test/claim/projection_dispatch/fold_step_receiver_test.dag b/src/v2/test/claim/projection_dispatch/fold_step_receiver_test.dag new file mode 100644 index 00000000000..18104f4a896 --- /dev/null +++ b/src/v2/test/claim/projection_dispatch/fold_step_receiver_test.dag @@ -0,0 +1,135 @@ +module v2.test.claim.projection_dispatch.fold_step_receiver + +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.extdeps.languages.dag { dag_language_model } +import extdeps.communication.medium { Lossless, Medium } +import std.algebra { Cons, Empty } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.logic { Bool } +import v2.std.text { String } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE JOIN THE EIGHT ACTUALLY REFUSE AT, ISOLATED TO ONE VARIABLE. The pinned subject's cause entry anchors +// `e.target` inside +// +// fold(root.children, init: false, f: fn(found, e) { found || g_tree_has_arrow_body(root: e.target) }) +// +// where `root.children` is `List` and `Edge` declares `target: Node`. So the compiler must type the +// fold's element binder from an IMPORTED record and then retrieve that record's declaration to read a field +// off it. v2.compiler.infer infer_projection_receiver performs that retrieval against +// ResolvedTree.resolved_declarations, which is filled from the resolved root of the module being inferred -- +// not from a closure-wide index of the providers it imports. +// +// THE TWO ROWS DIFFER IN EXACTLY ONE THING: whether the record is declared in the consuming module or in +// another module it imports. Everything else -- the fold, the element binder, the projected field name, the +// language model -- is held constant, so a disagreement between them isolates the retrieval and nothing +// else. This is the measurement the repair waits on; no arm is edited before it answers. +data fsr_provider_artifact: Artifact = Artifact { + kind: SourceFile, id: ^fsr_provider, file_path: "src/v2/pilot/fsr_provider.dag" +} + +data fsr_consumer_artifact: Artifact = Artifact { + kind: SourceFile, id: ^fsr_consumer, file_path: "src/v2/pilot/fsr_consumer.dag" +} + +fn fsr_assemble_one(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: fsr_consumer_artifact, + compilation_unit: ^fsr_consumer_cu, + source_root: V2Tree + }, + tail: Empty + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +fn fsr_assemble_two(provider: String, consumer: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: provider, fidelity: Lossless }, + artifact: fsr_provider_artifact, + compilation_unit: ^fsr_provider_cu, + source_root: V2Tree + }, + tail: Cons { + head: DagSourceReadWitness { + source: Medium { carried: consumer, fidelity: Lossless }, + artifact: fsr_consumer_artifact, + compilation_unit: ^fsr_consumer_cu, + source_root: V2Tree + }, + tail: Empty + } + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +// SAME MODULE: the record is declared beside the fold that projects it. +fn fsr_same_module_source() -> Outcome { + fsr_assemble_one( + src: "module p\n\ntype Leg {\n target: Int\n}\n\nfn total(legs: List) -> Int {\n fold(legs, init: 0, f: fn(acc, e) {\n acc + e.target\n })\n}\n" + ) +} + +// CROSS MODULE: byte-identical fold, record moved to an imported provider. +fn fsr_cross_module_source() -> Outcome { + fsr_assemble_two( + provider: "module q.r\n\ntype Leg {\n target: Int\n}\n", + consumer: "module p\n\nimport q.r { Leg }\n\nfn total(legs: List) -> Int {\n fold(legs, init: 0, f: fn(acc, e) {\n acc + e.target\n })\n}\n" + ) +} + +fn fsr_accepted(o: Outcome) -> Bool { + match o { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } +} + +fn fsr_reason(o: Outcome) -> String { + match o { + Rejected { diagnostics: r } => r.head.reason as String + Accepted { value: _, diagnostics: _ } => "accepted" + } +} + +// (1) THE SAME-MODULE FORM IS THE POSITIVE CONTROL. Without it a red on (2) proves nothing: the fold's +// element typing, the fn-literal binder and the field reader all have to work before the cross-module +// retrieval is even the question being asked. +test fn fsr_a_same_module_fold_step_projection_holds() -> Bool { + fsr_accepted(o: fsr_same_module_source()) +} + +// (2) AND THE CROSS-MODULE FORM IS THE SUBJECT. A red here beside a green (1) locates the defect at the +// retrieval of an imported record's declaration, which is the join the eight die on. +test fn fsr_a_cross_module_fold_step_projection_holds() -> Bool { + fsr_accepted(o: fsr_cross_module_source()) +} + +// (3) AND THE REASON IS CARRIED, so the receipt says WHICH refusal rather than only that one occurred. A +// cross-module red reported as projection_receiver_declares_no_fields is the conflation under test; any other +// reason is a different defect and must not be filed as this one. +test fn fsr_the_cross_module_reason_is_reported_holds() -> Bool { + !(fsr_reason(o: fsr_cross_module_source()) == "") +} + +// REASON BISECTION. claim_batch reports only a Bool, so the refusal reason is recovered by asserting +// candidates: the row that passes names it. These are a measurement instrument, not semantic claims. +test fn fsr_dbg_unbound() -> Bool { fsr_reason(o: fsr_same_module_source()) == "resolve_reason_unbound_symbol" } +test fn fsr_dbg_nofields() -> Bool { fsr_reason(o: fsr_same_module_source()) == "infer_reason_projection_receiver_declares_no_fields" } +test fn fsr_dbg_g0() -> Bool { fsr_reason(o: fsr_same_module_source()) == "parse_g0_tokens_remain" } +test fn fsr_dbg_overlap() -> Bool { fsr_reason(o: fsr_same_module_source()) == "parse_grammar_choice_overlap_residue" } +test fn fsr_dbg_fnbody() -> Bool { fsr_reason(o: fsr_same_module_source()) == "body_lowering_reason_function_value_body_unread" } +test fn fsr_dbg_unsupported() -> Bool { fsr_reason(o: fsr_same_module_source()) == "body_lowering_reason_unsupported_form" } diff --git a/src/v2/test/claim/projection_dispatch/receiver_disposition_test.dag b/src/v2/test/claim/projection_dispatch/receiver_disposition_test.dag new file mode 100644 index 00000000000..ac89c19e17b --- /dev/null +++ b/src/v2/test/claim/projection_dispatch/receiver_disposition_test.dag @@ -0,0 +1,66 @@ +module v2.test.claim.projection_dispatch.receiver_disposition + +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import v2.std.qualified_name { declaration_reference_node, declaration_reference_path_optional } +import v2.std.node_query { field_projection_node, field_projection_optional } +import v2.std.node { Atom, Node, Symbol, TypeNode, node_synthetic } +import std.occurrence_identity { OccurrenceSynthetic } +import v2.std.logic { Bool } +import v2.std.algebra { Cons, Empty } +import v2.std.optional { Absent, Present } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE TWO REPRESENTATIONS ARE DISJOINT, AND THAT IS THE FACT THE NEXT DIAGNOSIS DEPENDS ON. A declaration +// reference is a ONE-edge marked Conj whose child is a synthetic qualified-name spine; a field projection is +// an exact TWO-edge Conj carrying the base and field roles. The readers are therefore each other's +// negative, and these rows establish it by executing both readers on both shapes rather than by reading +// their contracts. +// +// WHY THIS IS WRITTEN BEFORE ANY REPAIR. The native eight refuse with +// infer_reason_projection_receiver_declares_no_fields over an anchor that IS a declaration reference +// carrying v2 -> std -> live_tree -> LiveTreeDisposition. Two readings fit that observation and they lead +// to opposite repairs: either a declaration reference is entering the projection reader (a representation +// overlap, repaired in the readers), or the anchor is the RECEIVER'S TYPE of a genuine projection whose base +// is typed by that declaration (no overlap at all, repaired in the receiver rule). The node shape alone does +// not separate them, and an earlier causal sentence in this lane was asserted from shape alone and +// falsified by the next run. So the disjointness is measured first and the causal claim waits. +fn rd_declaration_reference() -> Node { + declaration_reference_node( + qn: Cons { head: ^v2, tail: Cons { head: ^std, tail: Cons { head: ^live_tree, tail: Cons { head: ^LiveTreeDisposition, tail: Empty } } } }, + occurrence_id: OccurrenceSynthetic + ) +} + +fn rd_atom(name: Symbol) -> Node { + node_synthetic(kind: TypeNode { connective: Atom { identity: name } }, children: Empty) +} + +fn rd_real_projection() -> Node { + field_projection_node(base: rd_atom(name: ^b), field: rd_atom(name: ^tree), source: rd_atom(name: ^src)) +} + +// (1) A DECLARATION REFERENCE IS NOT READ AS A PROJECTION. +test fn rd_a_declaration_reference_is_not_a_projection_holds() -> Bool { + (match declaration_reference_path_optional(node: rd_declaration_reference()) { + Present { value: _ } => true + Absent => false + }) + && (match field_projection_optional(n: rd_declaration_reference()) { + Absent => true + Present { value: _ } => false + }) +} + +// (2) AND A PROJECTION IS NOT READ AS A DECLARATION REFERENCE. The pair is the control: either row alone is +// satisfied by a reader that always answers Absent. +test fn rd_a_projection_is_not_a_declaration_reference_holds() -> Bool { + (match field_projection_optional(n: rd_real_projection()) { + Present { value: _ } => true + Absent => false + }) + && (match declaration_reference_path_optional(node: rd_real_projection()) { + Absent => true + Present { value: _ } => false + }) +} diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 3383e363cf8..070af39cd31 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -485,26 +485,26 @@ fn dre_imported_reference_source() -> Outcome { // denote (it is a binding-to-type table, and the authored spelling is not a binding). Reading the // pre-resolve symbol_index instead is what made every named call's result typing fall to the frontier. // -// resolve walks ONE module root and mints resolved_declarations over it (resolved_declarations_of), so -// that index carries the SUBJECT module's declarations and no other. m.lib.helper is therefore absent -// from it, the lookup answers Absent, and the reference stays underived. NOTHING IS FABRICATED: the -// arm that would widen -- fall back to the authored index when the resolved one has no row -- is the -// absorbing fallback DESIGN section 5 forbids, and it is the exact widening whose answer was wrong. -// Re-deriving the return from the language's kernel spelling table inside infer is the other tempting -// arm and is a second resolution authority (DESIGN section 3), so it is refused too. +// resolve ONCE walked a single module root and minted resolved_declarations over it, so that index carried +// the SUBJECT module's declarations and no other. m.lib.helper was absent from it, the lookup answered +// Absent, and the reference stayed underived. The two tempting repairs were refused then and stay refused: +// falling back to the authored index is the absorbing fallback DESIGN section 5 forbids, and re-deriving the +// return from the language's kernel spelling table inside infer is a second resolution authority (section 3). // -// THE TRIGGER IS A RESOLVED-DECLARATION INDEX OVER EVERY RESOLVED MODULE ROOT -- the multi-root door -// v2.compiler.symbol_index_fill symbol_index_fill_module_roots applied to resolved roots rather than -// authored ones, minted by resolve beside the per-module carrier. That is resolve's fact to own, not -// infer's. Until it exists, a cross-module named call's result type is on the counted frontier. +// THE TRIGGER THIS ROW DECLARED HAS LANDED, which is why the row now reads in the opposite direction. It +// named "a resolved-declaration index over every resolved module root ... minted by resolve beside the +// per-module carrier", and that is what v2.compiler.resolve resolved_tree_of now receives: the closure's +// resolved roots, folded through the existing single-root door, assembled by v2.compiler.name_resolve +// closure_resolved_roots. The fact stayed resolve's to own, as this row said it must. // -// THIS ROW IS THE FRONTIER'S EXECUTING EVIDENCE, asserted in the direction that is true today: the -// imported call does NOT ground. It flips the moment the trigger lands, which is what makes it a -// transition signal rather than a permanent green. The single-module rows above are the positive -// controls that the mechanism itself works, so this row cannot be satisfied by the mechanism being -// broken everywhere. -test fn dre_an_imported_reference_does_not_yet_ground_holds() -> Bool { - dre_call_is_grounded_for(o: dre_imported_reference_source(), wanted: ^helper) == false +// DESIGN section 4b(4) IS WHY THIS DID NOT RETIRE. An expecting-red probe that greens when its wall lands +// flips to a permanent regression control; it does not disappear, because the climb's evidence is what keeps +// the higher rung real. So the row keeps its subject and its fixture and inverts its assertion, and its name +// now states what holds rather than what is missing. The single-module rows above remain the positive +// controls that the mechanism works at all, so this row cannot be satisfied by grounding being broken +// everywhere -- and it goes red again the moment the closure index stops reaching an imported declaration. +test fn dre_an_imported_reference_grounds_through_the_closure_index_holds() -> Bool { + dre_call_is_grounded_for(o: dre_imported_reference_source(), wanted: ^helper) } // THE ASYMMETRY THE TWO REFUSAL ROWS ABOVE DEPEND ON, MEASURED RATHER THAN ASSERTED. A declared `Int` From 54725fe66b3f370cf1794726a5e3b63adc600f69 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 01:31:21 +0000 Subject: [PATCH 47/90] File the diagnosed root: infer cannot derive a child's context from a sibling's result DIAGNOSIS ONLY. This is the first commit of the charter that will build the capability, and it deliberately adds no part of the repair. The preceding charter stopped at discovery; this preserves what it discovered before any traversal work begins, so the reasoning does not live only in a closed session. v2.compiler.infer infers every child subtree independently before its parent's behavior row executes. A Loop domain's SYNTHESIZED element type is needed to instantiate the step member formal's fresh type variable, and no inference traversal can carry one child's settled result into another child's inference context. The fold encoding compounds it: fold_recurrence_encoding emits the Positional step as child 0 and the named loop_domain_edge as child 1, so ordinary left-to-right child order cannot supply the relation either. The harm is a loud refusal of a valid program -- `fold(root.children, init: false, f: fn(found, e) { found || g_tree_has_arrow_body(root: e.target) })` refuses at `e.target` with infer_reason_projection_receiver_declares_no_fields while the domain carries `root.children: List` and Edge declares `target: Node`. THE ROW'S MOST USEFUL CONTENT IS THE DISPROVEN ROUTE, because it is the one a later reader would otherwise re-attempt. Converting infer's gather from NodeFold to NodeFoldTopDown does NOT carry the fact. NodeFold moves synthesized results child-to-parent only; NodeFoldTopDown moves inherited context parent-to-child only, and its `child_context: fn(Node, A, Edge) -> A` sees the parent node, the inherited context and the current edge, never a folded sibling result. So neither algebra expresses "infer the domain child, derive the member instance, then infer the step child under it", and the conversion would change the shape of the stage's single walk across every behavior arm while still not delivering the type. An earlier statement of this root in session named that conversion as the gap; it was wrong, and the row records the correction rather than the first reading. THREE CAUSES WERE MEASURED AND EXCLUDED, which is what makes the row narrow enough to act on: it is not a missing loop_domain_edge reader, not cross-module declaration retrieval (a cross-module record projection now infers), and not a projection-classification defect (the two representations were shown disjoint by executing both readers on both shapes). The three facts the repair will consume all already exist and are cited: the variable is minted by fresh_type_variable, the existing parameter-scope route correctly derives the member formal AS that variable, and infer_projection_receiver is the rule that refuses. So the repair instantiates an existing variable; it must not introduce a second parameter-typing route. RUNG: mitigatable -- typed and located, nothing fabricated, but valid fold programs cannot type. CEILING: structurally guaranteed. TRIGGER, as a capability: an infer-local dependent-child driver schedules the Loop domain before the step, derives the collection-fold member instance from the domain type, extends a lexical TypeVariableInstance frame FOR THE STEP SUBTREE ONLY, and preserves the existing behavior rows, sufficient for the unchanged production helper to establish `e: Edge` and `e.target: Node`, with a mutation deleting the domain-to-step join making that control red. NO CONTROL IS ADDED HERE. The unchanged native subject is the executing red, and a unit row written now to justify the file would be documentation work rather than evidence. The focused production-shaped control belongs to the commit that builds the capability. The roster and the markdown projection are derived and untracked, so this file is the whole append; both regenerate around it. Co-Authored-By: Claude Opus 5 (1M context) --- ...text_cannot_depend_on_a_sibling_result.dag | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag diff --git a/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag b/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag new file mode 100644 index 00000000000..add92a68642 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag @@ -0,0 +1,26 @@ +module gunbc.recurring_failure_mode.infer_child_context_cannot_depend_on_a_sibling_result + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data infer_child_context_cannot_depend_on_a_sibling_result: RecurringFailureMode = RecurringFailureMode { + identity: "infer_child_context_cannot_depend_on_a_sibling_result" as NonEmptyStr, + receipts: [ + "INVALID STATE: v2.compiler.infer infers every child subtree independently before its parent's behavior row executes. A Loop domain's SYNTHESIZED element type is required to instantiate the step member formal's fresh type variable, but no inference traversal can carry one child's settled result into another child's inference context. The fold encoding compounds it by storing the step BEFORE the domain (v2.compiler.fold_lowering fold_recurrence_encoding emits Positional step as child 0 and the named loop_domain_edge as child 1), so ordinary left-to-right child order cannot supply the relation either. HARM: the valid helper `fold(root.children, init: false, f: fn(found, e) { found || g_tree_has_arrow_body(root: e.target) })` is refused at `e.target` with infer_reason_projection_receiver_declares_no_fields -- `e` stays typed as its fresh variable instead of Edge, although the Loop domain carries `root.children: List` and Edge declares `target: Node`. The refusal is typed and located and nothing is fabricated, so this is a capability gap on the loud side of the ladder, never a silent wrong answer.", + "DISTINGUISHING FACTS: this is NOT a missing loop_domain_edge reader, NOT cross-module declaration retrieval, and NOT a projection-classification defect -- each of those was measured and excluded. v2.std.node fold_node carries SYNTHESIZED results only from child to parent (step: fn(R, Edge, R) -> R, whose third argument is the child's already-folded result, computed by a recursive call that receives nothing from the parent). v2.std.node fold_node_topdown carries INHERITED context only from parent to child, and its child_context: fn(Node, A, Edge) -> A receives the parent node, the inherited context and the current edge -- no folded sibling result. So neither algebra expresses `infer the domain child, derive the member instance, then infer the step child under it`, and converting infer's gather from NodeFold to NodeFoldTopDown is a DISPROVEN route rather than the trigger: it would change the shape of the stage's single walk across all behavior arms and still not carry the fact. Adding the role reader, the domain consumer or the List element relation before the traversal exists would make each declaration unreachable from any production verdict, which is the dangling modeling DESIGN section 3c forbids. The three facts the repair consumes already exist and are cited below: the fresh variable is minted by v2.std.anonymous_binder fresh_type_variable, the existing parameter-scope route correctly derives the member formal AS that variable (v2.compiler.infer infer_parameter_scope_search), and the receiver rule that refuses is infer_projection_receiver -- so no second parameter-typing route may be introduced; the existing variable must be instantiated.", + "RUNG FOUND AT: mitigatable -- the compiler refuses rather than fabricating a field-bearing type, but valid fold programs cannot type. ATTAINABLE CEILING: structurally guaranteed -- the element relation is decidable from the domain's own type, and every fact it needs is already established by a stage that runs before the step subtree is judged; what is missing is one driver whose behavior-specific child schedule can infer a dependency child once, derive a scoped context from its settled result, and infer the dependent child once under that context. NEXT-RUNG TRIGGER, stated as the capability: an infer-local dependent-child driver schedules the Loop domain before the step, derives the collection-fold member instance from the domain type, extends a lexical TypeVariableInstance frame FOR THE STEP SUBTREE ONLY, and preserves the existing behavior rows, SUFFICIENT FOR the unchanged production helper to establish `e: Edge` and `e.target: Node`. Its discriminating red is a mutation deleting the domain-to-step context join, which must make that control fail. The element relation must be scoped by FOLD REALIZATION rather than by assuming every Loop is List, and the role authority (the step callable's actual 0 is the carrier and actual 1 is the domain member, stated today in v2.compiler.fold_lowering) belongs in one decoder rather than being re-read per consumer.", + ], + evidence: [ + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_entries_for_tree", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_gather_fold_algebra", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_gather_loop_row_on_entries", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.std.node", decl_name: "fold_node", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.std.node", decl_name: "fold_node_topdown", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "fold_recurrence_encoding", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.parse.expression_bodied_fn_decl_parse", decl_name: "g_tree_has_arrow_body", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_parameter_scope_search", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_projection_receiver", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.std.anonymous_binder", decl_name: "fresh_type_variable", field: WholeDeclaration }, + ], +} From 14a8710905b0e13ee9672393506f270d12e0daa3 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 01:56:33 +0000 Subject: [PATCH 48/90] Revert the canonical-symbol derivation: main narrowed the set it reproduced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts 6b3490b2621. That commit replaced `dag_canonical_symbol_map`'s route to the carried symbol set -- reading the grammar's fields directly instead of building a whole `Node` encoding and sweeping it for atoms -- and proved EXACT SET EQUALITY against the old derivation as its central evidence. The equality proof is what makes it wrong to keep. main has since NARROWED that set deliberately, and its reasoning names a silent-wrongness class: carrying the grammar's atoms "made an unbound `node` in a body resolve, Accepted and silent, to a grammar atom, and a declaration spelled `output` resolve to the kernel symbol rather than to its path". main's set is now the lex token classes plus the kernel bindings -- what the model mints as atoms a resolved body may carry -- and NOT production names or contextual-keyword terminals, which parse consumes in their grammar positions and which never reach resolution as references. So the reverted commit is a faithful, measured, well-controlled reproduction of a defect. Its equality control established that the new route reproduced the old set exactly, which is precisely the property main removed. Resolving the merge toward it would have reverted a correctness fix for a class DESIGN §5 forbids outright -- a wrong answer passing silently -- in exchange for a demand-minimization win already measured at roughly 0.1 s of a 201.8 s native context, since `dag_language_model` is prepared once per run. NOTHING IS REBASED ONTO THE NARROWER SET, because against it there is likely nothing left to minimize: main's derivation is token classes plus kernel bindings and does not go through `grammar_to_node` at all. Keeping `grammar_carried_symbol_map`'s rewritten form and its equality control would leave an authority for a set nothing consumes, which is the dangling modeling DESIGN §3c forbids. `grammar_carried_symbol_map` returns to its serialized-image form, which main carries independently of this lane and which `grammar_carries_symbol` still consumes, so the revert removes only what this lane added. WHAT THE REVERT DOES NOT DISCARD is the reading that motivated it: a route that builds a representation nothing consumes, purely so a walker can read back facts its source already names, is authored duplication. That remains true of the old route and is recorded here rather than in a live declaration, because the subject it applied to no longer exists. The three canonical_demand claim files go with it, including the consumer control whose citation was sharpened in a509caabc92: it exercised the wide set's membership through resolve, so it asserts a property main has deliberately removed. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/std/grammar.dag | 91 -------------- .../canonical_consumer_test.dag | 114 ------------------ .../canonical_cost_probe_test.dag | 26 ---- .../canonical_set_equality_test.dag | 54 --------- 4 files changed, 285 deletions(-) delete mode 100644 src/v2/test/claim/canonical_demand/canonical_consumer_test.dag delete mode 100644 src/v2/test/claim/canonical_demand/canonical_cost_probe_test.dag delete mode 100644 src/v2/test/claim/canonical_demand/canonical_set_equality_test.dag diff --git a/src/v2/std/grammar.dag b/src/v2/std/grammar.dag index 566f234a565..fc06c816068 100644 --- a/src/v2/std/grammar.dag +++ b/src/v2/std/grammar.dag @@ -2423,98 +2423,7 @@ fn grammar_node_insert_atom_identities(m: Map, n: Node) -> Map Map` so each arm is an insertion and each -// composite a composition, copying nothing. -// -// THAT FORM DOES NOT COMPILE HERE, and the refusal is the language's, not a style objection: applying a -// fn-valued PARAMETER or a let-bound fn value is not admitted -- `left(acc)` refuses with "function 'left' -// not found in scope", as does `insert(m)` over a let-bound fold result. Only a direct call to a named -// declaration is a call. So the residue stands, and its next-rung trigger is application of fn-valued -// bindings in .dag, not a cheaper spelling of this fold. -// -// Measured so the residue is priced rather than assumed: this derivation takes dag_language_model() from -// 103,518 eval steps to 47,464, and what remains in the carried map is this fold plus its insertions. -fn grammar_expr_carried_symbols(expr: GrammarExpr) -> List { - fold_grammar_expr( - expr: expr, - algebra: GrammarExprFold { - terminal: fn(token_class, stamp) { - match stamp { - StampBinding { binding: b } => [token_class, b] - StampClass => [token_class] - StampLexeme => [token_class, ^grammar_terminal_stamp_lexeme] - } - }, - literal_terminal: fn(token_class, lexeme) { [token_class, lexeme] }, - nonterminal: fn(production) { [production] }, - sequence: fn(left, right) { concat(left, right) }, - choice: fn(left, right) { concat(left, right) }, - optional: fn(element) { element }, - repeat: fn(element) { element }, - expect: fn(element, reason) { - concat(element, [parse_refusal_reason_symbol(reason: reason)]) - } - } - ) -} - -// THE CARRIED SYMBOLS ARE FOLDED FROM THE GRAMMAR'S OWN STRUCTURE, NOT FROM A SERIALIZED IMAGE OF IT. -// This read `grammar_node_insert_atom_identities(grammar_to_node(grammar))`: it encoded the WHOLE grammar -// into a Node tree and then recovered the symbols by a generic atom walk over that tree. The encoding -// exists for the serialization direction (DESIGN section 4, one grammar read both ways); using it to -// enumerate symbols is a round trip through a representation built for a different consumer, and it is -// paid in full on every construction of a LanguageModel whether or not one membership question is asked. -// -// WHAT THE ATOMS ACTUALLY ARE, read off the encoders rather than guessed: grammar_root_to_node emits -// grammar_atom(root.start) and the productions node; grammar_production_to_node emits -// grammar_atom(production.name), the expression's encoding, and the production's emitted node. Every -// wrapper in between is a Conj with Named edges, which carries no Atom identity of its own -- so folding -// start, each name, each expression encoding and each emitted node collects exactly the same symbols and -// builds none of the wrappers. -// -// root.sync_tokens IS DELIBERATELY NOT INCLUDED, because grammar_root_to_node does not encode it. A fold -// that added it would be a different set, which is why the equality control over the real dag_grammar() -// is not a formality: v2.test.claim.canonical_demand.canonical_set_equality compares this map to the -// serialized derivation key for key, in both directions. fn grammar_carried_symbol_map(grammar: ParseGrammar) -> Map { - match grammar { - VoidGrammar => grammar_node_insert_atom_identities(m: empty_map(), n: grammar_empty_node()) - ModeledGrammar { root: root } => - fold_list( - xs: root.productions, - empty: grammar_symbol_map_put(m: empty_map(), sym: root.start), - cons: fn(acc, production) { - grammar_node_insert_atom_identities( - m: grammar_symbol_list_put( - m: grammar_symbol_map_put(m: acc, sym: production.name), - xs: grammar_expr_carried_symbols(expr: production.expression) - ), - n: production.emitted - ) - } - ) - } -} - -// THE SERIALIZED DERIVATION, KEPT AS THE EQUALITY ORACLE AND FOR NOTHING ELSE. It is what the structural -// fold above is proven equal to; no production path calls it. -fn grammar_carried_symbol_map_via_serialized_image(grammar: ParseGrammar) -> Map { grammar_node_insert_atom_identities(m: empty_map(), n: grammar_to_node(grammar: grammar)) } diff --git a/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag b/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag deleted file mode 100644 index d40d27b6317..00000000000 --- a/src/v2/test/claim/canonical_demand/canonical_consumer_test.dag +++ /dev/null @@ -1,114 +0,0 @@ -module v2.test.claim.canonical_demand.canonical_consumer - -import v2.compiler.resolve { ResolvedTree } -import v2.compiler.name_resolve { Admission, ResolutionSubject } -import v2.compiler.program_assembly { assemble_program_from_ingest } -import v2.compiler.source_authority { DagSourceReadWitness } -import v2.extdeps.languages.dag { dag_language_model } -import extdeps.communication.medium { Lossless, Medium } -import std.algebra { Cons, Empty } -import v2.std.cross_tree.import_model { V2Tree } -import v2.std.artifact { Artifact, SourceFile } -import v2.std.diagnostic { Accepted, Outcome, Rejected } -import v2.std.logic { Bool } -import v2.std.integer { Int } -import v2.std.text { String } -import v2.std.node { Atom, Node, NodeFold, TypeNode, fold_node } -import v2.std.node_query { find_named_child } -import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } -import v2.std.collection { List } -import v2.std.qualified_name { declaration_reference_path_optional } - -// THE CANONICAL SET IS CONSULTED BY AUTHORED SOURCE, NOT ONLY BY A UNIT CALL. v2.compiler.resolve -// resolved_reference_identity asks namespace_has_canonical_symbol for a resolved reference's LEAF: a -// canonical leaf becomes ResolvedToKernelSymbol and anything else becomes ResolvedToDeclaration. So the -// set's membership answer decides, for every module-level name a program mentions, whether it is a kernel -// symbol or a reference to a declaration. -// -// THE ROUTE THESE ROWS TRAVERSE, NAMED. That producer has exactly one consumer, -// v2.compiler.resolve resolve_atom_bound -- the single enforcement site every door that binds a -// module-level name accepts through, reached once the lexical frame has missed. So an annotation -// naming a kernel spelling arrives there as an unbound-in-frame name and the canonical answer is -// what classifies it. Naming the consumer rather than a line is DESIGN §3's standing rule, and it -// matters more than usual here: the rows below observe the classification INDIRECTLY, through -// whether a declaring path survives on the resolved tree, so a reader who cannot find the deciding -// call cannot tell what the rows are discriminating. -// -// WHY THIS FILE EXISTS BESIDE THE EQUALITY CONTROL. The equality control proves the new derivation names -// the same symbols as the one it replaced; it cannot show that resolve still ASKS, or that the answer still -// discriminates. A derivation that silently returned the empty set would pass equality against another -// empty set and would also let a 4-line module assemble, because a module that mentions no kernel spelling -// asks nothing -- which is precisely how a cost measurement can look like a success. These rows make the -// question observable from source, in both directions. -data cc_artifact: Artifact = Artifact { - kind: SourceFile, id: ^canonical_consumer_artifact, file_path: "src/v2/pilot/canonical_consumer.dag" -} - -fn cc_assemble(src: String) -> Outcome { - assemble_program_from_ingest( - ingest: Cons { - head: DagSourceReadWitness { - source: Medium { carried: src, fidelity: Lossless }, - artifact: cc_artifact, compilation_unit: ^canonical_consumer_cu, source_root: V2Tree - }, - tail: Empty - }, - admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, - lm: dag_language_model() - ) -} - -// A module whose parameter is annotated with the KERNEL spelling `Int`, and one annotated with a type the -// module itself declares. Same shape otherwise, so the annotation is the only variable. -fn cc_kernel_annotation() -> Outcome { - cc_assemble(src: "module p\n\nfn f(x: Int) -> Int {\n x\n}\n") -} - -fn cc_declared_annotation() -> Outcome { - cc_assemble(src: "module p\n\ntype Box {\n tree: Int\n}\n\nfn f(x: Box) -> Box {\n x\n}\n") -} - -// Every node in the tree that carries a declaration-reference path, counted. A kernel symbol resolves to a -// canonical ATOM and carries none; a declaration reference carries its declaring path. -fn cc_declaration_reference_count(o: Outcome) -> Int { - match o { - Rejected { diagnostics: _ } => 0 - 1 - Accepted { value: t, diagnostics: _ } => - fold_node( - n: t.root, - algebra: NodeFold { - init: fn(n0) { - match declaration_reference_path_optional(node: n0) { - Present { value: _ } => 1 - Absent => 0 - } - }, - step: fn(acc, _e, child) { acc + child } - } - ) - } -} - -// (1) BOTH MODULES RESOLVE, so the rows below are about classification rather than about a refusal. -test fn cc_both_modules_resolve_holds() -> Bool { - (match cc_kernel_annotation() { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false }) - && (match cc_declared_annotation() { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false }) -} - -// (2) THE DECLARED TYPE BECOMES A DECLARATION REFERENCE. If the canonical set wrongly claimed `Box`, this -// would classify as a kernel symbol and the count would drop -- so this row goes red on a set that is too -// LARGE. -test fn cc_a_declared_type_is_a_declaration_reference_holds() -> Bool { - cc_declaration_reference_count(o: cc_declared_annotation()) > 0 -} - -// (3) AND THE KERNEL SPELLING IS NOT ONE. `Int` is carried by the language model's binding map, so resolve -// must classify it ResolvedToKernelSymbol and mint a canonical atom with no declaring path. If the -// derivation returned too SMALL a set -- the fail-open direction, since the set's absence makes resolve -// treat a kernel spelling as a corpus declaration -- this row goes red. -// -// The two rows are the two signs of one question and neither is redundant: (2) fails on an over-large set -// and (3) on an under-small one, so a derivation that drifted either way is caught by source. -test fn cc_a_kernel_spelling_is_not_a_declaration_reference_holds() -> Bool { - cc_declaration_reference_count(o: cc_kernel_annotation()) == 0 -} diff --git a/src/v2/test/claim/canonical_demand/canonical_cost_probe_test.dag b/src/v2/test/claim/canonical_demand/canonical_cost_probe_test.dag deleted file mode 100644 index 8bf47ca9744..00000000000 --- a/src/v2/test/claim/canonical_demand/canonical_cost_probe_test.dag +++ /dev/null @@ -1,26 +0,0 @@ -module v2.test.claim.canonical_demand.canonical_cost_probe - -import v2.extdeps.languages.dag { dag_grammar, dag_language_model, dag_lex_rules } -import v2.std.grammar { grammar_carried_symbol_map } -import v2.std.logic { Bool } - -// THE INSTRUMENT FOR THE CANONICAL-SYMBOL COST, NOT A SEMANTIC CLAIM. DESIGN §6 requires a measurement to -// be cited by naming the producer that re-derives it rather than by copying its number into prose, so the -// figure for "what dag_language_model() costs" needs an entry point. These two rows are it: read eval_steps -// from claim_batch's [witness] lines. The verdicts are deliberately trivial, and that is the honest -// description -- neither row discriminates anything, so neither is evidence for any behaviour. The -// behavioural evidence for this slice is canonical_set_equality (the derivation names the same symbols) and -// canonical_consumer (resolve still asks, and the answer still separates in both directions). -// -// TWO ROWS AND NOT TEN. The discovery partition that located the cost ran per-link over the grammar -// encoder, the emitted-node walk and the expression encodings; those rows answered their question once and -// are not kept, because a probe nothing reads is DESIGN §3c's dangling declaration whatever its shape. What -// survives is the pair that bounds the claim: the whole model, and the one link the repair changed, so a -// later reader can attribute a regression to that link instead of re-deriving the partition. -test fn cc_whole_language_model() -> Bool { - match dag_language_model() { _ => true } -} - -test fn cc_carried_symbol_map_only() -> Bool { - match grammar_carried_symbol_map(grammar: dag_grammar()) { _ => true } -} diff --git a/src/v2/test/claim/canonical_demand/canonical_set_equality_test.dag b/src/v2/test/claim/canonical_demand/canonical_set_equality_test.dag deleted file mode 100644 index 6e517124415..00000000000 --- a/src/v2/test/claim/canonical_demand/canonical_set_equality_test.dag +++ /dev/null @@ -1,54 +0,0 @@ -module v2.test.claim.canonical_demand.canonical_set_equality - -import v2.extdeps.languages.dag { dag_grammar } -import v2.std.grammar { - grammar_carried_symbol_map, - grammar_carried_symbol_map_via_serialized_image, -} -import v2.std.collection { List, Map, map_lookup } -import v2.std.logic { Bool } -import v2.std.node { Symbol } -import v2.std.optional { Absent, Present } -import v2.std.integer { Int } - -// EXACT SET EQUALITY AGAINST THE DERIVATION IT REPLACES, OVER THE REAL GRAMMAR, IN BOTH DIRECTIONS. -// v2.std.grammar grammar_carried_symbol_map now folds the grammar's own structure; it used to recover the -// same symbols by encoding the whole grammar into a Node and walking its atoms. That image derivation is -// retained as grammar_carried_symbol_map_via_serialized_image for exactly this comparison and is called -// from no production path. -// -// BOTH DIRECTIONS, BECAUSE EITHER ALONE ADMITS A DEFECT WITH OPPOSITE SIGN. A structural fold that MISSED -// an atom would shrink the canonical set, and the set's only consumer is v2.compiler.resolve's unbound -// fallback -- so a missing symbol makes resolve admit a name it should refuse, which is a fail-open and -// the reason this control is not optional. A fold that ADDED one (root.sync_tokens is the live candidate, -// since grammar_root_to_node does not encode it) would refuse names that are legal today. -// -// THE COMPARISON IS STRUCTURAL MAP EQUALITY, NOT A COUNT. Two maps of equal size can differ in membership, -// and DESIGN section 5 rules that completeness is an identity join rather than a count equality. -fn cse_structural() -> Map { grammar_carried_symbol_map(grammar: dag_grammar()) } -fn cse_image() -> Map { grammar_carried_symbol_map_via_serialized_image(grammar: dag_grammar()) } - -test fn cse_the_two_derivations_are_the_same_map_holds() -> Bool { - cse_structural() == cse_image() -} - -// AND THE MAP IS NOT EMPTY, so the equality above is not two empty maps agreeing -- the vacuity this file -// would otherwise be wide open to. -test fn cse_the_derivation_is_not_empty_holds() -> Bool { - match map_lookup(m: cse_structural(), key: ^dag_surface_fn_decl) { - Present { value: _ } => true - Absent => false - } -} - -// A DISCRIMINATING NEGATIVE FOR THE EQUALITY ITSELF: a symbol the grammar does not carry is absent from -// both. Without it, an equality over two maps that answered Present for everything would pass. -test fn cse_an_uncarried_symbol_is_in_neither_holds() -> Bool { - (match map_lookup(m: cse_structural(), key: ^cse_definitely_not_a_grammar_symbol) { - Absent => true - Present { value: _ } => false - }) && (match map_lookup(m: cse_image(), key: ^cse_definitely_not_a_grammar_symbol) { - Absent => true - Present { value: _ } => false - }) -} From f498eb6339fb14e6f30f11a3fbebb2e26f688297 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 03:19:49 +0000 Subject: [PATCH 49/90] Re-point the parameter reader at main's route; record the match-binder loss at its refusal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integration repair for the main merge. Two rows in v2.test.claim.field_projection.field_projection_stages went red against main's change to how a named fn's parameter grounds, and they needed opposite treatments because only one of them was still asserting something achievable. REPAIRED: THE PARAMETER COMPARISON'S READER, NOT ITS ASSERTION. `fps_body_atom_type_optional` searched the inferred tree for a bare `Atom { identity: ^i }`. main now mints a named fn's parameter as a path-keyed PARAMETER REFERENCE grounded through the resolved declarations, so that search found nothing and `fps_int_parameter_type_optional` answered Absent. A probe separated the halves: the field's own grounding was still present and only the parameter side had gone, so the row failed on its COMPARISON BASIS rather than on its subject. The reader now asks `parameter_reference_path_optional` and matches the declaring path's leaf through a new `fps_path_leaf_is`. What the row establishes is unchanged -- two independent inference routes agree on one type -- which is why the reader moved and the assertion did not. Weakening it to compare against a directly constructed `Int` node would have made the control share its derivation with its subject. RECORDED AT THE REFUSAL: THE MATCH-ARM BINDER AS A RECEIVER. That capability is gunbc#12641's, it is LANE-ONLY and never reached main, and main froze `infer_parameter_scope_search` to LAMBDA parameters. A match-arm binder is neither a lambda parameter nor a path-keyed reference, so it falls between the two routes: the fixture resolves and then stops with `infer_grounding_not_derived` and no more specific cause. `fps_a_match_binder_receiver_does_not_yet_infer` now asserts that refusal, and `fps_a_match_binder_projection_is_in_the_resolved_tree` reads the RESOLVED root rather than the inferred one -- reading the inferred tree would be vacuous, there being no inferred tree to read, and the row exists to keep the refusal row from being satisfied by an absent subject. Neither row claims a wall; an absent field off a match binder is not refused either, which is the ordinary consequence of an underived receiver and not a fail-open. The restoration trigger is stated as a capability, and under DESIGN §4b(4) both rows become the controls that the restoration stays real rather than retiring. A §4b(3) rung-drop row on #12641's subject may also be owed; it is flagged in the header rather than filed, because that capability belongs to that change and not to this lane. THE CAUSE WAS ATTRIBUTED BY PROBE, NOT BY ARGUMENT, which matters because three of this lane's own changes were the obvious suspects. Each was bypassed in turn and the rows failed identically every time: the value-binder gate, the closure resolved-declaration index (restored to `resolved_declarations_of`), and the match-arm walk, whose admitted body is byte-equivalent to main's inlined version with only the gate wrapped around it. The `ArmBinder` route survived the merge intact -- nine of ten occurrences, the single loss being a COMMENT line -- and the search's entry points and `resolve_pattern_binders` are byte-identical to their pre-merge forms. So the lane's code is present and its inputs moved. ALSO IN THIS COMMIT, all consequences of the same merge: `infer_type_decl_formation_facts_from_entries` threads the `resolved` this lane added to the product row beneath it, which main's caller could not have passed. `resolved_declarations_over` passes main's new `resource_declarations`, and the inherited-roster note now covers resources as well as records. `resolve_match_arm_admitted` carries main's `kind: LexicalFrame` and is called by the gate rather than duplicated inside it. `native_test_closure_resolved_roots` is DELETED: main's refactor moved index, active_roots and policy into the resolution context, so `closure_resolved_roots` takes two arguments and the wrapper became a thin alias -- the call is inlined instead. Two helpers the rewrite left with no callers (`fps_match_binder_projection_grounds`, `fps_match_no_projection_source`) are removed as §3c dangling, and a kept discriminator is renamed from its probe name to `fps_a_multi_root_ingest_does_not_itself_break_retrieval`. RECEIPTS: field_projection_stages 24/24, declaration_reference_evidence 11/0, cross_module_reference_resolution 15/0, projection_dispatch.receiver_disposition 2/0 -- 52/52 -- and `00_compile` compiles with 0 blocking errors. The claim files alone did not establish that last one: at the point every claim set was green, 00_compile still carried 5 blocking errors, all of them in the wrapper this commit deletes. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/00_compile.dag | 26 +--- src/v2/compiler/03_resolve.dag | 37 ++--- src/v2/compiler/04_infer.dag | 8 +- .../field_projection_stages_test.dag | 132 +++++++++++------- 4 files changed, 98 insertions(+), 105 deletions(-) diff --git a/src/v2/compiler/00_compile.dag b/src/v2/compiler/00_compile.dag index 022c753aad0..bc499f8b826 100644 --- a/src/v2/compiler/00_compile.dag +++ b/src/v2/compiler/00_compile.dag @@ -3265,30 +3265,6 @@ fn native_import_target_refusal_diagnostics(target: NativeTestFileRefusal, resol } } -// THE CLOSURE'S RESOLVED ROOTS FOR THE NATIVE PER-MODULE DOOR. This door resolves ONE module at a time and -// does not hold its siblings, so rather than thread an accumulator through every caller it asks the SHARED -// resolution context -- the same context each module is already resolved against -- for the closure, exactly -// as the assembly path does through closure_resolved_roots. One reading of the dependency, two call sites. -// -// THE ARGUMENTS ARE INVARIANT ACROSS THE LANE'S MODULES, and that is what keeps this from being quadratic: -// every module in one run passes the same context and the same shared resolution, so the call memo answers -// all but the first from cache and the closure is resolved once per run rather than once per module. That is -// a COST claim about a memo, not a correctness claim -- the reading is correct either way, and if the memo -// does not collapse it the work is N squared and must be replaced by an accumulator threaded through the -// callers. It is therefore measured rather than assumed. -fn native_test_closure_resolved_roots( - context: NativeTestContext, - shared: ResolutionContext -) -> FreeMonoid { - closure_resolved_roots( - context: context.resolution, - shared: shared, - index: context.index, - active_roots: context.active_roots, - policy: v2.std.resolution_policy.default_name_resolution_policy() - ) -} - fn native_module_resolve_verdict( context: NativeTestContext, refusal_index: Map, @@ -3318,7 +3294,7 @@ fn native_module_resolve_verdict( resolved: resolved_tree_of( root: resolved, symbol_index: shared.symbol_index, - closure_roots: native_test_closure_resolved_roots(context: step.context, shared: shared) + closure_roots: closure_resolved_roots(context: step.context, shared: shared) ) } Rejected { diagnostics: r } => diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index d19e692b1cd..6ff32d6d898 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -263,8 +263,8 @@ fn resolved_tree_of( // is total and order-independent, and no index-union authority needs to exist. A root whose module name does // not resolve contributes nothing, which is that function's existing behaviour and not a new drop. // -// RECORD PAYLOAD MARKS ARE NOT CARRIED HERE, AND THAT IS INHERITED RATHER THAN CHOSEN. The fill takes a -// record_declarations carrier captured at normalize, and resolved_declarations_of passed Empty for it before +// RECORD AND RESOURCE PAYLOAD MARKS ARE NOT CARRIED HERE, AND THAT IS INHERITED RATHER THAN CHOSEN. The +// fill takes record_declarations and resource_declarations carriers captured at normalize, and resolved_declarations_of passed Empty for it before // this change; the fold passes Empty for every provider root for the same reason -- a resolved Node does not // carry the normalize-time record roster, and inventing one here would be a second authority for a fact // v2.compiler.normalize owns. The consequence is precise and bounded: v2.std.symbol_index @@ -284,11 +284,12 @@ fn resolved_declarations_over(roots: FreeMonoid, subject: Node) -> SymbolI xs: roots, empty: empty_symbol_index(), cons: fn(acc, r) { - symbol_index_fill_module_declarations(index: acc, root: r, record_declarations: Empty) + symbol_index_fill_module_declarations(index: acc, root: r, record_declarations: Empty, resource_declarations: Empty) } ), root: subject, - record_declarations: Empty + record_declarations: Empty, + resource_declarations: Empty ) } @@ -2883,30 +2884,7 @@ fn resolve_match_arm_walk(ctx: ResolveContext, arm: Node) -> ResolveNodeWalk { Accepted { value: pat, diagnostics: _ } => match gate_value_binders(names: pattern_binder_name_list(ctx: ctx, pat: pat), outer: ctx.scope, at: pat) { BinderGateRefused { diagnostic: d } => resolve_walk_refused_one(chain: diagnostics_singleton(d: d)) - BinderGateAdmitted => - let arm_ctx = resolve_ctx_with_scope( - ctx: ctx, - scope: ScopeFrame { - locals: resolve_pattern_binders(ctx: ctx, pat: pat, acc: empty_map()), - outer: ctx.scope, - kind: LexicalFrame - } - ) - child_walk_node(n: arm, w: fold(arm.children, init: child_walk_init(), f: fn(acc, e) { - child_walk_step( - w: acc, - e: e, - r: match e.label { - Named { name: label } => - if label == match_arm_pattern { - resolve_pattern_node_walk(ctx: arm_ctx, pat: e.target) - } else { - resolve_node_walk(ctx: arm_ctx, n: e.target) - } - Positional => resolve_node_walk(ctx: arm_ctx, n: e.target) - } - ) - })) + BinderGateAdmitted => resolve_match_arm_admitted(ctx: ctx, arm: arm, pat: pat) } } } @@ -2916,7 +2894,8 @@ fn resolve_match_arm_admitted(ctx: ResolveContext, arm: Node, pat: Node) -> Reso ctx: ctx, scope: ScopeFrame { locals: resolve_pattern_binders(ctx: ctx, pat: pat, acc: empty_map()), - outer: ctx.scope + outer: ctx.scope, + kind: LexicalFrame } ) child_walk_node(n: arm, w: fold(arm.children, init: child_walk_init(), f: fn(acc, e) { diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 0ca55877793..76b681397da 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -754,14 +754,20 @@ fn infer_formation_facts_from_entries( ) -> Outcome { match binder_node_parts(n: node) { Present { value: parts } => infer_binder_node_facts_from_entries(node: node, parts: parts, entries: entries) - Absent => infer_type_decl_formation_facts_from_entries(node: node, entries: entries, partials: partials) + Absent => infer_type_decl_formation_facts_from_entries(node: node, entries: entries, partials: partials, resolved: resolved) } } +// THE CARRIER REACHES THE PRODUCT ROW THROUGH THIS DISPATCHER, which is a merge seam worth naming. main +// introduced this function while this lane widened infer_product_facts_from_entries to take the +// ResolvedTree its projection arm needs, so main's caller passed three arguments to a row that now requires +// four. Threading `resolved` here is the resolution: the dispatcher above already holds it, and every arm +// that does not need it ignores it, so no arm gains a dependency it does not use. fn infer_type_decl_formation_facts_from_entries( node: Node, entries: List, partials: List, + resolved: ResolvedTree, ) -> Outcome { match type_decl_view(target: node) { TypeAlias { binders: _, aliased: _ } => infer_declaration_wrapper_facts_from_entries(node: node, entries: entries, partials: partials) diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag index 6ef7efd8d4f..f78e2991faa 100644 --- a/src/v2/test/claim/field_projection/field_projection_stages_test.dag +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -16,7 +16,9 @@ import v2.std.diagnostic { diagnostics_has_reason, Accepted, NodeLocus, Outcome, Rejected } import v2.std.logic { Bool } import v2.std.node { Atom, Conj, Node, Symbol, TypeNode, symbol_eq } -import v2.std.qualified_name { declaration_reference_path_optional, qualified_name_last_segment, qualified_name_spine_shape_present } +import v2.std.qualified_name { + parameter_reference_path_optional, + qualified_name_last_segment, declaration_reference_path_optional, qualified_name_spine_shape_present } import v2.std.node_query { FieldProjection, declared_field_named, field_projection_optional } import v2.std.symbol_index { symbol_index_lookup } import v2.compiler.infer { @@ -254,14 +256,37 @@ fn fps_int_parameter_type_optional() -> Optional { } } +// The leaf of a parameter reference's declaring path, compared by symbol. The path is the declaration's +// own, so the parameter is identified by its last segment rather than by a bare spelling. +fn fps_path_leaf_is(path: QualifiedName, name: Symbol) -> Bool { + match qualified_name_last_segment(qn: path) { + Present { value: leaf } => symbol_eq(a: leaf, b: name) + Absent => false + } +} + +// THE PARAMETER IS READ AS A PARAMETER REFERENCE, NOT AS A BARE ATOM, which is where this helper had to +// move. It used to hunt the inferred tree for `Atom { identity: ^i }` and read that node's facts. A named +// fn's parameter no longer reaches infer that way: v2.compiler.resolve now mints it as a path-keyed +// parameter reference and infer grounds it through the resolved declarations, so the bare-atom search found +// nothing and `fps_int_parameter_type_optional` answered Absent -- which failed +// `fps_the_int_field_grounds_as_an_int_parameter_does` on its COMPARISON BASIS rather than on its subject: +// a probe run at the time established that the field's own grounding was still present and only the +// parameter half had gone Absent. +// +// THE ROW'S SUBJECT IS UNCHANGED BY THIS REPAIR, and that is the point of fixing the reader rather than the +// assertion. What the row establishes is that two independent inference routes agree on one type: a +// projected field grounds to the same Int a parameter does. Re-pointing the reader keeps that comparison; +// weakening it to compare against a directly constructed Int node would have made the row share its +// derivation with its subject, which is a control that cannot discriminate. fn fps_body_atom_type_optional(inferred: InferredTree) -> Optional { fold_node( n: inferred.root, algebra: NodeFold { init: fn(n0) { - match n0.kind { - TypeNode { connective: Atom { identity: id } } => - if symbol_eq(a: id, b: ^i) { + match parameter_reference_path_optional(node: n0) { + Present { value: path } => + if fps_path_leaf_is(path: path, name: ^i) { match inferred.facts.lookup(n0) { Absent => optional_absent() Present { value: facts } => @@ -446,9 +471,6 @@ fn fps_match_binder_source(field: String) -> Outcome { fps_assemble(src: "module p\n\ntype Art {\n tree: Int\n}\n\ntype Holder\n = Wrapped { value: Art }\n\nfn f(h: Holder) -> Int {\n match h {\n Wrapped { value: artifact } => artifact." + field + "\n }\n}\n") } -fn fps_match_no_projection_source() -> Outcome { - fps_assemble(src: "module p\n\ntype Art {\n tree: Int\n}\n\ntype Holder\n = Wrapped { value: Art }\n\nfn f(h: Holder) -> Int {\n match h {\n Wrapped { value: artifact } => 7\n }\n}\n") -} // RESOLVE REACHES THE MATCH BINDER'S PROJECTION: the head is bound on the chain and the whole path names // no declaration, so resolve commits the projection shape exactly as it does for a parameter receiver. @@ -456,58 +478,68 @@ test fn fps_a_match_binder_receiver_resolves_holds() -> Bool { fps_resolves(o: fps_match_binder_source(field: "tree")) } -// THE MATCH-FORM BLOCKER IS RETIRED, AND THE ROW IS REWRITTEN TO THE BOUNDARY THAT IS ACTUALLY LIVE. -// The row above predicted its own transition -- "if a later change makes match arms infer, this claim goes -// red and the projection claim beside it becomes the live question" -- and that change landed (gunbc#12641 -// types a match over a declared generic coproduct and its arm binders). The match form now infers with a -// projection body AND with a literal body, so the refusal this row counted no longer exists and the row -// does not retire: it becomes the control that the retirement stays real (DESIGN section 4b(4)). +// A MATCH-ARM BINDER AS A RECEIVER NO LONGER INFERS, AND THESE TWO ROWS ARE WRITTEN AT THAT REFUSAL RATHER +// THAN AT THE CAPABILITY THEY USED TO ASSERT. This is a declared loss, not a discovery: the capability came +// from gunbc#12641, which types a match over a declared coproduct and its arm binders, and it is LANE-ONLY -- +// it never reached main. Merging main into the lane leaves its code present and its inputs changed, so the +// fixture resolves and then stops with infer_grounding_not_derived and no more specific cause: the +// receiver's type is simply not derived. // -// WHAT IS LIVE INSTEAD IS NARROWER AND IS STATED, NOT CLAIMED AS A WALL. The projection NODE is in the -// resolved tree, the module infers, and the projection's grounding is NOT derived -- for the valid field -// and for an absent one alike. So the receiver's type is underived at this seam and the projection sits on -// the counted frontier rather than being typed. +// THE CAUSE IS MAIN'S NARROWING OF PARAMETER GROUNDING, NOT A MERGE DEFECT, and that was established by +// probe rather than by argument. Three candidate causes in this lane were each bypassed and the rows failed +// identically every time: the value-binder gate, the closure resolved-declaration index (restored to +// resolved_declarations_of), and the match-arm walk, whose admitted body is byte-equivalent to main's inlined +// version with only the gate wrapped around it. The ArmBinder route itself survived the merge intact -- nine +// of ten occurrences, the single loss being a COMMENT line -- and infer_parameter_scope_search's entry points +// and resolve_pattern_binders are byte-identical to their pre-merge forms. main froze that search to LAMBDA +// parameters, because a named fn's parameter now arrives as a path-keyed parameter reference; a match-arm +// binder is neither, so it falls between the two routes. // -// THE CONSEQUENCE IS NAMED HERE SO IT IS NOT MISREAD AS A WALL: because the projection is on the frontier, -// an ABSENT field off a match binder is NOT refused either. That is the frontier's ordinary behaviour, not -// a fail-open, and this row asserts it in the direction that is true rather than asserting a refusal that -// does not happen. It is exactly the vacuity the earlier version of this section confessed to, now written -// so that it cannot recur: the two grounding conjuncts below would both have to change for this row to go -// green under a broken implementation. +// WHAT IS ASSERTED IS WHAT HOLDS TODAY, IN THE DIRECTION THAT IS TRUE. Resolve still commits the projection +// shape off a match binder -- fps_a_match_binder_receiver_resolves above -- and the projection NODE is in the +// resolved tree, which is why the second row reads the RESOLVED root rather than the inferred one: reading +// the inferred tree would make it vacuous, since there is no inferred tree to read. Neither row claims a +// wall. An absent field off a match binder is not refused either, which is the ordinary consequence of a +// receiver whose type is underived and not a fail-open. // -// WHY THIS IS INFORMATIVE AND NOT "EVERYTHING IS UNDERIVED": the PARAMETER receiver in this same file -// grounds (fps_a_valid_field_projection_grounds_holds) and its absent field IS refused -// (fps_an_absent_field_does_not_infer_holds). Those two are the discriminating positive controls, so the -// frontier asserted here is specific to the match-arm binder as a receiver. +// WHY THIS IS NOT "EVERYTHING IS UNDERIVED": the PARAMETER receiver in this same file grounds +// (fps_a_valid_field_projection_grounds) and its absent field IS refused (fps_an_absent_field_does_not_infer). +// Those are the discriminating positive controls, so what these rows record is specific to the match-arm +// binder as a receiver. // -// THE TRIGGER is a derived type for a match-arm binder AT THE PROJECTION'S RECEIVER SEAM -- the binder is -// typed by gunbc#12641 within the match, and what this row measures is that the projection's own receiver -// read does not reach that fact. When it does, the two grounding conjuncts flip and the absent-field -// conjunct becomes a refusal claim. -test fn fps_the_match_form_infers_and_the_projection_stays_on_the_frontier_holds() -> Bool { - fps_infers(o: fps_match_binder_source(field: "tree")) - && fps_infers(o: fps_match_no_projection_source()) - && fps_match_binder_projection_grounds(field: "tree") == false - && fps_match_binder_projection_grounds(field: "span_index") == false -} - -// THE PROJECTION NODE IS THERE, so the two grounding conjuncts above are about a node that exists rather -// than vacuously true of an absent one -- the same guard fps_the_resolved_tree_carries_a_field_projection -// provides for the parameter receiver. -test fn fps_a_match_binder_projection_is_in_the_tree_holds() -> Bool { - match fps_projection_facts(o: fps_match_binder_source(field: "tree")) { - Present { value: _ } => true - Absent => false +// RESTORATION TRIGGER, STATED AS THE CAPABILITY: a match-arm binder reference grounds through the same route +// that now grounds a named fn's parameter -- resolve mints it as a path-keyed reference and infer grounds it +// from the resolved declarations -- or infer_parameter_scope_search's arm route is re-derived against that +// change. Either way these two rows flip, and under DESIGN section 4b(4) they do not retire: they become the +// controls that the restoration stays real. A rung-drop row on #12641's subject may also be owed; it is +// flagged rather than filed here, because the capability belongs to that change and not to this lane. +test fn fps_a_match_binder_receiver_does_not_yet_infer_holds() -> Bool { + match fps_match_binder_source(field: "tree") { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => + diagnostics_has_reason(d: Some { diagnostics: r }, reason: ^infer_grounding_not_derived) + } } } -fn fps_match_binder_projection_grounds(field: String) -> Bool { - match fps_projection_facts(o: fps_match_binder_source(field: field)) { - Absent => false - Present { value: facts } => inferred_facts_grounding_derived(facts: facts) +// THE PROJECTION NODE IS THERE, IN THE RESOLVED TREE, so the row above is about a receiver whose projection +// exists rather than about a fixture that never built one. It reads the resolved root for the reason stated +// above, and it is the guard that keeps the refusal row from being satisfied by an absent subject. +test fn fps_a_match_binder_projection_is_in_the_resolved_tree_holds() -> Bool { + match fps_match_binder_source(field: "tree") { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match fps_projection_node_optional(root: resolved.root) { + Present { value: _ } => true + Absent => false + } } } + // A MODULE-LEVEL `data` VALUE IS STILL PROJECTABLE, AND THIS ROW EXISTS SO THE 3d REPAIR CANNOT QUIETLY // BECOME "ONLY LEXICAL VALUES MAY HAVE FIELDS". resolve_projection_base admits a BoundAtRoot head only // where there are no field segments, because a dotted path through a module-root DECLARATION is a @@ -733,6 +765,6 @@ fn fps_same_module_projection_in_two_root_ingest() -> Outcome { ) } -test fn fps_dbg_same_module_in_two_root_ingest() -> Bool { +test fn fps_a_multi_root_ingest_does_not_itself_break_retrieval_holds() -> Bool { fps_infers(o: fps_same_module_projection_in_two_root_ingest()) } From e60150d56a9d1791fa6a5a132f2dff49f7df64a9 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 04:05:33 +0000 Subject: [PATCH 50/90] The consolidated callable arrow closes its own lowered image main changed all three inlined arrow builders to wrap in `close_lowered_image` while this lane had replaced those three copies with one `body_lower_callable_arrow`. The helper now applies it, so every caller gets main's behaviour from one place rather than from three, and main's `declared_signature(params: [], source: shell)` correction -- a real change carried inside the same restructuring hunk -- is taken rather than lost to the structural difference. That is the second time in this merge that main's side held a small correction inside a large restructuring, so taking "our" side on the structure would have silently reverted it. One call site kept main's second closing paren, which `close_lowered_image(node_lowered_from(` needs and the helper does not. It surfaced from the closure loader as `reference_closure: has no readable reference set (cause=parse-failed)` rather than as a located syntax error, so the site had to be bisected -- worth knowing that a parse break in a FOLLOWED module reports as an unreadable reference set and not as a syntax error with a line. RECEIPTS at this head: `00_compile` 0 blocking errors / 221 files emitted; field_projection_stages 24/0, declaration_reference_evidence 11/0, cross_module_reference_resolution 15/0, projection_dispatch.receiver_disposition 2/0, fold_lowering 20/0 -- 72/72. fold_lowering is included because this merge touched body lowering, and the claim files that were green before it would not have caught a regression there. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/body_lowering_fold.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index d51232ebdf7..d575861354d 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -4102,7 +4102,7 @@ fn body_lower_fn_decl_arrow(shell: Node, sig: BodyLowerFnSignature, body: Option Present { value: b } => optional_present(value: body_lower_position_function_values(node: b, path: body_lower_unpositioned_path())) } - )), + ), fn_name: sig.fn_name, source: shell ) From f92c9617d8dae77178a9b2b208f14ee39c9b9661 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 04:26:46 +0000 Subject: [PATCH 51/90] Compile every consumer of the widened constructor, not the five files the matrix chose The required floor refused with four causes, all in modules this lane's claim matrix never compiles. One is this lane's own omission and three are the merge's consequences in test modules main moved under. MINE: v2.compiler.self_host.closure_emission is a THIRD caller of resolved_tree_outcome. Widening that constructor, I fixed the two call sites I knew about and never enumerated the rest. It resolves one member module and holds no closure, so an empty closure_roots is both correct and behaviour-preserving -- which is exactly what the helper's header says an empty list means, so the fix was never hard; finding the site was the whole problem. MAIN'S, IN LANE TEST MODULES: a ScopeFrame literal gains main's new kind field (LexicalFrame); a ResolveContext literal gains main's new declaring field, set to NotDeclaring, because a supplied context for a binder-admission slice declares nothing and a path there would be a fabricated fact; and declaration_reference_spine_optional is now the kind-parameterised resolved_reference_spine_optional, asked with DeclarationReferenceKind. THE PROCESS FAILURE IS THE POINT. A standing note says to compile every consumer closure after widening a signature, and the floor's population is the corpus while mine was five files I picked. 72/72 green claim rows plus a clean 00_compile is not evidence about closure_emission or these two test modules, because none of them is in those closures. The number was real and the population was not. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/self_host/closure_emission.dag | 2 +- .../claim/binder_admission/callable_binder_slice_test.dag | 7 +++++-- .../claim/callexec/declaration_reference_eval_test.dag | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/v2/compiler/self_host/closure_emission.dag b/src/v2/compiler/self_host/closure_emission.dag index 4ce6083dcb9..92258c739c9 100644 --- a/src/v2/compiler/self_host/closure_emission.dag +++ b/src/v2/compiler/self_host/closure_emission.dag @@ -348,7 +348,7 @@ fn closure_resolve_member( policy: default_name_resolution_policy() )), admission: Admission { subject: ResolutionSubject { name: member_module }, imports: Empty } - ).walk, symbol_index: symbol_index) + ).walk, symbol_index: symbol_index, closure_roots: []) } ) } diff --git a/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag index 2ef201f6171..fb6662d40be 100644 --- a/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag +++ b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag @@ -1,6 +1,8 @@ module v2.test.claim.binder_admission.callable_binder_slice import v2.compiler.resolve { + LexicalFrame, + NotDeclaring, BinderAdmission, BinderDuplicateInFrame, BinderHidesVisibleValue, @@ -165,7 +167,7 @@ fn cbs_root_scope_declaring(name: Symbol) -> Scope { } fn cbs_frame_binding(name: Symbol) -> Scope { - ScopeFrame { locals: map_insert(empty_map(), name, name), outer: cbs_root_scope() } + ScopeFrame { locals: map_insert(empty_map(), name, name), outer: cbs_root_scope(), kind: LexicalFrame } } fn cbs_admitted_has(a: BinderAdmission, name: Symbol) -> Bool { @@ -245,7 +247,8 @@ fn cbs_ctx(scope: Scope) -> ResolveContext { policy: default_name_resolution_policy(), position: Empty, lm: void_language_model(identity: ^cbs_void_language), - under_module_root: false + under_module_root: false, + declaring: NotDeclaring } } diff --git a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag index 77661afd5e9..0f01c5de2b2 100644 --- a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag +++ b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag @@ -26,6 +26,7 @@ import v2.std.diagnostic { } import v2.std.logic { Bool } import v2.std.node { + DeclarationReferenceKind, Symbol, symbol_eq, Conj, @@ -46,7 +47,7 @@ import v2.std.runtime { RuntimePrimitive, RuntimeValue } import v2.std.algebra { fold_list } import v2.std.qualified_name { qualified_name_last_segment, declaration_reference_path_optional, - declaration_reference_spine_optional + resolved_reference_spine_optional } // THE EVALUATOR BOUNDARY FOR A NAMED CALL, ISOLATED FROM THE NATIVE ROUTE. The reference-evidence @@ -172,7 +173,7 @@ fn cref_callee_reference_optional(n: Node) -> Optional { // marker itself is excluded, because the marker IS the node that stands where the reference stood and // a refusal anchored there would be a different finding from a refusal anchored on path data. fn cref_spine_nodes(reference: Node) -> List { - match declaration_reference_spine_optional(node: reference) { + match resolved_reference_spine_optional(node: reference, kind: DeclarationReferenceKind) { Absent => Empty Present { value: spine } => fold_node( From 7f3a0e6775d9937f00296d3219514fa89d8e6f11 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 04:57:24 +0000 Subject: [PATCH 52/90] The native door passes the resolution, not the whole NativeTestContext The emitted crate failed to typecheck: closure_resolved_roots wants the Outcome that a NativeTestContext HOLDS, and this call site handed it the context itself. expected Rc>>, found Rc The field is step.context.resolution, which is what the sibling reader native_test_resolve_module_walk already passes. WHY THE FRONT END DID NOT CATCH IT, which is the part worth keeping. `gunbc compile --dry-run` reports 0 blocking errors on this same source. The two types only become distinct after lowering to Rust, where each becomes its own Rc<...>, so for this lane a clean .dag front end is NOT evidence that the emitted Rust compiles. The required emit-build lane is what establishes that, and the gap between them is exactly the one that let a wrong readiness claim stand: claim rows and 00_compile were green while the emitted crate did not build. RECEIPT: emitted to a kept directory and built directly -- 225 files emitted, then `cargo build --release` with RUSTFLAGS="-D warnings" finishes clean. Reproducing it that way rather than through the instrument is what made the error readable: the instrument reports EmittedCompilerBuildFailed with diagnostic=unattributed and a truncated stderr_tail, and its probe root is removed before it can be inspected. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/00_compile.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/compiler/00_compile.dag b/src/v2/compiler/00_compile.dag index b93d15365ae..285391d79f4 100644 --- a/src/v2/compiler/00_compile.dag +++ b/src/v2/compiler/00_compile.dag @@ -3300,7 +3300,7 @@ fn native_module_resolve_verdict( resolved: resolved_tree_of( root: resolved, symbol_index: shared.symbol_index, - closure_roots: closure_resolved_roots(context: step.context, shared: shared) + closure_roots: closure_resolved_roots(context: step.context.resolution, shared: shared) ) } Rejected { diagnostics: r } => From 8c23004680764677ea0511215bd643f2c6faf392 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 05:28:15 +0000 Subject: [PATCH 53/90] Update the two citations of the renamed fold-carrier row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The floor's declarations phase refused with CITED-DECLARATION-ABSENT: gunbc.generic_binder_field_projection_deficit cites v2.test.claim.fold_lowering lowered_loop_binds_carrier_binder, which that module no longer declares. Earlier in this lane that row was renamed to lowered_loop_carrier_is_a_generated_slot_not_the_authored_binder, when the fold carrier became a generated slot rather than the authored step formal, and the citing sites were not updated with it. THERE WERE TWO, AND THE FLOOR NAMED ONE. The second is a membership string inside v2.workflow.floor_cost_debt's roster, which the declarations check does not reach, so a grep for the old name is what found it. That is the §3 argument for citing a symbol rather than a position doing its job in reverse: the rename was decidable from the name, and both sites were mechanically findable once one of them refused. Co-Authored-By: Claude Opus 5 (1M context) --- dag/gunbc/generic_binder_field_projection_deficit.dag | 2 +- src/v2/workflow/floor_cost_debt.dag | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dag/gunbc/generic_binder_field_projection_deficit.dag b/dag/gunbc/generic_binder_field_projection_deficit.dag index fbc223d68d4..7f97999e7e2 100644 --- a/dag/gunbc/generic_binder_field_projection_deficit.dag +++ b/dag/gunbc/generic_binder_field_projection_deficit.dag @@ -77,7 +77,7 @@ data generic_binder_field_projection_sites: List = GenericBinderProjectionSite { site: decl_ref( module_path: "v2.test.claim.fold_lowering", - decl_name: "lowered_loop_binds_carrier_binder" + decl_name: "lowered_loop_carrier_is_a_generated_slot_not_the_authored_binder" ), index_coverage: OutsideDeclIndexTestWitnessModule, scrutinee_type: "Optional", diff --git a/src/v2/workflow/floor_cost_debt.dag b/src/v2/workflow/floor_cost_debt.dag index 317ede86d6a..7d9e4fa8985 100644 --- a/src/v2/workflow/floor_cost_debt.dag +++ b/src/v2/workflow/floor_cost_debt.dag @@ -721,7 +721,7 @@ fn floor_cost_debt_proven_chunk_06() -> List { } fn floor_cost_debt_proven_chunk_07() -> List { - Cons { head: "v2.test.claim.dag_acceptance.acceptance_translate_row_is_determinate", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_reject_is_typed_not_silent", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_smoke_recomputed_by_execution_holds", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_smoke_totality_holds", tail: Cons { head: "v2.test.claim.enforcement.grammar_coverage_witness.grammar_coverage_reject_is_typed_not_silent", tail: Cons { head: "v2.test.claim.enforcement.grammar_coverage_witness.grammar_coverage_smoke_recomputed_by_execution_holds", tail: Cons { head: "v2.test.claim.enforcement.grammar_coverage_witness.grammar_coverage_smoke_totality_holds", tail: Cons { head: "v2.test.claim.fold_lowering.algebra_carrying_fold_is_step_form_unresolved", tail: Cons { head: "v2.test.claim.fold_lowering.bodyless_fold_call_refuses_no_loop_fabricated", tail: Cons { head: "v2.test.claim.fold_lowering.bodyless_fold_is_step_form_unresolved", tail: Cons { head: "v2.test.claim.fold_lowering.duplicate_carrier_edge_is_malformed", tail: Cons { head: "v2.test.claim.fold_lowering.fold_call_lowers_to_terminating_loop", tail: Cons { head: "v2.test.claim.fold_lowering.fold_callee_name_survives_parse", tail: Cons { head: "v2.test.claim.fold_lowering.fold_collection_name_survives_parse", tail: Cons { head: "v2.test.claim.fold_lowering.fold_probe_ingest_is_accepted", tail: Cons { head: "v2.test.claim.fold_lowering.lowered_loop_binds_carrier_binder", tail: Cons { head: "v2.test.claim.fold_lowering.named_step_fold_is_step_form_unresolved", tail: Cons { head: "v2.test.claim.fold_lowering.non_fold_call_is_not_a_fold_disposition", tail: Cons { head: "v2.test.claim.fold_lowering.non_fold_call_refuses", tail: Cons { head: "v2.test.claim.fold_lowering.paramless_step_fold_is_step_shape_invalid", tail: Empty {} } } } } } } } } } } } } } } } } } } } } + Cons { head: "v2.test.claim.dag_acceptance.acceptance_translate_row_is_determinate", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_reject_is_typed_not_silent", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_smoke_recomputed_by_execution_holds", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_smoke_totality_holds", tail: Cons { head: "v2.test.claim.enforcement.grammar_coverage_witness.grammar_coverage_reject_is_typed_not_silent", tail: Cons { head: "v2.test.claim.enforcement.grammar_coverage_witness.grammar_coverage_smoke_recomputed_by_execution_holds", tail: Cons { head: "v2.test.claim.enforcement.grammar_coverage_witness.grammar_coverage_smoke_totality_holds", tail: Cons { head: "v2.test.claim.fold_lowering.algebra_carrying_fold_is_step_form_unresolved", tail: Cons { head: "v2.test.claim.fold_lowering.bodyless_fold_call_refuses_no_loop_fabricated", tail: Cons { head: "v2.test.claim.fold_lowering.bodyless_fold_is_step_form_unresolved", tail: Cons { head: "v2.test.claim.fold_lowering.duplicate_carrier_edge_is_malformed", tail: Cons { head: "v2.test.claim.fold_lowering.fold_call_lowers_to_terminating_loop", tail: Cons { head: "v2.test.claim.fold_lowering.fold_callee_name_survives_parse", tail: Cons { head: "v2.test.claim.fold_lowering.fold_collection_name_survives_parse", tail: Cons { head: "v2.test.claim.fold_lowering.fold_probe_ingest_is_accepted", tail: Cons { head: "v2.test.claim.fold_lowering.lowered_loop_carrier_is_a_generated_slot_not_the_authored_binder", tail: Cons { head: "v2.test.claim.fold_lowering.named_step_fold_is_step_form_unresolved", tail: Cons { head: "v2.test.claim.fold_lowering.non_fold_call_is_not_a_fold_disposition", tail: Cons { head: "v2.test.claim.fold_lowering.non_fold_call_refuses", tail: Cons { head: "v2.test.claim.fold_lowering.paramless_step_fold_is_step_shape_invalid", tail: Empty {} } } } } } } } } } } } } } } } } } } } } } fn floor_cost_debt_proven_chunk_08() -> List { From edd978839962cd5258a989f83d13b224e4960378 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 16:33:33 +0000 Subject: [PATCH 54/90] Facts-key collision controls assert the separation #12582 achieved, not the collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three rows in v2.test.claim.callexec.synthetic_facts_key_collision asserted `total > 1` -- that more than one inferred-facts entry stood under a single key, because structurally identical synthetic nodes keyed one entry and eval could refuse at a node that was not its subject. gunbc#12582's facts-key work eliminated that, so all three failed. MEASURED BEFORE FLIPPING: exactly ONE entry stands under the key, for both fixtures. That matters for how the rows are written -- `== 1` rather than `<= 1`, because a fixture that stopped producing the node at all would answer 0, and a `<= 1` row would sit green while testing nothing. The count is enumerated rather than inferred from a lookup, which the original rows did for the same reason: a lookup answering something proves only that SOME entry matched, while counting proves how many stand there. DESIGN §4b(4) IS WHY THEY FLIPPED RATHER THAN RETIRED. A climb deletes the lower-rung production handling and keeps the evidence, so each row keeps its fixture and its counting method and now asserts the separation: exactly one entry under the key, that entry grounded, and the same separation reached by a second fixture -- which is what the original third row established in the opposite direction, that the property belongs to the keying relation over ordinary programs rather than to one source text. They go red the moment two entries share a key again. 4/4. Co-Authored-By: Claude Opus 5 (1M context) --- .../synthetic_facts_key_collision_test.dag | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag index 77a6427932a..a5237d0a561 100644 --- a/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag +++ b/src/v2/test/claim/callexec/synthetic_facts_key_collision_test.dag @@ -136,42 +136,43 @@ test fn sfk_infer_produces_entries_holds() -> Bool { } } -// MORE THAN ONE ENTRY SHARES THE KEY. Enumerated, not inferred from a lookup answer: a lookup returning -// something proves only that SOME entry matched, while counting the entries proves the key is shared. -test fn sfk_more_than_one_entry_shares_one_key_holds() -> Bool { - sfk_count_under_key(o: cref_source()) > 1 -} - -// AND THEY NOW AGREE, WHICH NARROWS THE FINDING WITHOUT DISSOLVING IT. This row asserted a DISAGREEMENT -// -- at least one derived entry and at least one underived one under the same key -- and that was the -// standing when it was written. It is no longer: every entry under the shared key reports grounding -// derived. The underived entries were the named-call sites this lane has since repaired (a reference read -// from the AUTHORED symbol_index whose return spelling would not denote, and a callee arrow read -// facts-blind so no argument was judged), so the conflict this row observed was DOWNSTREAM of two defects -// rather than intrinsic to the keying relation. +// THE KEY IS NO LONGER SHARED, AND THESE THREE ROWS ARE THE FLIPPED EVIDENCE OF THAT CLIMB. This file's +// whole subject was a facts-key COLLISION: structurally identical synthetic nodes keyed one facts entry, so +// more than one entry stood under a single key and eval could refuse at a node that was not its subject. +// Every row here asserted `total > 1` -- that the collision existed -- and all three now fail because +// gunbc#12582's facts-key work eliminated it. MEASURED: exactly ONE entry stands under the key, for both +// fixtures. // -// WHAT IS STILL TRUE IS THE ROW ABOVE: more than one entry carries one key. The relation still admits a -// conflict, because facts_map_from_entries checks each entry's own subject correspondence and establishes -// NO uniqueness or conflict condition across entries that compare equal -- so today's agreement is a -// property of this corpus, not a guarantee about the relation. +// DESIGN section 4b(4) IS WHY THEY ARE FLIPPED RATHER THAN DELETED. A climb deletes the lower-rung +// production handling and KEEPS the evidence: the discriminating red becomes the permanent control that the +// higher rung stays real. So each row keeps its fixture and its counting method and asserts the separation +// instead of the collision. They go red the moment two entries share a key again, which is the transition +// the identity owner needs to hear about. // -// SO THE CLAIM IS WRITTEN AT THE STANDING RATHER THAN AT THE SAFETY CONCLUSION (DESIGN section 4d: do not -// assert as deduced what is only inferred). It does NOT say a consumer is currently misled -- the -// observed misleading consumer was eval's refusal at a node that was not its subject, and that refusal is -// gone. It says the entries under one key agree today, which is falsifiable and goes red the moment a -// conflict reappears. That transition is the thing the identity owner needs to be told about, and a row -// asserting a disagreement that no longer happens would never tell anyone anything again. -test fn sfk_entries_under_one_key_currently_agree_on_grounding_holds() -> Bool { +// THE COUNT IS ENUMERATED, NOT INFERRED FROM A LOOKUP, and that was true of the original rows for the same +// reason it is true of these: a lookup answering something proves only that SOME entry matched, while +// counting proves how many stand under the key. Asserting `== 1` rather than `<= 1` is deliberate -- it +// keeps the row non-vacuous, because a fixture that stopped producing the node at all would answer 0 and a +// `<= 1` row would stay green while testing nothing. +test fn sfk_exactly_one_entry_stands_under_the_key_holds() -> Bool { + sfk_count_under_key(o: cref_source()) == 1 +} + +// AND THAT ENTRY IS GROUNDED, which is the half of the original pair that still says something about +// quality rather than about cardinality: separation would be worthless if the surviving entry were +// underived. +test fn sfk_the_single_entry_under_the_key_is_derived_holds() -> Bool { let total = sfk_count_under_key(o: cref_source()) let derived = sfk_count_derived_under_key(o: cref_source()) - (total > 1) && (derived == total) + (total == 1) && (derived == total) } -// THE SHARED KEY IS NOT AN ARTIFACT OF ONE SOURCE TEXT, which is the part of the original pair that -// survives: a second fixture reaches the same collision, so it is a property of the keying relation over -// ordinary programs. The agreement is asserted here too, for the same reason as above. -test fn sfk_the_shared_key_is_not_specific_to_one_fixture_holds() -> Bool { +// THE SEPARATION IS NOT AN ARTIFACT OF ONE SOURCE TEXT, which is what the original third row established in +// the opposite direction: a second fixture reached the same collision, so it was a property of the keying +// relation over ordinary programs. It now reaches the same separation, so the property still holds of the +// relation and not of one text. +test fn sfk_the_separation_is_not_specific_to_one_fixture_holds() -> Bool { let total = sfk_count_under_key(o: cref_bool_pair_source()) let derived = sfk_count_derived_under_key(o: cref_bool_pair_source()) - (total > 1) && (derived == total) + (total == 1) && (derived == total) } From 72e10bb8206d8b4962fbb78f2dcd6553332398ba Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 17:10:30 +0000 Subject: [PATCH 55/90] WIP, NOT YET EFFECTIVE: one full-path key for named-parameter bind and lookup INCOMPLETE ON PURPOSE, and the title says so because the next reader must not take this for a working repair. It introduces the keying gunbc#12766 declared as a frontier owned by this lane's named-call route, and that keying cannot take effect until a separate gate is opened. WHAT IS HERE. `eval_parameter_binding_key` derives an EnvironmentBindingKey from a full parameter path and is the ONLY place either side derives one, so binding and lookup cannot disagree -- which is the whole point, since binding by bare label while the body looks the parameter up by its declaring path is the two-authorities defect the frontier existed to prevent. `eval_formal_binding_key` decides which keying a formal gets: a callee that is a resolved declaration reference supplies a path and its formals key by path; a LAMBDA supplies none and keeps the lexical bare-label route #12766 deliberately left it on. A named declaration gets NO bare-label fallback, because binding both spellings would make the route look repaired while preserving both authorities. The callee's path is read off the reference the call already carries, so eval stays a consumer of resolution's answer and never becomes a second naming authority. The full path is keyed, never the leaf, so `p.f.x` and `p.g.x` cannot alias. WHY IT IS NOT EFFECTIVE, measured. The three argument-dependent cref controls still refuse, and NOT because of this keying: `v2.std.node` `arrow_body_admits_eval_entry` returns false for `ParameterReferenceBody` (v2.std.node:1101), and `eval_callee_body_refusal_reason` maps that same arm to ^eval_rejected_parameter_reference_unbound (05_eval:1791). So the reason has TWO PRODUCERS and the one firing is the pre-binding entry gate, located at callee_target, before eval_bind_arrow_params runs at all. A parameter-bodied callee never reaches binding; a constant-bodied one does, which is why four probes showed the named branch taken, non-empty ApplicationBound slots, and successful binds while the failing fixture was unaffected by any of them. THE REMAINING STEP, so it is not re-derived: admit ParameterReferenceBody through the canonical arrow_body_admits_eval_entry policy rather than with an eval-local exception, which would be a second authority for which body forms may enter eval. Then this keying is on the path and the lookup here answers. A genuinely unbound or wrong-path reference must still refuse with the same reason -- what disappears is the categorical refusal based only on the body's FORM. AND A WARNING FOR WHOEVER CONTINUES: do not infer a key mismatch from that reason. I did, and spent four probes on it. The first action is one claim comparing the fatal locus against the exact callee_target and body nodes; the source predicts callee_target. Nothing regresses: v2.compiler.eval resolves clean and the cref matrix is unchanged at 7 PASS / 3 FAIL, the same three rows and the same reason as before this commit. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/05_eval.dag | 77 +++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/src/v2/compiler/05_eval.dag b/src/v2/compiler/05_eval.dag index 69535355ac1..e2aa47145f2 100644 --- a/src/v2/compiler/05_eval.dag +++ b/src/v2/compiler/05_eval.dag @@ -34,7 +34,9 @@ import v2.std.collection { list_at_optional, list_nth } -import v2.std.qualified_name { declaration_reference_path_optional, parameter_reference_path_optional } +import v2.std.qualified_name { + qualified_name_snoc, + qualified_name_to_dotted_string, declaration_reference_path_optional, parameter_reference_path_optional } import v2.std.optional { Absent, Optional, @@ -94,6 +96,7 @@ import v2.std.runtime { runtime_value_resolved_type } import v2.std.node { + symbol_intern_lexeme, positional_edges, ArrowBodyForm, Behavior, @@ -1332,10 +1335,21 @@ fn eval_type_node_atom( ) Absent => match parameter_reference_path_optional(node: node) { - Present { value: _ } => - outcome_rejected( - d: eval_diagnostic(reason: ^eval_rejected_parameter_reference_unbound, node: node) - ) + Present { value: path } => + // THE REFERENCE CARRIES ITS OWN KEY, so the body looks the actual up under the path the call bound it + // at -- the same key eval_parameter_binding_key produced, which is why that constructor is the only + // place either side derives one. The refusal that stood here answered for EVERY parameter reference, + // which was honest while no route evaluated a declared body over resolver output and became a wall + // against the capability once one did. It does not disappear: a reference whose path the environment + // does not hold still refuses with the same reason, so a wrong path or an unbound parameter is still + // a located refusal and only a BOUND one now executes. + match environment.bindings.lookup(eval_parameter_binding_key(parameter_path: path)) { + Present { value: v } => Accepted { value: v, diagnostics: None } + Absent => + outcome_rejected( + d: eval_diagnostic(reason: ^eval_rejected_parameter_reference_unbound, node: node) + ) + } Absent => match node.kind { TypeNode { connective: Atom { identity: _ } } => @@ -1369,12 +1383,48 @@ fn eval_type_node_atom( // for each declared parameter, which of them fills it -- the same plan v2.compiler.infer checked, so // the two stages cannot bind one call differently. A domain with no named binder keeps its positional // edges, bound in their own order. +// THE PARAMETER BINDING KEY, AND IT IS ONE CONSTRUCTOR BECAUSE BINDING AND LOOKUP MUST NOT DISAGREE. +// gunbc#12766 made a named fn's parameter use a PATH-KEYED parameter reference and declared eval's keying a +// frontier whose trigger is this lane's named-call route. Binding by the bare label while the body looks the +// parameter up by its declaring path is exactly the two-authorities defect that frontier existed to prevent, +// so the key is derived here and nowhere else: the callee's declaring path, then the formal's label. +// +// THE FULL PATH, NEVER THE LEAF. `p.f.x` and `p.g.x` are different parameters of different declarations, and +// keying on `x` would alias them -- one function's actual answering another function's body. That is the same +// reasoning v2.compiler.infer records for symbol_index_lookup, which asks for the whole declaring path +// precisely because a leaf answer fabricated a type from an unrelated declaration. +// +// THE KEY IS INTERNED, because EnvironmentBindingKey carries a Symbol (v2.std.runtime) rather than a path, +// and the dotted rendering is v2.std.qualified_name's own. A separator that could occur inside a segment +// would make two distinct paths collide, and `.` cannot: it is the segment separator of the very grammar +// these names come from. +fn eval_parameter_binding_key(parameter_path: QualifiedName) -> EnvironmentBindingKey { + environment_binding_key_for_symbol( + sym: symbol_intern_lexeme(lexeme: qualified_name_to_dotted_string(qn: parameter_path)) + ) +} + +// WHICH KEYING A FORMAL GETS, decided once. A call whose callee is a resolved declaration reference supplies +// a declaring path, so its formals are keyed by path. A LAMBDA supplies none: gunbc#12766 deliberately left +// lambda parameters on their lexical bare-label route, because an anonymous Arrow has no index path to key +// by, so Absent keeps exactly the behaviour that route already had. This is not a fallback for the +// path-keyed case -- a named declaration NEVER keys by bare label, because binding both spellings would make +// the route look repaired while preserving the two authorities the frontier forbids. +fn eval_formal_binding_key(callee_path: Optional, label: Symbol) -> EnvironmentBindingKey { + match callee_path { + Present { value: path } => + eval_parameter_binding_key(parameter_path: qualified_name_snoc(qn: path, segment: label)) + Absent => environment_binding_key_for_symbol(sym: label) + } +} + fn eval_bind_arrow_params( arrow: Node, args: List, environment: EvaluationEnvironment, bind: BindInterpreter, - node: Node + node: Node, + callee_path: Optional ) -> Outcome { bind_outcome( o: eval_nth_child(node: arrow, wanted: 0, absent_reason: ^eval_rejected_callee_absent), @@ -1394,7 +1444,13 @@ fn eval_bind_arrow_params( absent: eval_diagnostic(reason: ^eval_rejected_argument_bind_absent, node: node) ) ), - f: fn(value) { bind.bind_value(environment_binding_key_for_symbol(sym: slot.label), value, env) } + f: fn(value) { + bind.bind_value( + eval_formal_binding_key(callee_path: callee_path, label: slot.label), + value, + env + ) + } ) }) }) @@ -1787,7 +1843,12 @@ fn eval_transform_node( args: args, environment: environment, bind: interpretation.bind, - node: node + node: node, + // THE CALLEE'S OWN DECLARING PATH, read off the reference the call carries rather than + // re-resolved: eval is a consumer of resolution's answer here, never a second naming + // authority. A callee that is not a declaration reference (a lambda) yields Absent and + // its formals stay on the lexical route. + callee_path: declaration_reference_path_optional(node: callee_edge.target) ), f: fn(call_environment) { match body_form { From 2eb06760aa5c39f0e2115b50bec367e0a1c8ec13 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 17:55:28 +0000 Subject: [PATCH 56/90] Close #12766's eval frontier: a parameter-reference body is admitted and resolves by path The three argument-dependent controls in v2.test.claim.callexec.declaration_reference_eval now pass. gunbc#12766 made a named fn's parameter use a path-keyed reference and declared eval's keying a frontier whose trigger is this lane's named-call route; this closes it. 12/12. TWO DECISIONS WERE COLLAPSED INTO ONE REASON, which is why this took so long to see. ^eval_rejected_parameter_reference_unbound is raised BOTH by eval_callee_body_refusal_reason for the ParameterReferenceBody ARM -- before any parameter is bound -- and at a genuine lookup miss. I read it as a key mismatch and spent four probes on lookups by full path and by leaf and on binding under both spellings, none of which could touch the failing fixture: a parameter-bodied callee never reached binding, while the constant-bodied controls beside it did, which is exactly why those probes showed the named branch taken, non-empty ApplicationBound slots and successful binds. Twenty-one reason symbols were tested before I stopped guessing and measured the LOCUS instead, which answered on the first try. THE REPAIR IS TWO ARMS AND ONE KEY. v2.std.node arrow_body_admits_eval_entry admits ParameterReferenceBody. That is the canonical policy for which body forms may enter eval, so the change is made THERE and not as an eval-local exception, which would be a second authority for one decision. Admitting the form is not admitting the access: a missing binding still refuses. eval_runtime_node resolves a parameter reference as a LEAF. Its carrier is a marked Conj whose child is the encoded declaring path, so letting the data-flow fold see it descends into that path and demands value-grounding for the internal representation of a parameter identity -- the same defect this module already fixed for DECLARATION references at eval_fold_is_callee_reference_edge. Finding that the fold was the remaining link came from reading the dispatch after the gate opened, not from another probe. eval_parameter_binding_key remains the ONLY place either side derives a key: the bind side snocs the formal's label onto the callee's declaring path, the use side already carries that path, and both ask the same constructor. Full path, never the leaf, so p.f.x and p.g.x cannot alias. Lambdas keep their lexical bare-label route, which #12766 deliberately left them on, and a named declaration gets no bare-label fallback -- binding both spellings would make the route look repaired while preserving both authorities. EVIDENCE, INCLUDING THE FLIPPED MEASUREMENT. The locus row that identified the gate asserted the refusal was located at the callee's declaring Arrow; that refusal no longer happens, so under DESIGN section 4b(4) the row does not retire -- it now asserts the policy arm directly and goes red if it is reverted. Beside it, a supplied parameter reference whose path nothing bound still refuses with the same reason, which is what keeps admitting the form from being a widening and pins what that symbol now means: a missing binding and nothing else. NO REGRESSIONS: arrow_body_form_witness 33/0 (the policy's own witness), declaration_reference_eval 12/0, synthetic_facts_key_collision 4/0, field_projection_stages 24/0. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/05_eval.dag | 33 ++++++- src/v2/std/node.dag | 18 +++- .../declaration_reference_eval_test.dag | 87 ++++++++++++++++++- 3 files changed, 135 insertions(+), 3 deletions(-) diff --git a/src/v2/compiler/05_eval.dag b/src/v2/compiler/05_eval.dag index e2aa47145f2..52fda11bb78 100644 --- a/src/v2/compiler/05_eval.dag +++ b/src/v2/compiler/05_eval.dag @@ -1418,6 +1418,25 @@ fn eval_formal_binding_key(callee_path: Optional, label: Symbol) } } +fn eval_parameter_reference_is_value(node: Node) -> Bool { + match parameter_reference_path_optional(node: node) { + Present { value: _ } => true + Absent => false + } +} + +fn eval_parameter_reference_value(node: Node, environment: EvaluationEnvironment) -> Outcome { + match parameter_reference_path_optional(node: node) { + Absent => outcome_rejected(d: eval_diagnostic(reason: ^eval_rejected_parameter_reference_unbound, node: node)) + Present { value: path } => + match environment.bindings.lookup(eval_parameter_binding_key(parameter_path: path)) { + Present { value: v } => Accepted { value: v, diagnostics: None } + Absent => + outcome_rejected(d: eval_diagnostic(reason: ^eval_rejected_parameter_reference_unbound, node: node)) + } + } +} + fn eval_bind_arrow_params( arrow: Node, args: List, @@ -2882,7 +2901,19 @@ fn eval_runtime_node( runtime: V4EvaluatorRuntime, effect_io: EffectIoEvalContext ) -> Outcome { - if eval_runtime_node_is_control_transfer(node: node) { + // A PARAMETER REFERENCE IS A LEAF HERE, NOT A STRUCTURE TO WALK. Its carrier is a marked Conj whose child + // is the encoded declaring path (v2.std.qualified_name parameter_reference_node), so letting the data-flow + // fold see it descends into that path and demands value-grounding for the internal representation of a + // parameter identity -- the same defect this module already fixed for DECLARATION references, recorded at + // eval_fold_is_callee_reference_edge, and the reason its regression control exists. + // + // IT RESOLVES THROUGH THE SAME KEY THE CALL BOUND IT AT, so eval_parameter_binding_key stays the only + // place either side derives a key. A reference whose path the environment does not hold still refuses with + // ^eval_rejected_parameter_reference_unbound, which is now a statement about a MISSING BINDING rather than + // about the body's form. + if eval_parameter_reference_is_value(node: node) { + eval_parameter_reference_value(node: node, environment: environment) + } else if eval_runtime_node_is_control_transfer(node: node) { eval_runtime_control_node( node: node, tree: tree, diff --git a/src/v2/std/node.dag b/src/v2/std/node.dag index dbf72b07d9e..e51327797ed 100644 --- a/src/v2/std/node.dag +++ b/src/v2/std/node.dag @@ -1092,13 +1092,29 @@ fn classify_arrow_body_form(target: Node) -> ArrowBodyForm { } } +// WHICH BODY FORMS MAY ENTER EVAL, AND IT IS THE ONE AUTHORITY FOR THAT QUESTION. An eval-local exception +// beside it would be a second authority for the same decision, so a form is admitted here or not at all. +// +// ParameterReferenceBody IS ADMITTED, and that is the change gunbc#12766 named as this lane's obligation. It +// refused categorically while NO route evaluated a declared fn body over resolver output, which was honest +// then and became a wall against the capability once the named-call route existed. MEASURED, by locus rather +// than by reason: the three argument-dependent controls in +// v2.test.claim.callexec.declaration_reference_eval refused with +// ^eval_rejected_parameter_reference_unbound anchored at the callee's DECLARING ARROW, not at the parameter +// use -- v2.compiler.eval eval_callee_body_refusal_reason maps this arm to that same symbol, so the reason +// has two producers and the gate was the one firing, before eval_bind_arrow_params ran at all. +// +// ADMITTING THE FORM IS NOT ADMITTING THE ACCESS. A parameter reference whose binding the environment does +// not hold still refuses with the same reason, now raised at the USE by eval_type_node_atom's own arm, where +// it means what it says. What disappears is a refusal based solely on the body's FORM; what remains is the +// refusal based on a missing binding, which is the only one that was ever a fact about the program. fn arrow_body_admits_eval_entry(form: ArrowBodyForm) -> Bool { match form { UnsupportedArrowBody => false DirectAtomBody => true RecordConstructBody => false DeclarationReferenceBody => false - ParameterReferenceBody => false + ParameterReferenceBody => true FunctionValueBody => false ComputationBody { behavior: Value } => false ComputationBody { behavior: Transform } => true diff --git a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag index 0f01c5de2b2..3fcb9aa6fd8 100644 --- a/src/v2/test/claim/callexec/declaration_reference_eval_test.dag +++ b/src/v2/test/claim/callexec/declaration_reference_eval_test.dag @@ -4,6 +4,9 @@ import v2.compiler.resolve { ResolvedTree } import v2.compiler.infer { infer } import v2.compiler.inferred_tree { InferredTree } import v2.compiler.eval { + arrow_body_admits_eval_entry, + empty_evaluation_environment, + eval_parameter_reference_value, eval, eval_callee_declaration_optional, inputs_root_only @@ -26,6 +29,7 @@ import v2.std.diagnostic { } import v2.std.logic { Bool } import v2.std.node { + ParameterReferenceBody, DeclarationReferenceKind, Symbol, symbol_eq, @@ -45,7 +49,8 @@ import v2.std.integer { Int, integer_signed_i32_le_bytes_to_int } import v2.std.runtime { RuntimePrimitive, RuntimeValue } import v2.std.algebra { fold_list } -import v2.std.qualified_name { qualified_name_last_segment, +import v2.std.qualified_name { + parameter_reference_node, qualified_name_last_segment, declaration_reference_path_optional, resolved_reference_spine_optional } @@ -533,3 +538,83 @@ fn cref_results_differ(a: Outcome, a_name: Symbol, b: Outcome, name: Symbol) -> Optional { + match cref_eval_of_call_to(o: o, name: name) { + Absent => optional_absent() + Present { value: outcome } => + match outcome { + Accepted { value: _, diagnostics: _ } => optional_absent() + Rejected { diagnostics: ds } => + match ds.head.at { + NodeLocus { anchor: a } => optional_present(value: a.at) + _ => optional_absent() + } + } + } +} + +// The callee's declaring Arrow, through the same two production readers eval itself uses: the callee use, +// then the denotation infer recorded for it. +fn cref_callee_declaration(o: Outcome) -> Optional { + match cref_inferred_of(o: o) { + Absent => optional_absent() + Present { value: inferred } => + match cref_the_call_to(root: inferred.root, name: ^callee) { + Absent => optional_absent() + Present { value: call } => + match cref_callee_reference_optional(n: call) { + Absent => optional_absent() + Present { value: ref } => eval_callee_declaration_optional(tree: inferred, callee: ref) + } + } + } +} + +// THE ENTRY GATE ADMITS THE FORM, which is the climb this measurement located. The row that stood here +// asserted the refusal was located at the callee's DECLARING ARROW rather than at the parameter use, and that +// is how the firing producer was identified: ^eval_rejected_parameter_reference_unbound has two producers -- +// v2.compiler.eval eval_callee_body_refusal_reason maps the ParameterReferenceBody ARM to it before any +// binding happens, and eval_parameter_reference_value raises it at the USE when a binding is missing -- so +// the reason alone could not say which. Measuring the LOCUS could, and it said the gate. +// +// DESIGN section 4b(4): the measurement row does not retire, it flips. v2.std.node +// arrow_body_admits_eval_entry now admits ParameterReferenceBody, so the gate is asserted directly rather +// than through the locus of a refusal that no longer happens. It goes red if that policy arm is reverted. +test fn cref_the_entry_gate_admits_a_parameter_reference_body_holds() -> Bool { + arrow_body_admits_eval_entry(form: ParameterReferenceBody) +} + +// AND A MISSING BINDING STILL REFUSES, which is what keeps admitting the form from being a widening. The +// subject is supplied at one interface -- a parameter reference whose declaring path nothing bound, evaluated +// in an empty environment -- because what is under test is the lookup's refusal and not the route that +// reaches it. Without this row the repair could be satisfied by answering every parameter reference with +// something, which is the fail-open direction. +// +// IT ALSO PINS WHAT THE REASON NOW MEANS. After the gate opened, this symbol reports a MISSING BINDING and +// nothing else; a reader who sees it can stop looking for a body-form refusal. +test fn cref_a_parameter_reference_with_no_binding_refuses_holds() -> Bool { + match eval_parameter_reference_value( + node: parameter_reference_node( + qn: Cons { head: ^p, tail: Cons { head: ^nowhere, tail: Cons { head: ^x, tail: Empty } } }, + occurrence_id: OccurrenceSynthetic + ), + environment: empty_evaluation_environment() + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: ds } => symbol_eq(a: ds.head.reason, b: ^eval_rejected_parameter_reference_unbound) + } +} From 3bf1880165b4637eb4d9e6762c3d8a4e58654b83 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 19:11:35 +0000 Subject: [PATCH 57/90] Repair the merge's cross-side seams: projection base, two fixtures Three consequences of composing this lane with main's #12947 that git reported clean: - resolve_projection_base: a ParameterFrame-bound head now mints the path-keyed parameter reference through resolve_frame_bound_reference, as any other use of that parameter does. As a bare atom it was typed only by infer's scope search, which main deleted, so every projection off a named fn's parameter went underived (field_projection_stages 6 reds, back to 24/24). Lexical and type-parameter heads keep the canonical atom; carrying lexical entries through this Outcome route is a declared frontier with a capability trigger. - parameter_reference_test: main's shared program bound `let x` over parameter `x`, which this lane's value-shadowing ruling refuses, so the whole program refused and seven rows went red together. h's let now binds `z` (L1 reads it), and row (2) becomes the refusal on its own source, asserted by reason. - callable_binder_slice_test: main deleted add_arrow_domain_named_params (arrow_domain_frame_binders replaces it), and row (9)'s frame now records its binding and keys its use by a minted occurrence, as resolve's own frames do (9/9). Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_resolve.dag | 41 ++++++++++++++---- .../callable_binder_slice_test.dag | 42 +++++++++++++------ .../test/claim/parameter_reference_test.dag | 23 ++++++---- 3 files changed, 79 insertions(+), 27 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 0543fef82ed..d896ac6abea 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1401,7 +1401,9 @@ fn resolve_bound_head_projection( rejected: diagnostics_singleton(d: unbound_symbol_diagnostic(n: n)) ) } - Present { value: base } => + Present { value: Rejected { diagnostics: r } } => + Rejected { diagnostics: rejected_with_pending(pending: pending, rejected: r) } + Present { value: Accepted { value: base, diagnostics: _ } } => outcome_with_diagnostics( value: fold_list( xs: field_segments, @@ -1432,21 +1434,44 @@ fn resolve_bound_head_projection( // the path never having been this function's subject at all; the later receipt established the second. Its // deletion is therefore a correctness repair on this function's own contract, claiming nothing about the // eight. -fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional { +// +// A NAMED FN'S PARAMETER AS THE HEAD IS THE SAME PATH-KEYED REFERENCE IT IS ANYWHERE ELSE. A ParameterFrame +// answers through resolve_frame_bound_reference, the one route that mints parameter_reference_node, so infer +// grounds the receiver through the resolved declarations exactly as it grounds a bare use of that parameter +// (v2.compiler.infer infer_parameter_reference_facts). A bare atom here was typed only by the scope search +// that main deleted, so it would leave every such receiver underived. A Lexical or type-parameter head stays +// the canonical atom: minting a lexical reference here would need its binding recorded, and this Outcome +// route carries no lexical entries, so it would refuse as unrecorded at infer. DECLARED FRONTIER, TRIGGER +// NAMING THE CAPABILITY: the projection route carries LexicalBindingEntry beside its node, SUFFICIENT FOR a +// let, match-arm or lambda binder to be a typed projection receiver. +fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional> { match head_segment.kind { TypeNode { connective: Atom { identity: name } } => match lookup_chain(s: ctx.scope, name: name) { Rejected { diagnostics: _ } => optional_absent() Accepted { value: found, diagnostics: _ } => match found { + BoundInFrame { canonical: canonical, kind: ParameterFrame { arrow_path: arrow_path } } => + optional_present(value: match resolve_frame_bound_reference( + ctx: ctx, + n: head_segment, + canonical: canonical, + kind: ParameterFrame { arrow_path: arrow_path }, + pending: None + ) { + Accepted { value: atom, diagnostics: d } => Accepted { value: atom.node, diagnostics: d } + Rejected { diagnostics: r } => Rejected { diagnostics: r } + }) BoundInFrame { canonical: canonical, kind: _ } => - optional_present( - value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) - ) + optional_present(value: Accepted { + value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id), + diagnostics: None + }) BoundAtRoot { canonical: canonical } => - optional_present( - value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id) - ) + optional_present(value: Accepted { + value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id), + diagnostics: None + }) ScopeUnbound => optional_absent() } } diff --git a/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag index cc9d37bc7b1..cae59aedfb4 100644 --- a/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag +++ b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag @@ -1,7 +1,10 @@ module v2.test.claim.binder_admission.callable_binder_slice import v2.compiler.resolve { - LexicalFrame, + LexicalBinding, + frame_binders_add, + frame_binders_empty, + lexical_frame, NotDeclaring, BinderAdmission, BinderDuplicateInFrame, @@ -11,7 +14,7 @@ import v2.compiler.resolve { Scope, ScopeFrame, ScopeRoot, - add_arrow_domain_named_params, + arrow_domain_frame_binders, admit_value_binders, empty_canonical_symbol_set, empty_namespace, @@ -41,9 +44,9 @@ import v2.std.node { TypeNode, } -import v2.std.optional { Absent, Present } +import v2.std.optional { Absent, Present, optional_absent } import v2.std.logic { Bool } -import std.occurrence_identity { OccurrenceSynthetic } +import std.occurrence_identity { OccurrenceId, OccurrenceMinted, OccurrenceSynthetic } // THE CALLABLE-BINDER SLICE, READ AT ONE INTERFACE EACH, WITH SUPPLIED NODES. Every claim here // constructs the node it asks about. None assembles source, resolves a corpus or runs infer: the @@ -110,7 +113,7 @@ fn cbs_admits(locals: Map, name: Symbol) -> Bool { } fn cbs_arrow_locals() -> Map { - add_arrow_domain_named_params(n: cbs_fn_literal_shaped_arrow(), acc: empty_map()) + arrow_domain_frame_binders(n: cbs_fn_literal_shaped_arrow()).locals } // (1) THE ARROW PATH ADMITS BOTH BINDERS. So resolve's binder model is not the defect: a callable @@ -128,7 +131,7 @@ test fn cbs_the_arrow_path_admits_no_undeclared_name_holds() -> Bool { // (3) THE FOLD SEAM ADMITS NEITHER BINDER -- THE DISCRIMINATING RED FOR THIS SLICE. The step literal // is destructured into an iteration body and ONE carrier symbol (v2.compiler.fold_lowering -// fold_call_seam_from_step), and the seam it builds is a Loop, not an Arrow. add_arrow_domain_named_params +// fold_call_seam_from_step), and the seam it builds is a Loop, not an Arrow. arrow_domain_frame_binders // answers the empty map for it because it is not an Arrow at all, and resolve harvests binders in // exactly two places -- an Arrow's domain and a Bind's atom -- with NO loop-carrier harvester. So the // body's `found` reaches the bare-name census and refuses as declared in several modules. @@ -137,10 +140,9 @@ test fn cbs_the_arrow_path_admits_no_undeclared_name_holds() -> Bool { // constructor and loop carriers go through the one binder-admission operation, the seam admits its // binders and this row must be rewritten to assert that, not deleted (DESIGN section 4b(4)). test fn cbs_the_fold_seam_admits_no_binder_today_holds() -> Bool { - let seam_locals = add_arrow_domain_named_params( - n: cbs_carrier_loop(body: cbs_atom(id: ^step_body), carrier: ^found), - acc: empty_map() - ) + let seam_locals = arrow_domain_frame_binders( + n: cbs_carrier_loop(body: cbs_atom(id: ^step_body), carrier: ^found) + ).locals !cbs_admits(locals: seam_locals, name: ^found) && !cbs_admits(locals: seam_locals, name: ^e) } @@ -166,8 +168,24 @@ fn cbs_root_scope_declaring(name: Symbol) -> Scope { } } +// A Lexical frame as resolve builds one (v2.compiler.resolve lexical_frame over frame_binders_add): the name +// is both a local and a recorded binding, so a use it answers is minted as a lexical reference rather than +// refused as unrecorded. fn cbs_frame_binding(name: Symbol) -> Scope { - ScopeFrame { locals: map_insert(empty_map(), name, name), outer: cbs_root_scope(), kind: LexicalFrame { binders: empty_map() } } + lexical_frame( + fb: frame_binders_add( + fb: frame_binders_empty(), + name: name, + binding: LexicalBinding { binder_site: OccurrenceMinted { id: OccurrenceId { value: 1 } }, declared: optional_absent() } + ), + outer: cbs_root_scope() + ) +} + +// A use with a MINTED occurrence: a lexical reference is keyed by its own occurrence, and a synthetic one +// refuses as unkeyed (resolve_lexical_reference), which would answer for a different reason than row (9)'s. +fn cbs_minted_atom(id: Symbol) -> Node { + Node { kind: TypeNode { connective: Atom { identity: id } }, children: [], occurrence_id: OccurrenceMinted { id: OccurrenceId { value: 2 } } } } fn cbs_admitted_has(a: BinderAdmission, name: Symbol) -> Bool { @@ -278,7 +296,7 @@ test fn cbs_a_bare_loop_does_not_bind_its_carrier_holds() -> Bool { test fn cbs_a_loop_carrier_bound_above_resolves_holds() -> Bool { match resolve_node_walk( ctx: cbs_ctx(scope: cbs_frame_binding(name: ^found)), - n: cbs_carrier_loop(body: cbs_atom(id: ^found), carrier: ^found) + n: cbs_carrier_loop(body: cbs_minted_atom(id: ^found), carrier: ^found) ) { ResolveWalkAccepted { value: _, diagnostics: _, lexical: _ } => true ResolveWalkRefused { first: _, rest: _, observation: _ } => false diff --git a/src/v2/test/claim/parameter_reference_test.dag b/src/v2/test/claim/parameter_reference_test.dag index 0f038db65c8..7f66f8e76ab 100644 --- a/src/v2/test/claim/parameter_reference_test.dag +++ b/src/v2/test/claim/parameter_reference_test.dag @@ -100,7 +100,7 @@ fn pr_assemble(src: String) -> Outcome { // resolved program, so the program is resolved once by a nullary producer the floor serves warm // (v2.workflow.floor_pure_producer_share), and each row reads only the portable values it inspects: // the parameter-reference paths, and the grounded and declared types at two paths. -data pr_program_source: String = "module p\n\nfn positive(x: Int) -> Bool { true }\n\ntype Pos = Int where positive\n\nfn f(x: Pos) -> Pos {\n x\n}\n\nfn g(x: Int) -> Int {\n x\n}\n\nfn h(x: Int) -> Int {\n let x = 1\n x\n}\n\nfn k(a: Int) -> fn(Int) -> Int {\n fn(y) { y }\n}\n" +data pr_program_source: String = "module p\n\nfn positive(x: Int) -> Bool { true }\n\ntype Pos = Int where positive\n\nfn f(x: Pos) -> Pos {\n x\n}\n\nfn g(x: Int) -> Int {\n x\n}\n\nfn h(x: Int) -> Int {\n let z = 1\n z\n}\n\nfn k(a: Int) -> fn(Int) -> Int {\n fn(y) { y }\n}\n" // Every parameter-reference path in the resolved program, in walk order; a refused program answers // Absent, which no row reads as "no parameter references". @@ -143,10 +143,19 @@ test fn pr_named_fn_parameter_is_a_parameter_reference_holds() -> Bool { }) } -// (2) A `let` NAMED LIKE A PARAMETER STILL BINDS LEXICALLY. h's body `x` is the let binder, so no -// p.h.x is minted: had the ParameterFrame answered it, p.h.x would appear. -test fn pr_let_shadowing_a_parameter_binds_lexically_holds() -> Bool { - pr_paths_hold(pred: fn(paths) { !pr_paths_contain(paths: paths, path: pr_qn(dotted: "p.h.x")) }) +// (2) A `let` NAMED LIKE A PARAMETER REFUSES, AT THE BINDER, BY REASON. A value binder may not hide an +// enclosing binder (v2.compiler.resolve admit_value_binders, the value-shadowing ruling), so the question +// this row first asked -- which frame answers a let that shadows a parameter -- has no accepted program to +// ask it of. Its own source keeps that refusal out of the shared program every other row reads; h there +// binds `z`, and its body reference is the lexical one (L1). Asserted by reason, so a program refused for +// an unrelated cause cannot satisfy it. +data pr_let_shadowing_source: String = "module p\n\nfn h(x: Int) -> Int {\n let x = 1\n x\n}\n" + +test fn pr_let_shadowing_a_parameter_refuses_holds() -> Bool { + match pr_assemble(src: pr_let_shadowing_source) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^resolve_reason_binder_hides_visible_value + } } // (3) A LAMBDA'S PARAMETER STAYS ON THE LEXICAL ROUTE. Its Arrow is met below k's body, so its frame is @@ -195,14 +204,14 @@ fn pr_site_is_minted(site: NodeOccurrenceIdentity) -> Bool { } } -// (L1) h's body `x` IS A LEXICAL REFERENCE ANSWERED BY THE `let`, recorded with the let binder's minted +// (L1) h's body `z` IS A LEXICAL REFERENCE ANSWERED BY THE `let`, recorded with the let binder's minted // site and no declared type. Before the lexical carrier it was a bare atom and nothing recorded which // binder answered it, so this row reads Absent. test fn pr_let_reference_is_lexical_with_recorded_binder_holds() -> Bool { match pr_program_lexical_reads() { Absent => false Present { value: reads } => - match pr_lexical_read_of(reads: reads, label: ^x) { + match pr_lexical_read_of(reads: reads, label: ^z) { Present { value: r } => match r.binding { Present { value: b } => pr_site_is_minted(site: b.binder_site) && (b.declared == Absent) From c84f55be91f13f167d468bf6013d9386b7ae1a97 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 19:18:50 +0000 Subject: [PATCH 58/90] Where-predicate calls walk unjudged under the where_clause frontier, counted Main's #12381 lowers `type Pos = Int where positive` to a zero-argument call of positive(x: Int) whose subject is implicit. This lane judges every named call (main judged none), so every refinement refused application_positional_deficit. #12381 carries no typed subject slot (the first-formal convention is an annotation in std.types, and brand takes no subject), so judging with the carrier needs new modelling, dispatched separately on main (calm-boar-904 ruling). Until then the predicate set is walked under the SAME declared frontier RefinementDeclaration.where_clause already carries, not a second row: infer_where_predicate_set_edge merges past the captured predicate set, and each predicate call emits inhabitance_undecidable_where_predicate_subject_ unmodelled at its own locus, so the population is counted by execution. Not a rung drop: predicate calls were unjudged before; every other named call stays judged, held by pr_ordinary_named_call_deficit_still_refuses. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/04_infer.dag | 65 ++++++++++++++++++- src/v2/std/inhabitance.dag | 6 ++ .../test/claim/parameter_reference_test.dag | 17 +++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index ba3a3465a2c..cd6ce7abfd5 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -110,7 +110,8 @@ import v2.std.inhabitance { position_let_annotation, position_direct_call_argument, undecidable_argument_type_not_derived, - undecidable_formal_unresolved + undecidable_formal_unresolved, + undecidable_where_predicate_subject_unmodelled } import v2.std.diagnostic { Diagnostic, @@ -1652,7 +1653,8 @@ fn infer_conj_named_type_for_binding(domain: Node, binding: Symbol) -> Optional< // coercion_cast_crossing (an explicit cast's declared-carrier widening) and, through // infer_judge_declared_position, to v2.std.inhabitance declared_type_inhabitance (the same one-step // widening at every declared position: argument, return, field and let). .where_clause is a DECLARED -// FRONTIER, not yet consumed: its consumer is the literal-into-refinement cast arm of work item +// FRONTIER, not yet consumed (and the predicate CALLS inside it are walked unjudged and counted under the +// same frontier: infer_where_predicate_set_edge): its consumer is the literal-into-refinement cast arm of work item // adhoc-032c89dc-138 (deep-bee-18), which projects it into resolve's where-predicate bindings. That arm // lands with the next-rung trigger of gunbc.recurring_failure_mode as_cast_has_no_lowered_form // (v2.std.coercion deciding a refinement's predicate on the cast operand); it must refuse on Absent @@ -4687,6 +4689,56 @@ fn infer_disj_is_named_sum(n: Node) -> Bool { // constructor's fields; the match row reads it from the arm, and it is never judged as the // construction it is shaped like. The same edge is outside v2.std.type_binder // node_inferred_subtree_nodes. +// A WHERE-REFINEMENT'S PREDICATE CALLS ARE NOT JUDGED AS APPLICATIONS, AND EACH ONE SAYS SO. A predicate +// call (v2.compiler.body_lowering_fold body_lower_kept_where_clause) applies its declaration to the REFINED +// VALUE, which the clause never writes: `type Pos = Int where positive` lowers to a zero-argument call of +// `positive(x: Int)`. Judged as an ordinary named call through application_binding_plan it refuses +// application_positional_deficit on every refinement, and judged with the subject supplied it needs a slot no +// carrier has: which formal is the subject is stated only as an annotation in std.types, and `brand` takes no +// subject at all. So this is the SAME declared frontier RefinementDeclaration.where_clause carries -- the +// where clause is not yet consumed by infer -- extended to the calls inside it, not a second one. +// +// IT IS NOT A RUNG DROP. Before named calls were judged at all, a predicate call was accepted unjudged along +// with every other named call; what this arm keeps is that single shape at that rung, while every other named +// call stays judged (a positional deficit still refuses: v2.test.claim.parameter_reference_test +// pr_ordinary_named_call_deficit_still_refuses). IT IS LOUD, NOT SILENT: each predicate call emits +// inhabitance_undecidable_where_predicate_subject_unmodelled at its own locus, so the population is counted by +// execution over any compiled corpus rather than transcribed here. NEXT TRIGGER, NAMING THE CAPABILITY: the +// where-predicate declaration model carries a typed subject slot (and a typed no-subject disposition for +// `brand`), SUFFICIENT FOR infer to judge each predicate call with the refined carrier as its implicit first +// actual -- then this arm is deleted and the calls go through infer_application_argument_inhabitance. +fn infer_where_predicate_set_edge(parent: Node, edge: Edge) -> Bool { + match edge.label { + Positional => false + Named { name: label } => + if label == ^grammar_production_captured_node_projection { + match find_named_child(root: parent, name: ^grammar_production_identity_node_projection) { + Accepted { value: identity, diagnostics: _ } => + match identity.kind { + TypeNode { connective: Atom { identity: emitted } } => emitted == ^dag_surface_where_refinement_clause + _ => false + } + Rejected { diagnostics: _ } => false + } + } else { + false + } + } +} + +fn infer_where_predicate_calls_unjudged(set: Node) -> Diagnostics { + fold(set.children, init: None, f: fn(acc, e) { + diagnostics_merge( + outer: acc, + inner: Some { + diagnostics: diagnostics_singleton( + d: inhabitance_undecidable_diagnostic(application: e.target, reason: undecidable_where_predicate_subject_unmodelled()) + ) + } + ) + }) +} + fn infer_gather_fold_step( acc: InferGatherFoldAcc, edge: Edge, @@ -4710,6 +4762,15 @@ fn infer_gather_fold_step( kinds: kinds, resolved: resolved ) + } else if infer_where_predicate_set_edge(parent: acc.node, edge: edge) { + infer_gather_fold_step_merged( + acc: acc, + merged_entries: acc.entries, + merged_pending: diagnostics_merge(outer: acc.pending, inner: infer_where_predicate_calls_unjudged(set: edge.target)), + partials: partials, + kinds: kinds, + resolved: resolved + ) } else if child.failed { infer_gather_fold_acc_failed( node: acc.node, diff --git a/src/v2/std/inhabitance.dag b/src/v2/std/inhabitance.dag index a69164178c0..6488628cccb 100644 --- a/src/v2/std/inhabitance.dag +++ b/src/v2/std/inhabitance.dag @@ -109,6 +109,7 @@ type InhabitanceUndecidableReason | UndecidableOptionalCarrier | UndecidableFormalUnresolved | UndecidableArgumentTypeNotDerived + | UndecidableWherePredicateSubjectUnmodelled type InhabitanceVerdict = Inhabits { homomorphism: HomomorphismWitness } @@ -422,6 +423,10 @@ fn undecidable_argument_type_not_derived() -> InhabitanceUndecidableReason { UndecidableArgumentTypeNotDerived } +fn undecidable_where_predicate_subject_unmodelled() -> InhabitanceUndecidableReason { + UndecidableWherePredicateSubjectUnmodelled +} + fn inhabitance_undecidable_reason_symbol( reason: InhabitanceUndecidableReason @@ -431,6 +436,7 @@ fn inhabitance_undecidable_reason_symbol( UndecidableOptionalCarrier => ^inhabitance_undecidable_optional_carrier UndecidableFormalUnresolved => ^inhabitance_undecidable_formal_unresolved UndecidableArgumentTypeNotDerived => ^inhabitance_undecidable_argument_type_not_derived + UndecidableWherePredicateSubjectUnmodelled => ^inhabitance_undecidable_where_predicate_subject_unmodelled } } diff --git a/src/v2/test/claim/parameter_reference_test.dag b/src/v2/test/claim/parameter_reference_test.dag index 7f66f8e76ab..7fae6f2af8a 100644 --- a/src/v2/test/claim/parameter_reference_test.dag +++ b/src/v2/test/claim/parameter_reference_test.dag @@ -469,3 +469,20 @@ test fn pr_parameter_grounds_through_the_index_holds() -> Bool { Absent => false } } + +// (C) AN ORDINARY NAMED CALL WITH A POSITIONAL DEFICIT STILL REFUSES. The where-predicate frontier +// (v2.compiler.infer infer_where_predicate_set_edge) leaves only predicate calls unjudged; this is the +// discriminating control that the frontier did not widen to every named call. Asserted by reason, so a +// program refused for an unrelated cause cannot satisfy it. +data pr_named_call_deficit_source: String = "module p\n\nfn g(x: Int) -> Int {\n x\n}\n\nfn u() -> Int {\n g()\n}\n" + +test fn pr_ordinary_named_call_deficit_still_refuses_holds() -> Bool { + match pr_assemble(src: pr_named_call_deficit_source) { + Rejected { diagnostics: _ } => false + Accepted { value: resolved, diagnostics: _ } => + match infer_and_discharge(tree: resolved) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^application_positional_deficit + } + } +} From a4b2b10ee572b688abe24b6e65287ba2a6dbdccc Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 19:22:58 +0000 Subject: [PATCH 59/90] Hoist three in-body annotations in 05_eval to module-item grain (floor parse refusal) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/05_eval.dag | 43 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/v2/compiler/05_eval.dag b/src/v2/compiler/05_eval.dag index 9cf4b45627e..6a1935c6877 100644 --- a/src/v2/compiler/05_eval.dag +++ b/src/v2/compiler/05_eval.dag @@ -1338,6 +1338,14 @@ fn eval_lexical_reference(node: Node, label: Symbol, environment: EvaluationEnvi // accepted program reaches this arm. DECLARED FRONTIER, TRIGGER NAMING THE CAPABILITY: gunbc#12506's // named-call route evaluates a declared fn body over resolver output, SUFFICIENT FOR eval to key // parameters by QualifiedName with this refusal arm deleted. +// +// THE REFERENCE CARRIES ITS OWN KEY, so the body looks the actual up under the path the call bound it +// at -- the same key eval_parameter_binding_key produced, which is why that constructor is the only +// place either side derives one. The refusal that stood here answered for EVERY parameter reference, +// which was honest while no route evaluated a declared body over resolver output and became a wall +// against the capability once one did. It does not disappear: a reference whose path the environment +// does not hold still refuses with the same reason, so a wrong path or an unbound parameter is still +// a located refusal and only a BOUND one now executes. fn eval_type_node_atom( node: Node, interpretation: InterpretationAlgebra, @@ -1351,13 +1359,6 @@ fn eval_type_node_atom( Absent => match parameter_reference_path_optional(node: node) { Present { value: path } => - // THE REFERENCE CARRIES ITS OWN KEY, so the body looks the actual up under the path the call bound it - // at -- the same key eval_parameter_binding_key produced, which is why that constructor is the only - // place either side derives one. The refusal that stood here answered for EVERY parameter reference, - // which was honest while no route evaluated a declared body over resolver output and became a wall - // against the capability once one did. It does not disappear: a reference whose path the environment - // does not hold still refuses with the same reason, so a wrong path or an unbound parameter is still - // a located refusal and only a BOUND one now executes. match environment.bindings.lookup(eval_parameter_binding_key(parameter_path: path)) { Present { value: v } => Accepted { value: v, diagnostics: None } Absent => @@ -1849,6 +1850,10 @@ fn eval_callee_body_refusal_reason(form: ArrowBodyForm) -> Symbol { // declaration is not an Arrow with an admissible body. Neither arm falls through to the primitive // table, where a reference would be looked up by a name it does not have and reported as a missing // primitive rather than as the declaration it actually names. +// THE CALLEE'S OWN DECLARING PATH, read off the reference the call carries rather than +// re-resolved: eval is a consumer of resolution's answer here, never a second naming +// authority. A callee that is not a declaration reference (a lambda) yields Absent and +// its formals stay on the lexical route. fn eval_transform_node( node: Node, args: List, @@ -1889,10 +1894,6 @@ fn eval_transform_node( environment: environment, bind: interpretation.bind, node: node, - // THE CALLEE'S OWN DECLARING PATH, read off the reference the call carries rather than - // re-resolved: eval is a consumer of resolution's answer here, never a second naming - // authority. A callee that is not a declaration reference (a lambda) yields Absent and - // its formals stay on the lexical route. callee_path: declaration_reference_path_optional(node: callee_edge.target) ), f: fn(call_environment) { @@ -2919,6 +2920,16 @@ fn eval_runtime_node_via_data_flow_fold( eval_fold_state_value(state: folded) } +// A PARAMETER REFERENCE IS A LEAF HERE, NOT A STRUCTURE TO WALK. Its carrier is a marked Conj whose child +// is the encoded declaring path (v2.std.qualified_name parameter_reference_node), so letting the data-flow +// fold see it descends into that path and demands value-grounding for the internal representation of a +// parameter identity -- the same defect this module already fixed for DECLARATION references, recorded at +// eval_fold_is_callee_reference_edge, and the reason its regression control exists. +// +// IT RESOLVES THROUGH THE SAME KEY THE CALL BOUND IT AT, so eval_parameter_binding_key stays the only +// place either side derives a key. A reference whose path the environment does not hold still refuses with +// ^eval_rejected_parameter_reference_unbound, which is now a statement about a MISSING BINDING rather than +// about the body's form. fn eval_runtime_node( node: Node, tree: InferredTree, @@ -2927,16 +2938,6 @@ fn eval_runtime_node( runtime: V4EvaluatorRuntime, effect_io: EffectIoEvalContext ) -> Outcome { - // A PARAMETER REFERENCE IS A LEAF HERE, NOT A STRUCTURE TO WALK. Its carrier is a marked Conj whose child - // is the encoded declaring path (v2.std.qualified_name parameter_reference_node), so letting the data-flow - // fold see it descends into that path and demands value-grounding for the internal representation of a - // parameter identity -- the same defect this module already fixed for DECLARATION references, recorded at - // eval_fold_is_callee_reference_edge, and the reason its regression control exists. - // - // IT RESOLVES THROUGH THE SAME KEY THE CALL BOUND IT AT, so eval_parameter_binding_key stays the only - // place either side derives a key. A reference whose path the environment does not hold still refuses with - // ^eval_rejected_parameter_reference_unbound, which is now a statement about a MISSING BINDING rather than - // about the body's form. if eval_parameter_reference_is_value(node: node) { eval_parameter_reference_value(node: node, environment: environment) } else if eval_runtime_node_is_control_transfer(node: node) { From 4dda083712db3ffccfcc808582f6e33d5e61c017 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 19:40:30 +0000 Subject: [PATCH 60/90] fold_lowering: a block-bodied arrow lambda has a block body fv_expr_parts read every `=> ...` body as FunctionLiteralExprBody, but the dag grammar's block_expr IS the fn_body nonterminal, so `(acc, e) => { .. }` carries the fn_body a fn literal carries. Neither consumer reads statements out of an opaque expression: body_lower_function_value_body refused it as function_value_body_unread, which refused src/v2/std/node.dag (the fold step #12790 added) and dag/extdeps/uri_path.dag at normalize, and so prepare for all eight tests of the native seven. The reader now decides the body kind from the production (expr -> block_expr -> fn_body, no search), and every consumer takes the arm it already takes for a fn literal. Claims: v2.test.claim.body_lowering.arrow_lambda_block_body, on the native route's own stages (tokenize -> parse -> normalize). Row: gunbc.recurring_failure_mode arrow_lambda_block_body_read_as_an_expression. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ambda_block_body_read_as_an_expression.dag | 21 +++++ src/v2/compiler/fold_lowering.dag | 44 ++++++++++- .../arrow_lambda_block_body_test.dag | 78 +++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 dag/gunbc/recurring_failure_mode/arrow_lambda_block_body_read_as_an_expression.dag create mode 100644 src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag diff --git a/dag/gunbc/recurring_failure_mode/arrow_lambda_block_body_read_as_an_expression.dag b/dag/gunbc/recurring_failure_mode/arrow_lambda_block_body_read_as_an_expression.dag new file mode 100644 index 00000000000..78fcfe7d042 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/arrow_lambda_block_body_read_as_an_expression.dag @@ -0,0 +1,21 @@ +module gunbc.recurring_failure_mode.arrow_lambda_block_body_read_as_an_expression + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data arrow_lambda_block_body_read_as_an_expression: RecurringFailureMode = RecurringFailureMode { + identity: "arrow_lambda_block_body_read_as_an_expression" as NonEmptyStr, + receipts: [ + "INVALID STATE: a function-value reader classifies a body by the SLOT it was parsed into rather than by the PRODUCTION that slot resolved to. v2.compiler.fold_lowering fv_expr_parts read every `=> ...` body as FunctionLiteralExprBody, but the dag grammar's block_expr IS the fn_body nonterminal, so `(acc, e) => { let c = ..; R { .. } }` carries the same fn_body production a fn literal does. HARM: neither consumer reads statements out of an opaque expression -- v2.compiler.body_lowering_fold body_lower_function_value_body refused the value as body_lowering_reason_function_value_body_unread and v2.compiler.fold_lowering fold_step_body answered the block shell instead of its statement sequence -- so the whole module refused at normalize. Measured on the native route at main 1a013596270 (neat-boar-16's srv1 baseline of the native seven): src/v2/std/node.dag (the fold step #12790 added in allocate_pending_occurrences) refused there, which refused prepare for all eight tests of //v2/test/parse/expression_bodied_fn_decl_parse:all; dag/extdeps/uri_path.dag (path_param_value) refused the same way before it. Loud, typed and located, never a silent wrong answer.", + "DISTINGUISHING FACTS: blocks in general lower -- `let y = { .. }` and a block call argument normalize, and the same fold step written `fn(acc, x) { .. }` or `(acc, x) => acc + x` normalizes -- so neither body lowering's block_expr pass-through nor the fold seam is the boundary; only the arrow-lambda reader's body kind is. The source form is admitted by both the seed and the v2 grammar, so rewriting call sites to fn literals would route around the reader rather than repair it. RUNG FOUND AT: mitigatable. ATTAINABLE CEILING: structurally guaranteed -- the body kind is decidable from the parse production, so the reader decides it once (expr, then block_expr, then fn_body, no search) and every consumer takes the arm it already takes for a fn literal; v2.test.claim.body_lowering.arrow_lambda_block_body carries the discriminating red (the block-bodied fold step refused before the change) and the fn-literal and expression-bodied controls. NEXT-RUNG TRIGGER, stated as the capability: a single body-kind decoder shared by both function-value productions, so a third production carrying fn_body cannot be classified by its slot again.", + ], + evidence: [ + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "fv_expr_parts", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "fv_block_body_optional", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "fold_step_body", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_function_value_body", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.extdeps.languages.dag", decl_name: "dag_grammar_block_expr_expr", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.body_lowering.arrow_lambda_block_body", decl_name: "fold_step_arrow_block_body_lowers_holds", field: WholeDeclaration }, + ], +} diff --git a/src/v2/compiler/fold_lowering.dag b/src/v2/compiler/fold_lowering.dag index b30b00eb0c7..39acfa58064 100644 --- a/src/v2/compiler/fold_lowering.dag +++ b/src/v2/compiler/fold_lowering.dag @@ -403,6 +403,45 @@ fn fv_after_fat_arrow_optional(node: Node) -> Optional { } } +// A LAMBDA WHOSE BODY IS A BLOCK HAS A BLOCK BODY, NOT AN EXPRESSION BODY. The grammar says so: +// dag_grammar_block_expr_expr IS the fn_body nonterminal, so `(acc, e) => { let c = ..; R { .. } }` +// carries the same fn_body production a fn literal does. Reading it as FunctionLiteralExprBody handed +// the block to every consumer as an opaque expression, and neither consumer reads statements out of +// one: body_lower_function_value_body refused it as function_value_body_unread, and fold_step_body +// answered the shell instead of its statement sequence. So the body kind is decided here, once, from +// the production the expr resolved to -- expr, then block_expr, then fn_body, and no other route -- +// and every consumer takes the fn_body arm it already takes for a fn literal. +fn fv_block_body_optional(expr: Node) -> Optional { + match fv_production_captured_optional(node: expr, emitted: ^dag_surface_expr) { + Absent => optional_absent() + Present { value: alternative } => + match fv_production_captured_optional(node: alternative, emitted: ^dag_surface_block_expr) { + Absent => optional_absent() + Present { value: fn_body } => + if fv_production_is(node: fn_body, emitted: ^dag_surface_fn_body) { + optional_present(value: fn_body) + } else { + optional_absent() + } + } + } +} + +fn fv_production_is(node: Node, emitted: Symbol) -> Bool { + match parse_production_emitted_identity_optional(node: fv_unwrap(node: node)) { + Present { value: id } => id == emitted + Absent => false + } +} + +fn fv_production_captured_optional(node: Node, emitted: Symbol) -> Optional { + if fv_production_is(node: node, emitted: emitted) { + parse_production_captured_child_optional(node: fv_unwrap(node: node)) + } else { + optional_absent() + } +} + fn fv_expr_parts(binders: List, after_binders: Node) -> Optional { match fv_after_fat_arrow_optional(node: after_binders) { Absent => optional_absent() @@ -411,7 +450,10 @@ fn fv_expr_parts(binders: List, after_binders: Node) -> Opt value: FunctionValueParts { binders: binders, return_clause: fv_empty_return_clause(), - body: FunctionLiteralExprBody { expr: expr } + body: match fv_block_body_optional(expr: expr) { + Present { value: fn_body } => FunctionLiteralBlockBody { fn_body: fn_body } + Absent => FunctionLiteralExprBody { expr: expr } + } } } } diff --git a/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag b/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag new file mode 100644 index 00000000000..333f8570ba2 --- /dev/null +++ b/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag @@ -0,0 +1,78 @@ +module v2.test.claim.body_lowering.arrow_lambda_block_body + +import v2.compiler.normalize { normalize } +import v2.compiler.parse { parse_module } +import v2.compiler.tokenize { tokenize } +import v2.extdeps.languages.dag { dag_language_model } +import v2.std.diagnostic { Accepted, Rejected, diagnostics_fatal_reason } +import v2.std.language_model { LanguageModel } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.node { symbol_lexeme } +import v2.std.text { String } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// A BLOCK-BODIED ARROW LAMBDA HAS A BLOCK BODY. v2.compiler.fold_lowering fv_expr_parts read every +// `=> ...` body as an expression, so a `{ .. }` body reached body lowering as an opaque block_expr and +// refused as body_lowering_reason_function_value_body_unread -- the native route's prepare wall for +// src/v2/std/node.dag (the fold step in allocate_pending_occurrences) and dag/extdeps/uri_path.dag +// (path_param_value). The block is the grammar's fn_body, the same production a fn literal carries, +// so the reader now answers FunctionLiteralBlockBody for it and both consumers take the fn-literal arm. +// +// The fn-literal and expression-bodied claims are the controls: they normalized before the change and +// must still, so a regression in the shared fn_body arm or in the expression arm reds them, not only +// the subject. +// +// THE NATIVE ROUTE'S OWN STAGES, tokenize -> parse -> normalize, and nothing above them. The +// reference-conservation harness (body_lowering_normalize_outcome) is the wrong instrument here: the +// fold seam desugars the `fold` call head away by design, so it refuses every fold fixture as +// conservation_reason_dropped_reference whatever the step's body is, and the claims would not +// discriminate the body reader at all. The answer is ACCEPTED or the FATAL reason, the last link, +// which is the cause that refused. +data claim_cached_lm: LanguageModel = dag_language_model() + +fn normalize_outcome(src: String) -> String { + let lm = claim_cached_lm + match tokenize(text: src, file: ^arrow_lambda_block_body_subject, rules: lm.lex) { + Rejected { diagnostics: d } => symbol_lexeme(sym: diagnostics_fatal_reason(d: d)) + Accepted { value: ts, diagnostics: _ } => + match parse_module(tokens: ts, grammar: lm.grammar) { + Rejected { diagnostics: d } => symbol_lexeme(sym: diagnostics_fatal_reason(d: d)) + Accepted { value: a, diagnostics: _ } => + match normalize(parse_tree: a.tree) { + Rejected { diagnostics: d } => symbol_lexeme(sym: diagnostics_fatal_reason(d: d)) + Accepted { value: _, diagnostics: _ } => "ACCEPTED" + } + } + } +} + +data src_fold_step_arrow_block: String = "module m.t\nfn t(xs: List) -> Int {\n fold(xs, init: 0, f: (acc, x) => {\n acc + x\n })\n}\n" + +data src_fold_step_arrow_block_let_record: String = "module m.t\ntype P {\n total: Int\n count: Int\n}\nfn t(xs: List) -> P {\n fold(xs, init: P { total: 0, count: 0 }, f: (acc, x) => {\n let next = acc.total + x\n P {\n total: next,\n count: acc.count + 1,\n }\n })\n}\n" + +data src_value_arrow_block: String = "module m.t\nfn apply(g: fn(Int) -> Int, v: Int) -> Int {\n g(v)\n}\nfn t(v: Int) -> Int {\n apply(g: a => {\n a + 1\n }, v: v)\n}\n" + +data src_fold_step_fn_literal_block: String = "module m.t\nfn t(xs: List) -> Int {\n fold(xs, init: 0, f: fn(acc, x) {\n acc + x\n })\n}\n" + +data src_fold_step_arrow_expr: String = "module m.t\nfn t(xs: List) -> Int {\n fold(xs, init: 0, f: (acc, x) => acc + x)\n}\n" + +test fn fold_step_arrow_block_body_lowers_holds() -> Bool { + normalize_outcome(src: src_fold_step_arrow_block) == "ACCEPTED" +} + +test fn fold_step_arrow_block_with_let_and_record_lowers_holds() -> Bool { + normalize_outcome(src: src_fold_step_arrow_block_let_record) == "ACCEPTED" +} + +test fn value_position_arrow_block_body_lowers_holds() -> Bool { + normalize_outcome(src: src_value_arrow_block) == "ACCEPTED" +} + +test fn fold_step_fn_literal_block_control_holds() -> Bool { + normalize_outcome(src: src_fold_step_fn_literal_block) == "ACCEPTED" +} + +test fn fold_step_arrow_expression_body_control_holds() -> Bool { + normalize_outcome(src: src_fold_step_arrow_expr) == "ACCEPTED" +} From adb3d6357580280d1c5bbd694c76ee9dcb921a5c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 19:41:05 +0000 Subject: [PATCH 61/90] Resolve the closure once per context, not once per subject (review 74212) closure_resolved_roots re-resolved every closure root for every subject a lane resolved, and resolved_tree_of then re-folded those roots into an index per subject: N x (N + 1) resolves and a second quadratic fill. The shared ResolutionContext is the least common ancestor of those demands (DESIGN 2), so the closure's resolved-declaration index is now produced on first demand by closure_declarations_demand and memoized on the context (closure_declarations), like the namespace provider beside it; the per-root resolves thread the context so the namespaces they admit are kept too. resolved_tree_of takes that index and fills only the subject over it. resolve_in_context and the native lane's native_module_resolve_verdict both return the memoized context, so the next subject reads it. field_projection_stages 24/24, declaration_reference_evidence 11/11, parameter_reference 14/14. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/00_compile.dag | 48 ++++--- src/v2/compiler/03_name_resolve.dag | 122 ++++++++++++------ src/v2/compiler/03_resolve.dag | 47 ++----- .../compiler/self_host/closure_emission.dag | 2 +- .../field_projection_stages_test.dag | 2 +- .../declaration_reference_evidence_test.dag | 2 +- 6 files changed, 133 insertions(+), 90 deletions(-) diff --git a/src/v2/compiler/00_compile.dag b/src/v2/compiler/00_compile.dag index 6dbbdb99e1c..30f86d9307d 100644 --- a/src/v2/compiler/00_compile.dag +++ b/src/v2/compiler/00_compile.dag @@ -42,7 +42,7 @@ import v2.compiler.emit { emit } import v2.compiler.eval { eval, eval_node, eval_default_interpretation, inputs_root_only } import v2.compiler.infer { InferredTree } import v2.compiler.name_resolve { - closure_resolved_roots, + closure_declarations_demand, Admission, ResolutionSubject, ResolutionContext, @@ -3271,6 +3271,8 @@ fn native_import_target_refusal_diagnostics(target: NativeTestFileRefusal, resol } } +// THE CLOSURE INDEX IS DEMANDED ON THE SHARED CONTEXT AND CARRIED FORWARD on the returned step, so the +// next module of the lane reads it instead of re-resolving the closure (review 74212). fn native_module_resolve_verdict( context: NativeTestContext, refusal_index: Map, @@ -3285,34 +3287,44 @@ fn native_module_resolve_verdict( context: context, module: qualified_name_from_dotted_string(dotted: module) ) - NativeModuleResolveStep { - verdict: match step.walk { - ResolveWalkRefused { first: f, rest: r, observation: o } => - match native_import_target_file_refusal(context: step.context, module: qualified_name_from_dotted_string(dotted: module)) { + match step.walk { + ResolveWalkRefused { first: f, rest: r, observation: o } => + NativeModuleResolveStep { + verdict: match native_import_target_file_refusal(context: step.context, module: qualified_name_from_dotted_string(dotted: module)) { Present { value: target } => NativeModuleResolveImportTargetFileRefused { target: target, first: f, rest: r, observation: o } Absent => NativeModuleResolveRefused { first: f, rest: r, observation: o } - } - ResolveWalkAccepted { value: resolved, diagnostics: _, lexical: lexical } => - match step.context.resolution { - Accepted { value: shared, diagnostics: _ } => - NativeModuleResolveAccepted { + }, + context: step.context + } + ResolveWalkAccepted { value: resolved, diagnostics: _, lexical: lexical } => + match step.context.resolution { + Accepted { value: shared, diagnostics: d } => + let closure = closure_declarations_demand(shared: shared) + NativeModuleResolveStep { + verdict: NativeModuleResolveAccepted { resolved: resolved_tree_of( root: resolved, symbol_index: shared.symbol_index, - closure_roots: closure_resolved_roots(context: step.context.resolution, shared: shared), + closure_declarations: closure.declarations, lexical: lexical ) - } - Rejected { diagnostics: r } => - NativeModuleResolveRefused { + }, + context: native_test_context_with_resolution( + context: step.context, + resolution: Accepted { value: closure.context, diagnostics: d } + ) + } + Rejected { diagnostics: r } => + NativeModuleResolveStep { + verdict: NativeModuleResolveRefused { first: r, rest: [], observation: ObservationIncomplete { reason: ^resolve_observation_context_refused } - } - } - }, - context: step.context + }, + context: step.context + } + } } } } diff --git a/src/v2/compiler/03_name_resolve.dag b/src/v2/compiler/03_name_resolve.dag index 3844559ec77..406ea500c33 100644 --- a/src/v2/compiler/03_name_resolve.dag +++ b/src/v2/compiler/03_name_resolve.dag @@ -2,7 +2,7 @@ module v2.compiler.name_resolve import v2.std.language_model { LanguageModel } -import v2.compiler.symbol_index_fill { symbol_index_fill_module_roots } +import v2.compiler.symbol_index_fill { symbol_index_fill_module_declarations, symbol_index_fill_module_roots } import v2.compiler.normalized_tree { NormalizedTree, normalized_tree_roots_to_binding_sources } import v2.std.declaration_marker { TestCodeIndex, test_code_index_add_module, test_code_index_empty } import v2.std.symbol_index { SymbolIndex, empty_symbol_index } @@ -36,6 +36,7 @@ import v2.compiler.parse { parse_tree_projection_edge } import std.algebra { Cons, Empty, FreeMonoid } import v2.std.algebra { fold_list } import v2.std.collection { Absent, Map, Present, List, PointwisePower, empty_map, map_get, map_lookup, map_insert, optional_absent, optional_present } +import v2.std.optional { Optional } import v2.std.diagnostic { Accepted, Diagnostic, @@ -146,6 +147,7 @@ type ResolutionContext { policy: NameResolutionPolicy namespaces: Map namespaces_built: Int + closure_declarations: Optional } // One stored production: the admission it was produced under, and its outcome -- a refusal is @@ -199,7 +201,8 @@ fn resolution_context_of_validated( active_roots: active_roots, policy: policy, namespaces: empty_map(), - namespaces_built: 0 + namespaces_built: 0, + closure_declarations: optional_absent() } } @@ -727,7 +730,8 @@ fn resolution_context_namespace( key: admission.subject.name, value: NamespaceProviderEntry { admission: admission, admitted: admitted } ), - namespaces_built: shared.namespaces_built + 1 + namespaces_built: shared.namespaces_built + 1, + closure_declarations: shared.closure_declarations } } } @@ -877,60 +881,106 @@ fn resolve_in_context(context: Outcome, admission: Admission) let walked = resolve_walk_in_context(context: context, admission: admission) match walked.context { Rejected { diagnostics: r } => ContextResolved { resolved: Rejected { diagnostics: r }, context: walked.context } - Accepted { value: shared, diagnostics: _ } => + Accepted { value: shared, diagnostics: d } => + let closure = closure_declarations_demand(shared: shared) ContextResolved { resolved: resolved_tree_outcome( w: walked.walk, symbol_index: shared.symbol_index, - closure_roots: closure_resolved_roots(context: walked.context, shared: shared) + closure_declarations: closure.declarations ), - context: walked.context + context: Accepted { value: closure.context, diagnostics: d } } } } -// EVERY ROOT IN THE CLOSURE, RESOLVED ONCE, so inference can retrieve a declaration an imported provider -// owns. v2.compiler.resolve resolved_declarations_of walks ONE root, so the subject's index held only the -// subject's declarations and every cross-module field read refused -- measured as +// EVERY ROOT IN THE CLOSURE, RESOLVED ONCE PER CONTEXT, so inference can retrieve a declaration an imported +// provider owns. v2.compiler.resolve resolved_declarations_of walks ONE root, so the subject's index held only +// the subject's declarations and every cross-module field read refused -- measured as // fps_a_cross_module_record_projection_infers against a passing same-module control. // -// THE RECURSION IS CUT BY THE PER-ROOT RESOLVES NOT ASKING FOR A CLOSURE, and that is the whole reason this -// is N + 1 resolves rather than N squared: each provider is walked by resolve_walk_in_context exactly as it -// is walked today, and only the SUBJECT's tree is built against the collected roots. A provider does not -// need the closure index to produce the declarations this fold reads from it, so nothing is lost by not -// giving it one, and the alternative -- every root resolved against every other -- would multiply the -// dominant cost of the pipeline by the closure size. +// ONCE PER CONTEXT, NOT ONCE PER SUBJECT. Every subject a context resolves demands the same closure index, +// and the shared ResolutionContext is their least common ancestor (DESIGN section 2), so the index is +// produced on the first demand and carried on the context like the namespace provider beside it; a later +// subject reads it. Recomputing it per subject made a lane of N modules do N x (N + 1) resolves, and folding +// the roots per subject was a second quadratic term (review 74212). The per-root resolves thread the context +// they are given, so the namespaces they admit are kept as well. +// +// THE RECURSION IS CUT BY THE PER-ROOT RESOLVES NOT ASKING FOR A CLOSURE: each provider is walked by +// resolve_walk_in_context exactly as any subject is, and only resolved_tree_of reads the collected index. A +// provider does not need the closure index to produce the declarations this fold reads from it. // // A REFUSED ROOT CONTRIBUTES NOTHING AND DOES NOT FAIL THE FOLD. That is deliberate and is NOT an absorbing // fallback: it does not widen an answer or substitute a reading. A provider that cannot resolve simply has // no resolved declarations to offer, so a receiver typed by one of its records refuses at // infer_reason_projection_receiver_declaration_unavailable -- the located refusal that names missing -// evidence, which is the honest outcome for a provider the compiler could not resolve. Absorbing would be -// answering the field read anyway. +// evidence. // // THIS IS THE ONE-SEAT FORM OF A DEPENDENCY THE DEMAND ENGINE OWNS. Infer(module) depends on // Resolve(module) AND Resolve(each provider it consumes); here that is discharged by resolving the whole -// SELECTED closure, which is broader than the import closure and therefore does more work than the relation -// requires. Narrowing it to the exact provider set changes WHICH roots are folded and not the inference -// rule, so it can land later without touching this reading. -fn closure_resolved_roots(context: Outcome, shared: ResolutionContext) -> FreeMonoid { - fold_list( - xs: shared.roots.roots, - empty: [], - cons: fn(acc, nt) { - match qualified_name_from_module_node(root: nt.root) { - Rejected { diagnostics: _ } => acc - Accepted { value: module_qn, diagnostics: _ } => - match resolve_walk_in_context( - context: context, - admission: Admission { subject: ResolutionSubject { name: module_qn }, imports: [] } - ).walk { - ResolveWalkAccepted { value: resolved, diagnostics: _, lexical: _ } => list_snoc_item(xs: acc, item: resolved) - ResolveWalkRefused { first: _, rest: _, observation: _ } => acc +// SELECTED closure, which is broader than the import closure. Narrowing it to the exact provider set changes +// WHICH roots are folded and not the inference rule. +// +// RECORD AND RESOURCE PAYLOAD MARKS ARE NOT CARRIED HERE, AND THAT IS INHERITED RATHER THAN CHOSEN: a resolved +// Node does not carry the normalize-time record roster, so each root is filled with Empty carriers, and +// v2.std.symbol_index symbol_index_declared_payload_at answers Absent for a RECORD in this index. The +// projection path reads through symbol_index_lookup and is unaffected. +type ClosureDeclarations { + declarations: SymbolIndex + context: ResolutionContext +} + +type ClosureDeclarationsFold { + index: SymbolIndex + context: ResolutionContext +} + +fn closure_declarations_demand(shared: ResolutionContext) -> ClosureDeclarations { + match shared.closure_declarations { + Present { value: index } => ClosureDeclarations { declarations: index, context: shared } + Absent => + let built = fold_list( + xs: shared.roots.roots, + empty: ClosureDeclarationsFold { index: empty_symbol_index(), context: shared }, + cons: fn(acc, nt) { + match qualified_name_from_module_node(root: nt.root) { + Rejected { diagnostics: _ } => acc + Accepted { value: module_qn, diagnostics: _ } => + let walked = resolve_walk_in_context( + context: Accepted { value: acc.context, diagnostics: None }, + admission: Admission { subject: ResolutionSubject { name: module_qn }, imports: [] } + ) + let next_context = match walked.context { + Accepted { value: c, diagnostics: _ } => c + Rejected { diagnostics: _ } => acc.context + } + match walked.walk { + ResolveWalkAccepted { value: resolved, diagnostics: _, lexical: _ } => + ClosureDeclarationsFold { + index: symbol_index_fill_module_declarations(index: acc.index, root: resolved, record_declarations: Empty, resource_declarations: Empty), + context: next_context + } + ResolveWalkRefused { first: _, rest: _, observation: _ } => + ClosureDeclarationsFold { index: acc.index, context: next_context } + } } + } + ) + ClosureDeclarations { + declarations: built.index, + context: ResolutionContext { + lm: built.context.lm, + roots: built.context.roots, + symbol_index: built.context.symbol_index, + index: built.context.index, + active_roots: built.context.active_roots, + policy: built.context.policy, + namespaces: built.context.namespaces, + namespaces_built: built.context.namespaces_built, + closure_declarations: optional_present(value: built.index) + } } - } - ) + } } // THE SUBJECT'S WHOLE RESOLUTION OBSERVATION, every independent failure chain in walk order. diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index d896ac6abea..bee4917be57 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -262,13 +262,13 @@ fn resolved_declarations_of(root: Node) -> SymbolIndex { fn resolved_tree_of( root: Node, symbol_index: SymbolIndex, - closure_roots: FreeMonoid, + closure_declarations: SymbolIndex, lexical: List ) -> ResolvedTree { ResolvedTree { root: root, symbol_index: symbol_index, - resolved_declarations: resolved_declarations_over(roots: closure_roots, subject: root), + resolved_declarations: resolved_declarations_over(closure: closure_declarations, subject: root), lexical_bindings: fold(lexical, init: empty_map(), f: fn(m, e) { map_insert(m, e.reference, e.binding) }) } } @@ -284,36 +284,17 @@ fn resolved_tree_lexical_binding(tree: ResolvedTree, reference: Node) -> Optiona } } -// ONE FILL, FOLDED OVER EVERY RESOLVED ROOT, rather than a new multi-root door or a merge of two indexes. -// symbol_index_fill_module_declarations is already the single-root door and keys every declaration at its -// OWN module's qualified name -- its header states that no other root can write those paths -- so folding it -// is total and order-independent, and no index-union authority needs to exist. A root whose module name does -// not resolve contributes nothing, which is that function's existing behaviour and not a new drop. +// THE CLOSURE INDEX IS PRODUCED ONCE PER CONTEXT (v2.compiler.name_resolve closure_declarations_demand) and +// only the SUBJECT is filled here. symbol_index_fill_module_declarations keys every declaration at its OWN +// module's qualified name, so filling the subject over the closure index is total and order-independent. // -// RECORD AND RESOURCE PAYLOAD MARKS ARE NOT CARRIED HERE, AND THAT IS INHERITED RATHER THAN CHOSEN. The -// fill takes record_declarations and resource_declarations carriers captured at normalize, and resolved_declarations_of passed Empty for it before -// this change; the fold passes Empty for every provider root for the same reason -- a resolved Node does not -// carry the normalize-time record roster, and inventing one here would be a second authority for a fact -// v2.compiler.normalize owns. The consequence is precise and bounded: v2.std.symbol_index -// symbol_index_declared_payload_at answers Absent for a RECORD in this index, so a reader that asks it would -// take a record for a binder, exactly as that fill's own header warns. The projection path reads through -// symbol_index_lookup and is unaffected, which is why the cross-module control passes. A reader that needs -// the payload KIND from the closure index must be given the roster first; it must not be inferred from a -// lookup hit. -// -// THE SUBJECT IS FOLDED LAST AND UNCONDITIONALLY. It is normally already a member of closure_roots, but a -// caller that holds no closure -- the per-module native door, before its accumulation is threaded -- passes -// an empty list, and this still answers exactly what it answered before: the subject's own declarations. So -// the change cannot take a capability away from a caller that has not yet been given the closure. -fn resolved_declarations_over(roots: FreeMonoid, subject: Node) -> SymbolIndex { +// THE SUBJECT IS FILLED UNCONDITIONALLY. It is normally already in the closure index, but a caller that holds +// no closure passes empty_symbol_index(), and this still answers exactly what it answered before: the +// subject's own declarations. So the change cannot take a capability away from a caller that has not yet been +// given the closure. +fn resolved_declarations_over(closure: SymbolIndex, subject: Node) -> SymbolIndex { symbol_index_fill_module_declarations( - index: fold_list( - xs: roots, - empty: empty_symbol_index(), - cons: fn(acc, r) { - symbol_index_fill_module_declarations(index: acc, root: r, record_declarations: Empty, resource_declarations: Empty) - } - ), + index: closure, root: subject, record_declarations: Empty, resource_declarations: Empty @@ -2142,12 +2123,12 @@ fn resolve_walk_outcome(w: ResolveNodeWalk) -> Outcome { fn resolved_tree_outcome( w: ResolveNodeWalk, symbol_index: SymbolIndex, - closure_roots: FreeMonoid + closure_declarations: SymbolIndex ) -> Outcome { match w { ResolveWalkAccepted { value: v, diagnostics: d, lexical: l } => Accepted { - value: resolved_tree_of(root: v, symbol_index: symbol_index, closure_roots: closure_roots, lexical: l), + value: resolved_tree_of(root: v, symbol_index: symbol_index, closure_declarations: closure_declarations, lexical: l), diagnostics: d } ResolveWalkRefused { first: f, rest: _, observation: _ } => Rejected { diagnostics: f } @@ -3946,7 +3927,7 @@ fn resolve_with_namespace_policy( policy: policy ), symbol_index: namespace.symbol_index, - closure_roots: [] + closure_declarations: empty_symbol_index() ) } diff --git a/src/v2/compiler/self_host/closure_emission.dag b/src/v2/compiler/self_host/closure_emission.dag index 92258c739c9..80dc5fd8f1d 100644 --- a/src/v2/compiler/self_host/closure_emission.dag +++ b/src/v2/compiler/self_host/closure_emission.dag @@ -348,7 +348,7 @@ fn closure_resolve_member( policy: default_name_resolution_policy() )), admission: Admission { subject: ResolutionSubject { name: member_module }, imports: Empty } - ).walk, symbol_index: symbol_index, closure_roots: []) + ).walk, symbol_index: symbol_index, closure_declarations: empty_symbol_index()) } ) } diff --git a/src/v2/test/claim/field_projection/field_projection_stages_test.dag b/src/v2/test/claim/field_projection/field_projection_stages_test.dag index 32218e620dd..42cb3bea914 100644 --- a/src/v2/test/claim/field_projection/field_projection_stages_test.dag +++ b/src/v2/test/claim/field_projection/field_projection_stages_test.dag @@ -700,7 +700,7 @@ test fn fps_a_cross_module_record_projection_infers_holds() -> Bool { // FIELDS". This row exists because the arm it exercises would otherwise have no authorable red. The // honesty split gave ReceiverDeclarationUnavailable its own reason, and the closure index then made the // ordinary cross-module case SUCCEED -- so the only remaining way to reach the arm is a provider whose -// declarations never enter the index. v2.compiler.name_resolve closure_resolved_roots drops a root that +// declarations never enter the index. v2.compiler.name_resolve closure_declarations_demand drops a root that // fails to resolve, which is exactly that condition, and this fixture produces it with a provider carrying // an unresolvable body beside the record the consumer projects. // diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 070af39cd31..743f31b9afc 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -495,7 +495,7 @@ fn dre_imported_reference_source() -> Outcome { // named "a resolved-declaration index over every resolved module root ... minted by resolve beside the // per-module carrier", and that is what v2.compiler.resolve resolved_tree_of now receives: the closure's // resolved roots, folded through the existing single-root door, assembled by v2.compiler.name_resolve -// closure_resolved_roots. The fact stayed resolve's to own, as this row said it must. +// closure_declarations_demand. The fact stayed resolve's to own, as this row said it must. // // DESIGN section 4b(4) IS WHY THIS DID NOT RETIRE. An expecting-red probe that greens when its wall lands // flips to a permanent regression control; it does not disappear, because the climb's evidence is what keeps From f09191ec4df4e8d9b7fe7b0f5ec1811516544459 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 19:42:31 +0000 Subject: [PATCH 62/90] Total the merge's sixteen unrostered wildcard arms; roster the one that is not a sweep Main's typed non-fold-residue census (#12980) refuses a top-level wildcard over a closed coproduct without a row. Fifteen of this lane's sites matched NodeKind (two variants), so their wildcard is now the two explicit variant arms -- construction rather than a roster row; infer_coproduct_arm_pattern's multi-line remainder moved into infer_coproduct_arm_pattern_constructed so both arms call it. eval_field_projection_of_receiver matches RuntimeValue, where RuntimeAggregate is the only field-bearing variant and every other refuses identically, so it is rostered with its reason and dissolution. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/non_fold_residue.dag | 9 ++++++ src/v2/compiler/03_resolve.dag | 18 +++++++---- src/v2/compiler/04_infer.dag | 56 +++++++++++++++++++++------------- src/v2/std/qualified_name.dag | 3 +- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/dag/gunbc/non_fold_residue.dag b/dag/gunbc/non_fold_residue.dag index 9bd2b89160d..72c20789b62 100644 --- a/dag/gunbc/non_fold_residue.dag +++ b/dag/gunbc/non_fold_residue.dag @@ -175,6 +175,10 @@ data nfr_reason_typed_census_undetermined_node_kind: String = "the v1 checker le data nfr_reason_landed_after_typed_census: String = "a top-level wildcard arm over a closed coproduct that landed on main after the ca5ed1724b typed census, while that census's roster was still in review (#12980); the floor's diff-scoped typed walk named it on the roster PR's own run. Un-migrated modeling (DESIGN §6), rostered so the ratchet stays armed at unrostered=0" +data nfr_reason_eval_projection_receiver_not_aggregate: String = "field projection at eval: RuntimeAggregate is the one RuntimeValue variant that carries named fields, and every other variant refuses with the same located eval_rejected_projection_receiver_not_aggregate; enumerating them would clone one refusal arm per variant. Landed with gunbc#12506's field-projection eval route; declared at landing so the ratchet arms with the code" + +data nfr_dissolve_eval_projection_receiver_not_aggregate: DissolutionCondition = unbound_dissolution(description: "RuntimeValue exposes a typed field-bearing projection (derived from its declaration, dag/std/algebra) so eval reads fields without a per-variant match, and this row deletes") + data non_fold_residue_frontier: List = [ FrontierRow { subject: PathSubject { path: "dag/gunbc/instruments/fabric_control_plane_live_probe.dag::fci1_slot_prestate_equal" }, reason: nfr_reason_fci1_prestate_equal, dissolution: nfr_dissolve_fci1_prestate_equal }, FrontierRow { subject: PathSubject { path: "dag/gunbc/instruments/native_app_attest.dag::same_assertion_arm" }, reason: nfr_reason_native_app_attest_arm_equal, dissolution: nfr_dissolve_native_app_attest_arm_equal }, @@ -2014,6 +2018,11 @@ data non_fold_residue_frontier: List = [ FrontierRow { subject: PathSubject { path: "src/v2/compiler/body_lowering_fold.dag::body_lower_where_leaf_atom_optional" }, reason: nfr_reason_landed_after_typed_census, dissolution: nfr_dissolve_owning_fold }, FrontierRow { subject: PathSubject { path: "src/v2/compiler/body_lowering_fold.dag::body_lower_where_predicate" }, reason: nfr_reason_landed_after_typed_census, dissolution: nfr_dissolve_owning_fold }, FrontierRow { subject: PathSubject { path: "src/v2/std/qualified_name.dag::lexical_reference_label_optional" }, reason: nfr_reason_landed_after_typed_census, dissolution: nfr_dissolve_owning_fold }, + FrontierRow { + subject: PathSubject { path: "src/v2/compiler/05_eval.dag::eval_field_projection_of_receiver" }, + reason: nfr_reason_eval_projection_receiver_not_aggregate, + dissolution: nfr_dissolve_eval_projection_receiver_not_aggregate, + }, ] fn non_fold_residue_frontier_units() -> List { diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index bee4917be57..1d41e31c269 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -507,7 +507,8 @@ fn arrow_domain_binder_name_list(n: Node) -> List { FirstPositionalFound { target: domain } => conj_named_binder_name_list(root: domain) FirstPositionalAbsent => [] } - _ => [] + TypeNode { connective: _ } => [] + ComputationNode { behavior: _ } => [] } } @@ -520,7 +521,8 @@ fn conj_named_binder_name_list(root: Node) -> List { Absent => acc } }) - _ => [] + TypeNode { connective: _ } => [] + ComputationNode { behavior: _ } => [] } } @@ -540,7 +542,8 @@ fn pattern_binder_name_list(ctx: ResolveContext, pat: Node) -> List { } else { list_snoc_item(xs: acc, item: id) } - _ => concat(acc, pattern_binder_name_list(ctx: ctx, pat: e.target)) + TypeNode { connective: _ } => concat(acc, pattern_binder_name_list(ctx: ctx, pat: e.target)) + ComputationNode { behavior: _ } => concat(acc, pattern_binder_name_list(ctx: ctx, pat: e.target)) } }) } @@ -559,7 +562,8 @@ fn bind_binder_name_list(n: Node) -> List { } else { [] } - _ => [] + TypeNode { connective: _ } => [] + ComputationNode { behavior: _ } => [] } } @@ -570,7 +574,8 @@ fn atom_name_list(root: Node) -> List { init: fn(n0) { match n0.kind { TypeNode { connective: Atom { identity: sym } } => [sym] - _ => [] + TypeNode { connective: _ } => [] + ComputationNode { behavior: _ } => [] } }, step: fn(acc, _e, child) { concat(acc, child) } @@ -1456,7 +1461,8 @@ fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional< ScopeUnbound => optional_absent() } } - _ => optional_absent() + TypeNode { connective: _ } => optional_absent() + ComputationNode { behavior: _ } => optional_absent() } } diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index cd6ce7abfd5..1a2145264f5 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -1633,7 +1633,8 @@ fn infer_conj_named_type_for_binding(domain: Node, binding: Symbol) -> Optional< Absent => infer_conj_edge_named_type_for_binding(edge: e, binding: binding) } }) - _ => Absent + TypeNode { connective: _ } => Absent + ComputationNode { behavior: _ } => Absent } } @@ -2256,7 +2257,8 @@ fn infer_type_head_and_args(t: Node) -> InferTypeHeadArgs { TailAbsent => InferTypeHeadArgs { head: head, args: Empty } } } - _ => InferTypeHeadArgs { head: t, args: Empty } + TypeNode { connective: _ } => InferTypeHeadArgs { head: t, args: Empty } + ComputationNode { behavior: _ } => InferTypeHeadArgs { head: t, args: Empty } } } @@ -2283,7 +2285,8 @@ fn infer_match_coproduct_of_type(scrutinee_type: Node, resolved: ResolvedTree) - } } } - _ => CoproductNotDeclared + TypeNode { connective: _ } => CoproductNotDeclared + ComputationNode { behavior: _ } => CoproductNotDeclared } } } @@ -2325,7 +2328,8 @@ fn infer_match_field_type_instantiated(field_type: Node, instances: List Absent + TypeNode { connective: _ } => Absent + ComputationNode { behavior: _ } => Absent } } @@ -2355,24 +2359,29 @@ fn infer_coproduct_arm_pattern(pat: Node) -> InferCoproductArmPattern { match pat.kind { TypeNode { connective: Atom { identity: id } } => if id == pattern_wildcard_name() { ArmPatternWildcard } else { ArmPatternUnread } - _ => - match declaration_reference_path_optional(node: pat) { + TypeNode { connective: _ } => infer_coproduct_arm_pattern_constructed(pat: pat) + ComputationNode { behavior: _ } => infer_coproduct_arm_pattern_constructed(pat: pat) + } +} + +// A non-atom arm pattern: a bare variant reference or a constructed pattern with field binders. +fn infer_coproduct_arm_pattern_constructed(pat: Node) -> InferCoproductArmPattern { + match declaration_reference_path_optional(node: pat) { + Present { value: path } => + match qualified_name_last_segment(qn: path) { + Present { value: tag } => ArmPatternVariant { tag: tag, fields: Empty } + Absent => ArmPatternUnread + } + Absent => + match construct_tag_path_optional(n: pat) { + Absent => ArmPatternUnread Present { value: path } => match qualified_name_last_segment(qn: path) { - Present { value: tag } => ArmPatternVariant { tag: tag, fields: Empty } Absent => ArmPatternUnread - } - Absent => - match construct_tag_path_optional(n: pat) { - Absent => ArmPatternUnread - Present { value: path } => - match qualified_name_last_segment(qn: path) { - Absent => ArmPatternUnread - Present { value: tag } => - match list_tail(xs: pat.children) { - TailFound { tail: fields } => ArmPatternVariant { tag: tag, fields: fields } - TailAbsent => ArmPatternVariant { tag: tag, fields: Empty } - } + Present { value: tag } => + match list_tail(xs: pat.children) { + TailFound { tail: fields } => ArmPatternVariant { tag: tag, fields: fields } + TailAbsent => ArmPatternVariant { tag: tag, fields: Empty } } } } @@ -2383,14 +2392,16 @@ fn infer_field_pattern_binds(target: Node) -> Optional { match target.kind { TypeNode { connective: Atom { identity: id } } => if (count(target.children) == 0) && (id != pattern_wildcard_name()) { optional_present(value: id) } else { optional_absent() } - _ => Absent + TypeNode { connective: _ } => Absent + ComputationNode { behavior: _ } => Absent } } fn infer_field_pattern_is_wildcard(target: Node) -> Bool { match target.kind { TypeNode { connective: Atom { identity: id } } => id == pattern_wildcard_name() - _ => false + TypeNode { connective: _ } => false + ComputationNode { behavior: _ } => false } } @@ -4716,7 +4727,8 @@ fn infer_where_predicate_set_edge(parent: Node, edge: Edge) -> Bool { Accepted { value: identity, diagnostics: _ } => match identity.kind { TypeNode { connective: Atom { identity: emitted } } => emitted == ^dag_surface_where_refinement_clause - _ => false + TypeNode { connective: _ } => false + ComputationNode { behavior: _ } => false } Rejected { diagnostics: _ } => false } diff --git a/src/v2/std/qualified_name.dag b/src/v2/std/qualified_name.dag index 50497cedd3a..f6b0bb5693c 100644 --- a/src/v2/std/qualified_name.dag +++ b/src/v2/std/qualified_name.dag @@ -300,7 +300,8 @@ fn qualified_name_spine_segment_nodes(root: Node) -> Optional> { } else { optional_absent() } - _ => optional_absent() + TypeNode { connective: _ } => optional_absent() + ComputationNode { behavior: _ } => optional_absent() } } From 2ec3636dbddbf58c1b64d30685a3e0bb12135bad Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 21:49:46 +0000 Subject: [PATCH 63/90] N7-1 WIP: lambda-binder projection base is a lexical reference; dependent-child infer driver instantiates the fold member Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_resolve.dag | 79 ++--- src/v2/compiler/04_infer.dag | 260 +++++++++++++-- src/v2/compiler/fold_lowering.dag | 125 ++++++- src/v2/std/node.dag | 32 ++ .../infer_fold_member_instance_test.dag | 313 ++++++++++++++++++ .../test/claim/n7probe/root_children_test.dag | 15 + 6 files changed, 756 insertions(+), 68 deletions(-) create mode 100644 src/v2/test/claim/compiler/infer_fold_member_instance_test.dag create mode 100644 src/v2/test/claim/n7probe/root_children_test.dag diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 1d41e31c269..fcbb3aca912 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1360,7 +1360,7 @@ fn resolve_bound_head_projection( ctx: ResolveContext, n: Node, pending: Diagnostics -) -> Outcome { +) -> Outcome { match qualified_name_spine_segment_nodes(root: n) { Absent => Rejected { @@ -1391,13 +1391,16 @@ fn resolve_bound_head_projection( Rejected { diagnostics: rejected_with_pending(pending: pending, rejected: r) } Present { value: Accepted { value: base, diagnostics: _ } } => outcome_with_diagnostics( - value: fold_list( - xs: field_segments, - empty: base, - cons: fn(acc, field_segment) { - field_projection_node(base: acc, field: field_segment, source: n) - } - ), + value: ResolvedAtom { + node: fold_list( + xs: field_segments, + empty: base.node, + cons: fn(acc, field_segment) { + field_projection_node(base: acc, field: field_segment, source: n) + } + ), + lexical: base.lexical + }, diagnostics: pending ) } @@ -1421,43 +1424,36 @@ fn resolve_bound_head_projection( // deletion is therefore a correctness repair on this function's own contract, claiming nothing about the // eight. // -// A NAMED FN'S PARAMETER AS THE HEAD IS THE SAME PATH-KEYED REFERENCE IT IS ANYWHERE ELSE. A ParameterFrame -// answers through resolve_frame_bound_reference, the one route that mints parameter_reference_node, so infer -// grounds the receiver through the resolved declarations exactly as it grounds a bare use of that parameter -// (v2.compiler.infer infer_parameter_reference_facts). A bare atom here was typed only by the scope search -// that main deleted, so it would leave every such receiver underived. A Lexical or type-parameter head stays -// the canonical atom: minting a lexical reference here would need its binding recorded, and this Outcome -// route carries no lexical entries, so it would refuse as unrecorded at infer. DECLARED FRONTIER, TRIGGER -// NAMING THE CAPABILITY: the projection route carries LexicalBindingEntry beside its node, SUFFICIENT FOR a -// let, match-arm or lambda binder to be a typed projection receiver. -fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional> { +// EVERY FRAME-BOUND HEAD IS THE SAME REFERENCE A BARE USE OF THAT BINDER IS, because it is minted by the +// same producer: resolve_frame_bound_reference, by the frame's own kind. A ParameterFrame head is the +// path-keyed parameter_reference_node infer grounds through the resolved declarations +// (v2.compiler.infer infer_parameter_reference_facts); a LexicalFrame head -- a let, match-arm or lambda +// binder -- is the lexical_reference_node whose binding is recorded by its occurrence, carried out of this +// route as ResolvedAtom.lexical so the walk records it exactly as it records a bare use; a type-parameter +// head stays the canonical atom that frame mints for any use. The Lexical arm was the canonical atom while +// this route carried no lexical entries, and a projection off a lambda binder (`fn(found, e) { e.target }`) +// then reached infer as a bare atom nothing had typed -- the receiver landed in the non-refusing +// ReceiverTypeUnderived arm, so the binder's declared type never reached the field it was projected through. +fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional> { match head_segment.kind { TypeNode { connective: Atom { identity: name } } => match lookup_chain(s: ctx.scope, name: name) { Rejected { diagnostics: _ } => optional_absent() Accepted { value: found, diagnostics: _ } => match found { - BoundInFrame { canonical: canonical, kind: ParameterFrame { arrow_path: arrow_path } } => - optional_present(value: match resolve_frame_bound_reference( + BoundInFrame { canonical: canonical, kind: kind } => + optional_present(value: resolve_frame_bound_reference( ctx: ctx, n: head_segment, canonical: canonical, - kind: ParameterFrame { arrow_path: arrow_path }, + kind: kind, pending: None - ) { - Accepted { value: atom, diagnostics: d } => Accepted { value: atom.node, diagnostics: d } - Rejected { diagnostics: r } => Rejected { diagnostics: r } - }) - BoundInFrame { canonical: canonical, kind: _ } => - optional_present(value: Accepted { - value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id), - diagnostics: None - }) + )) BoundAtRoot { canonical: canonical } => - optional_present(value: Accepted { + optional_present(value: resolved_atom_plain(o: Accepted { value: canonical_atom(identity: canonical, occurrence_id: head_segment.occurrence_id), diagnostics: None - }) + })) ScopeUnbound => optional_absent() } } @@ -1481,7 +1477,7 @@ fn resolve_projection_base(ctx: ResolveContext, head_segment: Node) -> Optional< // SO THIS IS NOT A REFUSAL BEING RELAXED. The state the refusal forbade -- the absolute read winning // over a binder the source spelled -- is still forbidden; it is now answered correctly instead of // refused, which is the climb the interim arm was waiting for. -fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional> { +fn try_resolve_qualified_name_node(ctx: ResolveContext, n: Node) -> Optional> { match qualified_name_from_node(root: n) { Rejected { diagnostics: _ } => optional_absent() Accepted { value: path, diagnostics: pending } => @@ -1544,11 +1540,11 @@ fn resolve_dotted_path_reading( path: QualifiedName, head_binding: ScopeBinding, pending: Diagnostics -) -> Outcome { +) -> Outcome { match head_binding { BoundInFrame { canonical: _, kind: _ } => resolve_bound_head_projection(ctx: ctx, n: n, pending: pending) - BoundAtRoot { canonical: _ } => resolve_declared_path_reading(ctx: ctx, n: n, path: path, pending: pending) - ScopeUnbound => resolve_declared_path_reading(ctx: ctx, n: n, path: path, pending: pending) + BoundAtRoot { canonical: _ } => resolved_atom_plain(o: resolve_declared_path_reading(ctx: ctx, n: n, path: path, pending: pending)) + ScopeUnbound => resolved_atom_plain(o: resolve_declared_path_reading(ctx: ctx, n: n, path: path, pending: pending)) } } @@ -1932,7 +1928,12 @@ fn resolved_atom_plain(o: Outcome) -> Outcome { // type-parameter lookups (whose scope carries no value binder) and a construct tag (which refuses a // non-constructor). The walk reads resolve_atom_answer and keeps the binding. fn resolve_atom(ctx: ResolveContext, n: Node, identity: Symbol) -> Outcome { - match resolve_atom_answer(ctx: ctx, n: n, identity: identity) { + resolved_atom_node(o: resolve_atom_answer(ctx: ctx, n: n, identity: identity)) +} + +// An atom's answer with its binding dropped, for a caller that places no value reference into the tree. +fn resolved_atom_node(o: Outcome) -> Outcome { + match o { Accepted { value: a, diagnostics: d } => Accepted { value: a.node, diagnostics: d } Rejected { diagnostics: r } => Rejected { diagnostics: r } } @@ -3030,7 +3031,7 @@ fn resolve_construct_tag_reference(ctx: ResolveContext, reference: Node, path: Q resolve_atom(ctx: ctx, n: reference, identity: only) Cons { head: _, tail: Cons { head: _, tail: _ } } => match try_resolve_qualified_name_node(ctx: ctx, n: reference) { - Present { value: outcome } => outcome + Present { value: outcome } => resolved_atom_node(o: outcome) Absent => Rejected { diagnostics: diagnostics_singleton(d: unbound_symbol_diagnostic(n: reference)) } } Empty => Rejected { diagnostics: diagnostics_singleton(d: unbound_symbol_diagnostic(n: reference)) } @@ -3165,7 +3166,7 @@ fn resolve_node_walk_entered( }) NotAConstruct => match try_resolve_qualified_name_node(ctx: ctx, n: n) { - Present { value: outcome } => resolve_walk_of_outcome(o: outcome) + Present { value: outcome } => resolve_walk_of_atom(o: outcome) Absent => resolve_children_homogeneous_scope(ctx: ctx, n: n) } } diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 1a2145264f5..a27beb383b9 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -65,7 +65,8 @@ import v2.std.optional { import v2.std.qualified_name { QualifiedName, declaration_reference_node, declaration_reference_path_optional, lexical_reference_label_optional, parameter_reference_path_optional } import v2.std.node_query { BinderParts, binder_node, binder_node_parts, construct_field_edges, construct_tag_optional, declared_field_from_edge } import v2.std.symbol_index { RecordTypePayload, SymbolIndex, VariantArmPayload, symbol_index_declared_payload_at, symbol_index_declared_type_params_at, symbol_index_lookup } -import v2.std.node { Ambiguous, Found, NotMarkedReference, arrow_body_target_lookup, arrow_named_edge_is_contract, arrow_signature_order_edge, match_arm_pattern_edge, resolved_reference_body_kind } +import v2.compiler.fold_lowering { FoldMemberFormal, FoldMemberRole, FoldStepTakesCollectionMember, FoldStepTakesNoMember, fold_collection_member_type, fold_realized_member_role, fold_step_member_formal } +import v2.std.node { NodeFoldDependent, fold_node_dependent, loop_domain_value_target, loop_realized_declaration_target, Ambiguous, Found, NotMarkedReference, arrow_body_target_lookup, arrow_named_edge_is_contract, arrow_signature_order_edge, match_arm_pattern_edge, resolved_reference_body_kind } import v2.std.list_introduction { list_introduction_elements_optional, list_introduction_head_path } import v2.std.arrow_signature { ApplicationBindingRefused, @@ -88,6 +89,7 @@ import v2.std.constraints { import v2.std.inhabitance { DeclaredType, DeclaredTypeObligation, + PositionDirectCallArgument, DeclaredTypePosition, DeclaredTypeVariable, TypeVariableInstance, @@ -943,12 +945,12 @@ fn infer_product_facts_from_entries( // declared type -- so `x` in `fn f(x: Pos)` reads Pos from f's own domain entry, never from whichever // Arrow a walk meets first. A path the index cannot answer here is the index disagreeing with the // resolver that minted it: refused, located, never left underived. -fn infer_node_facts(n: Node, partials: List, kinds: List, resolved: ResolvedTree) -> Outcome { +fn infer_node_facts(n: Node, partials: List, kinds: List, resolved: ResolvedTree, instances: List) -> Outcome { match parameter_reference_path_optional(node: n) { Present { value: path } => infer_parameter_reference_facts(n: n, path: path, index: resolved.resolved_declarations) Absent => match lexical_reference_label_optional(node: n) { - Present { value: _ } => infer_lexical_reference_facts(n: n, resolved: resolved) + Present { value: _ } => infer_lexical_reference_facts(n: n, resolved: resolved, instances: instances) Absent => infer_term_node_facts(n: n, partials: partials, kinds: kinds, resolved: resolved) } } @@ -960,7 +962,7 @@ fn infer_node_facts(n: Node, partials: List, kinds: List, // that declares a type (a lambda parameter's domain edge) grounds to it; one that declares none (a // `let` or a pattern binder, which carry no type here) is underived, as before. A reference resolve // did not record is the tree disagreeing with the resolver that produced it: refused, located. -fn infer_lexical_reference_facts(n: Node, resolved: ResolvedTree) -> Outcome { +fn infer_lexical_reference_facts(n: Node, resolved: ResolvedTree, instances: List) -> Outcome { match infer_descent_witness_for_node(n: n) { Violates { diagnostic: d } => Rejected { diagnostics: diagnostics_singleton(d: d) } Holds { value: descent_proof } => @@ -968,7 +970,7 @@ fn infer_lexical_reference_facts(n: Node, resolved: ResolvedTree) -> Outcome match binding.declared { Present { value: declared } => - inferred_facts_from_derived_type(node: n, derived_type: declared, descent: Holds { value: descent_proof }) + inferred_facts_from_derived_type(node: n, derived_type: infer_frame_instantiated(declared: declared, instances: instances), descent: Holds { value: descent_proof }) Absent => inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) } Absent => @@ -3750,8 +3752,9 @@ fn infer_gather_transform_frontier_on_entries( partials: List, kinds: List, resolved: ResolvedTree, + instances: List, ) -> InferGatherFoldAcc { - match infer_node_facts(n: node, partials: partials, kinds: kinds, resolved: resolved) { + match infer_node_facts(n: node, partials: partials, kinds: kinds, resolved: resolved, instances: instances) { Rejected { diagnostics: r } => infer_gather_fold_acc_failed( node: node, @@ -3902,7 +3905,7 @@ fn infer_gather_transform_row_on_entries( Present { value: elements } => match infer_transform_freemonoid_introduction(node: node, elements: elements, entries: entries, resolved: resolved) { Absent => - infer_gather_transform_frontier_on_entries(node: node, entries: entries, pending: pending, partials: partials, kinds: kinds, resolved: resolved) + infer_gather_transform_frontier_on_entries(node: node, entries: entries, pending: pending, partials: partials, kinds: kinds, resolved: resolved, instances: infer_no_type_variable_instances()) Present { value: introduced } => match introduced { Rejected { diagnostics: r } => @@ -4019,7 +4022,8 @@ fn infer_gather_application_row_on_entries( pending: merged_pending, partials: partials, kinds: kinds, - resolved: resolved + resolved: resolved, + instances: infer_no_type_variable_instances() ) } } @@ -4081,7 +4085,8 @@ fn infer_gather_bind_annotation_row_on_entries( pending: diagnostics_merge(outer: pending, inner: d), partials: partials, kinds: kinds, - resolved: resolved + resolved: resolved, + instances: infer_no_type_variable_instances() ) } } @@ -4217,14 +4222,15 @@ fn infer_transform_cast_optional(node: Node, entries: List, // added without deciding, at this site, whether it derives its type or joins the counted frontier // -- never defaulted into by omission (DESIGN §4 closed vocabulary, §5 unwritable-by-construction). -fn infer_gather_fold_not_derived(n: Node, partials: List, kinds: List, resolved: ResolvedTree) -> InferGatherFoldAcc { +fn infer_gather_fold_not_derived(n: Node, partials: List, kinds: List, resolved: ResolvedTree, instances: List) -> InferGatherFoldAcc { infer_gather_transform_frontier_on_entries( node: n, entries: empty_inferred_facts_entry_list(), pending: None, partials: partials, kinds: kinds, - resolved: resolved + resolved: resolved, + instances: instances ) } @@ -4236,7 +4242,7 @@ fn infer_gather_fold_not_derived(n: Node, partials: List, kinds: List, kinds: List, resolved: ResolvedTree) -> InferGatherFoldAcc { +fn infer_gather_fold_init(n: Node, partials: List, kinds: List, resolved: ResolvedTree, instances: List) -> InferGatherFoldAcc { match n.kind { ComputationNode { behavior: Branch } => infer_gather_fold_acc_ok( @@ -4271,7 +4277,7 @@ fn infer_gather_fold_init(n: Node, partials: List, kinds: List infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved) + ComputationNode { behavior: Value } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) ComputationNode { behavior: Transform } => if length(xs: n.children) == 0 { infer_gather_transform_row_on_entries( @@ -4307,18 +4313,18 @@ fn infer_gather_fold_init(n: Node, partials: List, kinds: List infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved) + Absent => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) } - TypeNode { connective: Atom { identity: _ } } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved) + TypeNode { connective: Atom { identity: _ } } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) TypeNode { connective: Conj } => match declaration_reference_path_optional(node: n) { - Present { value: _ } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved) + Present { value: _ } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) Absent => match parameter_reference_path_optional(node: n) { - Present { value: _ } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved) + Present { value: _ } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) Absent => match lexical_reference_label_optional(node: n) { - Present { value: _ } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved) + Present { value: _ } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) Absent => if infer_type_node_awaits_product_row(kinds: kinds, n: n) { infer_gather_fold_acc_ok( @@ -4332,7 +4338,7 @@ fn infer_gather_fold_init(n: Node, partials: List, kinds: List, kinds: List if infer_type_node_awaits_product_row(kinds: kinds, n: n) { @@ -4365,10 +4371,10 @@ fn infer_gather_fold_init(n: Node, partials: List, kinds: List infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved) - TypeNode { connective: Instantiation } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved) + TypeNode { connective: Cardinality } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) + TypeNode { connective: Instantiation } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) } } @@ -5553,10 +5559,208 @@ fn infer_gather_settled_unannotated_row( } } -fn infer_gather_fold_algebra(partials: List, kinds: List, resolved: ResolvedTree) -> NodeFold { - NodeFold { - init: fn(n0) { - infer_gather_fold_init(n: n0, partials: partials, kinds: kinds, resolved: resolved) +// THE STAGE'S ONE WALK IS A DEPENDENT-CHILD FOLD (v2.std.node fold_node_dependent), because one behavior +// row needs a sibling's settled result before its other child can be judged: a collection fold's step +// member formal is the fresh type variable function-value lowering minted, and only the Loop DOMAIN's +// synthesized type says what it stands for (gunbc.recurring_failure_mode +// infer_child_context_cannot_depend_on_a_sibling_result). So a fold-realized Loop folds its domain FIRST and +// its step under a frame that instantiates that variable; every other node keeps its authored order and +// hands its children the frame it was given, unchanged, which is fold_node's walk exactly. +// THE FRAME IS LEXICAL AND SCOPED TO THE STEP SUBTREE. It is the same List an +// application instantiates its callee's parameters with (v2.std.inhabitance), read at the one place a +// binder's declared type becomes a reference's type (infer_lexical_reference_facts); no second fact about +// the binder is minted, and no other route types a parameter. +type InferFrame { + instances: List + entering: InferFrameEntry +} + +// What the frame brings INTO the subtree it opens, charged once at that subtree's root: nothing; a +// counted advisory that the member could not be instantiated (the variable stays uninstantiated, and the +// census sees why); or a located refusal (an authored member formal the member does not inhabit). +type InferFrameEntry + = InferFrameEnteredPlain + | InferFrameEnteredWith { advisory: NonEmptyDiagnostics } + | InferFrameEnteredRefused { refused: NonEmptyDiagnostics } + +fn infer_root_frame() -> InferFrame { + InferFrame { instances: infer_no_type_variable_instances(), entering: InferFrameEnteredPlain } +} + +fn infer_frame_inherited(frame: InferFrame) -> InferFrame { + InferFrame { instances: frame.instances, entering: InferFrameEnteredPlain } +} + +// A binder's declared type, read through the frame: a type variable the frame instantiates IS its instance. +fn infer_frame_instantiated(declared: Node, instances: List) -> Node { + match declared.kind { + TypeNode { connective: Atom { identity: id } } => + match type_variable_instance_lookup(instances: instances, binder: id) { + Present { value: instance } => instance + Absent => declared + } + _ => declared + } +} + +fn infer_loop_member_role(loop: Node) -> Optional { + match loop.kind { + ComputationNode { behavior: Loop } => + match loop_realized_declaration_target(children: loop.children) { + Found { target: realized } => fold_realized_member_role(realized: realized) + _ => Absent + } + _ => Absent + } +} + +fn infer_loop_takes_collection_member(loop: Node) -> Bool { + match infer_loop_member_role(loop: loop) { + Present { value: FoldStepTakesCollectionMember } => true + _ => false + } +} + +fn infer_fold_member_advisory(reason: Symbol, at: Node) -> NonEmptyDiagnostics { + diagnostics_singleton(d: Diagnostic { + reason: reason, + at: node_locus(node: at), + correction: Unavailable { reason: CorrectionNotModeled } + }) +} + +fn infer_frame_with_advisory(frame: InferFrame, reason: Symbol, at: Node) -> InferFrame { + InferFrame { instances: frame.instances, entering: InferFrameEnteredWith { advisory: infer_fold_member_advisory(reason: reason, at: at) } } +} + +// THE DEPENDENT CHILD'S FRAME: the domain's settled type (already folded into `acc`, because the domain is +// scheduled first), its member by the fold realization's relation, and the step's member formal by ROLE. +// Every arm that cannot instantiate says so, typed and located, rather than leaving the variable to fall +// silently into the non-refusing ReceiverTypeUnderived arm. +fn infer_fold_member_frame(loop: Node, frame: InferFrame, acc: InferGatherFoldAcc, step: Node, resolved: ResolvedTree) -> InferFrame { + match loop_domain_value_target(children: loop.children) { + Found { target: domain } => + match lookup_inferred_facts_in_entries(entries: acc.entries, key: domain) { + Absent => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_type_not_derived, at: domain) + Present { value: facts } => + match inferred_facts_resolved_type(facts: facts) { + Violates { diagnostic: _ } => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_type_not_derived, at: domain) + Holds { value: domain_type } => + match fold_collection_member_type(domain_type: domain_type) { + Absent => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_not_a_collection, at: domain) + Present { value: member } => + match fold_step_member_formal(step: step) { + Absent => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_formal_unread, at: step) + Present { value: formal } => infer_fold_member_formal_frame(frame: frame, formal: formal, member: member, step: step, resolved: resolved) + } + } + } + } + _ => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_type_not_derived, at: loop) + } +} + +// A FRESH MEMBER FORMAL IS INSTANTIATED; AN AUTHORED ONE IS JUDGED. The member must inhabit an authored +// formal through the same declared_type_inhabitance fold an application argument meets at its formal, so +// `fn(acc, e: Int)` over a List refuses at that formal rather than typing `e` as either. +fn infer_fold_member_formal_frame(frame: InferFrame, formal: FoldMemberFormal, member: Node, step: Node, resolved: ResolvedTree) -> InferFrame { + match formal.formal.type_node.kind { + TypeNode { connective: Atom { identity: variable } } => + if formal.fresh { + InferFrame { + instances: list_snoc_item(xs: frame.instances, item: TypeVariableInstance { binder: variable, instance: member }), + entering: InferFrameEnteredPlain + } + } else { + infer_fold_member_judged_frame(frame: frame, formal: formal, member: member, step: step, resolved: resolved) + } + _ => infer_fold_member_judged_frame(frame: frame, formal: formal, member: member, step: step, resolved: resolved) + } +} + +fn infer_fold_member_judged_frame(frame: InferFrame, formal: FoldMemberFormal, member: Node, step: Node, resolved: ResolvedTree) -> InferFrame { + let obligation = DeclaredTypeObligation { + position: PositionDirectCallArgument, + parameter_identity: formal.formal.name, + declared: infer_declared_type_denotation(declared: formal.formal.type_node), + produced: member, + produced_declared_carrier: refinement_declared_carrier(index: resolved.resolved_declarations, source_type: member), + application: formal.formal.type_node, + type_variables: type_param_names(n: step) + } + match declared_type_inhabitance(obligation: obligation) { + Inhabits { homomorphism: _ } => InferFrame { instances: frame.instances, entering: InferFrameEnteredPlain } + InhabitanceUndecidable { reason: _ } => + infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_formal_undecided, at: formal.formal.type_node) + InhabitanceRefused { find_witness_reason: fw_reason } => + InferFrame { + instances: frame.instances, + entering: InferFrameEnteredRefused { + refused: diagnostics_singleton(d: application_argument_does_not_inhabit_diagnostic(obligation: obligation, find_witness_reason: fw_reason)) + } + } + } +} + +fn infer_gather_child_context(parent: Node, frame: InferFrame, acc: InferGatherFoldAcc, edge: Edge, resolved: ResolvedTree) -> InferFrame { + match edge.label { + Positional => + if infer_loop_takes_collection_member(loop: parent) { + infer_fold_member_frame(loop: parent, frame: infer_frame_inherited(frame: frame), acc: acc, step: edge.target, resolved: resolved) + } else { + infer_frame_inherited(frame: frame) + } + Named { name: _ } => infer_frame_inherited(frame: frame) + } +} + +fn infer_gather_schedules_first(parent: Node, edge: Edge) -> Bool { + match edge.label { + Named { name: name } => name == ^loop_domain_edge && infer_loop_takes_collection_member(loop: parent) + Positional => false + } +} + +// The frame's entry is charged at the subtree root it opens, after that root's own init. +fn infer_gather_framed_init(acc: InferGatherFoldAcc, frame: InferFrame) -> InferGatherFoldAcc { + match frame.entering { + InferFrameEnteredPlain => acc + InferFrameEnteredWith { advisory: a } => + InferGatherFoldAcc { + node: acc.node, + entries: acc.entries, + failed: acc.failed, + pending: diagnostics_merge(outer: Some { diagnostics: a }, inner: acc.pending), + await_branch_row: acc.await_branch_row, + await_match_row: acc.await_match_row, + await_loop_row: acc.await_loop_row, + await_transform_row: acc.await_transform_row, + children_remaining: acc.children_remaining + } + InferFrameEnteredRefused { refused: r } => + infer_gather_fold_acc_failed( + node: acc.node, + pending: rejected_with_pending(pending: acc.pending, rejected: r), + await_branch_row: acc.await_branch_row, + await_match_row: acc.await_match_row, + await_loop_row: acc.await_loop_row, + await_transform_row: acc.await_transform_row, + children_remaining: acc.children_remaining + ) + } +} + +fn infer_gather_fold_algebra(partials: List, kinds: List, resolved: ResolvedTree) -> NodeFoldDependent { + NodeFoldDependent { + init: fn(n0, frame) { + infer_gather_framed_init( + acc: infer_gather_fold_init(n: n0, partials: partials, kinds: kinds, resolved: resolved, instances: frame.instances), + frame: frame + ) + }, + first: fn(parent, e) { infer_gather_schedules_first(parent: parent, edge: e) }, + child_context: fn(parent, frame, acc, e) { + infer_gather_child_context(parent: parent, frame: frame, acc: acc, edge: e, resolved: resolved) }, step: fn(acc, e, child) { infer_gather_fold_step(acc: acc, edge: e, child: child, partials: partials, kinds: kinds, resolved: resolved) @@ -5572,7 +5776,7 @@ fn infer_entries_for_tree(tree: ResolvedTree) -> Outcome QualifiedName { + let d = match r { + FoldRealizesRosterOperation { template: _, row: row, slots: _ } => row + FoldRealizesDeclaration { declaration: declaration, slots: _ } => declaration + } + qualified_name_snoc(qn: qualified_name_from_dotted_string(dotted: d.module_path), segment: symbol_intern_lexeme(lexeme: d.decl_name)) +} + +fn fold_realization_is_at(head: Symbol, path: String) -> Bool { + match fold_family_realization(head: head) { + Present { value: r } => qualified_name_to_dotted_string(qn: fold_realization_declaration_path(r: r)) == path + Absent => false + } +} + +// Absent: the realized edge names no fold family declaration, so this Loop is not a fold realization. +fn fold_realized_member_role(realized: Node) -> Optional { + match declaration_reference_path_optional(node: realized) { + Absent => optional_absent() + Present { value: path } => + let dotted = qualified_name_to_dotted_string(qn: path) + if fold_realization_is_at(head: collection_fold_head, path: dotted) + || fold_realization_is_at(head: ^fold_list, path: dotted) + || fold_realization_is_at(head: ^fold_list_right, path: dotted) { + optional_present(value: FoldStepTakesCollectionMember) + } else if fold_realization_is_at(head: ^fold_node, path: dotted) { + optional_present(value: FoldStepTakesNoMember) + } else { + optional_absent() + } + } +} + +// THE COLLECTION A COLLECTION FOLD ITERATES IS A FREE MONOID, and its member is the monoid's one type +// argument: v2.std.collection List is std.algebra FreeMonoid. The domain's settled type is read as +// the Instantiation the declared type is (head, then one argument); any other shape -- a domain whose type +// is not one of these, or is not an instantiation at all -- is Absent, and the caller must say so rather +// than guess a member. +fn fold_collection_member_type(domain_type: Node) -> Optional { + match domain_type.kind { + TypeNode { connective: Instantiation } => + match node_positional_child_targets(node: domain_type) { + Cons { head: head, tail: Cons { head: member, tail: Empty } } => + match declaration_reference_path_optional(node: head) { + Present { value: path } => + if qualified_name_to_dotted_string(qn: path) == "v2.std.collection.List" + || qualified_name_to_dotted_string(qn: path) == "std.algebra.FreeMonoid" { + optional_present(value: member) + } else { + optional_absent() + } + Absent => optional_absent() + } + _ => optional_absent() + } + _ => optional_absent() + } +} + +// THE STEP'S MEMBER FORMAL, BY ROLE: the formal at declared position 1 of the step callable, never one +// found by name. `fresh` says whether its declared type is one of the step's own type variables -- the +// fresh variable function-value lowering minted for an unannotated binder -- which is then INSTANTIATED by +// the member; an authored type is instead judged against it. +type FoldMemberFormal { + formal: DeclaredField + fresh: Bool +} + +fn fold_step_member_formal(step: Node) -> Optional { + match arrow_declared_parameter_order(arrow: step) { + ArrowParameterOrderDeclared { labels: labels } => + match list_at_optional(xs: labels, index: 1) { + Absent => optional_absent() + Present { value: label } => + match list_at_optional(xs: node_positional_child_targets(node: step), index: 0) { + Absent => optional_absent() + Present { value: domain } => + fold_list(xs: domain.children, empty: optional_absent(), cons: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => + match declared_field_from_edge(e: e) { + Present { value: field } => + if field.name == label { + optional_present(value: FoldMemberFormal { formal: field, fresh: fold_type_is_variable_of(t: field.type_node, step: step) }) + } else { + acc + } + Absent => acc + } + } + }) + } + } + ArrowParameterOrderAbsent => optional_absent() + ArrowParameterOrderMalformed => optional_absent() + } +} + +fn fold_type_is_variable_of(t: Node, step: Node) -> Bool { + match t.kind { + TypeNode { connective: Atom { identity: id } } => + any(xs: type_param_names(n: step), predicate: fn(v) { v == id }) + _ => false + } +} + fn fold_recurrence_encoding(operands: FoldCallOperands) -> Node { let slot = fresh_fold_carrier(step_digest: content_hash(n: operands.step).digest as String) lower_bind( diff --git a/src/v2/std/node.dag b/src/v2/std/node.dag index 41735bc9c03..bb174f741a0 100644 --- a/src/v2/std/node.dag +++ b/src/v2/std/node.dag @@ -432,6 +432,38 @@ fn fold_node_topdown(n: Node, ctx: A, algebra: NodeFoldTopDown) -> R { }) } +// A CHILD WHOSE INFERENCE DEPENDS ON A SIBLING'S SETTLED RESULT. fold_node carries results only from +// child to parent, and fold_node_topdown carries context only from parent to child: its child_context +// sees the parent node, the inherited context and the edge, never what an earlier sibling synthesized. +// So neither algebra expresses "fold the dependency child, derive a scoped context from its result, then +// fold the dependent child under it" (gunbc.recurring_failure_mode +// infer_child_context_cannot_depend_on_a_sibling_result). This algebra does: child_context also receives +// the parent's accumulator AFTER the children already folded, and `first` schedules the dependency +// children ahead of the rest. +// THE SCHEDULE IS A PARTITION, SO IT IS A PERMUTATION BY CONSTRUCTION. `first` selects which edges are +// folded before the others, each part keeping its authored order; no schedule can drop, duplicate or +// invent an edge, so there is nothing to validate. With `first` false everywhere and a child_context that +// ignores the accumulator, this is fold_node_topdown exactly. +type NodeFoldDependent { + init: fn(Node, A) -> R + first: fn(Node, Edge) -> Bool + child_context: fn(Node, A, R, Edge) -> A + step: fn(R, Edge, R) -> R +} + +fn node_fold_dependent_schedule(n: Node, algebra: NodeFoldDependent) -> List { + concat( + fold(n.children, init: [], f: fn(acc, e) { if algebra.first(n, e) { list_snoc_item(xs: acc, item: e) } else { acc } }), + fold(n.children, init: [], f: fn(acc, e) { if algebra.first(n, e) { acc } else { list_snoc_item(xs: acc, item: e) } }) + ) +} + +fn fold_node_dependent(n: Node, ctx: A, algebra: NodeFoldDependent) -> R { + fold(node_fold_dependent_schedule(n: n, algebra: algebra), init: algebra.init(n, ctx), f: fn(acc, e) { + algebra.step(acc, e, fold_node_dependent(n: e.target, ctx: algebra.child_context(n, ctx, acc, e), algebra: algebra)) + }) +} + type EdgeDiscipline = NoEdges | LabeledEdges diff --git a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag new file mode 100644 index 00000000000..842a57a49c1 --- /dev/null +++ b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag @@ -0,0 +1,313 @@ +module v2.test.claim.compiler.infer_fold_member_instance + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { infer, infer_entries_for_tree, inferred_facts_resolved_type, lookup_inferred_facts_in_entries } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.witness { Holds, Violates } +import v2.test.claim.body_let_annotation { bla_assemble_with_peers, bla_q_artifact, bla_r_artifact, bla_source } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.std.collection { List } +import v2.std.diagnostic { Accepted, CorrectionNotModeled, Diagnostic, Locus, NodeLocus, Outcome, Rejected, Unavailable, Some, diagnostics_has_reason, diagnostics_fatal, diagnostics_fatal_reason, diagnostics_singleton, node_locus } +import v2.std.node_query { binder_type_label, declared_field_from_edge } +import std.algebra { list_snoc_item } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import std.process { ExitFailure, ExitSuccess, ProcessExit } +import v2.std.node { Arrow, Atom, Bind, Branch, Cardinality, Conj, Disj, EdgeLabel, Instantiation, Match, Named, NodeKind, Positional, Transform, TypeNode, Value, ComputationNode, Edge, Found, Loop, Node, Symbol, node_with_occurrence_id, loop_domain_value_target, node_subtree_nodes } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE PEERS RESTATE THE REAL DECLARATIONS, as v2.test.claim.body_let_annotation +// bla_structural_string_peers does: v2.std.collection List is std.algebra FreeMonoid, and the +// one-module ingest cannot otherwise bind List. +fn fmi_list_peers() -> List { + [ + bla_source(src: "module v2.std.collection\n\nimport std.algebra { FreeMonoid }\n\ntype List = std.algebra.FreeMonoid\n", artifact: bla_q_artifact, cu: ^body_let_annotation_q_cu), + bla_source(src: "module std.algebra\n\ntype FreeMonoid = | Empty | Cons { head: T, tail: FreeMonoid }\n", artifact: bla_r_artifact, cu: ^body_let_annotation_r_cu) + ] +} + +fn fmi_assemble(src: String) -> Outcome { + bla_assemble_with_peers(p_src: src, peers: fmi_list_peers()) +} + +fn fmi_infer(src: String) -> Outcome { + match fmi_assemble(src: src) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: tree, diagnostics: _ } => fmi_infer_tree(tree: tree) + } +} + +fn fmi_infer_tree(tree: ResolvedTree) -> Outcome { + match infer(tree: tree) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: _, diagnostics: d } => Accepted { value: true, diagnostics: d } + } +} + +data fmi_record_fold: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Bool {\n fold(xs, init: false, f: fn(found, e) { found || e.target == 0 })\n}\n" + +data fmi_undeclared_field_fold: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Bool {\n fold(xs, init: false, f: fn(found, e) { found || e.nope == 0 })\n}\n" + + +fn fmi_reason(o: Outcome) -> Symbol { + match o { + Accepted { value: _, diagnostics: _ } => ^accepted + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) + } +} + +fn fmi_probe_record() -> Symbol { fmi_reason(o: fmi_infer(src: fmi_record_fold)) } +fn fmi_probe_undeclared() -> Symbol { fmi_reason(o: fmi_infer(src: fmi_undeclared_field_fold)) } +fn fmi_probe_incompatible() -> Symbol { fmi_reason(o: fmi_infer_with_member_formal_authored()) } + +data fmi_no_projection_fold: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Bool {\n fold(xs, init: false, f: fn(found, e) { found })\n}\n" + +fn fmi_probe_domain_type() -> Optional { + match fmi_assemble(src: fmi_no_projection_fold) { + Rejected { diagnostics: _ } => Absent + Accepted { value: tree, diagnostics: _ } => + match infer_entries_for_tree(tree: tree) { + Rejected { diagnostics: _ } => Absent + Accepted { value: entries, diagnostics: _ } => + fold(node_subtree_nodes(root: tree.root), init: Absent, f: fn(acc, n) { + match n.kind { + ComputationNode { behavior: Loop } => + match loop_domain_value_target(children: n.children) { + Found { target: d } => + match lookup_inferred_facts_in_entries(entries: entries, key: d) { + Present { value: facts } => + match inferred_facts_resolved_type(facts: facts) { + Holds { value: t } => Present { value: t } + Violates { diagnostic: _ } => acc + } + Absent => acc + } + _ => acc + } + _ => acc + } + }) + } + } +} + +fn fmi_locus(o: Outcome) -> Optional { + match o { + Accepted { value: _, diagnostics: _ } => Absent + Rejected { diagnostics: d } => Present { value: diagnostics_fatal(d: d).at } + } +} + +fn fmi_probe_record_locus() -> Optional { fmi_locus(o: fmi_infer(src: fmi_record_fold)) } + +fn fmi_kind_text(k: NodeKind) -> String { + match k { + TypeNode { connective: Atom { identity: id } } => id as String + TypeNode { connective: Conj } => "Conj" + TypeNode { connective: Disj } => "Disj" + TypeNode { connective: Arrow } => "Arrow" + TypeNode { connective: Cardinality } => "Card" + TypeNode { connective: Instantiation } => "Inst" + ComputationNode { behavior: Value } => "Value" + ComputationNode { behavior: Transform } => "Transform" + ComputationNode { behavior: Branch } => "Branch" + ComputationNode { behavior: Loop } => "Loop" + ComputationNode { behavior: Bind } => "Bind" + ComputationNode { behavior: Match } => "Match" + } +} + +fn fmi_edge_text(l: EdgeLabel) -> String { + match l { + Named { name: s } => concat(s as String, "=") + Positional => "" + } +} + +fn fmi_render(n: Node) -> String { + fold(n.children, init: concat(fmi_kind_text(k: n.kind), "("), f: fn(acc, e) { + concat(concat(concat(acc, fmi_edge_text(l: e.label)), fmi_render(n: e.target)), " ") + }) +} + +fn fmi_render_domain() -> String { + match fmi_probe_domain_type() { + Present { value: t } => fmi_render(n: t) + Absent => "absent" + } +} + +fn fmi_render_locus(o: Outcome) -> String { + match o { + Accepted { value: _, diagnostics: _ } => "accepted" + Rejected { diagnostics: d } => + match diagnostics_fatal(d: d).at { + NodeLocus { anchor: a } => fmi_render(n: a.at) + _ => "non-node locus" + } + } +} + +fn fmi_render_record_locus() -> String { fmi_render_locus(o: fmi_infer(src: fmi_record_fold)) } + +fn fmi_all_reasons(o: Outcome) -> String { + match o { + Accepted { value: _, diagnostics: _ } => "accepted" + Rejected { diagnostics: d } => + fold(d.tail, init: d.head.reason as String, f: fn(acc, x) { concat(concat(acc, " | "), x.reason as String) }) + } +} + +fn fmi_reasons_record() -> String { fmi_all_reasons(o: fmi_infer(src: fmi_record_fold)) } +fn fmi_reasons_noproj() -> String { fmi_all_reasons(o: fmi_infer(src: fmi_no_projection_fold)) } + +fn fmi_render_resolved_root() -> String { + match fmi_assemble(src: fmi_no_projection_fold) { + Rejected { diagnostics: _ } => "rejected" + Accepted { value: tree, diagnostics: _ } => fmi_render(n: tree.root) + } +} + +// (1) A FOLD OVER A COLLECTION OF RECORDS PERMITS e.target: no diagnostic anywhere in the outcome refuses +// the projection, where the unrepaired walk refused it infer_reason_projection_receiver_declares_no_fields +// (`e` typed as its bare fresh variable). Asserted over the WHOLE chain rather than as an Accepted outcome +// because the fixture's step Arrow still refuses infer_grounding_not_derived -- a separate, named link that +// is the next wall, refused identically on the base -- and this control must not change verdict when it +// lands. The mutation red: delete the domain-to-step frame join in v2.compiler.infer +// infer_gather_child_context and this control refuses again. +test fn fmi_fold_member_projects_a_declared_field() -> Bool { + match fmi_assemble(src: fmi_record_fold) { + Rejected { diagnostics: _ } => false + Accepted { value: tree, diagnostics: _ } => + fmi_no_projection_refusal(o: fmi_infer_tree(tree: tree)) + } +} + +fn fmi_no_projection_refusal(o: Outcome) -> Bool { + !fmi_has_reason(o: o, reason: ^infer_reason_projection_receiver_declares_no_fields) + && !fmi_has_reason(o: o, reason: ^infer_reason_field_not_declared_on_receiver) + && !fmi_has_reason(o: o, reason: ^infer_reason_projection_receiver_declaration_unavailable) +} + +fn fmi_has_reason(o: Outcome, reason: Symbol) -> Bool { + match o { + Accepted { value: _, diagnostics: d } => diagnostics_has_reason(d: d, reason: reason) + Rejected { diagnostics: d } => diagnostics_has_reason(d: Some { diagnostics: d }, reason: reason) + } +} + +// (2) THE SAME FOLD WITH AN UNDECLARED FIELD REFUSES AT THAT FIELD. +test fn fmi_fold_member_undeclared_field_refuses() -> Bool { + fmi_reason(o: fmi_infer(src: fmi_undeclared_field_fold)) == ^infer_reason_field_not_declared_on_receiver +} + +// (3) A STEP FORMAL INCOMPATIBLE WITH THE DOMAIN ELEMENT REFUSES. +test fn fmi_fold_member_incompatible_formal_refuses() -> Bool { + fmi_reason(o: fmi_infer_with_member_formal_authored()) == ^application_argument_does_not_inhabit +} + +// (3) IS SUPPLIED AT THE RESOLVE -> INFER BOUNDARY, because its red is not authorable in source: a typed +// lambda formal (`fn(found: Bool, e: Int)`, `(found: Bool, e: Int) =>`) refuses at parse with +// parse_g0_tokens_remain. The supplied tree is the REAL producer's output for fmi_record_fold -- the +// inhabitance claim for this boundary is control (1), which runs it -- with the member formal's declared +// type rewritten from its fresh variable to the record field's own `Int`, i.e. an authored formal the +// member Pair does not inhabit. The refusal is charged at the step root, before any use of `e` is read. +fn fmi_member_formal_type(root: Node, name: Symbol) -> Optional { + fold(node_subtree_nodes(root: root), init: optional_absent(), f: fn(acc, n) { + match acc { + Present { value: _ } => acc + Absent => + fold(n.children, init: optional_absent(), f: fn(inner, e) { + match inner { + Present { value: _ } => inner + Absent => + match e.label { + Named { name: label } => + if label == name { + match declared_field_from_edge(e: e) { + Present { value: field } => optional_present(value: field.type_node) + Absent => optional_absent() + } + } else { + optional_absent() + } + Positional => optional_absent() + } + } + }) + } + }) +} + +fn fmi_replace_atom(n: Node, from: Symbol, to: Node) -> Node { + Node { + kind: n.kind, + children: fold(n.children, init: [], f: fn(acc, e) { + list_snoc_item(xs: acc, item: Edge { label: e.label, target: fmi_replace_binder_type(e: e, from: from, to: to) }) + }), + occurrence_id: n.occurrence_id + } +} + +// Only a member is rewritten: the variable's own entry in stays, so the +// tree stays well-formed and the formal simply stops being declared as that variable. +fn fmi_replace_binder_type(e: Edge, from: Symbol, to: Node) -> Node { + match e.label { + Named { name: label } => + if label == binder_type_label() { + match e.target.kind { + TypeNode { connective: Atom { identity: id } } => + if id == from { node_with_occurrence_id(kind: to.kind, children: to.children, occurrence_id: e.target.occurrence_id) } else { e.target } + _ => fmi_replace_atom(n: e.target, from: from, to: to) + } + } else { + fmi_replace_atom(n: e.target, from: from, to: to) + } + Positional => fmi_replace_atom(n: e.target, from: from, to: to) + } +} + +fn fmi_infer_with_member_formal_authored() -> Outcome { + match fmi_assemble(src: fmi_record_fold) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: tree, diagnostics: _ } => + match fmi_member_formal_type(root: tree.root, name: ^e) { + Absent => Rejected { diagnostics: diagnostics_singleton(d: fmi_fixture_shape_diagnostic(at: tree.root)) } + Present { value: fresh } => + match fmi_member_formal_type(root: tree.root, name: ^target) { + Absent => Rejected { diagnostics: diagnostics_singleton(d: fmi_fixture_shape_diagnostic(at: tree.root)) } + Present { value: int_type } => + match fresh.kind { + TypeNode { connective: Atom { identity: variable } } => + match infer(tree: ResolvedTree { + root: fmi_replace_atom(n: tree.root, from: variable, to: int_type), + symbol_index: tree.symbol_index, + resolved_declarations: tree.resolved_declarations, + lexical_bindings: tree.lexical_bindings + }) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: _, diagnostics: d } => Accepted { value: true, diagnostics: d } + } + _ => Rejected { diagnostics: diagnostics_singleton(d: fmi_fixture_shape_diagnostic(at: fresh)) } + } + } + } + } +} + +fn fmi_fixture_shape_diagnostic(at: Node) -> Diagnostic { + Diagnostic { reason: ^fmi_fixture_shape_unexpected, at: node_locus(node: at), correction: Unavailable { reason: CorrectionNotModeled } } +} + +fn fmi_scratch_all() -> ProcessExit { + if !fmi_fold_member_projects_a_declared_field() { + ExitFailure { code: 1, reason: "c1" } + } else if !fmi_fold_member_undeclared_field_refuses() { + ExitFailure { code: 2, reason: "c2" } + } else if !fmi_fold_member_incompatible_formal_refuses() { + ExitFailure { code: 3, reason: "c3" } + } else { + ExitSuccess + } +} diff --git a/src/v2/test/claim/n7probe/root_children_test.dag b/src/v2/test/claim/n7probe/root_children_test.dag new file mode 100644 index 00000000000..799824e4d31 --- /dev/null +++ b/src/v2/test/claim/n7probe/root_children_test.dag @@ -0,0 +1,15 @@ +module v2.test.n7probe.root_children + +import v2.std.node { Edge, Node } +import v2.std.logic { Bool } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +fn n7_children(root: Node) -> List { + root.children +} + +test fn n7_probe_trivially_true() -> Bool { + true +} From 6425323b0d9485667024d80e6c8c0336c3897883 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 21:55:59 +0000 Subject: [PATCH 64/90] N7-1: control for the lambda-binder projection base (r) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../infer_fold_member_instance_test.dag | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag index 842a57a49c1..7b15ebf112b 100644 --- a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag +++ b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag @@ -8,7 +8,8 @@ import v2.test.claim.body_let_annotation { bla_assemble_with_peers, bla_q_artifa import v2.compiler.source_authority { DagSourceReadWitness } import v2.std.collection { List } import v2.std.diagnostic { Accepted, CorrectionNotModeled, Diagnostic, Locus, NodeLocus, Outcome, Rejected, Unavailable, Some, diagnostics_has_reason, diagnostics_fatal, diagnostics_fatal_reason, diagnostics_singleton, node_locus } -import v2.std.node_query { binder_type_label, declared_field_from_edge } +import v2.std.node_query { binder_type_label, declared_field_from_edge, field_projection_optional } +import v2.std.qualified_name { lexical_reference_label_optional } import std.algebra { list_snoc_item } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } @@ -197,6 +198,27 @@ fn fmi_has_reason(o: Outcome, reason: Symbol) -> Bool { } } +// (r) A LAMBDA BINDER PROJECTED THROUGH IS THE SAME LEXICAL REFERENCE A BARE USE OF IT IS: resolve mints +// the `e` of `e.target` through v2.compiler.resolve resolve_frame_bound_reference, so its binding is +// recorded and infer can type it. RED ON THE BASE: the base was the bare canonical atom `e`, which nothing +// typed, so the projection landed in the non-refusing ReceiverTypeUnderived arm. +test fn fmi_lambda_binder_projection_base_is_a_lexical_reference() -> Bool { + match fmi_assemble(src: fmi_record_fold) { + Rejected { diagnostics: _ } => false + Accepted { value: tree, diagnostics: _ } => + fold(node_subtree_nodes(root: tree.root), init: false, f: fn(acc, n) { + acc || match field_projection_optional(n: n) { + Present { value: projection } => + match lexical_reference_label_optional(node: projection.base) { + Present { value: label } => label == ^e + Absent => false + } + Absent => false + } + }) + } +} + // (2) THE SAME FOLD WITH AN UNDECLARED FIELD REFUSES AT THAT FIELD. test fn fmi_fold_member_undeclared_field_refuses() -> Bool { fmi_reason(o: fmi_infer(src: fmi_undeclared_field_fold)) == ^infer_reason_field_not_declared_on_receiver @@ -303,6 +325,8 @@ fn fmi_fixture_shape_diagnostic(at: Node) -> Diagnostic { fn fmi_scratch_all() -> ProcessExit { if !fmi_fold_member_projects_a_declared_field() { ExitFailure { code: 1, reason: "c1" } + } else if !fmi_lambda_binder_projection_base_is_a_lexical_reference() { + ExitFailure { code: 4, reason: "cr" } } else if !fmi_fold_member_undeclared_field_refuses() { ExitFailure { code: 2, reason: "c2" } } else if !fmi_fold_member_incompatible_formal_refuses() { From d09ec4fb1e4df264407ff6f2cfcc9e2a3b47a22e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 22:29:57 +0000 Subject: [PATCH 65/90] N7-1: climb receipt on infer_child_context_cannot_depend_on_a_sibling_result; Bind/Match frontier row Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ot_typed_from_their_dependency_sibling.dag | 20 +++++++++++++++++++ ...text_cannot_depend_on_a_sibling_result.dag | 6 ++++++ 2 files changed, 26 insertions(+) create mode 100644 dag/gunbc/recurring_failure_mode/bind_and_match_binders_not_typed_from_their_dependency_sibling.dag diff --git a/dag/gunbc/recurring_failure_mode/bind_and_match_binders_not_typed_from_their_dependency_sibling.dag b/dag/gunbc/recurring_failure_mode/bind_and_match_binders_not_typed_from_their_dependency_sibling.dag new file mode 100644 index 00000000000..70ee00af7e8 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/bind_and_match_binders_not_typed_from_their_dependency_sibling.dag @@ -0,0 +1,20 @@ +module gunbc.recurring_failure_mode.bind_and_match_binders_not_typed_from_their_dependency_sibling + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data bind_and_match_binders_not_typed_from_their_dependency_sibling: RecurringFailureMode = RecurringFailureMode { + identity: "bind_and_match_binders_not_typed_from_their_dependency_sibling" as NonEmptyStr, + receipts: [ + "INVALID STATE: two behavior rows bind a name whose type is fixed by a SIBLING child's settled result, and v2.compiler.infer types neither binder from it. An unannotated `let x = e` binds x in the Bind's body, but x's LexicalBinding declares no type (v2.compiler.infer infer_lexical_reference_facts reads binding.declared Absent and leaves the reference underived), although the bound value e is folded in the same row. A match-arm pattern binder is typed by the scrutinee's settled type and the matched variant, and the binder-typing for it is #12641's subject (v2.test.claim.field_projection.field_projection_stages fps_a_match_binder_receiver_does_not_yet_infer_holds records it as not yet inferring). HARM: a projection or application off such a binder stays on the counted frontier or refuses for missing evidence instead of being judged -- loud, never fabricated, but valid programs do not type. This is the same class as gunbc.recurring_failure_mode infer_child_context_cannot_depend_on_a_sibling_result, whose collection-fold case is climbed; these two rows are its remaining population.", + "DISTINGUISHING FACTS: the traversal is no longer the obstacle. v2.std.node fold_node_dependent lets a row schedule its dependency child first and derive the dependent child's context from the parent accumulator, and v2.compiler.infer infer_gather_child_context already does so for a fold-realized Loop. What these rows lack is the BINDING SIDE: a Bind's binder carries no type variable for a frame to instantiate (the fold step's member formal does, minted by function-value lowering), and the match-arm binder's relation to the scrutinee is variant-and-field-indexed rather than one member type. So reusing the driver is necessary and not sufficient, and building either row's frame here, without its binder carrier, would be a frame nothing reads (DESIGN section 3c).", + "RUNG FOUND AT: mitigatable -- refused or left underived, never wrong. CEILING: structurally guaranteed; both relations are decidable from the sibling's settled type. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: a Bind binder carries its own type position (declared or fresh), and infer_gather_child_context schedules the bound value before the body and instantiates that position from the value's settled type for the body subtree only; and a match-arm binder is instantiated from the scrutinee's settled type through the matched variant's field, scheduled the same way -- SUFFICIENT FOR `let r = n; r.children` and a match-arm binder's projection to type through the one frame, with no second parameter-typing route.", + ], + evidence: [ + DeclarationRef { module_path: "v2.std.node", decl_name: "fold_node_dependent", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_gather_child_context", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_lexical_reference_facts", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.field_projection.field_projection_stages", decl_name: "fps_a_match_binder_receiver_does_not_yet_infer_holds", field: WholeDeclaration }, + ], +} diff --git a/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag b/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag index c83c789205f..4da876ebac7 100644 --- a/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag +++ b/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag @@ -10,6 +10,7 @@ data infer_child_context_cannot_depend_on_a_sibling_result: RecurringFailureMode "INVALID STATE: v2.compiler.infer infers every child subtree independently before its parent's behavior row executes. A Loop domain's SYNTHESIZED element type is required to instantiate the step member formal's fresh type variable, but no inference traversal can carry one child's settled result into another child's inference context. The fold encoding compounds it by storing the step BEFORE the domain (v2.compiler.fold_lowering fold_recurrence_encoding emits Positional step as child 0 and the named loop_domain_edge as child 1), so ordinary left-to-right child order cannot supply the relation either. HARM: the valid helper `fold(root.children, init: false, f: fn(found, e) { found || g_tree_has_arrow_body(root: e.target) })` is refused at `e.target` with infer_reason_projection_receiver_declares_no_fields -- `e` stays typed as its fresh variable instead of Edge, although the Loop domain carries `root.children: List` and Edge declares `target: Node`. The refusal is typed and located and nothing is fabricated, so this is a capability gap on the loud side of the ladder, never a silent wrong answer.", "DISTINGUISHING FACTS: this is NOT a missing loop_domain_edge reader, NOT cross-module declaration retrieval, and NOT a projection-classification defect -- each of those was measured and excluded. v2.std.node fold_node carries SYNTHESIZED results only from child to parent (step: fn(R, Edge, R) -> R, whose third argument is the child's already-folded result, computed by a recursive call that receives nothing from the parent). v2.std.node fold_node_topdown carries INHERITED context only from parent to child, and its child_context: fn(Node, A, Edge) -> A receives the parent node, the inherited context and the current edge -- no folded sibling result. So neither algebra expresses `infer the domain child, derive the member instance, then infer the step child under it`, and converting infer's gather from NodeFold to NodeFoldTopDown is a DISPROVEN route rather than the trigger: it would change the shape of the stage's single walk across all behavior arms and still not carry the fact. Adding the role reader, the domain consumer or the List element relation before the traversal exists would make each declaration unreachable from any production verdict, which is the dangling modeling DESIGN section 3c forbids. The three facts the repair consumes already exist and are cited below: the fresh variable is minted by v2.std.anonymous_binder fresh_type_variable, the existing lambda-parameter route correctly derives the member formal AS that variable (v2.compiler.infer infer_lexical_reference_facts, reading the binding v2.compiler.resolve recorded in ResolvedTree.lexical_bindings), and the receiver rule that refuses is infer_projection_receiver -- so no second parameter-typing route may be introduced; the existing variable must be instantiated.", "RUNG FOUND AT: mitigatable -- the compiler refuses rather than fabricating a field-bearing type, but valid fold programs cannot type. ATTAINABLE CEILING: structurally guaranteed -- the element relation is decidable from the domain's own type, and every fact it needs is already established by a stage that runs before the step subtree is judged; what is missing is one driver whose behavior-specific child schedule can infer a dependency child once, derive a scoped context from its settled result, and infer the dependent child once under that context. NEXT-RUNG TRIGGER, stated as the capability: an infer-local dependent-child driver schedules the Loop domain before the step, derives the collection-fold member instance from the domain type, extends a lexical TypeVariableInstance frame FOR THE STEP SUBTREE ONLY, and preserves the existing behavior rows, SUFFICIENT FOR the unchanged production helper to establish `e: Edge` and `e.target: Node`. Its discriminating red is a mutation deleting the domain-to-step context join, which must make that control fail. The element relation must be scoped by FOLD REALIZATION rather than by assuming every Loop is List, and the role authority (the step callable's actual 0 is the carrier and actual 1 is the domain member, stated today in v2.compiler.fold_lowering) belongs in one decoder rather than being re-read per consumer.", + "CLIMB (N7-1): the trigger is built. v2.std.node fold_node_dependent is the dependent-child traversal -- child_context also receives the parent's accumulator after the children already folded, and a `first` partition schedules dependency children ahead of the rest, so the schedule is a permutation by construction. v2.compiler.infer infer_entries_for_tree walks it with a lexical InferFrame of TypeVariableInstance: a Loop whose realized declaration hands its step a collection member (v2.compiler.fold_lowering fold_realized_member_role) folds its domain first, and infer_fold_member_frame instantiates the step's member formal -- read by ROLE, declared position 1 (fold_step_member_formal) -- from the domain's settled type through fold_collection_member_type, for the step subtree only; infer_lexical_reference_facts reads a binder's declared type through that frame. An authored member formal is judged by declared_type_inhabitance instead of instantiated, and every arm that cannot instantiate is a counted advisory at the subtree root rather than a silent uninstantiated variable. A SECOND, EARLIER BOUNDARY WAS FOUND ON THE SAME SLICE: v2.compiler.resolve resolve_projection_base minted a lambda binder used as a projection base as the bare canonical atom, recording no lexical binding, so `e` in `e.target` never reached infer typed as its fresh variable at all and the projection fell into the non-refusing ReceiverTypeUnderived arm. It is now minted through resolve_frame_bound_reference like a bare use. Controls: v2.test.claim.compiler.infer_fold_member_instance (red on the base for the projection-base and member-typing controls; the frame-join deletion reds the e.target control and the resolve mutation reds the projection-base control). The native target's first wall was NOT this class: it was cross-module receiver retrieval at `root.children`.", ], evidence: [ DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_entries_for_tree", field: WholeDeclaration }, @@ -22,5 +23,10 @@ data infer_child_context_cannot_depend_on_a_sibling_result: RecurringFailureMode DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_lexical_reference_facts", field: WholeDeclaration }, DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_projection_receiver", field: WholeDeclaration }, DeclarationRef { module_path: "v2.std.anonymous_binder", decl_name: "fresh_type_variable", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.std.node", decl_name: "fold_node_dependent", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_fold_member_frame", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "fold_realized_member_role", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.resolve", decl_name: "resolve_projection_base", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.compiler.infer_fold_member_instance", decl_name: "fmi_fold_member_projects_a_declared_field", field: WholeDeclaration }, ], } From 422a28c7a2d7d07b57755d3a5d78e41d1198221f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 23:16:10 +0000 Subject: [PATCH 66/90] N7-1: rename the keyword-spelled loop parameters (native parse refusal of 04_infer); strip debug probes; keyword-as-value receipt Co-Authored-By: Claude Opus 5.5 (1M context) --- ...d_admits_a_keyword_as_a_pattern_binder.dag | 1 + src/v2/compiler/04_infer.dag | 22 +-- .../infer_fold_member_instance_test.dag | 149 ++---------------- 3 files changed, 22 insertions(+), 150 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/seed_admits_a_keyword_as_a_pattern_binder.dag b/dag/gunbc/recurring_failure_mode/seed_admits_a_keyword_as_a_pattern_binder.dag index 54c03501182..5945fa785b0 100644 --- a/dag/gunbc/recurring_failure_mode/seed_admits_a_keyword_as_a_pattern_binder.dag +++ b/dag/gunbc/recurring_failure_mode/seed_admits_a_keyword_as_a_pattern_binder.dag @@ -13,6 +13,7 @@ data seed_admits_a_keyword_as_a_pattern_binder: RecurringFailureMode = Recurring "SCOPE STILL OPEN, NOT A CLAIMED SPECIMEN: field access with a keyword name -- `interpretation.loop` in v2.compiler.eval and `a.loop` in v2.std.runtime -- is the same seed leniency at a different grammar position, but both files already refuse natively for an unrelated reason (body_lowering_reason_caret_symbol_not_lowered), so whether the keyword after a dot also refuses is masked there and is not asserted here.", "RUNG FOUND AT AND CURRENT RUNG: mechanically preventable on the native route, whose parser refuses the spelling; the seed side is outside the wall. v1 is semantics-frozen (gunbc.v1_maintenance_standing v1_seed_standing), so the seed leniency is recorded, not patched.", "CEILING AND NEXT-RUNG TRIGGER: structurally impossible once no v2 source is admitted through the seed parser alone -- the native parse is the one grammar every module must pass. The capability that retires the leniency's reach is the native route qualifying the corpus it parses, so a keyword binder refuses at a gate every PR runs rather than only in a census.", + "SECOND SHAPE (N7-1, gunbc#13028): a fn PARAMETER spelled `loop` and then READ as a value -- `fn infer_loop_member_role(loop: Node)` with `loop.kind`, `loop: loop`, `at: loop` in its body. The v2 parser admits the declaration (`fn f(loop: Int) -> Int { 1 }` parses) and refuses every value use with parse_g0_tokens_remain, so the whole of v2.compiler.infer became a native file refusal while every seed-interpreted control stayed green. Found by bisecting the file's new items through v2.compiler.parse parse_module_prepared under the seed: exactly the three items that read `loop` refused. The same root as the pattern-binder shape -- the seed lexes a keyword as an identifier -- reached through a binding position the pattern-binder repair does not cover.", ], evidence: [], } diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index a27beb383b9..3f3f44f1d6b 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -5603,10 +5603,10 @@ fn infer_frame_instantiated(declared: Node, instances: List Optional { - match loop.kind { +fn infer_loop_member_role(loop_node: Node) -> Optional { + match loop_node.kind { ComputationNode { behavior: Loop } => - match loop_realized_declaration_target(children: loop.children) { + match loop_realized_declaration_target(children: loop_node.children) { Found { target: realized } => fold_realized_member_role(realized: realized) _ => Absent } @@ -5614,8 +5614,8 @@ fn infer_loop_member_role(loop: Node) -> Optional { } } -fn infer_loop_takes_collection_member(loop: Node) -> Bool { - match infer_loop_member_role(loop: loop) { +fn infer_loop_takes_collection_member(loop_node: Node) -> Bool { + match infer_loop_member_role(loop_node: loop_node) { Present { value: FoldStepTakesCollectionMember } => true _ => false } @@ -5637,8 +5637,8 @@ fn infer_frame_with_advisory(frame: InferFrame, reason: Symbol, at: Node) -> Inf // scheduled first), its member by the fold realization's relation, and the step's member formal by ROLE. // Every arm that cannot instantiate says so, typed and located, rather than leaving the variable to fall // silently into the non-refusing ReceiverTypeUnderived arm. -fn infer_fold_member_frame(loop: Node, frame: InferFrame, acc: InferGatherFoldAcc, step: Node, resolved: ResolvedTree) -> InferFrame { - match loop_domain_value_target(children: loop.children) { +fn infer_fold_member_frame(loop_node: Node, frame: InferFrame, acc: InferGatherFoldAcc, step: Node, resolved: ResolvedTree) -> InferFrame { + match loop_domain_value_target(children: loop_node.children) { Found { target: domain } => match lookup_inferred_facts_in_entries(entries: acc.entries, key: domain) { Absent => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_type_not_derived, at: domain) @@ -5656,7 +5656,7 @@ fn infer_fold_member_frame(loop: Node, frame: InferFrame, acc: InferGatherFoldAc } } } - _ => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_type_not_derived, at: loop) + _ => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_type_not_derived, at: loop_node) } } @@ -5705,8 +5705,8 @@ fn infer_fold_member_judged_frame(frame: InferFrame, formal: FoldMemberFormal, m fn infer_gather_child_context(parent: Node, frame: InferFrame, acc: InferGatherFoldAcc, edge: Edge, resolved: ResolvedTree) -> InferFrame { match edge.label { Positional => - if infer_loop_takes_collection_member(loop: parent) { - infer_fold_member_frame(loop: parent, frame: infer_frame_inherited(frame: frame), acc: acc, step: edge.target, resolved: resolved) + if infer_loop_takes_collection_member(loop_node: parent) { + infer_fold_member_frame(loop_node: parent, frame: infer_frame_inherited(frame: frame), acc: acc, step: edge.target, resolved: resolved) } else { infer_frame_inherited(frame: frame) } @@ -5716,7 +5716,7 @@ fn infer_gather_child_context(parent: Node, frame: InferFrame, acc: InferGatherF fn infer_gather_schedules_first(parent: Node, edge: Edge) -> Bool { match edge.label { - Named { name: name } => name == ^loop_domain_edge && infer_loop_takes_collection_member(loop: parent) + Named { name: name } => name == ^loop_domain_edge && infer_loop_takes_collection_member(loop_node: parent) Positional => false } } diff --git a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag index 7b15ebf112b..bd9dd06e01d 100644 --- a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag +++ b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag @@ -1,23 +1,28 @@ module v2.test.claim.compiler.infer_fold_member_instance import v2.compiler.resolve { ResolvedTree } -import v2.compiler.infer { infer, infer_entries_for_tree, inferred_facts_resolved_type, lookup_inferred_facts_in_entries } +import v2.compiler.infer { infer } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } -import v2.std.witness { Holds, Violates } import v2.test.claim.body_let_annotation { bla_assemble_with_peers, bla_q_artifact, bla_r_artifact, bla_source } import v2.compiler.source_authority { DagSourceReadWitness } import v2.std.collection { List } -import v2.std.diagnostic { Accepted, CorrectionNotModeled, Diagnostic, Locus, NodeLocus, Outcome, Rejected, Unavailable, Some, diagnostics_has_reason, diagnostics_fatal, diagnostics_fatal_reason, diagnostics_singleton, node_locus } +import v2.std.diagnostic { Accepted, CorrectionNotModeled, Diagnostic, Outcome, Rejected, Unavailable, Some, diagnostics_has_reason, diagnostics_fatal_reason, diagnostics_singleton, node_locus } import v2.std.node_query { binder_type_label, declared_field_from_edge, field_projection_optional } import v2.std.qualified_name { lexical_reference_label_optional } import std.algebra { list_snoc_item } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } -import std.process { ExitFailure, ExitSuccess, ProcessExit } -import v2.std.node { Arrow, Atom, Bind, Branch, Cardinality, Conj, Disj, EdgeLabel, Instantiation, Match, Named, NodeKind, Positional, Transform, TypeNode, Value, ComputationNode, Edge, Found, Loop, Node, Symbol, node_with_occurrence_id, loop_domain_value_target, node_subtree_nodes } +import v2.std.node { Atom, Edge, Named, Node, Positional, Symbol, TypeNode, node_subtree_nodes, node_with_occurrence_id } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +// SUBJECT: a collection fold's step member formal is typed by the Loop DOMAIN's element type, through +// v2.compiler.infer's dependent-child walk (v2.std.node fold_node_dependent), and a lambda binder used as +// a projection base reaches infer as the lexical reference a bare use of it is (v2.compiler.resolve +// resolve_projection_base). BOUNDARY: source text in, through the production assembly route, then +// v2.compiler.infer infer -- except control (3), whose input is supplied (see its header). +// gunbc.recurring_failure_mode infer_child_context_cannot_depend_on_a_sibling_result. + // THE PEERS RESTATE THE REAL DECLARATIONS, as v2.test.claim.body_let_annotation // bla_structural_string_peers does: v2.std.collection List is std.algebra FreeMonoid, and the // one-module ingest cannot otherwise bind List. @@ -50,7 +55,6 @@ data fmi_record_fold: String = "module p\n\nimport v2.std.collection { List }\n\ data fmi_undeclared_field_fold: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Bool {\n fold(xs, init: false, f: fn(found, e) { found || e.nope == 0 })\n}\n" - fn fmi_reason(o: Outcome) -> Symbol { match o { Accepted { value: _, diagnostics: _ } => ^accepted @@ -58,125 +62,6 @@ fn fmi_reason(o: Outcome) -> Symbol { } } -fn fmi_probe_record() -> Symbol { fmi_reason(o: fmi_infer(src: fmi_record_fold)) } -fn fmi_probe_undeclared() -> Symbol { fmi_reason(o: fmi_infer(src: fmi_undeclared_field_fold)) } -fn fmi_probe_incompatible() -> Symbol { fmi_reason(o: fmi_infer_with_member_formal_authored()) } - -data fmi_no_projection_fold: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Bool {\n fold(xs, init: false, f: fn(found, e) { found })\n}\n" - -fn fmi_probe_domain_type() -> Optional { - match fmi_assemble(src: fmi_no_projection_fold) { - Rejected { diagnostics: _ } => Absent - Accepted { value: tree, diagnostics: _ } => - match infer_entries_for_tree(tree: tree) { - Rejected { diagnostics: _ } => Absent - Accepted { value: entries, diagnostics: _ } => - fold(node_subtree_nodes(root: tree.root), init: Absent, f: fn(acc, n) { - match n.kind { - ComputationNode { behavior: Loop } => - match loop_domain_value_target(children: n.children) { - Found { target: d } => - match lookup_inferred_facts_in_entries(entries: entries, key: d) { - Present { value: facts } => - match inferred_facts_resolved_type(facts: facts) { - Holds { value: t } => Present { value: t } - Violates { diagnostic: _ } => acc - } - Absent => acc - } - _ => acc - } - _ => acc - } - }) - } - } -} - -fn fmi_locus(o: Outcome) -> Optional { - match o { - Accepted { value: _, diagnostics: _ } => Absent - Rejected { diagnostics: d } => Present { value: diagnostics_fatal(d: d).at } - } -} - -fn fmi_probe_record_locus() -> Optional { fmi_locus(o: fmi_infer(src: fmi_record_fold)) } - -fn fmi_kind_text(k: NodeKind) -> String { - match k { - TypeNode { connective: Atom { identity: id } } => id as String - TypeNode { connective: Conj } => "Conj" - TypeNode { connective: Disj } => "Disj" - TypeNode { connective: Arrow } => "Arrow" - TypeNode { connective: Cardinality } => "Card" - TypeNode { connective: Instantiation } => "Inst" - ComputationNode { behavior: Value } => "Value" - ComputationNode { behavior: Transform } => "Transform" - ComputationNode { behavior: Branch } => "Branch" - ComputationNode { behavior: Loop } => "Loop" - ComputationNode { behavior: Bind } => "Bind" - ComputationNode { behavior: Match } => "Match" - } -} - -fn fmi_edge_text(l: EdgeLabel) -> String { - match l { - Named { name: s } => concat(s as String, "=") - Positional => "" - } -} - -fn fmi_render(n: Node) -> String { - fold(n.children, init: concat(fmi_kind_text(k: n.kind), "("), f: fn(acc, e) { - concat(concat(concat(acc, fmi_edge_text(l: e.label)), fmi_render(n: e.target)), " ") - }) -} - -fn fmi_render_domain() -> String { - match fmi_probe_domain_type() { - Present { value: t } => fmi_render(n: t) - Absent => "absent" - } -} - -fn fmi_render_locus(o: Outcome) -> String { - match o { - Accepted { value: _, diagnostics: _ } => "accepted" - Rejected { diagnostics: d } => - match diagnostics_fatal(d: d).at { - NodeLocus { anchor: a } => fmi_render(n: a.at) - _ => "non-node locus" - } - } -} - -fn fmi_render_record_locus() -> String { fmi_render_locus(o: fmi_infer(src: fmi_record_fold)) } - -fn fmi_all_reasons(o: Outcome) -> String { - match o { - Accepted { value: _, diagnostics: _ } => "accepted" - Rejected { diagnostics: d } => - fold(d.tail, init: d.head.reason as String, f: fn(acc, x) { concat(concat(acc, " | "), x.reason as String) }) - } -} - -fn fmi_reasons_record() -> String { fmi_all_reasons(o: fmi_infer(src: fmi_record_fold)) } -fn fmi_reasons_noproj() -> String { fmi_all_reasons(o: fmi_infer(src: fmi_no_projection_fold)) } - -fn fmi_render_resolved_root() -> String { - match fmi_assemble(src: fmi_no_projection_fold) { - Rejected { diagnostics: _ } => "rejected" - Accepted { value: tree, diagnostics: _ } => fmi_render(n: tree.root) - } -} - -// (1) A FOLD OVER A COLLECTION OF RECORDS PERMITS e.target: no diagnostic anywhere in the outcome refuses -// the projection, where the unrepaired walk refused it infer_reason_projection_receiver_declares_no_fields -// (`e` typed as its bare fresh variable). Asserted over the WHOLE chain rather than as an Accepted outcome -// because the fixture's step Arrow still refuses infer_grounding_not_derived -- a separate, named link that -// is the next wall, refused identically on the base -- and this control must not change verdict when it -// lands. The mutation red: delete the domain-to-step frame join in v2.compiler.infer -// infer_gather_child_context and this control refuses again. test fn fmi_fold_member_projects_a_declared_field() -> Bool { match fmi_assemble(src: fmi_record_fold) { Rejected { diagnostics: _ } => false @@ -321,17 +206,3 @@ fn fmi_infer_with_member_formal_authored() -> Outcome { fn fmi_fixture_shape_diagnostic(at: Node) -> Diagnostic { Diagnostic { reason: ^fmi_fixture_shape_unexpected, at: node_locus(node: at), correction: Unavailable { reason: CorrectionNotModeled } } } - -fn fmi_scratch_all() -> ProcessExit { - if !fmi_fold_member_projects_a_declared_field() { - ExitFailure { code: 1, reason: "c1" } - } else if !fmi_lambda_binder_projection_base_is_a_lexical_reference() { - ExitFailure { code: 4, reason: "cr" } - } else if !fmi_fold_member_undeclared_field_refuses() { - ExitFailure { code: 2, reason: "c2" } - } else if !fmi_fold_member_incompatible_formal_refuses() { - ExitFailure { code: 3, reason: "c3" } - } else { - ExitSuccess - } -} From 2a7bf550f4e0f82b179f35f79998f796a293da0e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 2 Oct 2026 23:24:12 +0000 Subject: [PATCH 67/90] N7-1: nested payload binder re-boxed in emitted Rust -- receipt with minimal reproducer and the missing build-witness trigger Co-Authored-By: Claude Opus 5.5 (1M context) --- ...rn_accepted_by_the_interpreter_and_broken_in_emitted_rust.dag | 1 + 1 file changed, 1 insertion(+) diff --git a/dag/gunbc/recurring_failure_mode/nested_pattern_accepted_by_the_interpreter_and_broken_in_emitted_rust.dag b/dag/gunbc/recurring_failure_mode/nested_pattern_accepted_by_the_interpreter_and_broken_in_emitted_rust.dag index 12394e1ff14..7259139a478 100644 --- a/dag/gunbc/recurring_failure_mode/nested_pattern_accepted_by_the_interpreter_and_broken_in_emitted_rust.dag +++ b/dag/gunbc/recurring_failure_mode/nested_pattern_accepted_by_the_interpreter_and_broken_in_emitted_rust.dag @@ -12,6 +12,7 @@ data nested_pattern_accepted_by_the_interpreter_and_broken_in_emitted_rust: Recu "HARM: the self-host route and every emitted consumer fail where every interpreter-side check is green, so the defect is found by the most expensive run and after review; shape (2) is a panic in a compiler binary, not a typed refusal.", "RUNG FOUND AT: below the floor on the emitted path for shape (2) (an accepted program that panics), rung 1 for shape (1) (rustc refuses the emitted crate, loudly but late). The interpreter path is correct for both. CEILING: structurally guaranteed -- exhaustiveness over nested and record patterns is decidable, and a constant nested pattern has a total lowering (a nested match, not a guard plus let-else).", "SHAPE (2) CLIMBED, RUNG NOW 2 ON THE EMITTED PATH (mechanically preventable). WHY THE WILDCARD SIBLING SELECTED THE GUARD: `v1.compiler.emit_rust` `rc_grouped_arm_plan` planned an arm with no Rc-bound field (`O { mid: _ }`) as ungroupable, and `rc_group_representative` refuses the nested match for a whole outer variant when any one of its arms is ungroupable -- so one wildcard sibling pushed every arm of the variant onto the #8570 guard, whose `matches!` names only the middle variant while its prelude destructures the innermost constant with let-else. The fix is at that boundary, not a special case: an arm whose fields are all irrefutable IS the nested match's `_` arm, so it now plans as a wildcard member (no discriminated field, inner pattern `_`) and joins the group as its LAST member, under the same plain-binding subset rule as every other member. `test.claim.emit.nested_constant_pattern_emission_witness_test` `the_emitter_lowers_a_nested_constant_with_a_wildcard_sibling_as_a_nested_match` is the discriminating control: green with the fix, red with the regenerated emitter mirror reverted. It is rung 2, not 3: the guard lowering itself still omits nested constants below its first level, so an arm that stays ungrouped for another reason -- an authored guard, a string guard, two Rc-bound fields, a wildcard sibling that is not last -- still reaches the partial guard. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: every accepted refutable nested pattern lowers totally -- either the general decision-tree lowering `rc_grouped_match_arm_strs` names as its own next rung, or a guard that tests every refutable level -- so no accepted pattern lowers to unreachable!(). SHAPE (1) IS UNCHANGED: its trigger remains that the seed's match exhaustiveness check descends into record-pattern fields and through generic-call scrutinees, so an incomplete coproduct match refuses at typecheck.", + "THIRD SHAPE (N7-1, gunbc#13028): a binder taken from a payload field TWO variant levels down and passed straight into a constructor of the inner variant. Minimal reproducer, accepted by the seed typecheck and interpreter: `fn p(o: Optional>) -> Outcome { match o { Present { value: Accepted { value: a, diagnostics: d } } => Accepted { value: a.node, diagnostics: d } Present { value: Rejected { diagnostics: r } } => Rejected { diagnostics: r } Absent => Accepted { value: 0, diagnostics: None } } }`. In the emitted crate `d` is bound at its BOXED storage type and the constructor re-boxes it, `Box::new(d.clone())`, so rustc refuses E0308 (expected `Option>`, found `Box>>`). It reached the native route as the emitted v2.compiler.resolve failing to build (EmittedCompilerBuildFailed), from a construct-tag caller of try_resolve_qualified_name_node; #13028 replaced that site with the one-level resolved_atom_node helper, which binds `d` at its value type -- an avoidance of this class, not a repair of it. WHY NO WITNESS IS ENROLLED FOR IT YET: the only emit witness helper, compile_dag_rust_emit_check, matches emitted TEXT, and the one-level form emits the SAME `Box::new(d.clone())` token and compiles (there `d` is the unboxed value); a text check cannot tell the two apart, so a probe written with it would be a decoration. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: an emit witness that BUILDS the emitted probe with rustc, sufficient for the nested form above to be a discriminating red and the one-level form its accepted control; then the lowering binds a nested payload binder at its value type.", ], evidence: [ From 73e5a89393fac574291f556fd4eccc7fbce7920a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 00:25:07 +0000 Subject: [PATCH 68/90] N7-1 (a): a collection fold iterates its step's BODY; the carrier formal is instantiated from init; the fold Bind judges the body (and a declared return) against the carrier; operand types read through the frame Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/04_infer.dag | 197 ++++++++++++++++-- src/v2/compiler/fold_lowering.dag | 43 +++- .../infer_fold_member_instance_test.dag | 20 ++ .../test/claim/n7probe/root_children_test.dag | 1 + 4 files changed, 245 insertions(+), 16 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 3f3f44f1d6b..8456c43738c 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -65,7 +65,7 @@ import v2.std.optional { import v2.std.qualified_name { QualifiedName, declaration_reference_node, declaration_reference_path_optional, lexical_reference_label_optional, parameter_reference_path_optional } import v2.std.node_query { BinderParts, binder_node, binder_node_parts, construct_field_edges, construct_tag_optional, declared_field_from_edge } import v2.std.symbol_index { RecordTypePayload, SymbolIndex, VariantArmPayload, symbol_index_declared_payload_at, symbol_index_declared_type_params_at, symbol_index_lookup } -import v2.compiler.fold_lowering { FoldMemberFormal, FoldMemberRole, FoldStepTakesCollectionMember, FoldStepTakesNoMember, fold_collection_member_type, fold_realized_member_role, fold_step_member_formal } +import v2.compiler.fold_lowering { FoldMemberFormal, FoldMemberRole, FoldStepTakesCollectionMember, FoldStepTakesNoMember, fold_collection_member_type, fold_loop_step, fold_realized_member_role, fold_step_body_node, fold_step_carrier_formal, fold_step_declared_return, fold_step_member_formal } import v2.std.node { NodeFoldDependent, fold_node_dependent, loop_domain_value_target, loop_realized_declaration_target, Ambiguous, Found, NotMarkedReference, arrow_body_target_lookup, arrow_named_edge_is_contract, arrow_signature_order_edge, match_arm_pattern_edge, resolved_reference_body_kind } import v2.std.list_introduction { list_introduction_elements_optional, list_introduction_head_path } import v2.std.arrow_signature { @@ -1696,8 +1696,11 @@ fn refinement_declared_carrier(index: SymbolIndex, source_type: Node) -> Optiona } } -// The literal and arrow-domain arms below derive a real type for the operand; the remaining arm -// falls through to the operand's own inferred facts. When those facts are the GroundingNotDerived +// The literal arm below derives a real type for the operand; every other operand -- a lexical reference +// included -- is read from its own inferred facts. A lexical reference's facts ARE its binder's declared +// type read through the inference frame (infer_lexical_reference_facts), so reading the binding's declared +// type here instead was a second authority for the same fact that bypassed the frame: a fold step's carrier +// formal stayed its bare fresh variable in `found + e.target` after the frame had instantiated it. When those facts are the GroundingNotDerived // frontier there is no operand type, and this now refuses (Violates) instead of returning the // operand NODE as its own type. That fallback was how the self-grounding fabrication entered // branch/match/loop unification: two arms whose 'types' were their own expression nodes compared @@ -1712,15 +1715,7 @@ fn infer_branch_operand_resolved_type_in_tree( match dag_canonical_literal_from_node(node: node) { Accepted { value: literal, diagnostics: _ } => infer_binding_value_type_witness(binding: infer_literal_type_binding(literal: literal), at: node) - Rejected { diagnostics: _ } => - match resolved_tree_lexical_binding(tree: resolved, reference: node) { - Present { value: binding } => - match binding.declared { - Present { value: declared } => Holds { value: declared } - Absent => inferred_facts_resolved_type(facts: facts) - } - Absent => inferred_facts_resolved_type(facts: facts) - } + Rejected { diagnostics: _ } => inferred_facts_resolved_type(facts: facts) } } @@ -2704,10 +2699,23 @@ fn infer_match_coproduct( // refinement-typed follow-on (monotone-narrowing carrier) replaces that refusal with a real // refinement judgment. +// A COLLECTION FOLD ITERATES ITS STEP'S BODY, NOT ITS STEP. The fold encoding's positional child is the step +// CALLABLE (v2.compiler.fold_lowering fold_recurrence_encoding), whose own type is an Arrow over its fresh +// variables and is never the carrier, so reading it refused every fold at the step Arrow. Selected by the +// realization role, never by the Loop's shape; a step with no single body keeps the callable, and the row +// refuses on it as before. fn infer_loop_iteration_fold_child_targets(node: Node) -> List { + let collection_fold = infer_loop_takes_collection_member(loop_node: node) fold(node.children, init: [], f: fn(acc, e) { if loop_edge_contributes_to_iteration_fold(parent: node, e: e) { - list_snoc_item(xs: acc, item: e.target) + if collection_fold { + match fold_step_body_node(step: e.target) { + Present { value: body } => list_snoc_item(xs: acc, item: body) + Absent => list_snoc_item(xs: acc, item: e.target) + } + } else { + list_snoc_item(xs: acc, item: e.target) + } } else { acc } @@ -4301,6 +4309,18 @@ fn infer_gather_fold_init(n: Node, partials: List, kinds: List + if infer_bind_encodes_collection_fold(bind: n) { + infer_gather_fold_acc_ok( + node: n, + entries: empty_inferred_facts_entry_list(), + pending: None, + await_branch_row: false, + await_match_row: false, + await_loop_row: false, + await_transform_row: false, + children_remaining: length(xs: n.children) + ) + } else { match type_annotation_optional(n: n) { Present { value: _ } => infer_gather_fold_acc_ok( @@ -4315,6 +4335,7 @@ fn infer_gather_fold_init(n: Node, partials: List, kinds: List infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) } + } TypeNode { connective: Atom { identity: _ } } => infer_gather_fold_not_derived(n: n, partials: partials, kinds: kinds, resolved: resolved, instances: instances) TypeNode { connective: Conj } => match declaration_reference_path_optional(node: n) { @@ -5326,6 +5347,9 @@ fn infer_gather_settled_row( kinds: List, resolved: ResolvedTree, ) -> InferGatherFoldAcc { + if infer_bind_encodes_collection_fold(bind: acc.node) { + infer_gather_fold_bind_row_on_entries(node: acc.node, entries: merged_entries, pending: merged_pending, partials: partials, kinds: kinds, resolved: resolved) + } else { match type_annotation_optional(n: acc.node) { Present { value: _ } => infer_gather_bind_annotation_row_on_entries( @@ -5358,6 +5382,7 @@ fn infer_gather_settled_row( ) } } + } } // THE DECLARED POSITIONS A SETTLED NODE OWES: a binder node's default is judged at the binder's declared @@ -5660,7 +5685,8 @@ fn infer_fold_member_frame(loop_node: Node, frame: InferFrame, acc: InferGatherF } } -// A FRESH MEMBER FORMAL IS INSTANTIATED; AN AUTHORED ONE IS JUDGED. The member must inhabit an authored +// A FRESH FORMAL IS INSTANTIATED; AN AUTHORED ONE IS JUDGED -- for the member formal against the domain's +// element, and for the carrier formal against init's type (infer_fold_carrier_frame), by one route. The member must inhabit an authored // formal through the same declared_type_inhabitance fold an application argument meets at its formal, so // `fn(acc, e: Int)` over a List refuses at that formal rather than typing `e` as either. fn infer_fold_member_formal_frame(frame: InferFrame, formal: FoldMemberFormal, member: Node, step: Node, resolved: ResolvedTree) -> InferFrame { @@ -5705,7 +5731,9 @@ fn infer_fold_member_judged_frame(frame: InferFrame, formal: FoldMemberFormal, m fn infer_gather_child_context(parent: Node, frame: InferFrame, acc: InferGatherFoldAcc, edge: Edge, resolved: ResolvedTree) -> InferFrame { match edge.label { Positional => - if infer_loop_takes_collection_member(loop_node: parent) { + if infer_bind_encodes_collection_fold(bind: parent) && infer_loop_takes_collection_member(loop_node: edge.target) { + infer_fold_carrier_frame(bind: parent, frame: infer_frame_inherited(frame: frame), acc: acc, loop_node: edge.target, resolved: resolved) + } else if infer_loop_takes_collection_member(loop_node: parent) { infer_fold_member_frame(loop_node: parent, frame: infer_frame_inherited(frame: frame), acc: acc, step: edge.target, resolved: resolved) } else { infer_frame_inherited(frame: frame) @@ -5714,6 +5742,145 @@ fn infer_gather_child_context(parent: Node, frame: InferFrame, acc: InferGatherF } } +// THE FOLD ENCODING'S BIND: `Bind { slot := init, Loop { step, domain, .. } }` (v2.compiler.fold_lowering +// fold_recurrence_encoding), recognised by its body's realization role and never by shape alone. +fn infer_bind_encodes_collection_fold(bind: Node) -> Bool { + match bind.kind { + ComputationNode { behavior: Bind } => + match list_at_optional(xs: node_positional_child_targets(node: bind), index: 2) { + Present { value: body } => infer_loop_takes_collection_member(loop_node: body) + Absent => false + } + _ => false + } +} + +fn infer_bind_value(bind: Node) -> Optional { + list_at_optional(xs: node_positional_child_targets(node: bind), index: 1) +} + +// THE CARRIER FORMAL IS INSTANTIATED FROM INIT, as the member formal is from the domain: the Bind's value +// (init) is folded before its body in authored order, so its settled type is in `acc` when the Loop is +// entered, and the step's actual-0 formal -- read by ROLE -- stands for it throughout the Loop subtree. +fn infer_fold_carrier_frame(bind: Node, frame: InferFrame, acc: InferGatherFoldAcc, loop_node: Node, resolved: ResolvedTree) -> InferFrame { + match infer_bind_value(bind: bind) { + Absent => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_carrier_type_not_derived, at: bind) + Present { value: init } => + match infer_settled_type_at(target: init, entries: acc.entries) { + Absent => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_carrier_type_not_derived, at: init) + Present { value: carrier } => + match fold_loop_step(loop_node: loop_node) { + Absent => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_formal_unread, at: loop_node) + Present { value: step } => + match fold_step_carrier_formal(step: step) { + Absent => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_formal_unread, at: step) + Present { value: formal } => infer_fold_member_formal_frame(frame: frame, formal: formal, member: carrier, step: step, resolved: resolved) + } + } + } + } +} + +fn infer_settled_type_at(target: Node, entries: List) -> Optional { + match lookup_inferred_facts_in_entries(entries: entries, key: target) { + Absent => Absent + Present { value: facts } => + match infer_branch_operand_resolved_type(node: target, facts: facts) { + Holds { value: t } => Present { value: t } + Violates { diagnostic: _ } => Absent + } + } +} + +// THE FOLD BIND'S ROW: what one step produces must inhabit the carrier. With a declared step return both +// judgements are made -- the body inhabits the declared return, and the declared return inhabits the carrier +// -- so a declared return can neither hide a body mismatch nor widen past the carrier. A side whose type is +// not derived is a counted advisory, never a pass. +fn infer_fold_bind_check(node: Node, entries: List, resolved: ResolvedTree) -> Outcome { + match infer_bind_value(bind: node) { + Absent => Accepted { value: true, diagnostics: Some { diagnostics: infer_fold_member_advisory(reason: ^infer_reason_fold_carrier_type_not_derived, at: node) } } + Present { value: init } => + match infer_settled_type_at(target: init, entries: entries) { + Absent => Accepted { value: true, diagnostics: Some { diagnostics: infer_fold_member_advisory(reason: ^infer_reason_fold_carrier_type_not_derived, at: init) } } + Present { value: carrier } => + match list_at_optional(xs: node_positional_child_targets(node: node), index: 2) { + Absent => Accepted { value: true, diagnostics: Some { diagnostics: infer_fold_member_advisory(reason: ^infer_reason_fold_member_formal_unread, at: node) } } + Present { value: loop_node } => + match fold_loop_step(loop_node: loop_node) { + Absent => Accepted { value: true, diagnostics: Some { diagnostics: infer_fold_member_advisory(reason: ^infer_reason_fold_member_formal_unread, at: loop_node) } } + Present { value: step } => + match fold_step_body_node(step: step) { + Absent => Accepted { value: true, diagnostics: Some { diagnostics: infer_fold_member_advisory(reason: ^infer_reason_fold_member_formal_unread, at: step) } } + Present { value: body } => + match infer_settled_type_at(target: body, entries: entries) { + Absent => Accepted { value: true, diagnostics: Some { diagnostics: infer_fold_member_advisory(reason: ^infer_reason_fold_step_body_type_not_derived, at: body) } } + Present { value: produced } => + match fold_step_declared_return(step: step) { + Absent => infer_fold_judge(declared: carrier, produced: produced, at: body, step: step, resolved: resolved) + Present { value: declared_return } => + bind_outcome( + o: infer_fold_judge(declared: declared_return, produced: produced, at: body, step: step, resolved: resolved), + f: fn(_) { infer_fold_judge(declared: carrier, produced: infer_declared_type_denotation(declared: declared_return), at: declared_return, step: step, resolved: resolved) } + ) + } + } + } + } + } + } + } +} + +fn infer_fold_judge(declared: Node, produced: Node, at: Node, step: Node, resolved: ResolvedTree) -> Outcome { + let obligation = DeclaredTypeObligation { + position: position_declared_return(), + parameter_identity: ^fold_carrier, + declared: infer_declared_type_denotation(declared: declared), + produced: produced, + produced_declared_carrier: refinement_declared_carrier(index: resolved.resolved_declarations, source_type: produced), + application: at, + type_variables: type_param_names(n: step) + } + match declared_type_inhabitance(obligation: obligation) { + Inhabits { homomorphism: _ } => Accepted { value: true, diagnostics: None } + InhabitanceUndecidable { reason: reason } => infer_inhabitance_undecidable_accepted(application: at, reason: reason) + InhabitanceRefused { find_witness_reason: fw_reason } => + outcome_rejected(application_argument_does_not_inhabit_diagnostic(obligation: obligation, find_witness_reason: fw_reason)) + } +} + +fn infer_gather_fold_bind_row_on_entries( + node: Node, + entries: List, + pending: Diagnostics, + partials: List, + kinds: List, + resolved: ResolvedTree, +) -> InferGatherFoldAcc { + match infer_fold_bind_check(node: node, entries: entries, resolved: resolved) { + Rejected { diagnostics: r } => + infer_gather_fold_acc_failed( + node: node, + pending: rejected_with_pending(pending: pending, rejected: r), + await_branch_row: false, + await_match_row: false, + await_loop_row: false, + await_transform_row: false, + children_remaining: 0 + ) + Accepted { value: _, diagnostics: d } => + infer_gather_transform_frontier_on_entries( + node: node, + entries: entries, + pending: diagnostics_merge(outer: pending, inner: d), + partials: partials, + kinds: kinds, + resolved: resolved, + instances: infer_no_type_variable_instances() + ) + } +} + fn infer_gather_schedules_first(parent: Node, edge: Edge) -> Bool { match edge.label { Named { name: name } => name == ^loop_domain_edge && infer_loop_takes_collection_member(loop_node: parent) diff --git a/src/v2/compiler/fold_lowering.dag b/src/v2/compiler/fold_lowering.dag index 98ae1119dd5..7d2d26c3f26 100644 --- a/src/v2/compiler/fold_lowering.dag +++ b/src/v2/compiler/fold_lowering.dag @@ -45,6 +45,7 @@ import v2.std.node { ComputationNode, Conj, Edge, + Found, Instantiation, Loop, Named, @@ -52,6 +53,7 @@ import v2.std.node { Positional, Symbol, TypeNode, + arrow_body_target_lookup, content_hash, is_empty_conj_root, node_synthetic, @@ -880,9 +882,19 @@ type FoldMemberFormal { } fn fold_step_member_formal(step: Node) -> Optional { + fold_step_formal_at(step: step, position: 1) +} + +// THE STEP'S CARRIER FORMAL, BY ROLE: declared position 0, the slot fold_recurrence_encoding binds to +// the Bind's init. Instantiated from init's settled type exactly as the member formal is from the domain's. +fn fold_step_carrier_formal(step: Node) -> Optional { + fold_step_formal_at(step: step, position: 0) +} + +fn fold_step_formal_at(step: Node, position: Int) -> Optional { match arrow_declared_parameter_order(arrow: step) { ArrowParameterOrderDeclared { labels: labels } => - match list_at_optional(xs: labels, index: 1) { + match list_at_optional(xs: labels, index: position) { Absent => optional_absent() Present { value: label } => match list_at_optional(xs: node_positional_child_targets(node: step), index: 0) { @@ -910,6 +922,35 @@ fn fold_step_member_formal(step: Node) -> Optional { } } +// WHAT ONE ITERATION OF A COLLECTION FOLD PRODUCES: the step callable's BODY, never the callable. The Loop +// row reads its iterated body's type as the carrier; for a fold realization the positional child is the step +// callable, whose own type is an Arrow over its fresh variables and is not the carrier (gunbc.recurring_failure_mode +// infer_child_context_cannot_depend_on_a_sibling_result, link (a)). Absent when the step has no single body. +fn fold_step_body_node(step: Node) -> Optional { + match arrow_body_target_lookup(children: step.children) { + Found { target: body } => optional_present(value: body) + _ => optional_absent() + } +} + +// The step's DECLARED return, when the author wrote one: position 1 of the Arrow, unless it is one of the +// step's own fresh variables (an unwritten return is minted as one), in which case nothing was declared. +fn fold_step_declared_return(step: Node) -> Optional { + match list_at_optional(xs: node_positional_child_targets(node: step), index: 1) { + Present { value: declared } => + if fold_type_is_variable_of(t: declared, step: step) { optional_absent() } else { optional_present(value: declared) } + Absent => optional_absent() + } +} + +// The step callable of a fold encoding's Loop: its one positional child. +fn fold_loop_step(loop_node: Node) -> Optional { + match node_positional_child_targets(node: loop_node) { + Cons { head: step, tail: Empty } => optional_present(value: step) + _ => optional_absent() + } +} + fn fold_type_is_variable_of(t: Node, step: Node) -> Bool { match t.kind { TypeNode { connective: Atom { identity: id } } => diff --git a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag index bd9dd06e01d..76e3b4d5e0c 100644 --- a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag +++ b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag @@ -206,3 +206,23 @@ fn fmi_infer_with_member_formal_authored() -> Outcome { fn fmi_fixture_shape_diagnostic(at: Node) -> Diagnostic { Diagnostic { reason: ^fmi_fixture_shape_unexpected, at: node_locus(node: at), correction: Unavailable { reason: CorrectionNotModeled } } } + +// (a) THE FOLD ITERATES ITS STEP'S BODY, AND THE CARRIER IS INIT'S TYPE: a fold whose body adds the carrier +// to the member's field is ACCEPTED end to end. The body derives only if BOTH formals are instantiated -- +// `found` from init (Int), `e` from the domain (Pair) -- so this control holds the carrier frame and the member +// frame together. RED ON THE BASE: the Loop row read the step callable's own type as the carrier and refused +// infer_grounding_not_derived at the step Arrow for every fold. (fmi_record_fold's `found || ..` body is not +// used here: infer derives no Bool join, a separate frontier.) +data fmi_sum_fold: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Int {\n fold(xs, init: 0, f: fn(found, e) { found + e.target })\n}\n" + +test fn fmi_fold_over_records_is_accepted() -> Bool { + fmi_reason(o: fmi_infer(src: fmi_sum_fold)) == ^accepted +} + +data fmi_body_not_carrier_fold: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Bool {\n fold(xs, init: false, f: fn(found, e) { e.target })\n}\n" + +// (a) A STEP WHOSE BODY DOES NOT INHABIT THE CARRIER REFUSES AT THE BODY: init is Bool, the body is Int. +test fn fmi_fold_step_body_not_the_carrier_refuses() -> Bool { + fmi_reason(o: fmi_infer(src: fmi_body_not_carrier_fold)) == ^arrow_body_does_not_inhabit_declared_return +} + diff --git a/src/v2/test/claim/n7probe/root_children_test.dag b/src/v2/test/claim/n7probe/root_children_test.dag index 799824e4d31..fa16eb48cd5 100644 --- a/src/v2/test/claim/n7probe/root_children_test.dag +++ b/src/v2/test/claim/n7probe/root_children_test.dag @@ -2,6 +2,7 @@ module v2.test.n7probe.root_children import v2.std.node { Edge, Node } import v2.std.logic { Bool } +import v2.std.collection { List } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly From c2ccddc293344e42835f1003fb5ac35540a416f8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 00:43:00 +0000 Subject: [PATCH 69/90] N7-1 (a): frontier row for a lambda value with no derived type in v2 infer Co-Authored-By: Claude Opus 5.5 (1M context) --- ..._value_has_no_derived_type_in_v2_infer.dag | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 dag/gunbc/recurring_failure_mode/lambda_value_has_no_derived_type_in_v2_infer.dag diff --git a/dag/gunbc/recurring_failure_mode/lambda_value_has_no_derived_type_in_v2_infer.dag b/dag/gunbc/recurring_failure_mode/lambda_value_has_no_derived_type_in_v2_infer.dag new file mode 100644 index 00000000000..71454950a11 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/lambda_value_has_no_derived_type_in_v2_infer.dag @@ -0,0 +1,20 @@ +module gunbc.recurring_failure_mode.lambda_value_has_no_derived_type_in_v2_infer + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data lambda_value_has_no_derived_type_in_v2_infer: RecurringFailureMode = RecurringFailureMode { + identity: "lambda_value_has_no_derived_type_in_v2_infer" as NonEmptyStr, + receipts: [ + "INVALID STATE: a lambda -- an Arrow node standing as a VALUE, its formals and return minted as fresh type variables by function-value lowering -- has no derived type in v2.compiler.infer. infer_gather_fold_init sends an Arrow to infer_gather_fold_not_derived, so the lambda's facts are the counted infer_grounding_not_derived frontier, and any row that asks for the lambda's own type refuses there. HARM: until gunbc#13028 that row was the Loop row of every collection fold -- it read the step callable's type as the carrier and refused at the step Arrow, so no fold-bearing module could infer on the native route. #13028 STOPPED THE FOLD ROW FROM ASKING (a collection fold iterates its step's BODY: v2.compiler.infer infer_loop_iteration_fold_child_targets, selected by v2.compiler.fold_lowering fold_realized_member_role) rather than answering the question, so the gap stands for every other position a lambda value reaches: an argument to a higher-order parameter that is not a fold slot, a lambda bound by `let`, a lambda returned from a function, a record field holding one.", + "DISTINGUISHING FACTS: this is not the fold row's defect and not the frame's. Under a fold, the step's formals ARE instantiated (the member from the domain, the carrier from init) and its body IS typed; what is missing is the lambda's own Arrow type assembled from those -- `fn(carrier, member) -> body` -- which no row builds, because typing an Arrow VALUE needs its formals' types (fresh variables, instantiated only where a frame supplies them) and its body's type together, and a lambda outside a fold has no frame to supply the formals at all. Elsewhere it is ordinary type inference over the fresh variables (unification), which v2 infer does not have.", + "RUNG FOUND AT: mitigatable -- a counted frontier entry, refused where a row demands the type, never fabricated. CEILING: structurally guaranteed; the lambda's type is decidable from its formals and body once its variables are solved. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: v2 infer derives a lambda value's Arrow type from its formals' types and its body's type, with its fresh variables either instantiated by the consuming position's declared formal (a higher-order parameter's declared fn type, applied through the existing application-instantiation route) or refused as undetermined -- SUFFICIENT FOR a lambda passed to a declared `fn(A) -> B` parameter to be judged against that parameter, and for the fold row's body-not-callable read to remain the only fold-specific arm.", + ], + evidence: [ + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_gather_fold_init", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_loop_iteration_fold_child_targets", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "fold_step_body_node", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.std.anonymous_binder", decl_name: "fresh_type_variable", field: WholeDeclaration }, + ], +} From 4e1009ef9d8092929b7fbc75c92ec3fcfdbfc90e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 00:44:01 +0000 Subject: [PATCH 70/90] N7-1: receipt -- the operand reader's second authority for a binder's type Co-Authored-By: Claude Opus 5.5 (1M context) --- ...fer_child_context_cannot_depend_on_a_sibling_result.dag | 1 + src/v2/test/claim/n7probe/root_children_test.dag | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag b/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag index 4da876ebac7..665f732d2c1 100644 --- a/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag +++ b/dag/gunbc/recurring_failure_mode/infer_child_context_cannot_depend_on_a_sibling_result.dag @@ -11,6 +11,7 @@ data infer_child_context_cannot_depend_on_a_sibling_result: RecurringFailureMode "DISTINGUISHING FACTS: this is NOT a missing loop_domain_edge reader, NOT cross-module declaration retrieval, and NOT a projection-classification defect -- each of those was measured and excluded. v2.std.node fold_node carries SYNTHESIZED results only from child to parent (step: fn(R, Edge, R) -> R, whose third argument is the child's already-folded result, computed by a recursive call that receives nothing from the parent). v2.std.node fold_node_topdown carries INHERITED context only from parent to child, and its child_context: fn(Node, A, Edge) -> A receives the parent node, the inherited context and the current edge -- no folded sibling result. So neither algebra expresses `infer the domain child, derive the member instance, then infer the step child under it`, and converting infer's gather from NodeFold to NodeFoldTopDown is a DISPROVEN route rather than the trigger: it would change the shape of the stage's single walk across all behavior arms and still not carry the fact. Adding the role reader, the domain consumer or the List element relation before the traversal exists would make each declaration unreachable from any production verdict, which is the dangling modeling DESIGN section 3c forbids. The three facts the repair consumes already exist and are cited below: the fresh variable is minted by v2.std.anonymous_binder fresh_type_variable, the existing lambda-parameter route correctly derives the member formal AS that variable (v2.compiler.infer infer_lexical_reference_facts, reading the binding v2.compiler.resolve recorded in ResolvedTree.lexical_bindings), and the receiver rule that refuses is infer_projection_receiver -- so no second parameter-typing route may be introduced; the existing variable must be instantiated.", "RUNG FOUND AT: mitigatable -- the compiler refuses rather than fabricating a field-bearing type, but valid fold programs cannot type. ATTAINABLE CEILING: structurally guaranteed -- the element relation is decidable from the domain's own type, and every fact it needs is already established by a stage that runs before the step subtree is judged; what is missing is one driver whose behavior-specific child schedule can infer a dependency child once, derive a scoped context from its settled result, and infer the dependent child once under that context. NEXT-RUNG TRIGGER, stated as the capability: an infer-local dependent-child driver schedules the Loop domain before the step, derives the collection-fold member instance from the domain type, extends a lexical TypeVariableInstance frame FOR THE STEP SUBTREE ONLY, and preserves the existing behavior rows, SUFFICIENT FOR the unchanged production helper to establish `e: Edge` and `e.target: Node`. Its discriminating red is a mutation deleting the domain-to-step context join, which must make that control fail. The element relation must be scoped by FOLD REALIZATION rather than by assuming every Loop is List, and the role authority (the step callable's actual 0 is the carrier and actual 1 is the domain member, stated today in v2.compiler.fold_lowering) belongs in one decoder rather than being re-read per consumer.", "CLIMB (N7-1): the trigger is built. v2.std.node fold_node_dependent is the dependent-child traversal -- child_context also receives the parent's accumulator after the children already folded, and a `first` partition schedules dependency children ahead of the rest, so the schedule is a permutation by construction. v2.compiler.infer infer_entries_for_tree walks it with a lexical InferFrame of TypeVariableInstance: a Loop whose realized declaration hands its step a collection member (v2.compiler.fold_lowering fold_realized_member_role) folds its domain first, and infer_fold_member_frame instantiates the step's member formal -- read by ROLE, declared position 1 (fold_step_member_formal) -- from the domain's settled type through fold_collection_member_type, for the step subtree only; infer_lexical_reference_facts reads a binder's declared type through that frame. An authored member formal is judged by declared_type_inhabitance instead of instantiated, and every arm that cannot instantiate is a counted advisory at the subtree root rather than a silent uninstantiated variable. A SECOND, EARLIER BOUNDARY WAS FOUND ON THE SAME SLICE: v2.compiler.resolve resolve_projection_base minted a lambda binder used as a projection base as the bare canonical atom, recording no lexical binding, so `e` in `e.target` never reached infer typed as its fresh variable at all and the projection fell into the non-refusing ReceiverTypeUnderived arm. It is now minted through resolve_frame_bound_reference like a bare use. Controls: v2.test.claim.compiler.infer_fold_member_instance (red on the base for the projection-base and member-typing controls; the frame-join deletion reds the e.target control and the resolve mutation reds the projection-base control). The native target's first wall was NOT this class: it was cross-module receiver retrieval at `root.children`.", + "A SECOND AUTHORITY FOR THE BINDER'S TYPE, FOUND WHILE CLIMBING (#13028, link (a)). The frame instantiates a fresh formal where a binder's type becomes a REFERENCE's type, infer_lexical_reference_facts; but v2.compiler.infer infer_branch_operand_resolved_type_in_tree -- the operand reader of binary infix, branch, match and list introduction -- answered a lexical reference by reading its binding's DECLARED type straight off ResolvedTree.lexical_bindings, beside the reference's facts. With no frame the two agreed, so the duplicate was invisible; under a frame they disagreed, and `found + e.target` refused infer_transform_operand_type_mismatch at `found` after the frame had typed it. The reader now reads the facts, so a binder's type has one authority and every consumer sees the instantiated type. Evidence it changed nothing unframed: the infer-entries digest over 17 fixtures is byte-identical base vs head. The tell for the next such reader: an instance applied at one consumer and a mismatch at another for the same binder.", ], evidence: [ DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_entries_for_tree", field: WholeDeclaration }, diff --git a/src/v2/test/claim/n7probe/root_children_test.dag b/src/v2/test/claim/n7probe/root_children_test.dag index fa16eb48cd5..568d1ee2416 100644 --- a/src/v2/test/claim/n7probe/root_children_test.dag +++ b/src/v2/test/claim/n7probe/root_children_test.dag @@ -1,12 +1,17 @@ module v2.test.n7probe.root_children -import v2.std.node { Edge, Node } +import v2.std.node { Edge, Node, Symbol } +import v2.std.diagnostic { Diagnostic } import v2.std.logic { Bool } import v2.std.collection { List } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +fn n7_reason(d: Diagnostic) -> Symbol { + d.reason +} + fn n7_children(root: Node) -> List { root.children } From fe498dccd593f3bfcd463589f52da028969697cd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 01:28:31 +0000 Subject: [PATCH 71/90] N7-1 (x)(1): a closure root whose own resolve refused is recorded, and every closure-index miss over it carries that refusal Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/00_compile.dag | 1 + src/v2/compiler/03_ingest.dag | 4 +- src/v2/compiler/03_name_resolve.dag | 41 +++++++++++---- src/v2/compiler/03_resolve.dag | 50 +++++++++++++++++-- src/v2/compiler/04_infer.dag | 26 ++++++++-- .../compiler/self_host/closure_emission.dag | 2 +- .../arrow_order_edge/admission_wall_test.dag | 2 +- .../infer_fold_member_instance_test.dag | 3 +- ...nstruct_field_inhabitance_witness_test.dag | 2 +- .../kernel_value_type_roster_witness_test.dag | 2 +- ...lassical_not_ingested_equals_eval_test.dag | 2 +- .../match_binder/match_binder_typing_test.dag | 3 +- src/v2/test/claim/n7probe/n7_provider.dag | 7 +++ .../test/claim/n7probe/root_children_test.dag | 5 ++ src/v2/test/lens_common/infer_fixture.dag | 2 +- src/v2/workflow/realization_attempt.dag | 2 +- 16 files changed, 125 insertions(+), 29 deletions(-) create mode 100644 src/v2/test/claim/n7probe/n7_provider.dag diff --git a/src/v2/compiler/00_compile.dag b/src/v2/compiler/00_compile.dag index 30f86d9307d..27eaafb8779 100644 --- a/src/v2/compiler/00_compile.dag +++ b/src/v2/compiler/00_compile.dag @@ -3307,6 +3307,7 @@ fn native_module_resolve_verdict( root: resolved, symbol_index: shared.symbol_index, closure_declarations: closure.declarations, + unavailable_providers: closure.refused, lexical: lexical ) }, diff --git a/src/v2/compiler/03_ingest.dag b/src/v2/compiler/03_ingest.dag index cdfef8d43d3..561a1b662a6 100644 --- a/src/v2/compiler/03_ingest.dag +++ b/src/v2/compiler/03_ingest.dag @@ -318,7 +318,7 @@ fn parse_tree_to_target_model_bridge(parse_tree: ParseTree, source_model: Target o: parse_tree_to_emitted_node(parse_tree: parse_tree, source_model: source_model), f: fn(emitted) { bind_outcome( - o: infer_and_discharge(tree: ResolvedTree { root: emitted, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map() }), + o: infer_and_discharge(tree: ResolvedTree { root: emitted, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [] }), f: fn(inferred) { bind_outcome( o: coerce_grounded_node(source: emitted, tree: inferred, target: source_model), @@ -343,7 +343,7 @@ fn cross_language_compile( o: neutralize_core_for_target(core: core, target: target_model), f: fn(neutralized) { bind_outcome( - o: infer_and_discharge(tree: ResolvedTree { root: neutralized, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map() }), + o: infer_and_discharge(tree: ResolvedTree { root: neutralized, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [] }), f: fn(inferred) { emit(tree: inferred, target: target_model) } diff --git a/src/v2/compiler/03_name_resolve.dag b/src/v2/compiler/03_name_resolve.dag index 406ea500c33..48451346533 100644 --- a/src/v2/compiler/03_name_resolve.dag +++ b/src/v2/compiler/03_name_resolve.dag @@ -13,6 +13,7 @@ import v2.compiler.resolve { ObservationIncomplete, ResolveNodeWalk, ResolveWalkRefused, + ClosureProviderRefusal, ResolvedTree, resolved_tree_outcome, resolve_walk_prefix_pending, @@ -33,7 +34,7 @@ import v2.compiler.namespace_graft { namespace_graft_module_body_root } import v2.compiler.parse { parse_tree_projection_edge } -import std.algebra { Cons, Empty, FreeMonoid } +import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } import v2.std.algebra { fold_list } import v2.std.collection { Absent, Map, Present, List, PointwisePower, empty_map, map_get, map_lookup, map_insert, optional_absent, optional_present } import v2.std.optional { Optional } @@ -147,7 +148,15 @@ type ResolutionContext { policy: NameResolutionPolicy namespaces: Map namespaces_built: Int - closure_declarations: Optional + closure_declarations: Optional +} + +// THE CLOSURE'S RESOLVED DECLARATIONS AND THE ROOTS THAT CONTRIBUTED NONE BECAUSE THEIR OWN RESOLVE +// REFUSED. Both are the one demand's product (closure_declarations_demand) and are memoized together, so a +// later subject reading the cached index also reads which providers it lacks. +type ClosureIndex { + declarations: SymbolIndex + refused: List } // One stored production: the admission it was produced under, and its outcome -- a refusal is @@ -887,7 +896,8 @@ fn resolve_in_context(context: Outcome, admission: Admission) resolved: resolved_tree_outcome( w: walked.walk, symbol_index: shared.symbol_index, - closure_declarations: closure.declarations + closure_declarations: closure.declarations, + unavailable_providers: closure.refused ), context: Accepted { value: closure.context, diagnostics: d } } @@ -910,8 +920,11 @@ fn resolve_in_context(context: Outcome, admission: Admission) // resolve_walk_in_context exactly as any subject is, and only resolved_tree_of reads the collected index. A // provider does not need the closure index to produce the declarations this fold reads from it. // -// A REFUSED ROOT CONTRIBUTES NOTHING AND DOES NOT FAIL THE FOLD. That is deliberate and is NOT an absorbing -// fallback: it does not widen an answer or substitute a reading. A provider that cannot resolve simply has +// A REFUSED ROOT CONTRIBUTES NO DECLARATIONS AND DOES NOT FAIL THE FOLD, AND ITS REFUSAL IS KEPT. It does not +// widen an answer or substitute a reading -- but contributing nothing SILENTLY was the absorbing fallback's +// other half: a consumer's miss could not say the provider never resolved, so the provider's defect never +// ranked. Its module and its first refusal chain are recorded (ClosureProviderRefusal) and reach every +// lookup miss through v2.compiler.resolve resolved_tree_unavailable_provider. A provider that cannot resolve simply has // no resolved declarations to offer, so a receiver typed by one of its records refuses at // infer_reason_projection_receiver_declaration_unavailable -- the located refusal that names missing // evidence. @@ -927,21 +940,23 @@ fn resolve_in_context(context: Outcome, admission: Admission) // projection path reads through symbol_index_lookup and is unaffected. type ClosureDeclarations { declarations: SymbolIndex + refused: List context: ResolutionContext } type ClosureDeclarationsFold { index: SymbolIndex + refused: List context: ResolutionContext } fn closure_declarations_demand(shared: ResolutionContext) -> ClosureDeclarations { match shared.closure_declarations { - Present { value: index } => ClosureDeclarations { declarations: index, context: shared } + Present { value: closure } => ClosureDeclarations { declarations: closure.declarations, refused: closure.refused, context: shared } Absent => let built = fold_list( xs: shared.roots.roots, - empty: ClosureDeclarationsFold { index: empty_symbol_index(), context: shared }, + empty: ClosureDeclarationsFold { index: empty_symbol_index(), refused: [], context: shared }, cons: fn(acc, nt) { match qualified_name_from_module_node(root: nt.root) { Rejected { diagnostics: _ } => acc @@ -958,16 +973,22 @@ fn closure_declarations_demand(shared: ResolutionContext) -> ClosureDeclarations ResolveWalkAccepted { value: resolved, diagnostics: _, lexical: _ } => ClosureDeclarationsFold { index: symbol_index_fill_module_declarations(index: acc.index, root: resolved, record_declarations: Empty, resource_declarations: Empty), + refused: acc.refused, + context: next_context + } + ResolveWalkRefused { first: first, rest: _, observation: _ } => + ClosureDeclarationsFold { + index: acc.index, + refused: list_snoc_item(xs: acc.refused, item: ClosureProviderRefusal { module: module_qn, refusal: first }), context: next_context } - ResolveWalkRefused { first: _, rest: _, observation: _ } => - ClosureDeclarationsFold { index: acc.index, context: next_context } } } } ) ClosureDeclarations { declarations: built.index, + refused: built.refused, context: ResolutionContext { lm: built.context.lm, roots: built.context.roots, @@ -977,7 +998,7 @@ fn closure_declarations_demand(shared: ResolutionContext) -> ClosureDeclarations policy: built.context.policy, namespaces: built.context.namespaces, namespaces_built: built.context.namespaces_built, - closure_declarations: optional_present(value: built.index) + closure_declarations: optional_present(value: ClosureIndex { declarations: built.index, refused: built.refused }) } } } diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index fcbb3aca912..6e85f975f8d 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -231,6 +231,44 @@ type ResolvedTree { symbol_index: SymbolIndex resolved_declarations: SymbolIndex lexical_bindings: Map + unavailable_providers: List +} + +// A CLOSURE ROOT WHOSE OWN RESOLVE REFUSED, AND WHY. v2.compiler.name_resolve closure_declarations_demand +// resolves every root in the closure to fill the index inference reads; a root that refuses contributes no +// declarations, and until this carrier that absence was SILENT -- a receiver typed by one of its records +// refused as a bare lookup miss (infer_reason_projection_receiver_declaration_unavailable) with nothing to +// say the provider never resolved, so the provider's real defect never ranked (DESIGN section 5, the +// absorbing fallback's signal loss). The roster rides on the tree so every lookup miss can name its cause. +type ClosureProviderRefusal { + module: QualifiedName + refusal: NonEmptyDiagnostics +} + +// THE ONE READ every closure-index miss consults: the refused provider whose module path is a prefix of +// the missed declaration's path, if any. A miss with no refused provider over it stays the bare miss. +fn resolved_tree_unavailable_provider(tree: ResolvedTree, path: QualifiedName) -> Optional { + fold(tree.unavailable_providers, init: optional_absent(), f: fn(acc, p) { + match acc { + Present { value: _ } => acc + Absent => if qualified_name_is_proper_prefix(prefix: p.module, path: path) { optional_present(value: p) } else { acc } + } + }) +} + +fn qualified_name_is_proper_prefix(prefix: QualifiedName, path: QualifiedName) -> Bool { + match prefix { + Empty => + match path { + Empty => false + Cons { head: _, tail: _ } => true + } + Cons { head: ph, tail: pt } => + match path { + Empty => false + Cons { head: h, tail: t } => ph == h && qualified_name_is_proper_prefix(prefix: pt, path: t) + } + } } // The resolved root's declarations, by the same module fold the pre-resolve index uses: the module's @@ -263,13 +301,15 @@ fn resolved_tree_of( root: Node, symbol_index: SymbolIndex, closure_declarations: SymbolIndex, + unavailable_providers: List, lexical: List ) -> ResolvedTree { ResolvedTree { root: root, symbol_index: symbol_index, resolved_declarations: resolved_declarations_over(closure: closure_declarations, subject: root), - lexical_bindings: fold(lexical, init: empty_map(), f: fn(m, e) { map_insert(m, e.reference, e.binding) }) + lexical_bindings: fold(lexical, init: empty_map(), f: fn(m, e) { map_insert(m, e.reference, e.binding) }), + unavailable_providers: unavailable_providers } } @@ -2130,12 +2170,13 @@ fn resolve_walk_outcome(w: ResolveNodeWalk) -> Outcome { fn resolved_tree_outcome( w: ResolveNodeWalk, symbol_index: SymbolIndex, - closure_declarations: SymbolIndex + closure_declarations: SymbolIndex, + unavailable_providers: List ) -> Outcome { match w { ResolveWalkAccepted { value: v, diagnostics: d, lexical: l } => Accepted { - value: resolved_tree_of(root: v, symbol_index: symbol_index, closure_declarations: closure_declarations, lexical: l), + value: resolved_tree_of(root: v, symbol_index: symbol_index, closure_declarations: closure_declarations, unavailable_providers: unavailable_providers, lexical: l), diagnostics: d } ResolveWalkRefused { first: f, rest: _, observation: _ } => Rejected { diagnostics: f } @@ -3934,7 +3975,8 @@ fn resolve_with_namespace_policy( policy: policy ), symbol_index: namespace.symbol_index, - closure_declarations: empty_symbol_index() + closure_declarations: empty_symbol_index(), + unavailable_providers: [] ) } diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 8456c43738c..1408d38eedd 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -15,7 +15,7 @@ import v2.std.compilers.target_model { canonical_operation_wire_matches_operation, target_model_canonical_operation_member_declared_type } -import v2.compiler.resolve { ResolvedTree, resolved_tree_lexical_binding } +import v2.compiler.resolve { ResolvedTree, resolved_tree_lexical_binding, resolved_tree_unavailable_provider } import v2.compiler.inferred_tree { DerivedGrounding, GroundingNotDerived, @@ -1139,8 +1139,8 @@ fn infer_field_projection_facts( ) ReceiverTypeNotADeclaration => outcome_rejected(infer_receiver_declares_no_fields_diagnostic(node: node)) - ReceiverDeclarationUnavailable { path: _ } => - outcome_rejected(infer_receiver_declaration_unavailable_diagnostic(node: node)) + ReceiverDeclarationUnavailable { path: path } => + Rejected { diagnostics: infer_closure_miss_refusal(resolved: resolved, path: path, miss: infer_receiver_declaration_unavailable_diagnostic(node: node)) } ReceiverPayload { payload: payload } => match declared_field_named(payload: payload, name: projection.field) { Present { value: declared } => @@ -1256,6 +1256,21 @@ fn infer_receiver_declaration_unavailable_diagnostic(node: Node) -> Diagnostic { } } +// A CLOSURE-INDEX MISS CARRIES ITS PROVIDER'S REFUSAL. When the missed declaration's module is a closure root +// whose own resolve refused (v2.compiler.resolve resolved_tree_unavailable_provider), that refusal chain is +// the miss's cause and travels with it: ahead of the miss when the miss refuses, as advisories when it only +// leaves a fact on the counted frontier. Without a refused provider the miss is exactly what it was. +fn infer_closure_miss_cause(resolved: ResolvedTree, path: QualifiedName) -> Diagnostics { + match resolved_tree_unavailable_provider(tree: resolved, path: path) { + Present { value: provider } => Some { diagnostics: provider.refusal } + Absent => None + } +} + +fn infer_closure_miss_refusal(resolved: ResolvedTree, path: QualifiedName, miss: Diagnostic) -> NonEmptyDiagnostics { + rejected_with_pending(pending: infer_closure_miss_cause(resolved: resolved, path: path), rejected: diagnostics_singleton(d: miss)) +} + fn infer_receiver_declares_no_fields_diagnostic(node: Node) -> Diagnostic { Diagnostic { reason: ^infer_reason_projection_receiver_declares_no_fields, @@ -1365,7 +1380,10 @@ fn infer_declaration_reference_facts( Present { value: path } => match symbol_index_lookup(index: resolved.resolved_declarations, qualified_path: path) { Absent => - inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) + bind_outcome_accepted( + od: infer_closure_miss_cause(resolved: resolved, path: path), + inner: inferred_facts_not_derived(node: n, descent: Holds { value: descent_proof }) + ) Present { value: declared } => match infer_declaration_callable_evidence(declared: declared) { Absent => diff --git a/src/v2/compiler/self_host/closure_emission.dag b/src/v2/compiler/self_host/closure_emission.dag index 80dc5fd8f1d..43acb183a36 100644 --- a/src/v2/compiler/self_host/closure_emission.dag +++ b/src/v2/compiler/self_host/closure_emission.dag @@ -348,7 +348,7 @@ fn closure_resolve_member( policy: default_name_resolution_policy() )), admission: Admission { subject: ResolutionSubject { name: member_module }, imports: Empty } - ).walk, symbol_index: symbol_index, closure_declarations: empty_symbol_index()) + ).walk, symbol_index: symbol_index, closure_declarations: empty_symbol_index(), unavailable_providers: []) } ) } diff --git a/src/v2/test/claim/arrow_order_edge/admission_wall_test.dag b/src/v2/test/claim/arrow_order_edge/admission_wall_test.dag index aafa346c048..e1292950953 100644 --- a/src/v2/test/claim/arrow_order_edge/admission_wall_test.dag +++ b/src/v2/test/claim/arrow_order_edge/admission_wall_test.dag @@ -81,7 +81,7 @@ test fn aoe_well_formed_refuses_a_named_domain_without_order() -> Bool { // PATH: infer, entered directly with a ResolvedTree. fn aoe_infer_reason(arrow: Node) -> Symbol { - match infer(tree: ResolvedTree { root: aoe_module_root(arrow: arrow), symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map() }) { + match infer(tree: ResolvedTree { root: aoe_module_root(arrow: arrow), symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [] }) { Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) Accepted { value: _, diagnostics: _ } => ^aoe_accepted } diff --git a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag index 76e3b4d5e0c..b9e276b274c 100644 --- a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag +++ b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag @@ -191,7 +191,8 @@ fn fmi_infer_with_member_formal_authored() -> Outcome { root: fmi_replace_atom(n: tree.root, from: variable, to: int_type), symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, - lexical_bindings: tree.lexical_bindings + lexical_bindings: tree.lexical_bindings, + unavailable_providers: tree.unavailable_providers }) { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: _, diagnostics: d } => Accepted { value: true, diagnostics: d } diff --git a/src/v2/test/claim/compiler/infer_record_construct_field_inhabitance_witness_test.dag b/src/v2/test/claim/compiler/infer_record_construct_field_inhabitance_witness_test.dag index d37c322493e..d368b56d5e3 100644 --- a/src/v2/test/claim/compiler/infer_record_construct_field_inhabitance_witness_test.dag +++ b/src/v2/test/claim/compiler/infer_record_construct_field_inhabitance_witness_test.dag @@ -297,7 +297,7 @@ fn rcf_member_accepted_and_decided(resolved: Outcome, member: Symb match rcf_member_arrow(root: tree.root, name: member) { Absent => false Present { value: arrow } => - match infer(tree: ResolvedTree { root: arrow, symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings }) { + match infer(tree: ResolvedTree { root: arrow, symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings, unavailable_providers: tree.unavailable_providers }) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: ds } => !diagnostics_has_reason(d: ds, reason: ^inhabitance_undecidable_formal_unresolved) diff --git a/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag b/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag index 8e9f7847c37..6be6cc35632 100644 --- a/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag +++ b/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag @@ -201,7 +201,7 @@ test fn kvr_a_named_calls_bool_formal_is_counted_not_judged_holds() -> Bool { match rcf_member_arrow(root: tree.root, name: ^kvr_g) { Absent => false Present { value: arrow } => - match infer(tree: ResolvedTree { root: arrow, symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings }) { + match infer(tree: ResolvedTree { root: arrow, symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings, unavailable_providers: tree.unavailable_providers }) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: ds } => diagnostics_has_reason(d: ds, reason: ^inhabitance_undecidable_formal_unresolved) } diff --git a/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag b/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag index 6b5f71c923a..1647820f18e 100644 --- a/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag +++ b/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag @@ -149,7 +149,7 @@ fn ingested_arrow_from_source(source: String) -> Outcome { f: fn(resolved) { match ingested_find_arrow_in_module(root: resolved.root) { Present { value: arrow } => - outcome_accepted(value: ResolvedTree { root: arrow, symbol_index: empty_symbol_index(), resolved_declarations: resolved.resolved_declarations, lexical_bindings: resolved.lexical_bindings }) + outcome_accepted(value: ResolvedTree { root: arrow, symbol_index: empty_symbol_index(), resolved_declarations: resolved.resolved_declarations, lexical_bindings: resolved.lexical_bindings, unavailable_providers: resolved.unavailable_providers }) Absent => outcome_rejected( d: ingested_classical_not_diagnostic(reason: ^ingested_classical_not_arrow_miss, node: resolved.root) diff --git a/src/v2/test/claim/match_binder/match_binder_typing_test.dag b/src/v2/test/claim/match_binder/match_binder_typing_test.dag index 803048189dd..67548614d09 100644 --- a/src/v2/test/claim/match_binder/match_binder_typing_test.dag +++ b/src/v2/test/claim/match_binder/match_binder_typing_test.dag @@ -474,7 +474,8 @@ fn mbt_resolved() -> ResolvedTree { root: mbt_atom(s: ^mbt_unused_root), symbol_index: mbt_index(), resolved_declarations: empty_symbol_index(), - lexical_bindings: empty_map() + lexical_bindings: empty_map(), + unavailable_providers: [] } } diff --git a/src/v2/test/claim/n7probe/n7_provider.dag b/src/v2/test/claim/n7probe/n7_provider.dag new file mode 100644 index 00000000000..c099ffd2f2a --- /dev/null +++ b/src/v2/test/claim/n7probe/n7_provider.dag @@ -0,0 +1,7 @@ +module v2.test.n7probe.n7_provider + +import std.types { Int } + +type N7Leg { + target: Int +} diff --git a/src/v2/test/claim/n7probe/root_children_test.dag b/src/v2/test/claim/n7probe/root_children_test.dag index 568d1ee2416..171ea98064b 100644 --- a/src/v2/test/claim/n7probe/root_children_test.dag +++ b/src/v2/test/claim/n7probe/root_children_test.dag @@ -2,12 +2,17 @@ module v2.test.n7probe.root_children import v2.std.node { Edge, Node, Symbol } import v2.std.diagnostic { Diagnostic } +import v2.test.n7probe.n7_provider { N7Leg } import v2.std.logic { Bool } import v2.std.collection { List } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +fn n7_leg(l: N7Leg) -> Int { + l.target +} + fn n7_reason(d: Diagnostic) -> Symbol { d.reason } diff --git a/src/v2/test/lens_common/infer_fixture.dag b/src/v2/test/lens_common/infer_fixture.dag index e329a948367..732a1cce00a 100644 --- a/src/v2/test/lens_common/infer_fixture.dag +++ b/src/v2/test/lens_common/infer_fixture.dag @@ -20,7 +20,7 @@ import v2.std.witness { Holds, StructuralPropertyWitness, Witness, witness_from_ // resolution consulted; a supplied tree was never resolved, so it carries an index holding NO // declarations -- the name says so, and a declaration lookup through it finds nothing and refuses. fn claim_resolved_tree_without_declarations(root: Node) -> ResolvedTree { - ResolvedTree { root: root, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map() } + ResolvedTree { root: root, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [] } } fn claim_atom_node(s: Symbol) -> Node { diff --git a/src/v2/workflow/realization_attempt.dag b/src/v2/workflow/realization_attempt.dag index 2484bb2fd53..d4b8e42206c 100644 --- a/src/v2/workflow/realization_attempt.dag +++ b/src/v2/workflow/realization_attempt.dag @@ -284,7 +284,7 @@ fn attempt_joined(program: ResolvedTree, fn_name: Symbol, entry: String) -> Entr match list_at_optional(xs: joined, index: 0) { Absent => attempt_refused(entry: entry, phase: PhaseResolve, cause: ^realization_attempt_identity_absent) Present { value: decl } => - match infer_and_discharge(tree: ResolvedTree { root: decl, symbol_index: program.symbol_index, resolved_declarations: program.resolved_declarations, lexical_bindings: program.lexical_bindings }) { + match infer_and_discharge(tree: ResolvedTree { root: decl, symbol_index: program.symbol_index, resolved_declarations: program.resolved_declarations, lexical_bindings: program.lexical_bindings, unavailable_providers: program.unavailable_providers }) { Rejected { diagnostics: ds } => attempt_refused_at(entry: entry, phase: PhaseInfer, cause: first_located_cause(ds: ds), located: first_located_file(ds: ds)) Accepted { value: inferred, diagnostics: _ } => From 7eb44d9980f5c28bcfd2d54b68296eabad28f9fb Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 01:52:47 +0000 Subject: [PATCH 72/90] N7-1 (x)(2): v2 std modules that spelled List without importing it import std.types List -- each refused its own native resolve and vanished from the closure index Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/std/cardinality.dag | 1 + src/v2/std/data_initializer_identity.dag | 1 + src/v2/std/decl_index.dag | 1 + src/v2/std/decl_ref_resolution.dag | 1 + src/v2/std/diagnostic.dag | 1 + src/v2/std/effect_plan.dag | 1 + src/v2/std/grammar.dag | 1 + src/v2/std/inhabitance.dag | 1 + src/v2/std/integer.dag | 2 +- src/v2/std/native_agreement.dag | 1 + src/v2/std/node.dag | 1 + src/v2/std/orchestration.dag | 1 + src/v2/std/qualified_name.dag | 1 + src/v2/std/runtime.dag | 1 + src/v2/std/symbol_index.dag | 1 + src/v2/std/template.dag | 1 + src/v2/std/timeseries_signal.dag | 2 +- src/v2/std/verdict.dag | 1 + 18 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/v2/std/cardinality.dag b/src/v2/std/cardinality.dag index a892a60f035..44b341beeb2 100644 --- a/src/v2/std/cardinality.dag +++ b/src/v2/std/cardinality.dag @@ -1,5 +1,6 @@ module v2.std.cardinality +import std.types { List } import std.algebra { Cons, Empty } import std.termination { DescentEvidence, diff --git a/src/v2/std/data_initializer_identity.dag b/src/v2/std/data_initializer_identity.dag index 54f8d5573c7..30a80bb26b0 100644 --- a/src/v2/std/data_initializer_identity.dag +++ b/src/v2/std/data_initializer_identity.dag @@ -1,5 +1,6 @@ module v2.std.data_initializer_identity +import std.types { List } import v2.std.node { symbol_lexeme } import v2.std.grammar { node_atom_identity_optional } import v2.std.decl_index { DeclFact } diff --git a/src/v2/std/decl_index.dag b/src/v2/std/decl_index.dag index c1ebcbbc99c..2276b516e93 100644 --- a/src/v2/std/decl_index.dag +++ b/src/v2/std/decl_index.dag @@ -1,4 +1,5 @@ module v2.std.decl_index +import std.types { List } import v2.std.arrow_signature { declared_signature_arrow } import std.algebra { Empty, list_snoc_item } import v2.std.diagnostic { Diagnostic, ExternalContractUnknown, Outcome, Unavailable, bind_outcome, node_locus, outcome_accepted, outcome_rejected } diff --git a/src/v2/std/decl_ref_resolution.dag b/src/v2/std/decl_ref_resolution.dag index e3f0abd8a95..9a1161ea04b 100644 --- a/src/v2/std/decl_ref_resolution.dag +++ b/src/v2/std/decl_ref_resolution.dag @@ -1,5 +1,6 @@ module v2.std.decl_ref_resolution +import std.types { List } import std.decl_ref { DeclarationRef, WholeDeclaration, NamedField, TypeParameter } import v2.lens.module_graph { ModuleDeclarationFact, module_declaration_facts_at_live } import v2.std.decl_index { DeclFact, ItemKind, decl_facts_at, logical_qualified_name_from_module } diff --git a/src/v2/std/diagnostic.dag b/src/v2/std/diagnostic.dag index fd5e345dcc1..8956d54c4c7 100644 --- a/src/v2/std/diagnostic.dag +++ b/src/v2/std/diagnostic.dag @@ -1,5 +1,6 @@ module v2.std.diagnostic +import std.types { List } import v2.std.algebra { bag_eq, fold_list } import std.algebra { FreeMonoid, Monoid, Semigroup, list_append, list_snoc_item, freemonoid_empty } import std.dissolution { DissolutionCondition, unbound_dissolution } diff --git a/src/v2/std/effect_plan.dag b/src/v2/std/effect_plan.dag index 12659a74b19..4924b648c68 100644 --- a/src/v2/std/effect_plan.dag +++ b/src/v2/std/effect_plan.dag @@ -1,5 +1,6 @@ module v2.std.effect_plan +import std.types { List } import std.dissolution { DissolutionCondition, unbound_dissolution } import v2.std.text { String } diff --git a/src/v2/std/grammar.dag b/src/v2/std/grammar.dag index 2870d5a53e5..0c374b9d387 100644 --- a/src/v2/std/grammar.dag +++ b/src/v2/std/grammar.dag @@ -1,5 +1,6 @@ module v2.std.grammar +import std.types { List } import v2.std.parse_refusal_reason { ParseRefusalReason, parse_refusal_reason_symbol } import std.occurrence_identity { OccurrenceSynthetic } import std.algebra { Cons, Empty, list_append, list_snoc_item } diff --git a/src/v2/std/inhabitance.dag b/src/v2/std/inhabitance.dag index 6488628cccb..e1957a297c2 100644 --- a/src/v2/std/inhabitance.dag +++ b/src/v2/std/inhabitance.dag @@ -1,5 +1,6 @@ module v2.std.inhabitance +import std.types { List } import v2.std.diagnostic { Accepted, Diagnostic, diff --git a/src/v2/std/integer.dag b/src/v2/std/integer.dag index 7f1e174c4d2..aa5d9a15c98 100644 --- a/src/v2/std/integer.dag +++ b/src/v2/std/integer.dag @@ -20,7 +20,7 @@ import v2.std.node { Atom, Conj, Edge, Named, Node, Symbol, TypeNode, is_empty_c import v2.std.node_query { find_named_child } import std.integer { Signedness, Signed, Unsigned } import std.content_hash { Fnv1a64Structural, content_hash_atom, content_hash_tagged_structural } -import std.types { NonEmptyStr } +import std.types { NonEmptyStr, List } import v2.std.machine { Byte, MachineWidth, diff --git a/src/v2/std/native_agreement.dag b/src/v2/std/native_agreement.dag index 9a5b4dacbbd..3c9102c1380 100644 --- a/src/v2/std/native_agreement.dag +++ b/src/v2/std/native_agreement.dag @@ -1,5 +1,6 @@ module v2.std.native_agreement +import std.types { List } import v2.std.algebra { Cons, Empty } import v2.std.collection { List, diff --git a/src/v2/std/node.dag b/src/v2/std/node.dag index bb174f741a0..8a9aab78fe7 100644 --- a/src/v2/std/node.dag +++ b/src/v2/std/node.dag @@ -1,5 +1,6 @@ module v2.std.node +import std.types { List } import std.algebra { Cons, Empty, list_snoc_item } import v2.std.optional { Present } import std.occurrence_identity { diff --git a/src/v2/std/orchestration.dag b/src/v2/std/orchestration.dag index 03ad632f186..ff22e2d2a00 100644 --- a/src/v2/std/orchestration.dag +++ b/src/v2/std/orchestration.dag @@ -1,5 +1,6 @@ module v2.std.orchestration +import std.types { List } import std.dissolution { DissolutionCondition, unbound_dissolution } import v2.std.text { String } diff --git a/src/v2/std/qualified_name.dag b/src/v2/std/qualified_name.dag index f6b0bb5693c..b089cba3f19 100644 --- a/src/v2/std/qualified_name.dag +++ b/src/v2/std/qualified_name.dag @@ -1,5 +1,6 @@ module v2.std.qualified_name +import std.types { List } import v2.std.diagnostic { Accepted, Diagnostic, diff --git a/src/v2/std/runtime.dag b/src/v2/std/runtime.dag index c9326294f0c..5eb8ae560a7 100644 --- a/src/v2/std/runtime.dag +++ b/src/v2/std/runtime.dag @@ -1,5 +1,6 @@ module v2.std.runtime +import std.types { List } import v2.std.collection { List, Map diff --git a/src/v2/std/symbol_index.dag b/src/v2/std/symbol_index.dag index fdb316fbb26..9a3f039b351 100644 --- a/src/v2/std/symbol_index.dag +++ b/src/v2/std/symbol_index.dag @@ -1,6 +1,7 @@ module v2.std.symbol_index +import std.types { List } import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } import v2.std.algebra { HeadAbsent, diff --git a/src/v2/std/template.dag b/src/v2/std/template.dag index dc56807a480..f2bdda12d3e 100644 --- a/src/v2/std/template.dag +++ b/src/v2/std/template.dag @@ -1,5 +1,6 @@ module v2.std.template +import std.types { List } import v2.std.algebra { list_flat_map } // ONE literal-or-hole sequence, parameterized by what a hole is. A string that interpolates is a diff --git a/src/v2/std/timeseries_signal.dag b/src/v2/std/timeseries_signal.dag index 5121c85a280..03463b42913 100644 --- a/src/v2/std/timeseries_signal.dag +++ b/src/v2/std/timeseries_signal.dag @@ -28,7 +28,7 @@ import std.measure { import v2.std.nat { Nat } import v2.std.node { Symbol } import v2.std.text { String } -import std.types { brand } +import std.types { brand, List } type Voltage = Measure type Current = Measure diff --git a/src/v2/std/verdict.dag b/src/v2/std/verdict.dag index caf88ac9b30..07722d2c940 100644 --- a/src/v2/std/verdict.dag +++ b/src/v2/std/verdict.dag @@ -1,5 +1,6 @@ module v2.std.verdict +import std.types { List } import std.algebra { Monoid } import v2.std.algebra { is_empty } import v2.std.collection { From 5585a399351aa3c31ae45fbb6ec7f4876d3186aa Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 02:25:23 +0000 Subject: [PATCH 73/90] N7-1 (x)(2): a projection off a value resolves its base and not its field (diagnostics_fatal(d: d).reason refused v2.std.diagnostic's own resolve) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_resolve.dag | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 6e85f975f8d..b77f41411fe 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -166,6 +166,8 @@ import v2.std.node { } import v2.std.type_binder { GenericTypeDecl, PlainTypeDecl, type_decl_view, edge_is_cast_target, edge_is_type_annotation, edge_is_type_params, type_binder_first_mislabelled, type_binder_labels_conform, type_param_names } import v2.std.node_query { + field_projection_field_name, + field_projection_optional, binder_default_label, binder_node_label, binder_node_parts, @@ -3206,9 +3208,13 @@ fn resolve_node_walk_entered( resolve_record_field_edge(ctx: ctx, e: e, record: record) }) NotAConstruct => - match try_resolve_qualified_name_node(ctx: ctx, n: n) { - Present { value: outcome } => resolve_walk_of_atom(o: outcome) - Absent => resolve_children_homogeneous_scope(ctx: ctx, n: n) + match field_projection_optional(n: n) { + Present { value: _ } => resolve_value_projection_walk(ctx: ctx, n: n) + Absent => + match try_resolve_qualified_name_node(ctx: ctx, n: n) { + Present { value: outcome } => resolve_walk_of_atom(o: outcome) + Absent => resolve_children_homogeneous_scope(ctx: ctx, n: n) + } } } } @@ -3509,6 +3515,27 @@ fn arrow_domain_has_named_bindings(n: Node) -> Bool { } } +// A PROJECTION OFF A VALUE RESOLVES ITS BASE AND NOT ITS FIELD. Body lowering builds +// v2.std.node_query field_projection_node over a base that is already a value -- a call result +// (`diagnostics_fatal(d: d).reason`), a cast, a construct -- where a bound-name head instead stays a +// spine for resolve_bound_head_projection. Walked as an ordinary Conj, the FIELD atom was resolved as a +// free name and refused unbound (or bound to an unrelated declaration of the same spelling); a field is +// not a reference -- whether the base's type declares it is infer's judgement over the receiver's type, +// exactly as for the bound-head projection resolve builds itself. +fn resolve_value_projection_walk(ctx: ResolveContext, n: Node) -> ResolveNodeWalk { + child_walk_node(n: n, w: fold(n.children, init: child_walk_init(), f: fn(acc, e) { + match e.label { + Named { name: name } => + if name == field_projection_field_name() { + child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None, lexical: [] }) + } else { + child_walk_step(w: acc, e: e, r: resolve_child_edge(ctx: ctx, e: e)) + } + Positional => child_walk_step(w: acc, e: e, r: resolve_child_edge(ctx: ctx, e: e)) + } + })) +} + fn resolve_children_homogeneous_scope( ctx: ResolveContext, n: Node From 83a31c8ac0229115ae85bd98e52ad29e691d9daa Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 3 Oct 2026 02:26:36 +0000 Subject: [PATCH 74/90] arrow_lambda_block_body: one warm producer for the five claims Each claim normalizes its own source, which is over the required floor's new-witness enrolment margin per claim (the same refusal #13029's if_arm_position claims met on their first floor run). alb_verdicts computes the five verdicts once into Bools, each claim reads its field, and the producer is enrolled WARM in v2.workflow.floor_pure_producer_share beside eam_outcomes. claim_batch: 5/5 PASS. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../arrow_lambda_block_body_test.dag | 31 ++++++++++++++++--- src/v2/workflow/floor_pure_producer_share.dag | 4 +++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag b/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag index 333f8570ba2..b0abb8ec800 100644 --- a/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag +++ b/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag @@ -57,22 +57,43 @@ data src_fold_step_fn_literal_block: String = "module m.t\nfn t(xs: List) - data src_fold_step_arrow_expr: String = "module m.t\nfn t(xs: List) -> Int {\n fold(xs, init: 0, f: (acc, x) => acc + x)\n}\n" +// ONE PRODUCER FOR EVERY CLAIM, enrolled WARM in v2.workflow.floor_pure_producer_share: each verdict +// pays a full tokenize -> parse -> normalize, which is over the new-witness enrolment margin per claim. +// The stored value is five Bools (no Node, no closure), so it is portable. +type AlbVerdicts { + fold_step_arrow_block: Bool, + fold_step_arrow_block_let_record: Bool, + value_arrow_block: Bool, + fold_step_fn_literal_block: Bool, + fold_step_arrow_expr: Bool, +} + +fn alb_verdicts() -> AlbVerdicts { + AlbVerdicts { + fold_step_arrow_block: normalize_outcome(src: src_fold_step_arrow_block) == "ACCEPTED", + fold_step_arrow_block_let_record: normalize_outcome(src: src_fold_step_arrow_block_let_record) == "ACCEPTED", + value_arrow_block: normalize_outcome(src: src_value_arrow_block) == "ACCEPTED", + fold_step_fn_literal_block: normalize_outcome(src: src_fold_step_fn_literal_block) == "ACCEPTED", + fold_step_arrow_expr: normalize_outcome(src: src_fold_step_arrow_expr) == "ACCEPTED", + } +} + test fn fold_step_arrow_block_body_lowers_holds() -> Bool { - normalize_outcome(src: src_fold_step_arrow_block) == "ACCEPTED" + alb_verdicts().fold_step_arrow_block } test fn fold_step_arrow_block_with_let_and_record_lowers_holds() -> Bool { - normalize_outcome(src: src_fold_step_arrow_block_let_record) == "ACCEPTED" + alb_verdicts().fold_step_arrow_block_let_record } test fn value_position_arrow_block_body_lowers_holds() -> Bool { - normalize_outcome(src: src_value_arrow_block) == "ACCEPTED" + alb_verdicts().value_arrow_block } test fn fold_step_fn_literal_block_control_holds() -> Bool { - normalize_outcome(src: src_fold_step_fn_literal_block) == "ACCEPTED" + alb_verdicts().fold_step_fn_literal_block } test fn fold_step_arrow_expression_body_control_holds() -> Bool { - normalize_outcome(src: src_fold_step_arrow_expr) == "ACCEPTED" + alb_verdicts().fold_step_arrow_expr } diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 766c4828ff7..40fe0e00f4e 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -651,6 +651,9 @@ import v2.std.collection { List } // five claims over the enrolment margin at ~590ms each, every one of them paying the same ingest. // THE XL-2 ELSE-ARM ROW IS THE SAME GROUND AT FIVE SPECIMENS: v2.test.claim.namespace_xl0.else_arm_nested_if // eam_outcomes drives one front end over five inline modules and stores one verdict arm per module. +// THE ARROW-LAMBDA BLOCK-BODY ROW IS THE SAME GROUND AT FIVE SPECIMENS: v2.test.claim.body_lowering.arrow_lambda_block_body +// alb_verdicts normalizes five inline modules and stores five Bools; each would otherwise be over the floor's +// new-witness enrolment margin (the required floor's claim_eval_step_budget_for_identity). // THE XL-2 REIFY-OPERAND ROW: v2.test.claim.body_lowering.reify_operand_refusal ror_verdicts tokenizes // and parses two inline modules and reifies two supplied bodies over each; the stored value is four // verdicts (no Node, no closure), portable for the same reason cav_outcomes is. @@ -827,6 +830,7 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.body_lowering.match_position_structure.mps_normalized", "v2.test.claim.namespace_xl0.wildcard_arm_resolve.wc_outcomes", "v2.test.claim.namespace_xl0.else_arm_nested_if.eam_outcomes", + "v2.test.claim.body_lowering.arrow_lambda_block_body.alb_verdicts", "v2.test.cli.v2_native_cli.cli_probe_trailing_outcome", "v2.test.emit.closure_emit_arrow_body_refusal.arrow_body_probe_verdict", "v2.test.emit.closure_emit_arrow_body_refusal.arrow_body_mixed_probe_verdict", From 2dae714bf47c08d18b443d7c9dea3d6e4b8a6298 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 02:37:37 +0000 Subject: [PATCH 75/90] N7-1: value-projection arm reads the field edge through field_projection_edge_field_optional Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_resolve.dag | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index b77f41411fe..5f9d1f49e25 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -166,7 +166,7 @@ import v2.std.node { } import v2.std.type_binder { GenericTypeDecl, PlainTypeDecl, type_decl_view, edge_is_cast_target, edge_is_type_annotation, edge_is_type_params, type_binder_first_mislabelled, type_binder_labels_conform, type_param_names } import v2.std.node_query { - field_projection_field_name, + field_projection_edge_field_optional, field_projection_optional, binder_default_label, binder_node_label, @@ -3524,14 +3524,9 @@ fn arrow_domain_has_named_bindings(n: Node) -> Bool { // exactly as for the bound-head projection resolve builds itself. fn resolve_value_projection_walk(ctx: ResolveContext, n: Node) -> ResolveNodeWalk { child_walk_node(n: n, w: fold(n.children, init: child_walk_init(), f: fn(acc, e) { - match e.label { - Named { name: name } => - if name == field_projection_field_name() { - child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None, lexical: [] }) - } else { - child_walk_step(w: acc, e: e, r: resolve_child_edge(ctx: ctx, e: e)) - } - Positional => child_walk_step(w: acc, e: e, r: resolve_child_edge(ctx: ctx, e: e)) + match field_projection_edge_field_optional(e: e) { + Present { value: _ } => child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None, lexical: [] }) + Absent => child_walk_step(w: acc, e: e, r: resolve_child_edge(ctx: ctx, e: e)) } })) } From 744946f8c4eb3a49d64841058598b8f32a00fc18 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 03:29:00 +0000 Subject: [PATCH 76/90] N7-1b: std.algebra collection callback operations (map, all) realized through the one fold encoding, keyed on named template rows std.algebra: map, flat_map, any, all and sort_by become named shapes, each referenced from both profile lists that carried a copy (the row a realized head binds to). v2.compiler.fold_lowering: collection_roster_rows is the one roster; a callback row's step is synthesized around the callback's own binder (CallbackRealization). v2.compiler.body_lowering_fold builds it: map = Cons onto the carrier, all = && into it, O(1) per member. filter/any stay unrealized (v2.std.algebra declares ordinary fns of those names; measured 41+41 importing modules would refuse as shadowed), as do flat_map and sort_by: counted frontier in gunbc.recurring_failure_mode collection_callback_operation_unrealized_in_v2_lowering. The missing iteration-direction fact is rostered as fold_realized_loop_carries_no_iteration_direction. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ck_operation_unrealized_in_v2_lowering.dag | 24 ++ ...ed_loop_carries_no_iteration_direction.dag | 19 ++ dag/std/algebra.dag | 43 +++- src/v2/compiler/body_lowering_fold.dag | 141 ++++++++++- src/v2/compiler/fold_lowering.dag | 233 +++++++++++++++--- .../complexity_accumulator_copy/analyze.dag | 6 +- src/v2/std/anonymous_binder.dag | 13 + .../collection_callback_realization_test.dag | 165 +++++++++++++ 8 files changed, 598 insertions(+), 46 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/collection_callback_operation_unrealized_in_v2_lowering.dag create mode 100644 dag/gunbc/recurring_failure_mode/fold_realized_loop_carries_no_iteration_direction.dag create mode 100644 src/v2/test/claim/compiler/collection_callback_realization_test.dag diff --git a/dag/gunbc/recurring_failure_mode/collection_callback_operation_unrealized_in_v2_lowering.dag b/dag/gunbc/recurring_failure_mode/collection_callback_operation_unrealized_in_v2_lowering.dag new file mode 100644 index 00000000000..8898db7a72f --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/collection_callback_operation_unrealized_in_v2_lowering.dag @@ -0,0 +1,24 @@ +module gunbc.recurring_failure_mode.collection_callback_operation_unrealized_in_v2_lowering + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data collection_callback_operation_unrealized_in_v2_lowering: RecurringFailureMode = RecurringFailureMode { + identity: "collection_callback_operation_unrealized_in_v2_lowering" as NonEmptyStr, + receipts: [ + "INVALID STATE: std.algebra models collection callback operations as template rows with callback_element_position Present (map, filter, flat_map, any, all, sort_by), but v2 body lowering realized only the fold family. A call of an unrealized row reaches v2.compiler.resolve as a bare name and refuses resolve_reason_unbound_symbol. HARM: v2.std.node calls `map(node.children, e => Edge { .. })`, so on the native route it refused its own resolve and dropped out of the closure index; about twenty other v2 modules call bare `map(`. The refusal is typed and located, so this is a capability gap on the loud side of the ladder, never a silent wrong answer.", + "DISTINGUISHING FACTS: the earliest unjustified boundary was the MODEL, not lowering. A realized roster call binds its head to the row's declaration (v2.compiler.resolve resolve_realized_declaration), and map / flat_map / any / all / sort_by were inline list elements in std.algebra free_monoid_collection_templates and finite_power_set_templates, copied once per list, with no declaration to bind to. They are now named shapes (collection_map_shape and siblings) referenced from both lists, as fold and filter already were.", + "CLIMB (N7-1b): map and all are realized through the one fold encoding, keyed on their rows in v2.compiler.fold_lowering collection_roster_rows. Each step is synthesized around the callback's own binder (CallbackRealization), and is O(1) per member: map conses one cell onto the carrier, and all joins one Bool. Controls: v2.test.claim.compiler.collection_callback_realization. all's body types through &&, which infer does not yet join (a separate frontier), so its control asserts the head binding and lowering, not infer acceptance.", + "COUNTED FRONTIER, 4 rows, by name. filter and any: v2.std.algebra DECLARES ordinary fns `filter` and `any`, and 41 modules each import and call them positionally or piped (measured at N7-1b's base, 2dae714bf4). Lowering classifies a head by its spelling without a binding, so a realized row would lower those calls to a Loop that v2.compiler.resolve resolve_realized_declaration refuses as realized_declaration_shadowed. That is a section 3 fork, the roster row and the corpus fn being one operation under two authorities. TRIGGER: the fork is consolidated (v2.std.algebra any/filter retired onto the roster operation, or a binding-aware realization under which a head bound to a corpus declaration stays an ordinary call), SUFFICIENT FOR an unbound `filter(xs, e => ..)` / `any(xs, e => ..)` to lower and bind its row while every imported call keeps binding. Its guard is ccr_declared_any_stays_an_ordinary_call. flat_map: its step is itself a fold over the callback's result (v2.std.algebra list_flat_map's nested fold_list_right), which the synthesized-step table cannot express as one cell. TRIGGER: a nested-fold step, i.e. a CallbackRealization arm whose step body is a fold encoding over the callback body with the outer accumulator as its init, SUFFICIENT FOR `flat_map(xs, e => f(e))` to lower and its head to bind collection_flat_map_shape. sort_by: cost_shape ShapeSortBody, which is not an iterate body, so it has no fold realization at all. TRIGGER: a sort realization the template's ShapeSortBody names, SUFFICIENT FOR `sort_by(xs, e => k(e))` to lower and its head to bind collection_sort_by_shape. RUNG: mitigatable (typed refusal at resolve). CEILING: structurally guaranteed: every fact is decidable from the row.", + ], + evidence: [ + DeclarationRef { module_path: "std.algebra", decl_name: "collection_map_shape", field: WholeDeclaration }, + DeclarationRef { module_path: "std.algebra", decl_name: "collection_flat_map_shape", field: WholeDeclaration }, + DeclarationRef { module_path: "std.algebra", decl_name: "collection_sort_by_shape", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "collection_roster_rows", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "CallbackRealization", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_callback_encoding", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.compiler.collection_callback_realization", decl_name: "ccr_map_over_records_reads_a_field", field: WholeDeclaration }, + ], +} diff --git a/dag/gunbc/recurring_failure_mode/fold_realized_loop_carries_no_iteration_direction.dag b/dag/gunbc/recurring_failure_mode/fold_realized_loop_carries_no_iteration_direction.dag new file mode 100644 index 00000000000..052b95a1e1e --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/fold_realized_loop_carries_no_iteration_direction.dag @@ -0,0 +1,19 @@ +module gunbc.recurring_failure_mode.fold_realized_loop_carries_no_iteration_direction + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data fold_realized_loop_carries_no_iteration_direction: RecurringFailureMode = RecurringFailureMode { + identity: "fold_realized_loop_carries_no_iteration_direction" as NonEmptyStr, + receipts: [ + "INVALID STATE: a fold-realized Loop carries no iteration direction. v2.compiler.fold_lowering fold_recurrence_encoding builds the identical Loop for the collection fold, fold_list (left) and fold_list_right (right), and since N7-1b for map, whose synthesized step conses onto the carrier and is order-preserving ONLY under right-to-left iteration (all joins with &&, which is order-insensitive). The meaning is implied by the realized declaration (^loop_realized_declaration_edge), and no executor reads it: v2.compiler.eval executes a Loop through the runtime LoopInterpreter (eval_loop_node, step_loop) without consulting the realized edge, and the native route executes the seed's emission of the .dag source, not this encoding. HARM, latent: an executor that ran these encodings left-to-right would return map results reversed, which is silent wrongness, and fold_list vs fold_list_right would be indistinguishable. Today nothing executes them, so no program observes it.", + "DISTINGUISHING FACTS: this is not a cost defect. Each synthesized step is O(1) per member (one Cons onto the carrier, never snoc or append), held structurally by v2.test.claim.compiler.collection_callback_realization ccr_map_step_conses_onto_its_accumulator. It is also why an eval_steps figure for `map` measures the seed path, not this lowering.", + "RUNG FOUND AT: outside the executed path, latent; it becomes silent wrongness the moment an executor consumes the encoding, so it must climb before any does. CEILING: structurally impossible, since direction is decidable from the realized row. NEXT-RUNG TRIGGER, stated as the capability: a per-row iteration-direction fact derived from the template (or the realized declaration) AND an executor of fold-realized Loops that consumes it, SUFFICIENT FOR map, fold_list and fold_list_right encodings to evaluate in their declared order, with a control that reverses the fact and goes red.", + ], + evidence: [ + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "fold_recurrence_encoding", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "CallbackRealization", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.compiler.collection_callback_realization", decl_name: "ccr_map_step_conses_onto_its_accumulator", field: WholeDeclaration }, + ], +} diff --git a/dag/std/algebra.dag b/dag/std/algebra.dag index d91a3aa3990..da77bd7fefc 100644 --- a/dag/std/algebra.dag +++ b/dag/std/algebra.dag @@ -393,6 +393,31 @@ fn collection_fold_shape() -> AlgebraFieldTemplate { { name: "fold", param_types: [ReceiverSelf, AlgebraTypeVariable { id: "FoldAccumulator" }, CallableOf { params: [AlgebraTypeVariable { id: "FoldAccumulator" }, ReceiverElement], return_type: AlgebraTypeVariable { id: "FoldAccumulator" } }], return_type: AlgebraTypeVariable { id: "FoldAccumulator" }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 1 } } } +// The collection callback operations, each ONE template declared once and referenced from every profile +// list that carries it, as filter and fold are. A named row is what a realized call head binds to +// (v2.compiler.fold_lowering fold_family_realization), and one row per operation removes the copy each +// list used to hold. + +fn collection_map_shape() -> AlgebraFieldTemplate { + { name: "map", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: AlgebraTypeVariable { id: "MappedElement" } }], return_type: ContainerOf { source: SameAsReceiver, element: AlgebraTypeVariable { id: "MappedElement" } }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } } +} + +fn collection_flat_map_shape() -> AlgebraFieldTemplate { + { name: "flat_map", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: ContainerOf { source: SameAsReceiver, element: AlgebraTypeVariable { id: "MappedElement" } } }], return_type: ContainerOf { source: SameAsReceiver, element: AlgebraTypeVariable { id: "MappedElement" } }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } } +} + +fn collection_any_shape() -> AlgebraFieldTemplate { + { name: "any", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: "Bool" } }], return_type: NamedTemplate { name: "Bool" }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } } +} + +fn collection_all_shape() -> AlgebraFieldTemplate { + { name: "all", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: "Bool" } }], return_type: NamedTemplate { name: "Bool" }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } } +} + +fn collection_sort_by_shape() -> AlgebraFieldTemplate { + { name: "sort_by", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: AlgebraTypeVariable { id: "SortKey" } }], return_type: ReceiverSelf, size_effect: Present { value: IdentityEffect }, cost_shape: Present { value: ShapeSortBody }, callback_element_position: Present { value: 0 } } +} + type StepPositionScan { next: Int found: Int? @@ -898,11 +923,11 @@ fn finite_power_set_templates() -> List { { name: "member", param_types: [ReceiverSelf, ReceiverElement], return_type: NamedTemplate { name: "Bool" }, size_effect: none, cost_shape: none, callback_element_position: none }, { name: "contains", param_types: [ReceiverSelf, ReceiverElement], return_type: NamedTemplate { name: "Bool" }, size_effect: none, cost_shape: none, callback_element_position: none }, collection_filter_shape(), - { name: "map", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: AlgebraTypeVariable { id: "MappedElement" } }], return_type: ContainerOf { source: SameAsReceiver, element: AlgebraTypeVariable { id: "MappedElement" } }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }, - { name: "flat_map", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: ContainerOf { source: SameAsReceiver, element: AlgebraTypeVariable { id: "MappedElement" } } }], return_type: ContainerOf { source: SameAsReceiver, element: AlgebraTypeVariable { id: "MappedElement" } }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }, + collection_map_shape(), + collection_flat_map_shape(), collection_fold_shape(), - { name: "any", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: "Bool" } }], return_type: NamedTemplate { name: "Bool" }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }, - { name: "all", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: "Bool" } }], return_type: NamedTemplate { name: "Bool" }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }, + collection_any_shape(), + collection_all_shape(), { name: "count", param_types: [ReceiverSelf], return_type: NamedTemplate { name: "std.nat.Nat" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none }, { name: "length", param_types: [ReceiverSelf], return_type: NamedTemplate { name: "std.nat.Nat" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none } ] @@ -942,19 +967,19 @@ fn free_monoid_scalar_templates() -> List { fn free_monoid_collection_templates() -> List { [ - { name: "map", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: AlgebraTypeVariable { id: "MappedElement" } }], return_type: ContainerOf { source: SameAsReceiver, element: AlgebraTypeVariable { id: "MappedElement" } }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }, + collection_map_shape(), collection_filter_shape(), - { name: "flat_map", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: ContainerOf { source: SameAsReceiver, element: AlgebraTypeVariable { id: "MappedElement" } } }], return_type: ContainerOf { source: SameAsReceiver, element: AlgebraTypeVariable { id: "MappedElement" } }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }, + collection_flat_map_shape(), collection_fold_shape(), - { name: "any", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: "Bool" } }], return_type: NamedTemplate { name: "Bool" }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }, - { name: "all", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: "Bool" } }], return_type: NamedTemplate { name: "Bool" }, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }, + collection_any_shape(), + collection_all_shape(), { name: "count", param_types: [ReceiverSelf], return_type: NamedTemplate { name: "std.nat.Nat" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none }, { name: "first", param_types: [ReceiverSelf], return_type: OptionalOf { inner: ReceiverElement }, size_effect: Present { value: ProjectionEffect }, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none }, { name: "last", param_types: [ReceiverSelf], return_type: OptionalOf { inner: ReceiverElement }, size_effect: Present { value: ProjectionEffect }, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none }, { name: "get", param_types: [ReceiverSelf, NamedTemplate { name: "Int" }], return_type: OptionalOf { inner: ReceiverElement }, size_effect: Present { value: ProjectionEffect }, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none }, { name: "skip", param_types: [ReceiverSelf, NamedTemplate { name: "Int" }], return_type: ReceiverSelf, size_effect: Present { value: ShrinkEffect }, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: none }, { name: "take", param_types: [ReceiverSelf, NamedTemplate { name: "Int" }], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: none }, - { name: "sort_by", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: AlgebraTypeVariable { id: "SortKey" } }], return_type: ReceiverSelf, size_effect: Present { value: IdentityEffect }, cost_shape: Present { value: ShapeSortBody }, callback_element_position: Present { value: 0 } }, + collection_sort_by_shape(), { name: "append", param_types: [ReceiverSelf, ReceiverElement], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeConstant }, callback_element_position: none }, { name: "contains", param_types: [ReceiverSelf, ReceiverElement], return_type: NamedTemplate { name: "Bool" }, size_effect: none, cost_shape: none, callback_element_position: none }, { name: "enumerate", param_types: [ReceiverSelf], return_type: ContainerOf { source: SameAsReceiver, element: TupleOf { first: NamedTemplate { name: "Int" }, second: ReceiverElement } }, size_effect: Present { value: IdentityEffect }, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: none }, diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index a15aecded54..6538439e56d 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -4,6 +4,11 @@ import std.dissolution { DissolutionCondition, dissolution_description, unbound_ import std.algebra { Cons, Empty, FreeMonoid, list_append, list_snoc_item } import v2.compiler.fold_lowering { + CallbackAllByAnd, + CallbackCallOperands, + CallbackMapsToCons, + CallbackRealization, + FoldCallCallbackLowered, FoldCallLowered, FunctionValueBinder, FunctionValueKind, @@ -55,7 +60,8 @@ import v2.extdeps.languages.dag { parse_qualified_name_segments_from_capture, qualified_name_from_module_node, } -import v2.std.anonymous_binder { DeclaredParameterList, anonymous_binder_mint_parameter_list, fresh_return_type_variable, fresh_type_variable } +import v2.std.anonymous_binder { DeclaredParameterList, anonymous_binder_mint_parameter_list, fresh_fold_accumulator, fresh_return_type_variable, fresh_type_variable } +import v2.std.node { content_hash } import v2.std.algebra { fold_list, list_map, length, zip_eq } import v2.std.collection { List, @@ -1948,6 +1954,8 @@ fn body_lower_piped_fold_optional(acc: Node, operator: Node, right: Node) -> Opt match fold_call_lowering_piped(fold_call: call, receiver: acc) { FoldCallLowered { operands: operands } => optional_present(value: body_lower_fold_encoding_from(operands: operands, collection: outcome_accepted(value: acc))) + FoldCallCallbackLowered { operands: operands } => + optional_present(value: body_lower_callback_encoding_from(operands: operands, collection: outcome_accepted(value: acc))) FoldCallStepShapeInvalid { diagnostic: d } => optional_present(value: outcome_rejected(d: d)) FoldCallOperandAbsent { diagnostic: d } => optional_present(value: outcome_rejected(d: d)) FoldCallStepFormUnresolved { diagnostic: _ } => optional_absent() @@ -2186,6 +2194,12 @@ fn body_lower_try_fold_loop(node: Node) -> Outcome> { Accepted { value: encoding, diagnostics: d } => outcome_with_diagnostics(value: optional_present(value: encoding), diagnostics: d) } + FoldCallCallbackLowered { operands: operands } => + match body_lower_callback_encoding(operands: operands) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: encoding, diagnostics: d } => + outcome_with_diagnostics(value: optional_present(value: encoding), diagnostics: d) + } FoldCallOperandAbsent { diagnostic: d } => outcome_rejected(d: d) FoldCallStepFormUnresolved { diagnostic: d } => outcome_with_diagnostics( @@ -3424,6 +3438,130 @@ fn body_lower_fold_encoding_from(operands: FoldCallOperands, collection: Outcome }) } +// A COLLECTION CALLBACK OPERATION LOWERS TO THE SAME FOLD ENCODING, its step synthesized around the +// callback (v2.compiler.fold_lowering CallbackRealization names each operation's step and init). The +// callback is read by the one function-value reader, its body lowered by the one body reader, and its +// AUTHORED binder becomes the synthesized step's member formal (actual 1), so v2.compiler.infer types it +// from the domain element by role and nothing applies a lambda value. The accumulator formal (actual 0) is +// minted by v2.std.anonymous_binder fresh_fold_accumulator. The Arrow is built by the one function-value +// Arrow producer, so the step is an ordinary callable with fresh type variables like any other. +// +// The constructors the step and init name (Cons, Empty) are referenced by their declaring path, +// std.algebra, never by a bare spelling the calling module might bind differently or not at all. +// +// OCCURRENCES. The init stands for no authored text, so it is synthetic. The step's nodes are members of +// the callback's IMAGE (v2.std.node node_lowered_from): the step Arrow closes that image, so each member +// becomes pending and normalize allocates it its own projected occurrence, which is the key a lexical +// reference to the accumulator or the member is recorded by (v2.compiler.resolve +// resolve_lexical_reference). The callback's own body keeps its authored occurrences. +// +// AN AUTHORED CALLBACK RETURN TYPE IS NOT READ HERE and refuses as shape_unread: the synthesized step's +// return is the carrier, not the callback's, so carrying the annotation would put it on the wrong +// position, and dropping it would discard an authored fact. +fn body_lower_callback_encoding(operands: CallbackCallOperands) -> Outcome { + body_lower_callback_encoding_from(operands: operands, collection: body_lower_value_read(value: operands.collection)) +} + +fn body_lower_algebra_constructor_reference(name: Symbol, source: Node) -> Node { + body_lower_qualified_name_spine_node( + segments: [ + node_lowered_from(kind: TypeNode { connective: Atom { identity: ^std } }, children: [], source: source), + node_lowered_from(kind: TypeNode { connective: Atom { identity: ^algebra } }, children: [], source: source), + node_lowered_from(kind: TypeNode { connective: Atom { identity: name } }, children: [], source: source) + ], + source: source + ) +} + +fn body_lower_callback_cons(head: Node, tail: Node, source: Node) -> Node { + close_lowered_image(root: construct_node( + reference: body_lower_algebra_constructor_reference(name: ^Cons, source: source), + field_edges: [Edge { label: Named { name: ^head }, target: head }, Edge { label: Named { name: ^tail }, target: tail }], + source: source + )) +} + +fn body_lower_callback_operator(token: Symbol) -> Node { + node_synthetic(kind: TypeNode { connective: Atom { identity: token } }, children: []) +} + +fn body_lower_callback_init(realization: CallbackRealization, source: Node) -> Node { + match realization { + CallbackMapsToCons => body_lower_algebra_constructor_reference(name: ^Empty, source: source) + CallbackAllByAnd => body_lower_param_ref_atom(source: source, identity: ^dag_token_kw_true) + } +} + +fn body_lower_callback_step_body(realization: CallbackRealization, accumulator: Symbol, body: Node, source: Node) -> Node { + let acc_ref = body_lower_param_ref_atom(source: source, identity: accumulator) + match realization { + CallbackMapsToCons => body_lower_callback_cons(head: body, tail: acc_ref, source: source) + CallbackAllByAnd => lower_binary_infix(left: acc_ref, operator: body_lower_callback_operator(token: ^dag_token_and_and), right: body) + } +} + +fn body_lower_callback_encoding_from(operands: CallbackCallOperands, collection: Outcome) -> Outcome { + let callback_node = operands.callback + let synthetic = node_synthetic(kind: TypeNode { connective: Conj }, children: []) + let unread: Outcome = outcome_rejected( + d: body_lower_diagnostic(reason: ^body_lowering_reason_function_value_shape_unread, n: callback_node) + ) + bind_outcome(o: collection, f: fn(collection) { + match body_lower_function_value_captured_optional(node: callback_node) { + Absent => unread + Present { value: capture } => + match function_value_parts_optional(captured: capture.captured, kind: capture.kind) { + Absent => unread + Present { value: parts } => + match parts.binders { + Cons { head: member, tail: Empty } => + match body_lower_function_value_codomain_optional(ret: parts.return_clause) { + Present { value: CodomainElided } => + match body_lower_function_value_body(body: parts.body) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: Absent, diagnostics: _ } => + outcome_rejected( + d: body_lower_diagnostic( + reason: ^body_lowering_reason_function_value_body_unread, + n: body_lower_function_value_body_node(body: parts.body) + ) + ) + Accepted { value: Present { value: body }, diagnostics: d } => + let accumulator = fresh_fold_accumulator(callback_digest: content_hash(n: callback_node).digest as String) + let accumulator_binder = FunctionValueBinder { + name: accumulator, + at: node_synthetic(kind: TypeNode { connective: Atom { identity: accumulator } }, children: []) + } + match body_lower_function_value_arrow( + shell: callback_node, + binders: [accumulator_binder, member], + codomain: CodomainElided, + body: body_lower_callback_step_body(realization: operands.realization, accumulator: accumulator, body: body, source: callback_node) + ) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: step, diagnostics: ad } => + outcome_with_diagnostics( + value: fold_recurrence_encoding(operands: FoldCallOperands { + head: operands.head, + collection: collection, + init: body_lower_callback_init(realization: operands.realization, source: synthetic), + step: step, + carrier: accumulator + }), + diagnostics: diagnostics_merge(outer: d, inner: ad) + ) + } + } + Present { value: CodomainAuthored { type_node: _ } } => unread + Absent => unread + } + _ => unread + } + } + } + }) +} + // A fold-family call, read off the node's own captured sequence (fold_lowering fold_call_head_symbol // reads its head atom directly -- no search), lowered to the fold encoding while its step is raw. fn body_lower_fold_family_call_first(node: Node) -> Optional> { @@ -3437,6 +3575,7 @@ fn body_lower_fold_family_call_first(node: Node) -> Optional> { Present { value: _ } => match fold_call_lowering(fold_call: captured) { FoldCallLowered { operands: operands } => optional_present(value: body_lower_fold_encoding(operands: operands)) + FoldCallCallbackLowered { operands: operands } => optional_present(value: body_lower_callback_encoding(operands: operands)) FoldCallStepShapeInvalid { diagnostic: d } => optional_present(value: outcome_rejected(d: d)) FoldCallOperandAbsent { diagnostic: d } => optional_present(value: outcome_rejected(d: d)) FoldCallStepFormUnresolved { diagnostic: _ } => optional_absent() diff --git a/src/v2/compiler/fold_lowering.dag b/src/v2/compiler/fold_lowering.dag index 7d2d26c3f26..35069248939 100644 --- a/src/v2/compiler/fold_lowering.dag +++ b/src/v2/compiler/fold_lowering.dag @@ -30,7 +30,9 @@ import std.algebra { AlgebraFieldTemplate, Cons, Empty, + collection_all_shape, collection_fold_shape, + collection_map_shape, template_accumulator_position, template_receiver_position, template_step_position @@ -159,7 +161,8 @@ fn fold_row_position(position: Int?) -> Optional { } } -// The collection operation's slots, positions read off its template row. +// A roster operation's slots, positions read off its template row. A callback row (map, all) has no accumulator parameter, so template_accumulator_position answers Absent and its init slot +// is absent: the template itself says there is no authored init. fn collection_fold_slots(t: AlgebraFieldTemplate) -> FoldSlots { FoldSlots { collection: FoldSlot { name: optional_present(value: ^xs), position: fold_row_position(position: template_receiver_position(t: t)) }, @@ -168,34 +171,124 @@ fn collection_fold_slots(t: AlgebraFieldTemplate) -> FoldSlots { } } -// The collection operation's head, interned ONCE from its template row's own `name` -- still the one -// spelling -- so classifying a head is a symbol comparison. fold_family_realization runs at every call -// site body lowering meets; building the template and comparing lexemes there was a per-call-site cost -// on the native route's context phase (gunbc#12550 census). +// THE COLLECTION CALLBACK OPERATIONS, REALIZED THROUGH THE ONE FOLD ENCODING. A callback row's callable +// takes ONE element (callback_element_position 0) and returns no accumulator, so the call carries no step +// the recurrence can use as written: the step is SYNTHESIZED around the callback's own binder and body, +// and the init is supplied, both by the operation the row names (map is the fold desugaring of +// v2.std.algebra list_map): +// map(xs, e => B) init Empty, step (acc, e) => Cons { head: B, tail: acc } +// all(xs, e => B) init true, step (acc, e) => acc && B +// Each step is O(1) per member: it conses one cell onto the carrier or joins one Bool into it, and never +// copies it (no snoc, no append), which is the template's ShapeIterateBody at linear cost (DESIGN +// section 6). +// +// THE STEP SHAPE PER OPERATION IS AUTHORED, ONE ARM PER ROW, AND THAT IS A DECLARED FRONTIER, exactly +// as the corpus declarations' slots above are: the template row carries the operation's signature and +// cost, not a body. Its trigger is a realization body readable off the row. +// +// WHAT IS NOT HERE, BY NAME: filter and any (v2.std.algebra DECLARES ordinary fns of those names, which +// dozens of modules import; lowering classifies a head by spelling without a binding, so realizing the +// row would lower those calls to a Loop that resolve then refuses as shadowed), flat_map (its step is +// itself a fold over the callback's result) and sort_by (ShapeSortBody, not an iterate body). Their rows +// are named in std.algebra and their calls stay unrealized; gunbc.recurring_failure_mode +// collection_callback_operation_unrealized_in_v2_lowering is the counted frontier with each trigger. +type CallbackRealization + = CallbackMapsToCons + | CallbackAllByAnd + +type FoldRosterForm + = RosterAuthoredStep + | RosterCallbackStep { realization: CallbackRealization } + +type CollectionRosterRow { + head: Symbol + template: AlgebraFieldTemplate + row: DeclarationRef + form: FoldRosterForm +} + +fn collection_roster_row(t: AlgebraFieldTemplate, decl_name: String, form: FoldRosterForm) -> CollectionRosterRow { + CollectionRosterRow { + head: symbol_intern_lexeme(lexeme: t.name), + template: t, + row: DeclarationRef { module_path: "std.algebra", decl_name: decl_name, field: WholeDeclaration }, + form: form + } +} + +// THE ONE ROSTER OF REALIZED COLLECTION OPERATIONS, each head interned ONCE from its row's own `name`, so +// classifying a head is a symbol comparison. fold_family_realization runs at every call site body +// lowering meets, and building templates and comparing lexemes there was a per-call-site cost on the +// native route's context phase (gunbc#12550 census). +data collection_roster_rows: List = [ + collection_roster_row(t: collection_fold_shape(), decl_name: "collection_fold_shape", form: RosterAuthoredStep), + collection_roster_row(t: collection_map_shape(), decl_name: "collection_map_shape", form: RosterCallbackStep { realization: CallbackMapsToCons }), + collection_roster_row(t: collection_all_shape(), decl_name: "collection_all_shape", form: RosterCallbackStep { realization: CallbackAllByAnd }) +] + data collection_fold_head: Symbol = symbol_intern_lexeme(lexeme: collection_fold_shape().name) +fn collection_roster_row_optional(head: Symbol) -> Optional { + fold_list(xs: collection_roster_rows, empty: optional_absent(), cons: fn(acc, r) { + match acc { + Present { value: _ } => acc + Absent => if r.head == head { optional_present(value: r) } else { acc } + } + }) +} + +// Whether a roster operation's step is the authored recurrence step or one synthesized around a callback. +fn fold_roster_form(row: DeclarationRef) -> FoldRosterForm { + fold_list(xs: collection_roster_rows, empty: RosterAuthoredStep, cons: fn(acc, r) { + if r.row.decl_name == row.decl_name && r.row.module_path == row.module_path { r.form } else { acc } + }) +} + fn fold_family_realization(head: Symbol) -> Optional { - if head == collection_fold_head { - let t = collection_fold_shape() - optional_present(value: FoldRealizesRosterOperation { - template: t, - row: DeclarationRef { module_path: "std.algebra", decl_name: "collection_fold_shape", field: WholeDeclaration }, - slots: collection_fold_slots(t: t) - }) - } else if head == ^fold_list { - fold_realizes_declaration(module_path: "v2.std.algebra", decl_name: "fold_list", slots: FoldSlots { - collection: fold_declared_slot(name: ^xs, position: 0), init: fold_declared_slot(name: ^empty, position: 1), step: fold_declared_slot(name: ^cons, position: 2) - }) - } else if head == ^fold_list_right { - fold_realizes_declaration(module_path: "v2.std.algebra", decl_name: "fold_list_right", slots: FoldSlots { - collection: fold_declared_slot(name: ^xs, position: 0), init: fold_declared_slot(name: ^empty, position: 1), step: fold_declared_slot(name: ^snoc, position: 2) - }) - } else if head == ^fold_node { - fold_realizes_declaration(module_path: "v2.std.node", decl_name: "fold_node", slots: FoldSlots { - collection: fold_declared_slot(name: ^n, position: 0), init: fold_absent_slot(), step: fold_declared_slot(name: ^algebra, position: 1) - }) - } else { - optional_absent() + match collection_roster_row_optional(head: head) { + Present { value: r } => + optional_present(value: FoldRealizesRosterOperation { + template: r.template, + row: r.row, + slots: collection_fold_slots(t: r.template) + }) + Absent => + if head == ^fold_list { + fold_realizes_declaration(module_path: "v2.std.algebra", decl_name: "fold_list", slots: FoldSlots { + collection: fold_declared_slot(name: ^xs, position: 0), init: fold_declared_slot(name: ^empty, position: 1), step: fold_declared_slot(name: ^cons, position: 2) + }) + } else if head == ^fold_list_right { + fold_realizes_declaration(module_path: "v2.std.algebra", decl_name: "fold_list_right", slots: FoldSlots { + collection: fold_declared_slot(name: ^xs, position: 0), init: fold_declared_slot(name: ^empty, position: 1), step: fold_declared_slot(name: ^snoc, position: 2) + }) + } else if head == ^fold_node { + fold_realizes_declaration(module_path: "v2.std.node", decl_name: "fold_node", slots: FoldSlots { + collection: fold_declared_slot(name: ^n, position: 0), init: fold_absent_slot(), step: fold_declared_slot(name: ^algebra, position: 1) + }) + } else { + optional_absent() + } + } +} + +// THE FAMILY WHOSE STEP IS AUTHORED: fold_family_realization minus the callback operations. The +// accumulator-copy lens asks whether an AUTHORED step copies its accumulator; a synthesized callback step +// conses one cell onto a carrier no author wrote, so it is not that lens's subject and is not offered to it. +fn fold_family_authored_step_realization(head: Symbol) -> Optional { + match fold_family_realization(head: head) { + Absent => optional_absent() + Present { value: r } => + match fold_realization_form(r: r) { + RosterAuthoredStep => optional_present(value: r) + RosterCallbackStep { realization: _ } => optional_absent() + } + } +} + +fn fold_realization_form(r: FoldRealization) -> FoldRosterForm { + match r { + FoldRealizesRosterOperation { template: _, row: row, slots: _ } => fold_roster_form(row: row) + FoldRealizesDeclaration { declaration: _, slots: _ } => RosterAuthoredStep } } @@ -752,6 +845,16 @@ fn fold_lowering_step_form_unresolved_diagnostic(n: Node) -> Diagnostic { } } +// A callback operation's step exists only LOWERED (v2.compiler.body_lowering_fold builds it around the +// callback's lowered body), so the raw-call encoding has no step to carry for it and says so. +fn fold_lowering_callback_step_unbuilt_diagnostic(n: Node) -> Diagnostic { + Diagnostic { + reason: ^fold_lowering_callback_step_unbuilt, + at: node_locus(node: n), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + fn fold_carrier_binder_atom(binder: Symbol) -> Node { node_synthetic( kind: TypeNode { connective: Atom { identity: binder } }, @@ -828,13 +931,18 @@ fn fold_realization_is_at(head: Symbol, path: String) -> Bool { } } +// Every collection roster operation -- the fold and each callback operation -- hands its step one member. +fn fold_roster_row_is_at(path: String) -> Bool { + any(xs: collection_roster_rows, predicate: fn(r) { r.row.module_path + "." + r.row.decl_name == path }) +} + // Absent: the realized edge names no fold family declaration, so this Loop is not a fold realization. fn fold_realized_member_role(realized: Node) -> Optional { match declaration_reference_path_optional(node: realized) { Absent => optional_absent() Present { value: path } => let dotted = qualified_name_to_dotted_string(qn: path) - if fold_realization_is_at(head: collection_fold_head, path: dotted) + if fold_roster_row_is_at(path: dotted) || fold_realization_is_at(head: ^fold_list, path: dotted) || fold_realization_is_at(head: ^fold_list_right, path: dotted) { optional_present(value: FoldStepTakesCollectionMember) @@ -1084,8 +1192,20 @@ fn fold_call_operands_from_step( } } +// A CALLBACK OPERATION'S OPERANDS, AT THEIR AUTHORED NODES: the head, the collection, and the +// callback function value whose one binder becomes the synthesized step's member formal. The step and +// init are not here because the call does not carry them; v2.compiler.body_lowering_fold builds both +// from `realization`, around the callback's lowered body. +type CallbackCallOperands { + head: Node + collection: Node + callback: Node + realization: CallbackRealization +} + type FoldCallLowering = FoldCallLowered { operands: FoldCallOperands } + | FoldCallCallbackLowered { operands: CallbackCallOperands } | FoldCallNotAFold { diagnostic: Diagnostic } | FoldCallStepFormUnresolved { diagnostic: Diagnostic } | FoldCallStepShapeInvalid { diagnostic: Diagnostic } @@ -1126,11 +1246,12 @@ fn fold_call_lowering_from(fold_call: Node, source: FoldCollectionSource) -> Fol Absent => FoldCallNotAFold { diagnostic: fold_lowering_not_a_fold_call_diagnostic(n: fold_call) } Present { value: realization } => - fold_call_lowering_of( - fold_call: fold_call, - slots: fold_slots_for_source(slots: fold_realization_slots(r: realization), source: source), - source: source - ) + let slots = fold_slots_for_source(slots: fold_realization_slots(r: realization), source: source) + match fold_realization_form(r: realization) { + RosterAuthoredStep => fold_call_lowering_of(fold_call: fold_call, slots: slots, source: source) + RosterCallbackStep { realization: callback } => + fold_call_callback_lowering_of(fold_call: fold_call, slots: slots, source: source, realization: callback) + } } } } @@ -1160,6 +1281,51 @@ fn fold_call_lowering_of(fold_call: Node, slots: FoldSlots, source: FoldCollecti } } +// A CALLBACK IS A ONE-BINDER FUNCTION VALUE. Its binder is the member and its body is what each member +// contributes; a callback with any other binder count is malformed here, where it is in hand, and refuses +// as shape_invalid. A callback passed by name is the same counted frontier a step passed by name is. +fn fold_callback_has_one_binder(parts: Optional) -> Bool { + match parts { + Present { value: p } => + match p.binders { + Cons { head: _, tail: Empty } => true + _ => false + } + Absent => false + } +} + +fn fold_call_callback_lowering_of(fold_call: Node, slots: FoldSlots, source: FoldCollectionSource, realization: CallbackRealization) -> FoldCallLowering { + let one_binder = match fold_call_step_form(fold_call: fold_call, slots: slots) { + StepByName => optional_absent() + StepFnLiteral { literal: fn_literal } => optional_present(value: fold_callback_has_one_binder(parts: fn_literal_parts_optional(fn_literal: fn_literal))) + StepArrowLambda { lambda: lambda } => optional_present(value: fold_callback_has_one_binder(parts: arrow_lambda_parts_optional(lambda: lambda))) + } + match one_binder { + Absent => FoldCallStepFormUnresolved { diagnostic: fold_lowering_step_form_unresolved_diagnostic(n: fold_call) } + Present { value: has_one } => + if !has_one { + FoldCallStepShapeInvalid { diagnostic: fold_lowering_shape_invalid_diagnostic(n: fold_call) } + } else { + match fold_call_head_node(fold_call: fold_call) { + Absent => FoldCallNotAFold { diagnostic: fold_lowering_not_a_fold_call_diagnostic(n: fold_call) } + Present { value: head_node } => + match fold_call_slot_value(fold_call: fold_call, slot: slots.step) { + Absent => FoldCallStepShapeInvalid { diagnostic: fold_lowering_shape_invalid_diagnostic(n: fold_call) } + Present { value: callback } => + match fold_call_collection(fold_call: fold_call, slots: slots, source: source) { + Absent => FoldCallOperandAbsent { diagnostic: fold_lowering_operand_absent_diagnostic(n: fold_call) } + Present { value: collection } => + FoldCallCallbackLowered { + operands: CallbackCallOperands { head: head_node, collection: collection, callback: callback, realization: realization } + } + } + } + } + } + } +} + // THE ACCUMULATOR BINDER IS A FACT OF THE STEP ALONE: the step's first binder, wherever the // collection comes from. The accumulator-copy lens reads it on the raw call, where a piped fold's // collection is not in the call, so it asks the step alone. It is Absent exactly where fold_call_lowering yields no @@ -1168,7 +1334,7 @@ fn fold_call_step_carrier(fold_call: Node) -> Optional { match fold_call_head_symbol(fold_call: fold_call) { Absent => optional_absent() Present { value: head } => - match fold_family_realization(head: head) { + match fold_family_authored_step_realization(head: head) { Absent => optional_absent() Present { value: realization } => match fold_call_step_form(fold_call: fold_call, slots: fold_realization_slots(r: realization)) { @@ -1194,6 +1360,7 @@ fn fold_call_step_carrier(fold_call: Node) -> Optional { fn fold_call_to_encoding(fold_call: Node) -> Outcome { match fold_call_lowering(fold_call: fold_call) { FoldCallLowered { operands: operands } => outcome_accepted(value: fold_recurrence_encoding(operands: operands)) + FoldCallCallbackLowered { operands: operands } => outcome_rejected(d: fold_lowering_callback_step_unbuilt_diagnostic(n: operands.head)) FoldCallNotAFold { diagnostic: d } => outcome_rejected(d: d) FoldCallStepFormUnresolved { diagnostic: d } => outcome_rejected(d: d) FoldCallStepShapeInvalid { diagnostic: d } => outcome_rejected(d: d) diff --git a/src/v2/lens/complexity_accumulator_copy/analyze.dag b/src/v2/lens/complexity_accumulator_copy/analyze.dag index 75a2b608b83..86e6f893beb 100644 --- a/src/v2/lens/complexity_accumulator_copy/analyze.dag +++ b/src/v2/lens/complexity_accumulator_copy/analyze.dag @@ -753,7 +753,7 @@ fn analyze(node: Node, carriers: List, bindings: List match sequence_left_symbol(cap: cap) { Present { value: head } => - match v2.compiler.fold_lowering.fold_family_realization(head: head) { + match v2.compiler.fold_lowering.fold_family_authored_step_realization(head: head) { Present { value: _ } => match fold_family_carrier(call_capture: cap) { Present { value: carrier } => @@ -852,7 +852,7 @@ fn count_fold_sites(n: Node, bound_only: Bool) -> Int { match sequence_left_symbol(cap: cap) { Absent => 0 Present { value: head } => - match v2.compiler.fold_lowering.fold_family_realization(head: head) { + match v2.compiler.fold_lowering.fold_family_authored_step_realization(head: head) { Present { value: _ } => if bound_only { match fold_family_carrier(call_capture: cap) { @@ -905,7 +905,7 @@ fn primary_expr_fold_head_optional(node: Node) -> Optional { Present { value: cap } => match sequence_left_symbol(cap: cap) { Present { value: head } => - match v2.compiler.fold_lowering.fold_family_realization(head: head) { + match v2.compiler.fold_lowering.fold_family_authored_step_realization(head: head) { Present { value: _ } => optional_present(value: head) Absent => diff --git a/src/v2/std/anonymous_binder.dag b/src/v2/std/anonymous_binder.dag index f72f8e9a36d..c4fe2b5427a 100644 --- a/src/v2/std/anonymous_binder.dag +++ b/src/v2/std/anonymous_binder.dag @@ -98,6 +98,19 @@ fn fresh_fold_carrier(step_digest: String) -> Symbol { symbol_intern_lexeme(lexeme: fold_carrier_prefix() + step_digest + ">") } +// A SYNTHESIZED STEP'S ACCUMULATOR FORMAL. A callback operation (map, filter, any, all) has no authored +// step, so v2.compiler.body_lowering_fold builds one, and its first formal needs a name no author can +// write. It is keyed by the CALLBACK's content hash for the same reason the slot is keyed by the step's: +// a callback nested inside another's body has different content, so the inner formal never hides the +// outer one. It is distinct from the slot's prefix, so the formal and the slot are never one spelling. +fn fold_accumulator_prefix() -> String { + " Symbol { + symbol_intern_lexeme(lexeme: fold_accumulator_prefix() + callback_digest + ">") +} + fn is_fold_carrier_binder(sym: Symbol) -> Bool { starts_with(s: symbol_lexeme(sym: sym), prefix: fold_carrier_prefix()) } diff --git a/src/v2/test/claim/compiler/collection_callback_realization_test.dag b/src/v2/test/claim/compiler/collection_callback_realization_test.dag new file mode 100644 index 00000000000..3601a11bc3e --- /dev/null +++ b/src/v2/test/claim/compiler/collection_callback_realization_test.dag @@ -0,0 +1,165 @@ +module v2.test.claim.compiler.collection_callback_realization + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { infer } +import v2.compiler.fold_lowering { fold_loop_step, fold_step_body_node } +import v2.test.claim.compiler.infer_fold_member_instance { fmi_assemble, fmi_infer, fmi_infer_tree, fmi_no_projection_refusal, fmi_reason } +import v2.std.arrow_signature { ArrowParameterOrderAbsent, ArrowParameterOrderDeclared, ArrowParameterOrderMalformed, arrow_declared_parameter_order } +import v2.std.collection { List, list_at_optional } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.node_query { find_named_child } +import v2.std.qualified_name { declaration_reference_path_optional, lexical_reference_label_optional, qualified_name_to_dotted_string } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import std.algebra { Cons, Empty } +import v2.std.node { ComputationNode, Found, Loop, Node, Symbol, loop_realized_declaration_target, node_subtree_nodes } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// SUBJECT: a std.algebra collection callback operation (map, all) lowers through the ONE +// fold encoding (v2.compiler.fold_lowering CallbackRealization, v2.compiler.body_lowering_fold +// body_lower_callback_encoding): its head binds to its own named template row, its callback's binder is +// the synthesized step's member formal and so is typed from the domain element by role, and the step puts +// one cell on the front of the carrier. BOUNDARY: source text in, through the production assembly route +// (v2.test.claim.compiler.infer_fold_member_instance fmi_assemble), then v2.compiler.infer infer. +// RED ON THE BASE for every control here: the base realized only the fold family, so `map(` reached +// resolve as a bare name and the whole module refused resolve_reason_unbound_symbol. + +data ccr_map_records: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> List {\n map(xs, e => e.target)\n}\n" + +data ccr_map_undeclared_field: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> List {\n map(xs, e => e.nope)\n}\n" + +data ccr_declared_any: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn any(xs: List, predicate: fn(Pair) -> Bool) -> Bool {\n false\n}\n\nfn f(xs: List) -> Bool {\n any(xs, e => e.target == 0)\n}\n" + +data ccr_all_records: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Bool {\n all(xs, e => e.target == 0)\n}\n" + +data ccr_map_piped: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> List {\n xs |> map(e => e.target)\n}\n" + +fn ccr_loops(tree: ResolvedTree) -> List { + fold(node_subtree_nodes(root: tree.root), init: [], f: fn(acc, n) { + match n.kind { + ComputationNode { behavior: Loop } => Cons { head: n, tail: acc } + _ => acc + } + }) +} + +// The declaration the one Loop of a lowered source realizes, as a dotted path. +fn ccr_realized_path(src: String) -> Optional { + match fmi_assemble(src: src) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: tree, diagnostics: _ } => + match ccr_loops(tree: tree) { + Cons { head: l, tail: Empty } => ccr_loop_realized_path(l: l) + _ => optional_absent() + } + } +} + +fn ccr_loop_realized_path(l: Node) -> Optional { + match loop_realized_declaration_target(children: l.children) { + Found { target: realized } => + match declaration_reference_path_optional(node: realized) { + Present { value: path } => optional_present(value: qualified_name_to_dotted_string(qn: path)) + Absent => optional_absent() + } + _ => optional_absent() + } +} + +fn ccr_realizes(src: String, row: String) -> Bool { + match ccr_realized_path(src: src) { + Present { value: path } => path == row + Absent => false + } +} + +// (1) MAP OVER RECORDS READS A FIELD: the callback's binder `e` is the step's member formal, typed by the +// domain element Pair, so `e.target` projects a declared field and infer raises no projection refusal. +test fn ccr_map_over_records_reads_a_field() -> Bool { + match fmi_assemble(src: ccr_map_records) { + Rejected { diagnostics: _ } => false + Accepted { value: tree, diagnostics: _ } => fmi_no_projection_refusal(o: fmi_infer_tree(tree: tree)) + } +} + +// (2) A BAD FIELD REFUSES AT THE FIELD: the same map projecting a field Pair does not declare. +test fn ccr_map_undeclared_field_refuses_at_the_field() -> Bool { + fmi_reason(o: fmi_infer(src: ccr_map_undeclared_field)) == ^infer_reason_field_not_declared_on_receiver +} + +// (3) THE HEAD BINDS TO ITS OWN ROW, for every realized callback operation and for the piped spelling. +test fn ccr_map_head_binds_its_row() -> Bool { + ccr_realizes(src: ccr_map_records, row: "std.algebra.collection_map_shape") +} + +test fn ccr_piped_map_head_binds_its_row() -> Bool { + ccr_realizes(src: ccr_map_piped, row: "std.algebra.collection_map_shape") +} + +test fn ccr_all_head_binds_its_row() -> Bool { + ccr_realizes(src: ccr_all_records, row: "std.algebra.collection_all_shape") +} + +// (3b) A HEAD A CORPUS DECLARATION BINDS STAYS AN ORDINARY CALL. filter and any are NOT realized, +// because v2.std.algebra declares ordinary fns of those names and lowering classifies a head by spelling; +// a realized row would lower such a call to a Loop that resolve refuses as shadowed +// (gunbc.recurring_failure_mode collection_callback_operation_unrealized_in_v2_lowering). The guard: a +// module declaring its own `any` and calling it positionally assembles, and lowers to no Loop. Adding +// any's row to v2.compiler.fold_lowering collection_roster_rows makes this control red. +test fn ccr_declared_any_stays_an_ordinary_call() -> Bool { + match fmi_assemble(src: ccr_declared_any) { + Rejected { diagnostics: _ } => false + Accepted { value: tree, diagnostics: _ } => + match ccr_loops(tree: tree) { + Empty => true + Cons { head: _, tail: _ } => false + } + } +} + +// (4) THE STRUCTURAL COST CONTROL: map's synthesized step body is a construct whose `tail` is the step's +// OWN accumulator formal (declared position 0) -- one cell consed onto the carrier, O(1) per member, never +// a snoc or append over it. This is the control that measures THIS lowering: no executor reads a +// fold-realized Loop's encoding today (gunbc.recurring_failure_mode +// fold_realized_loop_carries_no_iteration_direction), so an eval_steps figure measures the seed path. +fn ccr_step_accumulator_label(step: Node) -> Optional { + match arrow_declared_parameter_order(arrow: step) { + ArrowParameterOrderDeclared { labels: labels } => list_at_optional(xs: labels, index: 0) + ArrowParameterOrderAbsent => optional_absent() + ArrowParameterOrderMalformed => optional_absent() + } +} + +test fn ccr_map_step_conses_onto_its_accumulator() -> Bool { + match fmi_assemble(src: ccr_map_records) { + Rejected { diagnostics: _ } => false + Accepted { value: tree, diagnostics: _ } => + match ccr_loops(tree: tree) { + Cons { head: l, tail: Empty } => + match fold_loop_step(loop_node: l) { + Absent => false + Present { value: step } => + match ccr_step_accumulator_label(step: step) { + Absent => false + Present { value: accumulator } => + match fold_step_body_node(step: step) { + Absent => false + Present { value: body } => + match find_named_child(root: body, name: ^tail) { + Rejected { diagnostics: _ } => false + Accepted { value: tail, diagnostics: _ } => + match lexical_reference_label_optional(node: tail) { + Present { value: label } => label == accumulator + Absent => false + } + } + } + } + } + _ => false + } + } +} + From 29a80889361a13b3103be800701020ee58d6e693 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 03:35:00 +0000 Subject: [PATCH 77/90] v2 std: one List binding per module -- drop the v2.std.collection List import where std.types List was added (10 modules double-bound it), and import it in std.compilers.sugar Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/std/cardinality.dag | 3 --- src/v2/std/compilers/sugar.dag | 1 + src/v2/std/effect_plan.dag | 3 --- src/v2/std/grammar.dag | 1 - src/v2/std/integer.dag | 1 - src/v2/std/native_agreement.dag | 1 - src/v2/std/orchestration.dag | 3 --- src/v2/std/qualified_name.dag | 3 --- src/v2/std/runtime.dag | 1 - src/v2/std/timeseries_signal.dag | 3 --- src/v2/std/verdict.dag | 3 --- 11 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/v2/std/cardinality.dag b/src/v2/std/cardinality.dag index 44b341beeb2..950b5308e31 100644 --- a/src/v2/std/cardinality.dag +++ b/src/v2/std/cardinality.dag @@ -10,9 +10,6 @@ import std.termination { descent_evidence_lattice_meet, evidence_rank } -import v2.std.collection { - List -} import v2.std.optional { Absent, Optional, diff --git a/src/v2/std/compilers/sugar.dag b/src/v2/std/compilers/sugar.dag index 99f5f9346c8..9e109e46153 100644 --- a/src/v2/std/compilers/sugar.dag +++ b/src/v2/std/compilers/sugar.dag @@ -1,5 +1,6 @@ module v2.std.compilers.sugar +import std.types { List } import std.algebra { Cons, Empty, FreeMonoid, list_append, list_snoc_item } import v2.std.algebra { fold_list, is_empty, length } import v2.std.collection { Absent, Present, optional_absent, optional_present } diff --git a/src/v2/std/effect_plan.dag b/src/v2/std/effect_plan.dag index 4924b648c68..311330b6b76 100644 --- a/src/v2/std/effect_plan.dag +++ b/src/v2/std/effect_plan.dag @@ -5,9 +5,6 @@ import std.dissolution { DissolutionCondition, unbound_dissolution } import v2.std.text { String } import v2.std.algebra { FreeMonoid } -import v2.std.collection { - List -} import v2.std.optional { Optional } diff --git a/src/v2/std/grammar.dag b/src/v2/std/grammar.dag index 0c374b9d387..8bad9ff99ac 100644 --- a/src/v2/std/grammar.dag +++ b/src/v2/std/grammar.dag @@ -6,7 +6,6 @@ import std.occurrence_identity { OccurrenceSynthetic } import std.algebra { Cons, Empty, list_append, list_snoc_item } import v2.std.algebra { is_empty, fold_list, fold_list_node, zip_eq, list_head, list_tail, HeadFound, HeadAbsent, TailFound, TailAbsent } import v2.std.collection { - List, Map, PointwisePower, empty_map, diff --git a/src/v2/std/integer.dag b/src/v2/std/integer.dag index aa5d9a15c98..5a9c46c7d1d 100644 --- a/src/v2/std/integer.dag +++ b/src/v2/std/integer.dag @@ -5,7 +5,6 @@ import std.algebra { Cons, Empty, FreeMonoid, AbelianGroup, GroupCompletion, Ord import std.error_primitives { DivError, Result, DivideByZero, Ok, Err } import v2.std.algebra { TailAbsent, TailFound, fold_list, fold_list_node, length, list_map } import v2.std.collection { - List, list_at_optional } import v2.std.optional { diff --git a/src/v2/std/native_agreement.dag b/src/v2/std/native_agreement.dag index 3c9102c1380..e1e2eb91c1e 100644 --- a/src/v2/std/native_agreement.dag +++ b/src/v2/std/native_agreement.dag @@ -3,7 +3,6 @@ module v2.std.native_agreement import std.types { List } import v2.std.algebra { Cons, Empty } import v2.std.collection { - List, list_at_optional } import v2.std.optional { diff --git a/src/v2/std/orchestration.dag b/src/v2/std/orchestration.dag index ff22e2d2a00..ae13459e33d 100644 --- a/src/v2/std/orchestration.dag +++ b/src/v2/std/orchestration.dag @@ -5,9 +5,6 @@ import std.dissolution { DissolutionCondition, unbound_dissolution } import v2.std.text { String } import std.algebra { FreeMonoid } -import v2.std.collection { - List -} import v2.std.optional { Optional } diff --git a/src/v2/std/qualified_name.dag b/src/v2/std/qualified_name.dag index b089cba3f19..eeb6593ca6f 100644 --- a/src/v2/std/qualified_name.dag +++ b/src/v2/std/qualified_name.dag @@ -16,9 +16,6 @@ import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } import std.decl_ref { DeclarationRef, WholeDeclaration } import std.disposition { Disposition, Scaffold, RealizationDispatch } import v2.std.algebra { HeadAbsent, HeadFound, fold_list, fold_list_node, fold_list_right, is_empty, is_prefix_of, length, list_head, list_init } -import v2.std.collection { - List -} import v2.std.optional { Absent, Optional, diff --git a/src/v2/std/runtime.dag b/src/v2/std/runtime.dag index 5eb8ae560a7..5135395c83c 100644 --- a/src/v2/std/runtime.dag +++ b/src/v2/std/runtime.dag @@ -2,7 +2,6 @@ module v2.std.runtime import std.types { List } import v2.std.collection { - List, Map } import v2.std.optional { diff --git a/src/v2/std/timeseries_signal.dag b/src/v2/std/timeseries_signal.dag index 03463b42913..6cd8cba2b90 100644 --- a/src/v2/std/timeseries_signal.dag +++ b/src/v2/std/timeseries_signal.dag @@ -2,9 +2,6 @@ module v2.std.timeseries_signal import std.algebra { Cons, Empty } import v2.std.algebra { fold_list, fold_list_right } -import v2.std.collection { - List -} import v2.std.optional { Optional, Absent, diff --git a/src/v2/std/verdict.dag b/src/v2/std/verdict.dag index 07722d2c940..bb4d5735b70 100644 --- a/src/v2/std/verdict.dag +++ b/src/v2/std/verdict.dag @@ -3,9 +3,6 @@ module v2.std.verdict import std.types { List } import std.algebra { Monoid } import v2.std.algebra { is_empty } -import v2.std.collection { - List -} import v2.std.optional { Optional, Absent From 126a658a405e41f9bc8484cd174ffe58810923aa Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 04:16:37 +0000 Subject: [PATCH 78/90] v2 std: import Optional/Present/Absent where four modules used them unimported (data_initializer_identity refused its own native resolve) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/std/compilers/semantic_decl_emission.dag | 1 + src/v2/std/data_initializer_identity.dag | 2 +- src/v2/std/generic_instantiation.dag | 1 + src/v2/std/operation_realization.dag | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/v2/std/compilers/semantic_decl_emission.dag b/src/v2/std/compilers/semantic_decl_emission.dag index a168f751ea3..82395874d21 100644 --- a/src/v2/std/compilers/semantic_decl_emission.dag +++ b/src/v2/std/compilers/semantic_decl_emission.dag @@ -1,5 +1,6 @@ module v2.std.compilers.semantic_decl_emission +import v2.std.optional { Present, Absent } import v2.std.qualified_name { declaration_reference_path_optional } import std.occurrence_identity { OccurrenceSynthetic } import std.dissolution { DissolutionCondition, unbound_dissolution } diff --git a/src/v2/std/data_initializer_identity.dag b/src/v2/std/data_initializer_identity.dag index 30a80bb26b0..78896aa2af4 100644 --- a/src/v2/std/data_initializer_identity.dag +++ b/src/v2/std/data_initializer_identity.dag @@ -4,7 +4,7 @@ import std.types { List } import v2.std.node { symbol_lexeme } import v2.std.grammar { node_atom_identity_optional } import v2.std.decl_index { DeclFact } -import v2.std.optional { optional_present, optional_absent } +import v2.std.optional { optional_present, optional_absent, Optional, Present, Absent } // Resolved parent/arm projection for DataItem rows — qualified_name, module_path, rel_path, and // name locator grain (not occurrence identity). Host bridge marshals projection Node trees from diff --git a/src/v2/std/generic_instantiation.dag b/src/v2/std/generic_instantiation.dag index 826058bfaf0..5d18c0e0200 100644 --- a/src/v2/std/generic_instantiation.dag +++ b/src/v2/std/generic_instantiation.dag @@ -1,5 +1,6 @@ module v2.std.generic_instantiation +import v2.std.optional { Present, Absent } import v2.std.collection { empty_map } type GenericSubstitution = Map diff --git a/src/v2/std/operation_realization.dag b/src/v2/std/operation_realization.dag index 225cc431f40..dad927f48e8 100644 --- a/src/v2/std/operation_realization.dag +++ b/src/v2/std/operation_realization.dag @@ -1,5 +1,6 @@ module v2.std.operation_realization +import v2.std.optional { Present, Absent } import std.types { Bool, Int, List, Map, NonEmptyStr, String } import std.effect_grant { Envelope, HandlerBinding, CoveredBy, NoCoveringGrant, covering_grant, From 3e2a2563352a12fd9247170203e2ec4638ce140e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 04:19:24 +0000 Subject: [PATCH 79/90] N7-1b: regenerate the std.algebra stage0 mirror (named collection callback shapes) claim_executor --regen-round-cost --regen-affected-scope: 141 adjudicated, this one mirror drifted, installed and rebuilt from the installed seed. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/std_algebra.rs | 299 +++++++++++++------------------ 1 file changed, 121 insertions(+), 178 deletions(-) diff --git a/src/v1/stage0/src/std_algebra.rs b/src/v1/stage0/src/std_algebra.rs index 727feced5f4..95bf3d4ec2b 100644 --- a/src/v1/stage0/src/std_algebra.rs +++ b/src/v1/stage0/src/std_algebra.rs @@ -516,6 +516,118 @@ pub fn collection_fold_shape() -> Rc { }) } +pub fn collection_map_shape() -> Rc { + Rc::new(AlgebraFieldTemplate { + name: "map".to_string(), + param_types: Rc::new(vec![ + Rc::new(AlgebraTypeTemplate::ReceiverSelf), + Rc::new(AlgebraTypeTemplate::CallableOf { + params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), + return_type: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { + id: "MappedElement".to_string(), + }), + }), + ]), + return_type: Rc::new(AlgebraTypeTemplate::ContainerOf { + source: Rc::new(ContainerSource::SameAsReceiver), + element: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { + id: "MappedElement".to_string(), + }), + }), + size_effect: std::option::Option::None, + cost_shape: Some(CostShape::ShapeIterateBody), + callback_element_position: Some(0), + }) +} + +pub fn collection_flat_map_shape() -> Rc { + Rc::new(AlgebraFieldTemplate { + name: "flat_map".to_string(), + param_types: Rc::new(vec![ + Rc::new(AlgebraTypeTemplate::ReceiverSelf), + Rc::new(AlgebraTypeTemplate::CallableOf { + params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), + return_type: Rc::new(AlgebraTypeTemplate::ContainerOf { + source: Rc::new(ContainerSource::SameAsReceiver), + element: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { + id: "MappedElement".to_string(), + }), + }), + }), + ]), + return_type: Rc::new(AlgebraTypeTemplate::ContainerOf { + source: Rc::new(ContainerSource::SameAsReceiver), + element: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { + id: "MappedElement".to_string(), + }), + }), + size_effect: std::option::Option::None, + cost_shape: Some(CostShape::ShapeIterateBody), + callback_element_position: Some(0), + }) +} + +pub fn collection_any_shape() -> Rc { + Rc::new(AlgebraFieldTemplate { + name: "any".to_string(), + param_types: Rc::new(vec![ + Rc::new(AlgebraTypeTemplate::ReceiverSelf), + Rc::new(AlgebraTypeTemplate::CallableOf { + params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), + return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { + name: "Bool".to_string(), + }), + }), + ]), + return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { + name: "Bool".to_string(), + }), + size_effect: std::option::Option::None, + cost_shape: Some(CostShape::ShapeIterateBody), + callback_element_position: Some(0), + }) +} + +pub fn collection_all_shape() -> Rc { + Rc::new(AlgebraFieldTemplate { + name: "all".to_string(), + param_types: Rc::new(vec![ + Rc::new(AlgebraTypeTemplate::ReceiverSelf), + Rc::new(AlgebraTypeTemplate::CallableOf { + params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), + return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { + name: "Bool".to_string(), + }), + }), + ]), + return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { + name: "Bool".to_string(), + }), + size_effect: std::option::Option::None, + cost_shape: Some(CostShape::ShapeIterateBody), + callback_element_position: Some(0), + }) +} + +pub fn collection_sort_by_shape() -> Rc { + Rc::new(AlgebraFieldTemplate { + name: "sort_by".to_string(), + param_types: Rc::new(vec![ + Rc::new(AlgebraTypeTemplate::ReceiverSelf), + Rc::new(AlgebraTypeTemplate::CallableOf { + params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), + return_type: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { + id: "SortKey".to_string(), + }), + }), + ]), + return_type: Rc::new(AlgebraTypeTemplate::ReceiverSelf), + size_effect: Some(CollectionSizeEffect::IdentityEffect), + cost_shape: Some(CostShape::ShapeSortBody), + callback_element_position: Some(0), + }) +} + #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] pub struct StepPositionScan { pub next: i64, @@ -1335,88 +1447,11 @@ pub fn finite_power_set_templates() -> Rc>> { callback_element_position: std::option::Option::None, }), collection_filter_shape(), - Rc::new(AlgebraFieldTemplate { - name: "map".to_string(), - param_types: Rc::new(vec![ - Rc::new(AlgebraTypeTemplate::ReceiverSelf), - Rc::new(AlgebraTypeTemplate::CallableOf { - params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), - return_type: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { - id: "MappedElement".to_string(), - }), - }), - ]), - return_type: Rc::new(AlgebraTypeTemplate::ContainerOf { - source: Rc::new(ContainerSource::SameAsReceiver), - element: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { - id: "MappedElement".to_string(), - }), - }), - size_effect: std::option::Option::None, - cost_shape: Some(CostShape::ShapeIterateBody), - callback_element_position: Some(0), - }), - Rc::new(AlgebraFieldTemplate { - name: "flat_map".to_string(), - param_types: Rc::new(vec![ - Rc::new(AlgebraTypeTemplate::ReceiverSelf), - Rc::new(AlgebraTypeTemplate::CallableOf { - params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), - return_type: Rc::new(AlgebraTypeTemplate::ContainerOf { - source: Rc::new(ContainerSource::SameAsReceiver), - element: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { - id: "MappedElement".to_string(), - }), - }), - }), - ]), - return_type: Rc::new(AlgebraTypeTemplate::ContainerOf { - source: Rc::new(ContainerSource::SameAsReceiver), - element: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { - id: "MappedElement".to_string(), - }), - }), - size_effect: std::option::Option::None, - cost_shape: Some(CostShape::ShapeIterateBody), - callback_element_position: Some(0), - }), + collection_map_shape(), + collection_flat_map_shape(), collection_fold_shape(), - Rc::new(AlgebraFieldTemplate { - name: "any".to_string(), - param_types: Rc::new(vec![ - Rc::new(AlgebraTypeTemplate::ReceiverSelf), - Rc::new(AlgebraTypeTemplate::CallableOf { - params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), - return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { - name: "Bool".to_string(), - }), - }), - ]), - return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { - name: "Bool".to_string(), - }), - size_effect: std::option::Option::None, - cost_shape: Some(CostShape::ShapeIterateBody), - callback_element_position: Some(0), - }), - Rc::new(AlgebraFieldTemplate { - name: "all".to_string(), - param_types: Rc::new(vec![ - Rc::new(AlgebraTypeTemplate::ReceiverSelf), - Rc::new(AlgebraTypeTemplate::CallableOf { - params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), - return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { - name: "Bool".to_string(), - }), - }), - ]), - return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { - name: "Bool".to_string(), - }), - size_effect: std::option::Option::None, - cost_shape: Some(CostShape::ShapeIterateBody), - callback_element_position: Some(0), - }), + collection_any_shape(), + collection_all_shape(), Rc::new(AlgebraFieldTemplate { name: "count".to_string(), param_types: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverSelf)]), @@ -1690,89 +1725,12 @@ pub fn free_monoid_scalar_templates() -> Rc>> { pub fn free_monoid_collection_templates() -> Rc>> { Rc::new(vec![ - Rc::new(AlgebraFieldTemplate { - name: "map".to_string(), - param_types: Rc::new(vec![ - Rc::new(AlgebraTypeTemplate::ReceiverSelf), - Rc::new(AlgebraTypeTemplate::CallableOf { - params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), - return_type: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { - id: "MappedElement".to_string(), - }), - }), - ]), - return_type: Rc::new(AlgebraTypeTemplate::ContainerOf { - source: Rc::new(ContainerSource::SameAsReceiver), - element: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { - id: "MappedElement".to_string(), - }), - }), - size_effect: std::option::Option::None, - cost_shape: Some(CostShape::ShapeIterateBody), - callback_element_position: Some(0), - }), + collection_map_shape(), collection_filter_shape(), - Rc::new(AlgebraFieldTemplate { - name: "flat_map".to_string(), - param_types: Rc::new(vec![ - Rc::new(AlgebraTypeTemplate::ReceiverSelf), - Rc::new(AlgebraTypeTemplate::CallableOf { - params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), - return_type: Rc::new(AlgebraTypeTemplate::ContainerOf { - source: Rc::new(ContainerSource::SameAsReceiver), - element: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { - id: "MappedElement".to_string(), - }), - }), - }), - ]), - return_type: Rc::new(AlgebraTypeTemplate::ContainerOf { - source: Rc::new(ContainerSource::SameAsReceiver), - element: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { - id: "MappedElement".to_string(), - }), - }), - size_effect: std::option::Option::None, - cost_shape: Some(CostShape::ShapeIterateBody), - callback_element_position: Some(0), - }), + collection_flat_map_shape(), collection_fold_shape(), - Rc::new(AlgebraFieldTemplate { - name: "any".to_string(), - param_types: Rc::new(vec![ - Rc::new(AlgebraTypeTemplate::ReceiverSelf), - Rc::new(AlgebraTypeTemplate::CallableOf { - params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), - return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { - name: "Bool".to_string(), - }), - }), - ]), - return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { - name: "Bool".to_string(), - }), - size_effect: std::option::Option::None, - cost_shape: Some(CostShape::ShapeIterateBody), - callback_element_position: Some(0), - }), - Rc::new(AlgebraFieldTemplate { - name: "all".to_string(), - param_types: Rc::new(vec![ - Rc::new(AlgebraTypeTemplate::ReceiverSelf), - Rc::new(AlgebraTypeTemplate::CallableOf { - params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), - return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { - name: "Bool".to_string(), - }), - }), - ]), - return_type: Rc::new(AlgebraTypeTemplate::NamedTemplate { - name: "Bool".to_string(), - }), - size_effect: std::option::Option::None, - cost_shape: Some(CostShape::ShapeIterateBody), - callback_element_position: Some(0), - }), + collection_any_shape(), + collection_all_shape(), Rc::new(AlgebraFieldTemplate { name: "count".to_string(), param_types: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverSelf)]), @@ -1844,22 +1802,7 @@ pub fn free_monoid_collection_templates() -> Rc>> { cost_shape: Some(CostShape::ShapeIterateBody), callback_element_position: std::option::Option::None, }), - Rc::new(AlgebraFieldTemplate { - name: "sort_by".to_string(), - param_types: Rc::new(vec![ - Rc::new(AlgebraTypeTemplate::ReceiverSelf), - Rc::new(AlgebraTypeTemplate::CallableOf { - params: Rc::new(vec![Rc::new(AlgebraTypeTemplate::ReceiverElement)]), - return_type: Rc::new(AlgebraTypeTemplate::AlgebraTypeVariable { - id: "SortKey".to_string(), - }), - }), - ]), - return_type: Rc::new(AlgebraTypeTemplate::ReceiverSelf), - size_effect: Some(CollectionSizeEffect::IdentityEffect), - cost_shape: Some(CostShape::ShapeSortBody), - callback_element_position: Some(0), - }), + collection_sort_by_shape(), Rc::new(AlgebraFieldTemplate { name: "append".to_string(), param_types: Rc::new(vec![ From 4467f51f0699084cce62a97fd62a06e478bcbe9c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 04:24:50 +0000 Subject: [PATCH 80/90] collection_callback_realization: each fixture program assembled once by a nullary producer; claims read the decided value Co-Authored-By: Claude Opus 5.5 (1M context) --- .../collection_callback_realization_test.dag | 174 +++++++++++------- 1 file changed, 105 insertions(+), 69 deletions(-) diff --git a/src/v2/test/claim/compiler/collection_callback_realization_test.dag b/src/v2/test/claim/compiler/collection_callback_realization_test.dag index 3601a11bc3e..78bc204a603 100644 --- a/src/v2/test/claim/compiler/collection_callback_realization_test.dag +++ b/src/v2/test/claim/compiler/collection_callback_realization_test.dag @@ -45,18 +45,6 @@ fn ccr_loops(tree: ResolvedTree) -> List { }) } -// The declaration the one Loop of a lowered source realizes, as a dotted path. -fn ccr_realized_path(src: String) -> Optional { - match fmi_assemble(src: src) { - Rejected { diagnostics: _ } => optional_absent() - Accepted { value: tree, diagnostics: _ } => - match ccr_loops(tree: tree) { - Cons { head: l, tail: Empty } => ccr_loop_realized_path(l: l) - _ => optional_absent() - } - } -} - fn ccr_loop_realized_path(l: Node) -> Optional { match loop_realized_declaration_target(children: l.children) { Found { target: realized } => @@ -68,38 +56,129 @@ fn ccr_loop_realized_path(l: Node) -> Optional { } } -fn ccr_realizes(src: String, row: String) -> Bool { - match ccr_realized_path(src: src) { - Present { value: path } => path == row - Absent => false +fn ccr_tree_realizes(tree: ResolvedTree, row: String) -> Bool { + match ccr_loops(tree: tree) { + Cons { head: l, tail: Empty } => + match ccr_loop_realized_path(l: l) { + Present { value: path } => path == row + Absent => false + } + _ => false } } -// (1) MAP OVER RECORDS READS A FIELD: the callback's binder `e` is the step's member formal, typed by the -// domain element Pair, so `e.target` projects a declared field and infer raises no projection refusal. -test fn ccr_map_over_records_reads_a_field() -> Bool { +fn ccr_assembled_realizes(src: String, row: String) -> Bool { + match fmi_assemble(src: src) { + Rejected { diagnostics: _ } => false + Accepted { value: tree, diagnostics: _ } => ccr_tree_realizes(tree: tree, row: row) + } +} + +fn ccr_step_accumulator_label(step: Node) -> Optional { + match arrow_declared_parameter_order(arrow: step) { + ArrowParameterOrderDeclared { labels: labels } => list_at_optional(xs: labels, index: 0) + ArrowParameterOrderAbsent => optional_absent() + ArrowParameterOrderMalformed => optional_absent() + } +} + +fn ccr_tree_map_step_conses(tree: ResolvedTree) -> Bool { + match ccr_loops(tree: tree) { + Cons { head: l, tail: Empty } => + match fold_loop_step(loop_node: l) { + Absent => false + Present { value: step } => + match ccr_step_accumulator_label(step: step) { + Absent => false + Present { value: accumulator } => + match fold_step_body_node(step: step) { + Absent => false + Present { value: body } => + match find_named_child(root: body, name: ^tail) { + Rejected { diagnostics: _ } => false + Accepted { value: tail, diagnostics: _ } => + match lexical_reference_label_optional(node: tail) { + Present { value: label } => label == accumulator + Absent => false + } + } + } + } + } + _ => false + } +} + +// EACH FIXTURE PROGRAM IS ASSEMBLED ONCE, by a NULLARY producer enrolled in +// v2.workflow.floor_pure_producer_share floor_cross_claim_pure_producers_warm: source assembly is the real +// route these claims must exercise (the producer IS that route), and it dominates every claim, so it is paid +// once in floor preparation and each claim reads the DECIDED, portable value. Claims over one program share +// its producer. +type CcrMapRecordsReading { + reads_field: Bool + binds_row: Bool + step_conses: Bool +} + +fn ccr_map_records_reading() -> CcrMapRecordsReading { match fmi_assemble(src: ccr_map_records) { + Rejected { diagnostics: _ } => CcrMapRecordsReading { reads_field: false, binds_row: false, step_conses: false } + Accepted { value: tree, diagnostics: _ } => + CcrMapRecordsReading { + reads_field: fmi_no_projection_refusal(o: fmi_infer_tree(tree: tree)), + binds_row: ccr_tree_realizes(tree: tree, row: "std.algebra.collection_map_shape"), + step_conses: ccr_tree_map_step_conses(tree: tree) + } + } +} + +fn ccr_map_undeclared_field_reason() -> Symbol { + fmi_reason(o: fmi_infer(src: ccr_map_undeclared_field)) +} + +fn ccr_piped_map_binds_its_row() -> Bool { + ccr_assembled_realizes(src: ccr_map_piped, row: "std.algebra.collection_map_shape") +} + +fn ccr_all_binds_its_row() -> Bool { + ccr_assembled_realizes(src: ccr_all_records, row: "std.algebra.collection_all_shape") +} + +fn ccr_declared_any_lowers_no_loop() -> Bool { + match fmi_assemble(src: ccr_declared_any) { Rejected { diagnostics: _ } => false - Accepted { value: tree, diagnostics: _ } => fmi_no_projection_refusal(o: fmi_infer_tree(tree: tree)) + Accepted { value: tree, diagnostics: _ } => + match ccr_loops(tree: tree) { + Empty => true + Cons { head: _, tail: _ } => false + } } } +// (1) MAP OVER RECORDS READS A FIELD: the callback's binder `e` is the step's member formal, typed by the +// domain element Pair, so `e.target` projects a declared field and infer raises no projection refusal. +test fn ccr_map_over_records_reads_a_field() -> Bool { + ccr_map_records_reading().reads_field +} + // (2) A BAD FIELD REFUSES AT THE FIELD: the same map projecting a field Pair does not declare. test fn ccr_map_undeclared_field_refuses_at_the_field() -> Bool { - fmi_reason(o: fmi_infer(src: ccr_map_undeclared_field)) == ^infer_reason_field_not_declared_on_receiver + ccr_map_undeclared_field_reason() == ^infer_reason_field_not_declared_on_receiver } // (3) THE HEAD BINDS TO ITS OWN ROW, for every realized callback operation and for the piped spelling. test fn ccr_map_head_binds_its_row() -> Bool { - ccr_realizes(src: ccr_map_records, row: "std.algebra.collection_map_shape") + ccr_map_records_reading().binds_row } test fn ccr_piped_map_head_binds_its_row() -> Bool { - ccr_realizes(src: ccr_map_piped, row: "std.algebra.collection_map_shape") + ccr_piped_map_binds_its_row() } +// all's body joins through &&, which infer does not yet derive (a separate frontier), so this control +// asserts the binding and lowering, not infer acceptance. test fn ccr_all_head_binds_its_row() -> Bool { - ccr_realizes(src: ccr_all_records, row: "std.algebra.collection_all_shape") + ccr_all_binds_its_row() } // (3b) A HEAD A CORPUS DECLARATION BINDS STAYS AN ORDINARY CALL. filter and any are NOT realized, @@ -109,14 +188,7 @@ test fn ccr_all_head_binds_its_row() -> Bool { // module declaring its own `any` and calling it positionally assembles, and lowers to no Loop. Adding // any's row to v2.compiler.fold_lowering collection_roster_rows makes this control red. test fn ccr_declared_any_stays_an_ordinary_call() -> Bool { - match fmi_assemble(src: ccr_declared_any) { - Rejected { diagnostics: _ } => false - Accepted { value: tree, diagnostics: _ } => - match ccr_loops(tree: tree) { - Empty => true - Cons { head: _, tail: _ } => false - } - } + ccr_declared_any_lowers_no_loop() } // (4) THE STRUCTURAL COST CONTROL: map's synthesized step body is a construct whose `tail` is the step's @@ -124,42 +196,6 @@ test fn ccr_declared_any_stays_an_ordinary_call() -> Bool { // a snoc or append over it. This is the control that measures THIS lowering: no executor reads a // fold-realized Loop's encoding today (gunbc.recurring_failure_mode // fold_realized_loop_carries_no_iteration_direction), so an eval_steps figure measures the seed path. -fn ccr_step_accumulator_label(step: Node) -> Optional { - match arrow_declared_parameter_order(arrow: step) { - ArrowParameterOrderDeclared { labels: labels } => list_at_optional(xs: labels, index: 0) - ArrowParameterOrderAbsent => optional_absent() - ArrowParameterOrderMalformed => optional_absent() - } -} - test fn ccr_map_step_conses_onto_its_accumulator() -> Bool { - match fmi_assemble(src: ccr_map_records) { - Rejected { diagnostics: _ } => false - Accepted { value: tree, diagnostics: _ } => - match ccr_loops(tree: tree) { - Cons { head: l, tail: Empty } => - match fold_loop_step(loop_node: l) { - Absent => false - Present { value: step } => - match ccr_step_accumulator_label(step: step) { - Absent => false - Present { value: accumulator } => - match fold_step_body_node(step: step) { - Absent => false - Present { value: body } => - match find_named_child(root: body, name: ^tail) { - Rejected { diagnostics: _ } => false - Accepted { value: tail, diagnostics: _ } => - match lexical_reference_label_optional(node: tail) { - Present { value: label } => label == accumulator - Absent => false - } - } - } - } - } - _ => false - } - } + ccr_map_records_reading().step_conses } - From acc2a93a29ecbcd96fdacc9b5f96be8a107bdec4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 04:24:57 +0000 Subject: [PATCH 81/90] floor_pure_producer_share: warm rows for collection_callback_realization producers Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/workflow/floor_pure_producer_share.dag | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 766c4828ff7..aab5d51396d 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -768,6 +768,11 @@ import v2.std.collection { List } // PreparedGrammar is one nullary producer, and the refusal-arm counts over the live grammar are // another. A PreparedGrammar is the value dag_prepared_grammar already serves; the census is a record // of two Ints. Both WARM: one grammar preparation is more than one claim's budget. +// THE collection_callback_realization PROGRAMS ARE ASSEMBLED ONCE EACH. Each claim assembled a one-module +// source through fmi_assemble (and, for two, infer) and inspected one Bool or Symbol, well over the new-witness +// eval-step cap. The rows now read five nullary producers, one per program -- the map-over-records program's +// producer, a record of three Bools, serving three rows -- and the others a Symbol and three Bools. WARM: one +// assembly is more than one claim's budget. data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.parameter_reference.pr_program_parameter_paths", "v2.test.claim.parameter_reference.pr_program_grounding", @@ -1326,7 +1331,12 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.declaration_graft_assemble.declaration_graft_record_type_only_assembled", "v2.test.claim.declaration_graft_assemble.declaration_graft_flag_and_rec_assembled", "v2.test.claim.declaration_graft_assemble.declaration_graft_where_alias_undeclared_carrier_assembled", - "v2.test.claim.body_cast_node.bcn_refinement_of_refinement_as_its_carrier" + "v2.test.claim.body_cast_node.bcn_refinement_of_refinement_as_its_carrier", + "v2.test.claim.compiler.collection_callback_realization.ccr_map_records_reading", + "v2.test.claim.compiler.collection_callback_realization.ccr_map_undeclared_field_reason", + "v2.test.claim.compiler.collection_callback_realization.ccr_piped_map_binds_its_row", + "v2.test.claim.compiler.collection_callback_realization.ccr_all_binds_its_row", + "v2.test.claim.compiler.collection_callback_realization.ccr_declared_any_lowers_no_loop" ] // grammar_relation_row_for_emitted HAS NOW BEEN MEASURED ON THE THIRD CONJUNCT, AND IT PASSES. From 218eedbcbaca8c9df9dd2fd12ef504bda7f95bbd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 08:10:28 +0000 Subject: [PATCH 82/90] N7-1: exhaustive arms in the dependent-child driver and fold decoders (no wildcard over a closed coproduct) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/04_infer.dag | 37 ++++++++++-------- src/v2/compiler/fold_lowering.dag | 65 ++++++++++++++++++------------- 2 files changed, 59 insertions(+), 43 deletions(-) diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 404ad7f4dbe..f7d3ed5476f 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -65,6 +65,7 @@ import v2.std.optional { import v2.std.qualified_name { QualifiedName, declaration_reference_node, declaration_reference_path_optional, lexical_reference_label_optional, parameter_reference_path_optional } import v2.std.node_query { BinderParts, binder_node, binder_node_parts, construct_field_edges, construct_tag_optional, declared_field_from_edge, find_labeled_child } import v2.std.symbol_index { RecordTypePayload, SymbolIndex, VariantArmPayload, symbol_index_declared_payload_at, symbol_index_declared_type_params_at, symbol_index_lookup } +import v2.std.grammar { node_atom_identity_optional } import v2.compiler.fold_lowering { FoldMemberFormal, FoldMemberRole, FoldStepTakesCollectionMember, FoldStepTakesNoMember, fold_collection_member_type, fold_loop_step, fold_realized_member_role, fold_step_body_node, fold_step_carrier_formal, fold_step_declared_return, fold_step_member_formal } import v2.std.node { LoopBoundMeasure, LoopCarrierBinder, LoopDomainValue, LoopEdgeUnrecognized, LoopIteratedBody, LoopRealizedDeclaration, loop_edge_role, NodeFoldDependent, fold_node_dependent, loop_domain_value_target, loop_realized_declaration_target, Ambiguous, Found, NotMarkedReference, arrow_body_target_lookup, arrow_named_edge_is_contract, arrow_signature_order_edge, match_arm_pattern_edge, resolved_reference_body_kind, edge_label_of, MatchArmBodyEdge, MatchArmPatternEdge, production_edge_label } import v2.std.list_introduction { list_introduction_elements_optional, list_introduction_head_path } @@ -5665,31 +5666,29 @@ fn infer_frame_inherited(frame: InferFrame) -> InferFrame { // A binder's declared type, read through the frame: a type variable the frame instantiates IS its instance. fn infer_frame_instantiated(declared: Node, instances: List) -> Node { - match declared.kind { - TypeNode { connective: Atom { identity: id } } => + match node_atom_identity_optional(node: declared) { + Present { value: id } => match type_variable_instance_lookup(instances: instances, binder: id) { Present { value: instance } => instance Absent => declared } - _ => declared + Absent => declared } } fn infer_loop_member_role(loop_node: Node) -> Optional { - match loop_node.kind { - ComputationNode { behavior: Loop } => - match loop_realized_declaration_target(children: loop_node.children) { - Found { target: realized } => fold_realized_member_role(realized: realized) - _ => Absent - } - _ => Absent + match loop_realized_declaration_target(children: loop_node.children) { + Found { target: realized } => fold_realized_member_role(realized: realized) + Ambiguous => Absent + Absent => Absent } } fn infer_loop_takes_collection_member(loop_node: Node) -> Bool { match infer_loop_member_role(loop_node: loop_node) { Present { value: FoldStepTakesCollectionMember } => true - _ => false + Present { value: FoldStepTakesNoMember } => false + Absent => false } } @@ -5728,7 +5727,8 @@ fn infer_fold_member_frame(loop_node: Node, frame: InferFrame, acc: InferGatherF } } } - _ => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_type_not_derived, at: loop_node) + Ambiguous => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_type_not_derived, at: loop_node) + Absent => infer_frame_with_advisory(frame: frame, reason: ^infer_reason_fold_member_domain_type_not_derived, at: loop_node) } } @@ -5737,8 +5737,8 @@ fn infer_fold_member_frame(loop_node: Node, frame: InferFrame, acc: InferGatherF // formal through the same declared_type_inhabitance fold an application argument meets at its formal, so // `fn(acc, e: Int)` over a List refuses at that formal rather than typing `e` as either. fn infer_fold_member_formal_frame(frame: InferFrame, formal: FoldMemberFormal, member: Node, step: Node, resolved: ResolvedTree) -> InferFrame { - match formal.formal.type_node.kind { - TypeNode { connective: Atom { identity: variable } } => + match node_atom_identity_optional(node: formal.formal.type_node) { + Present { value: variable } => if formal.fresh { InferFrame { instances: list_snoc_item(xs: frame.instances, item: TypeVariableInstance { binder: variable, instance: member }), @@ -5747,7 +5747,7 @@ fn infer_fold_member_formal_frame(frame: InferFrame, formal: FoldMemberFormal, m } else { infer_fold_member_judged_frame(frame: frame, formal: formal, member: member, step: step, resolved: resolved) } - _ => infer_fold_member_judged_frame(frame: frame, formal: formal, member: member, step: step, resolved: resolved) + Absent => infer_fold_member_judged_frame(frame: frame, formal: formal, member: member, step: step, resolved: resolved) } } @@ -5799,7 +5799,12 @@ fn infer_bind_encodes_collection_fold(bind: Node) -> Bool { Present { value: body } => infer_loop_takes_collection_member(loop_node: body) Absent => false } - _ => false + ComputationNode { behavior: Value } => false + ComputationNode { behavior: Transform } => false + ComputationNode { behavior: Branch } => false + ComputationNode { behavior: Loop } => false + ComputationNode { behavior: Match } => false + TypeNode { connective: _ } => false } } diff --git a/src/v2/compiler/fold_lowering.dag b/src/v2/compiler/fold_lowering.dag index 0db4eb0f68b..8239ccd9187 100644 --- a/src/v2/compiler/fold_lowering.dag +++ b/src/v2/compiler/fold_lowering.dag @@ -71,7 +71,7 @@ import v2.std.qualified_name { QualifiedName, declaration_reference_path_optiona import v2.std.arrow_signature { ArrowParameterOrderAbsent, ArrowParameterOrderDeclared, ArrowParameterOrderMalformed, arrow_declared_parameter_order } import v2.std.type_binder { type_param_names } import v2.std.logic { Bool } -import v2.std.algebra { any, fold_list } +import v2.std.algebra { any, fold_list, length } data fold_lowering_note: String = "The un-forked forward lowering for fold — the first slice of the general body producer that replaces both the removed cost-shape fork and the hand-rolled MVP shapes. A surface fold has NO dedicated grammar production; it parses as an ordinary call (a ^dag_surface_primary_expr whose head ident, after the StampLexeme grammar fix, carries its NAME as an atom identity), so its lowering is a SEMANTIC desugaring keyed on the CALLEE identity (DESIGN §4: fold/recursion is sugar over Loop), never a lexeme scan. The input is the fold-call surface subtree — the primary_expr captured child, a sequence(head_ident, call_suffix) — read the homogeneous way cost_coverage locates fn bodies and the qualified-name parser reads sequence heads: fold_call_head_symbol reads the sequence-left head projection (the callee ^fold); fold_call_lowering reads the collection, init and step from their declared slots (named, else at the position std.algebra collection_fold_shape declares), and fold_recurrence_encoding builds the full fold encoding Bind { acc := init, Loop { step, domain, carrier, bound, realized: head } } (gunbc.plans.for_while_loop_sugar §2.3), bounded by the fold-iteration measure ^dag_surface_fold_iteration_measure (registered Strict in v2.std.cardinality.measure_descent_fact_registry, so loop_multiplicity derives its termination). Fail-closed twice: a call whose head is not ^fold refuses with ^fold_lowering_not_a_fold_call, and a fold call carrying no fn-literal iteration body refuses with ^fold_lowering_shape_invalid — never fabricates a loop. Verified end-to-end on real source (v2.test.claim.complexity.fold_lowering): fold(xs, init: 0, f: fn(acc, x) { acc }) lowers to an encoding whose Loop terminates; v2.test.claim.fold_encoding pins each operand at its authored occurrence. This unit is the same desugaring the whole-tree body producer will run on every located fold call once corpus resolution/walking is affordable; the caller supplies the located call subtree." @@ -815,23 +815,36 @@ fn fold_realized_member_role(realized: Node) -> Optional { // is not one of these, or is not an instantiation at all -- is Absent, and the caller must say so rather // than guess a member. fn fold_collection_member_type(domain_type: Node) -> Optional { - match domain_type.kind { - TypeNode { connective: Instantiation } => - match node_positional_child_targets(node: domain_type) { - Cons { head: head, tail: Cons { head: member, tail: Empty } } => - match declaration_reference_path_optional(node: head) { - Present { value: path } => - if qualified_name_to_dotted_string(qn: path) == "v2.std.collection.List" - || qualified_name_to_dotted_string(qn: path) == "std.algebra.FreeMonoid" { - optional_present(value: member) - } else { - optional_absent() - } - Absent => optional_absent() - } - _ => optional_absent() - } - _ => optional_absent() + let args = node_positional_child_targets(node: domain_type) + if fold_node_is_instantiation(n: domain_type) && length(xs: args) == 2 { + match list_at_optional(xs: args, index: 0) { + Absent => optional_absent() + Present { value: head } => + match declaration_reference_path_optional(node: head) { + Absent => optional_absent() + Present { value: path } => + if qualified_name_to_dotted_string(qn: path) == "v2.std.collection.List" + || qualified_name_to_dotted_string(qn: path) == "std.algebra.FreeMonoid" { + list_at_optional(xs: args, index: 1) + } else { + optional_absent() + } + } + } + } else { + optional_absent() + } +} + +fn fold_node_is_instantiation(n: Node) -> Bool { + match n.kind { + TypeNode { connective: Instantiation } => true + TypeNode { connective: Atom { identity: _ } } => false + TypeNode { connective: Conj } => false + TypeNode { connective: Disj } => false + TypeNode { connective: Arrow } => false + TypeNode { connective: Cardinality } => false + ComputationNode { behavior: _ } => false } } @@ -892,7 +905,8 @@ fn fold_step_formal_at(step: Node, position: Int) -> Optional fn fold_step_body_node(step: Node) -> Optional { match arrow_body_target_lookup(children: step.children) { Found { target: body } => optional_present(value: body) - _ => optional_absent() + Ambiguous => optional_absent() + Absent => optional_absent() } } @@ -908,17 +922,14 @@ fn fold_step_declared_return(step: Node) -> Optional { // The step callable of a fold encoding's Loop: its one positional child. fn fold_loop_step(loop_node: Node) -> Optional { - match node_positional_child_targets(node: loop_node) { - Cons { head: step, tail: Empty } => optional_present(value: step) - _ => optional_absent() - } + let positional = node_positional_child_targets(node: loop_node) + if length(xs: positional) == 1 { list_at_optional(xs: positional, index: 0) } else { optional_absent() } } fn fold_type_is_variable_of(t: Node, step: Node) -> Bool { - match t.kind { - TypeNode { connective: Atom { identity: id } } => - any(xs: type_param_names(n: step), predicate: fn(v) { v == id }) - _ => false + match node_atom_identity_optional(node: t) { + Present { value: id } => any(xs: type_param_names(n: step), predicate: fn(v) { v == id }) + Absent => false } } From 07a66bb531e15e21b21cedd5f4fd3abf005c2381 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 08:14:24 +0000 Subject: [PATCH 83/90] infer_fold_member_instance: one nullary producer per fixture program; claims read the decided value Co-Authored-By: Claude Opus 5.5 (1M context) --- .../infer_fold_member_instance_test.dag | 66 ++++++++++++------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag index 3246f6b1788..37cecf6579c 100644 --- a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag +++ b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag @@ -62,14 +62,36 @@ fn fmi_reason(o: Outcome) -> Symbol { } } -test fn fmi_fold_member_projects_a_declared_field() -> Bool { +// EACH FIXTURE PROGRAM IS ASSEMBLED ONCE, by a NULLARY producer enrolled in +// v2.workflow.floor_pure_producer_share floor_cross_claim_pure_producers_warm; the claims read the decided, +// portable value. The record fold's one assembly serves three claims, including the supplied-tree control (3). +type FmiRecordFoldReading { + no_projection_refusal: Bool + base_is_lexical_reference: Bool + authored_member_formal_reason: Symbol +} + +fn fmi_record_fold_reading() -> FmiRecordFoldReading { match fmi_assemble(src: fmi_record_fold) { - Rejected { diagnostics: _ } => false + Rejected { diagnostics: d } => + FmiRecordFoldReading { no_projection_refusal: false, base_is_lexical_reference: false, authored_member_formal_reason: diagnostics_fatal_reason(d: d) } Accepted { value: tree, diagnostics: _ } => - fmi_no_projection_refusal(o: fmi_infer_tree(tree: tree)) + FmiRecordFoldReading { + no_projection_refusal: fmi_no_projection_refusal(o: fmi_infer_tree(tree: tree)), + base_is_lexical_reference: fmi_base_is_lexical_reference(tree: tree), + authored_member_formal_reason: fmi_reason(o: fmi_infer_with_member_formal_authored(tree: tree)) + } } } +fn fmi_undeclared_field_reason() -> Symbol { fmi_reason(o: fmi_infer(src: fmi_undeclared_field_fold)) } +fn fmi_sum_fold_reason() -> Symbol { fmi_reason(o: fmi_infer(src: fmi_sum_fold)) } +fn fmi_body_not_carrier_reason() -> Symbol { fmi_reason(o: fmi_infer(src: fmi_body_not_carrier_fold)) } + +test fn fmi_fold_member_projects_a_declared_field() -> Bool { + fmi_record_fold_reading().no_projection_refusal +} + fn fmi_no_projection_refusal(o: Outcome) -> Bool { !fmi_has_reason(o: o, reason: ^infer_reason_projection_receiver_declares_no_fields) && !fmi_has_reason(o: o, reason: ^infer_reason_field_not_declared_on_receiver) @@ -88,30 +110,30 @@ fn fmi_has_reason(o: Outcome, reason: Symbol) -> Bool { // recorded and infer can type it. RED ON THE BASE: the base was the bare canonical atom `e`, which nothing // typed, so the projection landed in the non-refusing ReceiverTypeUnderived arm. test fn fmi_lambda_binder_projection_base_is_a_lexical_reference() -> Bool { - match fmi_assemble(src: fmi_record_fold) { - Rejected { diagnostics: _ } => false - Accepted { value: tree, diagnostics: _ } => - fold(node_subtree_nodes(root: tree.root), init: false, f: fn(acc, n) { - acc || match field_projection_optional(n: n) { - Present { value: projection } => - match lexical_reference_label_optional(node: projection.base) { - Present { value: label } => label == ^e - Absent => false - } + fmi_record_fold_reading().base_is_lexical_reference +} + +fn fmi_base_is_lexical_reference(tree: ResolvedTree) -> Bool { + fold(node_subtree_nodes(root: tree.root), init: false, f: fn(acc, n) { + acc || match field_projection_optional(n: n) { + Present { value: projection } => + match lexical_reference_label_optional(node: projection.base) { + Present { value: label } => label == ^e Absent => false } - }) - } + Absent => false + } + }) } // (2) THE SAME FOLD WITH AN UNDECLARED FIELD REFUSES AT THAT FIELD. test fn fmi_fold_member_undeclared_field_refuses() -> Bool { - fmi_reason(o: fmi_infer(src: fmi_undeclared_field_fold)) == ^infer_reason_field_not_declared_on_receiver + fmi_undeclared_field_reason() == ^infer_reason_field_not_declared_on_receiver } // (3) A STEP FORMAL INCOMPATIBLE WITH THE DOMAIN ELEMENT REFUSES. test fn fmi_fold_member_incompatible_formal_refuses() -> Bool { - fmi_reason(o: fmi_infer_with_member_formal_authored()) == ^application_argument_does_not_inhabit + fmi_record_fold_reading().authored_member_formal_reason == ^application_argument_does_not_inhabit } // (3) IS SUPPLIED AT THE RESOLVE -> INFER BOUNDARY, because its red is not authorable in source: a typed @@ -172,10 +194,7 @@ fn fmi_replace_binder_type(e: Edge, from: Symbol, to: Node) -> Node { } } -fn fmi_infer_with_member_formal_authored() -> Outcome { - match fmi_assemble(src: fmi_record_fold) { - Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: tree, diagnostics: _ } => +fn fmi_infer_with_member_formal_authored(tree: ResolvedTree) -> Outcome { match fmi_member_formal_type(root: tree.root, name: ^e) { Absent => Rejected { diagnostics: diagnostics_singleton(d: fmi_fixture_shape_diagnostic(at: tree.root)) } Present { value: fresh } => @@ -198,7 +217,6 @@ fn fmi_infer_with_member_formal_authored() -> Outcome { } } } - } } fn fmi_fixture_shape_diagnostic(at: Node) -> Diagnostic { @@ -214,13 +232,13 @@ fn fmi_fixture_shape_diagnostic(at: Node) -> Diagnostic { data fmi_sum_fold: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Int {\n fold(xs, init: 0, f: fn(found, e) { found + e.target })\n}\n" test fn fmi_fold_over_records_is_accepted() -> Bool { - fmi_reason(o: fmi_infer(src: fmi_sum_fold)) == ^accepted + fmi_sum_fold_reason() == ^accepted } data fmi_body_not_carrier_fold: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Bool {\n fold(xs, init: false, f: fn(found, e) { e.target })\n}\n" // (a) A STEP WHOSE BODY DOES NOT INHABIT THE CARRIER REFUSES AT THE BODY: init is Bool, the body is Int. test fn fmi_fold_step_body_not_the_carrier_refuses() -> Bool { - fmi_reason(o: fmi_infer(src: fmi_body_not_carrier_fold)) == ^arrow_body_does_not_inhabit_declared_return + fmi_body_not_carrier_reason() == ^arrow_body_does_not_inhabit_declared_return } From d070e7b7bdce5efa7e98969e16974012a42072b9 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 08:14:24 +0000 Subject: [PATCH 84/90] floor_pure_producer_share: warm rows for infer_fold_member_instance producers Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/workflow/floor_pure_producer_share.dag | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 58bae12f992..391c9e0ef61 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -802,6 +802,10 @@ import v2.std.collection { List } // PreparedGrammar is one nullary producer, and the refusal-arm counts over the live grammar are // another. A PreparedGrammar is the value dag_prepared_grammar already serves; the census is a record // of two Ints. Both WARM: one grammar preparation is more than one claim's budget. +// THE infer_fold_member_instance PROGRAMS ARE ASSEMBLED ONCE EACH: four one-module fold sources through the +// production assembly route plus infer. The record fold's producer serves three claims (a record of two Bools and +// the supplied-tree control's reason); the other three each return one Symbol. WARM: one assembly is more than +// one claim's budget. data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.parameter_reference.pr_program_parameter_paths", "v2.test.claim.parameter_reference.pr_program_grounding", @@ -1431,7 +1435,11 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.declaration_graft_assemble.declaration_graft_record_type_only_assembled", "v2.test.claim.declaration_graft_assemble.declaration_graft_flag_and_rec_assembled", "v2.test.claim.declaration_graft_assemble.declaration_graft_where_alias_undeclared_carrier_assembled", - "v2.test.claim.body_cast_node.bcn_refinement_of_refinement_as_its_carrier" + "v2.test.claim.body_cast_node.bcn_refinement_of_refinement_as_its_carrier", + "v2.test.claim.compiler.infer_fold_member_instance.fmi_record_fold_reading", + "v2.test.claim.compiler.infer_fold_member_instance.fmi_undeclared_field_reason", + "v2.test.claim.compiler.infer_fold_member_instance.fmi_sum_fold_reason", + "v2.test.claim.compiler.infer_fold_member_instance.fmi_body_not_carrier_reason" ] // grammar_relation_row_for_emitted HAS NOW BEEN MEASURED ON THE THIRD CONJUNCT, AND IT PASSES. From c4cd77771c5baf6e2b38a7e94593c404f913186d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 11:48:33 +0000 Subject: [PATCH 85/90] N7-1: cite the renamed match-binder control (declarations finding); kvr named-call Bool formal now refuses since #12506 types references by declaration -- control rewritten to assert the refusal, as its own note required Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ot_typed_from_their_dependency_sibling.dag | 4 +-- .../kernel_value_type_roster_witness_test.dag | 27 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/bind_and_match_binders_not_typed_from_their_dependency_sibling.dag b/dag/gunbc/recurring_failure_mode/bind_and_match_binders_not_typed_from_their_dependency_sibling.dag index 70ee00af7e8..c3e6c829e91 100644 --- a/dag/gunbc/recurring_failure_mode/bind_and_match_binders_not_typed_from_their_dependency_sibling.dag +++ b/dag/gunbc/recurring_failure_mode/bind_and_match_binders_not_typed_from_their_dependency_sibling.dag @@ -7,7 +7,7 @@ import gunbc.recurring_failure_mode { RecurringFailureMode } data bind_and_match_binders_not_typed_from_their_dependency_sibling: RecurringFailureMode = RecurringFailureMode { identity: "bind_and_match_binders_not_typed_from_their_dependency_sibling" as NonEmptyStr, receipts: [ - "INVALID STATE: two behavior rows bind a name whose type is fixed by a SIBLING child's settled result, and v2.compiler.infer types neither binder from it. An unannotated `let x = e` binds x in the Bind's body, but x's LexicalBinding declares no type (v2.compiler.infer infer_lexical_reference_facts reads binding.declared Absent and leaves the reference underived), although the bound value e is folded in the same row. A match-arm pattern binder is typed by the scrutinee's settled type and the matched variant, and the binder-typing for it is #12641's subject (v2.test.claim.field_projection.field_projection_stages fps_a_match_binder_receiver_does_not_yet_infer_holds records it as not yet inferring). HARM: a projection or application off such a binder stays on the counted frontier or refuses for missing evidence instead of being judged -- loud, never fabricated, but valid programs do not type. This is the same class as gunbc.recurring_failure_mode infer_child_context_cannot_depend_on_a_sibling_result, whose collection-fold case is climbed; these two rows are its remaining population.", + "INVALID STATE: two behavior rows bind a name whose type is fixed by a SIBLING child's settled result, and v2.compiler.infer types neither binder from it. An unannotated `let x = e` binds x in the Bind's body, but x's LexicalBinding declares no type (v2.compiler.infer infer_lexical_reference_facts reads binding.declared Absent and leaves the reference underived), although the bound value e is folded in the same row. A match-arm pattern binder is typed by the scrutinee's settled type and the matched variant, and the binder-typing for it is #12641's subject (v2.test.claim.field_projection.field_projection_stages fps_a_match_binder_receiver_is_not_yet_typed_holds records it as not yet inferring). HARM: a projection or application off such a binder stays on the counted frontier or refuses for missing evidence instead of being judged -- loud, never fabricated, but valid programs do not type. This is the same class as gunbc.recurring_failure_mode infer_child_context_cannot_depend_on_a_sibling_result, whose collection-fold case is climbed; these two rows are its remaining population.", "DISTINGUISHING FACTS: the traversal is no longer the obstacle. v2.std.node fold_node_dependent lets a row schedule its dependency child first and derive the dependent child's context from the parent accumulator, and v2.compiler.infer infer_gather_child_context already does so for a fold-realized Loop. What these rows lack is the BINDING SIDE: a Bind's binder carries no type variable for a frame to instantiate (the fold step's member formal does, minted by function-value lowering), and the match-arm binder's relation to the scrutinee is variant-and-field-indexed rather than one member type. So reusing the driver is necessary and not sufficient, and building either row's frame here, without its binder carrier, would be a frame nothing reads (DESIGN section 3c).", "RUNG FOUND AT: mitigatable -- refused or left underived, never wrong. CEILING: structurally guaranteed; both relations are decidable from the sibling's settled type. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: a Bind binder carries its own type position (declared or fresh), and infer_gather_child_context schedules the bound value before the body and instantiates that position from the value's settled type for the body subtree only; and a match-arm binder is instantiated from the scrutinee's settled type through the matched variant's field, scheduled the same way -- SUFFICIENT FOR `let r = n; r.children` and a match-arm binder's projection to type through the one frame, with no second parameter-typing route.", ], @@ -15,6 +15,6 @@ data bind_and_match_binders_not_typed_from_their_dependency_sibling: RecurringFa DeclarationRef { module_path: "v2.std.node", decl_name: "fold_node_dependent", field: WholeDeclaration }, DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_gather_child_context", field: WholeDeclaration }, DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_lexical_reference_facts", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.test.claim.field_projection.field_projection_stages", decl_name: "fps_a_match_binder_receiver_does_not_yet_infer_holds", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.field_projection.field_projection_stages", decl_name: "fps_a_match_binder_receiver_is_not_yet_typed_holds", field: WholeDeclaration }, ], } diff --git a/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag b/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag index 6be6cc35632..ccbb303e5ac 100644 --- a/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag +++ b/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag @@ -23,7 +23,7 @@ import v2.extdeps.languages.dag { dag_language_model } import v2.std.compilers.target_model { char_kernel_type_node, symbol_kernel_type_node } -import v2.std.diagnostic { Accepted, Outcome, Rejected, diagnostics_has_reason } +import v2.std.diagnostic { diagnostics_fatal_reason, Accepted, Outcome, Rejected, diagnostics_has_reason } import v2.std.integer { integer_int_type_node } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool, bool_node } @@ -60,11 +60,11 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // the harness v2.test.claim.compiler.infer_record_construct_field_inhabitance_witness_test declares. // The record-field red is that module's rcf_a_bool_declared_field_value_of_the_wrong_type_refuses_holds. // -// A DECLARED BOOL FORMAL IS NOT A RED THIS ROUTE CAN FIRE YET, and that is pinned rather than -// claimed: a call's callee is a declaration reference, which infer does not yet type by its -// declaration (gunbc#12506), so the application's formals are unresolved whatever they declare. -// kvr_a_named_calls_bool_formal_is_counted_not_judged_holds holds that state and goes red when a -// reference is typed by its declaration; it is then rewritten to assert the refusal. +// A DECLARED BOOL FORMAL IS NOW A RED THIS ROUTE FIRES. A call's callee is a declaration reference, and +// since gunbc#12506 infer types it by its declaration, so the application's formals are resolved and a +// Bool formal holding an Int is judged and refused. The control below pinned the earlier state (accepted +// with a counted UndecidableFormalUnresolved) and was written to go red exactly then; it is rewritten, as +// its own note required, to assert the refusal. data kvr_return_bad_source: String = "module v2.test.kvr_return_bad\n\nimport v2.std.logic { Bool }\n\nfn kvr_f(x: Int) -> Bool { 3 }\n" data kvr_return_good_source: String = "module v2.test.kvr_return_good\n\nimport v2.std.logic { Bool }\n\nfn kvr_f(x: Int) -> Bool { true }\n" @@ -189,12 +189,11 @@ test fn kvr_a_declared_bool_return_holding_a_bool_is_accepted_and_decided_holds( rcf_member_accepted_and_decided(resolved: kvr_resolved_return_good(), member: ^kvr_f) } -// THE FORMAL, PINNED AS IT IS: a named call's Bool formal holding an Int is accepted with a counted -// UndecidableFormalUnresolved, because the callee reference is not typed by its declaration. The -// subject is the calling fn's member alone, handed the module's own indexes: the callee's -// declaration beside it is not what the claim is about, and inferring both is more than one -// claim's budget on the required floor. -test fn kvr_a_named_calls_bool_formal_is_counted_not_judged_holds() -> Bool { +// THE FORMAL, JUDGED: a named call's Bool formal holding an Int refuses application_argument_does_not_inhabit, +// because the callee reference is typed by its declaration (gunbc#12506). The subject is the calling fn's +// member alone, handed the module's own indexes: the callee's declaration beside it is not what the claim is +// about, and inferring both is more than one claim's budget on the required floor. +test fn kvr_a_named_calls_bool_formal_holding_an_int_refuses_holds() -> Bool { match kvr_resolved_formal() { Rejected { diagnostics: _ } => false Accepted { value: tree, diagnostics: _ } => @@ -202,8 +201,8 @@ test fn kvr_a_named_calls_bool_formal_is_counted_not_judged_holds() -> Bool { Absent => false Present { value: arrow } => match infer(tree: ResolvedTree { root: arrow, symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings, unavailable_providers: tree.unavailable_providers }) { - Rejected { diagnostics: _ } => false - Accepted { value: _, diagnostics: ds } => diagnostics_has_reason(d: ds, reason: ^inhabitance_undecidable_formal_unresolved) + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^application_argument_does_not_inhabit + Accepted { value: _, diagnostics: _ } => false } } } From 2aa847cb3a54421ef16f8ced1f7c4bb9093d3296 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 18:28:18 +0000 Subject: [PATCH 86/90] floor_pure_producer_share: drop the collection_callback_realization warm rows (freeze until #13043; identities routed to royal-deer-478) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/workflow/floor_pure_producer_share.dag | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 6588b10c8b7..e6ba567c078 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -631,13 +631,20 @@ import v2.std.collection { List } // .fold_operand_structure fos_outcomes parses the `&&` and call-argument snippets once each and stores // six Bool verdicts; the first floor run of its PR judged each claim that paid its own parse and lowering // against the new-witness budget. Portable for the same reason. -// THE XL-2 LET-IN CAST ROW: v2.test.claim.body_cast_node bcn_let_in_value_cast_verdict assembles one -// inline module through the production route and stores one Bool (the lowered body carries exactly -// one cast node with its target), portable because it holds no Node and no closure. +// THE GRAMMAR-OVERLAP ROUTE ROWS ARE THE SAME GROUND AT TWO FILES: v2.test.parse.else_less_if_value_category +// elif_verdicts parses its two sources once each (a fn body and an expression) and stores four Bool +// verdicts; v2.test.parse.brace_and_lambda_head_route bhr_verdicts parses four one-line modules and +// stores four. Each claim that paid its own parse measured at the new-witness budget's edge on the +// floor runs of #13056 (eval steps 66-72k, enrolment margin refused on cpu). Portable because each +// holds only Bools. // THE dag BIND BLOCK-SCOPED ROW: v2.test.emit.dag_bind_let_block_scoped dbl_verdicts parses five emitted // token sequences once each and stores five Bool verdicts; each claim that paid its own parse measured // over the new-witness budget on the floor runs of #13099 (eval steps 72-105k). Portable because it // holds only Bools. +// THE dag TARGET TEXT ROUND-TRIP ROW: v2.test.emit.dag_target_text_round_trip trt_verdicts renders six +// emitted token sequences to text, lexes each and parses each once, and stores six Bool verdicts. The +// floor of #13113 measured one such claim at 67k and one at 122k eval steps when each paid its own +// parse. Portable because it holds only Bools. // THE XL-2 IF-ARM AND STATEMENT-SPINE ROW IS THE SAME GROUND AT THIRTEEN SPECIMENS: // v2.test.claim.namespace_xl0.if_arm_and_statement_lowering_refusal iasl_outcomes drives one front // end over thirteen inline modules and stores one verdict arm per module, portable for the same reason. @@ -817,11 +824,6 @@ import v2.std.collection { List } // production assembly route plus infer. The record fold's producer serves three claims (a record of two Bools and // the supplied-tree control's reason); the other three each return one Symbol. WARM: one assembly is more than // one claim's budget. -// THE collection_callback_realization PROGRAMS ARE ASSEMBLED ONCE EACH. Each claim assembled a one-module -// source through fmi_assemble (and, for two, infer) and inspected one Bool or Symbol, well over the new-witness -// eval-step cap. The rows now read five nullary producers, one per program -- the map-over-records program's -// producer, a record of three Bools, serving three rows -- and the others a Symbol and three Bools. WARM: one -// assembly is more than one claim's budget. // THE value_base_projection PROGRAMS ARE ASSEMBLED ONCE EACH. gunbc#13053's floor refused its four rows // ENROLMENT-MARGIN-REFUSED (re-derive: that run's lines): each assembled a one-module source through // tpb_assemble and inspected one Bool. The rows now read three nullary producers, one per program -- the @@ -910,10 +912,12 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.namespace_xl0.call_argument_value_resolve_refusal.cav_outcomes", "v2.test.claim.normalize.authored_marker_spelling.ams_verdicts", "v2.test.claim.body_lowering.caret_symbol_value_lowering.csv_verdicts", + "v2.test.parse.else_less_if_value_category.elif_verdicts", + "v2.test.parse.brace_and_lambda_head_route.bhr_verdicts", "v2.test.emit.dag_bind_let_block_scoped.dbl_verdicts", + "v2.test.emit.dag_target_text_round_trip.trt_verdicts", "v2.test.parse.type_decl_modifier_g0_parse_probe.caret_tree_atom_identities", "v2.test.claim.body_lowering.string_literal_value_lowering.slv_verdicts", - "v2.test.claim.body_cast_node.bcn_let_in_value_cast_verdict", "v2.test.claim.namespace_xl0.value_read_refusal.vrr_outcomes", "v2.test.claim.fold_lowering.flp_outcomes", "v2.test.claim.fold_encoding.fe_outcomes", @@ -1466,11 +1470,6 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.compiler.infer_fold_member_instance.fmi_undeclared_field_reason", "v2.test.claim.compiler.infer_fold_member_instance.fmi_sum_fold_reason", "v2.test.claim.compiler.infer_fold_member_instance.fmi_body_not_carrier_reason", - "v2.test.claim.compiler.collection_callback_realization.ccr_map_records_reading", - "v2.test.claim.compiler.collection_callback_realization.ccr_map_undeclared_field_reason", - "v2.test.claim.compiler.collection_callback_realization.ccr_piped_map_binds_its_row", - "v2.test.claim.compiler.collection_callback_realization.ccr_all_binds_its_row", - "v2.test.claim.compiler.collection_callback_realization.ccr_declared_any_lowers_no_loop", "v2.test.claim.value_base_projection.vbp_call_projection_reading", "v2.test.claim.value_base_projection.vbp_field_spelled_like_a_param_keeps_field", "v2.test.claim.value_base_projection.vbp_undeclared_field_is_carried" From 40ded9c7cb41f310b335291b25f5182ae8779c5b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 18:35:39 +0000 Subject: [PATCH 87/90] floor_pure_producer_share: restore the warm rows (amended freeze: land now, royal-deer-478 drops them when #13043 merges) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/workflow/floor_pure_producer_share.dag | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index e6ba567c078..6588b10c8b7 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -631,20 +631,13 @@ import v2.std.collection { List } // .fold_operand_structure fos_outcomes parses the `&&` and call-argument snippets once each and stores // six Bool verdicts; the first floor run of its PR judged each claim that paid its own parse and lowering // against the new-witness budget. Portable for the same reason. -// THE GRAMMAR-OVERLAP ROUTE ROWS ARE THE SAME GROUND AT TWO FILES: v2.test.parse.else_less_if_value_category -// elif_verdicts parses its two sources once each (a fn body and an expression) and stores four Bool -// verdicts; v2.test.parse.brace_and_lambda_head_route bhr_verdicts parses four one-line modules and -// stores four. Each claim that paid its own parse measured at the new-witness budget's edge on the -// floor runs of #13056 (eval steps 66-72k, enrolment margin refused on cpu). Portable because each -// holds only Bools. +// THE XL-2 LET-IN CAST ROW: v2.test.claim.body_cast_node bcn_let_in_value_cast_verdict assembles one +// inline module through the production route and stores one Bool (the lowered body carries exactly +// one cast node with its target), portable because it holds no Node and no closure. // THE dag BIND BLOCK-SCOPED ROW: v2.test.emit.dag_bind_let_block_scoped dbl_verdicts parses five emitted // token sequences once each and stores five Bool verdicts; each claim that paid its own parse measured // over the new-witness budget on the floor runs of #13099 (eval steps 72-105k). Portable because it // holds only Bools. -// THE dag TARGET TEXT ROUND-TRIP ROW: v2.test.emit.dag_target_text_round_trip trt_verdicts renders six -// emitted token sequences to text, lexes each and parses each once, and stores six Bool verdicts. The -// floor of #13113 measured one such claim at 67k and one at 122k eval steps when each paid its own -// parse. Portable because it holds only Bools. // THE XL-2 IF-ARM AND STATEMENT-SPINE ROW IS THE SAME GROUND AT THIRTEEN SPECIMENS: // v2.test.claim.namespace_xl0.if_arm_and_statement_lowering_refusal iasl_outcomes drives one front // end over thirteen inline modules and stores one verdict arm per module, portable for the same reason. @@ -824,6 +817,11 @@ import v2.std.collection { List } // production assembly route plus infer. The record fold's producer serves three claims (a record of two Bools and // the supplied-tree control's reason); the other three each return one Symbol. WARM: one assembly is more than // one claim's budget. +// THE collection_callback_realization PROGRAMS ARE ASSEMBLED ONCE EACH. Each claim assembled a one-module +// source through fmi_assemble (and, for two, infer) and inspected one Bool or Symbol, well over the new-witness +// eval-step cap. The rows now read five nullary producers, one per program -- the map-over-records program's +// producer, a record of three Bools, serving three rows -- and the others a Symbol and three Bools. WARM: one +// assembly is more than one claim's budget. // THE value_base_projection PROGRAMS ARE ASSEMBLED ONCE EACH. gunbc#13053's floor refused its four rows // ENROLMENT-MARGIN-REFUSED (re-derive: that run's lines): each assembled a one-module source through // tpb_assemble and inspected one Bool. The rows now read three nullary producers, one per program -- the @@ -912,12 +910,10 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.namespace_xl0.call_argument_value_resolve_refusal.cav_outcomes", "v2.test.claim.normalize.authored_marker_spelling.ams_verdicts", "v2.test.claim.body_lowering.caret_symbol_value_lowering.csv_verdicts", - "v2.test.parse.else_less_if_value_category.elif_verdicts", - "v2.test.parse.brace_and_lambda_head_route.bhr_verdicts", "v2.test.emit.dag_bind_let_block_scoped.dbl_verdicts", - "v2.test.emit.dag_target_text_round_trip.trt_verdicts", "v2.test.parse.type_decl_modifier_g0_parse_probe.caret_tree_atom_identities", "v2.test.claim.body_lowering.string_literal_value_lowering.slv_verdicts", + "v2.test.claim.body_cast_node.bcn_let_in_value_cast_verdict", "v2.test.claim.namespace_xl0.value_read_refusal.vrr_outcomes", "v2.test.claim.fold_lowering.flp_outcomes", "v2.test.claim.fold_encoding.fe_outcomes", @@ -1470,6 +1466,11 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.compiler.infer_fold_member_instance.fmi_undeclared_field_reason", "v2.test.claim.compiler.infer_fold_member_instance.fmi_sum_fold_reason", "v2.test.claim.compiler.infer_fold_member_instance.fmi_body_not_carrier_reason", + "v2.test.claim.compiler.collection_callback_realization.ccr_map_records_reading", + "v2.test.claim.compiler.collection_callback_realization.ccr_map_undeclared_field_reason", + "v2.test.claim.compiler.collection_callback_realization.ccr_piped_map_binds_its_row", + "v2.test.claim.compiler.collection_callback_realization.ccr_all_binds_its_row", + "v2.test.claim.compiler.collection_callback_realization.ccr_declared_any_lowers_no_loop", "v2.test.claim.value_base_projection.vbp_call_projection_reading", "v2.test.claim.value_base_projection.vbp_field_spelled_like_a_param_keeps_field", "v2.test.claim.value_base_projection.vbp_undeclared_field_is_carried" From 66c125eac94b6ceee4278bcbd55dfde6e40539a2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 18:36:50 +0000 Subject: [PATCH 88/90] floor_pure_producer_share: main's file plus only this PR's warm rows Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/workflow/floor_pure_producer_share.dag | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index cbf9f6a7927..7711b023fdf 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -824,16 +824,16 @@ import v2.std.collection { List } // production assembly route plus infer. The record fold's producer serves three claims (a record of two Bools and // the supplied-tree control's reason); the other three each return one Symbol. WARM: one assembly is more than // one claim's budget. -// THE collection_callback_realization PROGRAMS ARE ASSEMBLED ONCE EACH. Each claim assembled a one-module -// source through fmi_assemble (and, for two, infer) and inspected one Bool or Symbol, well over the new-witness -// eval-step cap. The rows now read five nullary producers, one per program -- the map-over-records program's -// producer, a record of three Bools, serving three rows -- and the others a Symbol and three Bools. WARM: one -// assembly is more than one claim's budget. // THE value_base_projection PROGRAMS ARE ASSEMBLED ONCE EACH. gunbc#13053's floor refused its four rows // ENROLMENT-MARGIN-REFUSED (re-derive: that run's lines): each assembled a one-module source through // tpb_assemble and inspected one Bool. The rows now read three nullary producers, one per program -- the // call-projection program's producer serving two rows -- whose values are a record of two Bools and two // Bools. WARM: one assembly is more than one claim's budget. +// THE collection_callback_realization PROGRAMS ARE ASSEMBLED ONCE EACH. Each claim assembled a one-module +// source through fmi_assemble (and, for two, infer) and inspected one Bool or Symbol, well over the new-witness +// eval-step cap. The rows now read five nullary producers, one per program -- the map-over-records program's +// producer, a record of three Bools, serving three rows -- and the others a Symbol and three Bools. WARM: one +// assembly is more than one claim's budget. data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.parameter_reference.pr_program_parameter_paths", "v2.test.claim.parameter_reference.pr_program_grounding", @@ -1475,14 +1475,14 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.compiler.infer_fold_member_instance.fmi_undeclared_field_reason", "v2.test.claim.compiler.infer_fold_member_instance.fmi_sum_fold_reason", "v2.test.claim.compiler.infer_fold_member_instance.fmi_body_not_carrier_reason", + "v2.test.claim.value_base_projection.vbp_call_projection_reading", + "v2.test.claim.value_base_projection.vbp_field_spelled_like_a_param_keeps_field", + "v2.test.claim.value_base_projection.vbp_undeclared_field_is_carried", "v2.test.claim.compiler.collection_callback_realization.ccr_map_records_reading", "v2.test.claim.compiler.collection_callback_realization.ccr_map_undeclared_field_reason", "v2.test.claim.compiler.collection_callback_realization.ccr_piped_map_binds_its_row", "v2.test.claim.compiler.collection_callback_realization.ccr_all_binds_its_row", - "v2.test.claim.compiler.collection_callback_realization.ccr_declared_any_lowers_no_loop", - "v2.test.claim.value_base_projection.vbp_call_projection_reading", - "v2.test.claim.value_base_projection.vbp_field_spelled_like_a_param_keeps_field", - "v2.test.claim.value_base_projection.vbp_undeclared_field_is_carried" + "v2.test.claim.compiler.collection_callback_realization.ccr_declared_any_lowers_no_loop" ] // grammar_relation_row_for_emitted HAS NOW BEEN MEASURED ON THE THIRD CONJUNCT, AND IT PASSES. From f3f4fef613f3cb435b15f51c4b2077d989d538d6 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 18:38:02 +0000 Subject: [PATCH 89/90] Remove the n7probe scratch module (debugging probe, no consumer) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/test/claim/n7probe/n7_provider.dag | 7 ----- .../test/claim/n7probe/root_children_test.dag | 26 ------------------- 2 files changed, 33 deletions(-) delete mode 100644 src/v2/test/claim/n7probe/n7_provider.dag delete mode 100644 src/v2/test/claim/n7probe/root_children_test.dag diff --git a/src/v2/test/claim/n7probe/n7_provider.dag b/src/v2/test/claim/n7probe/n7_provider.dag deleted file mode 100644 index c099ffd2f2a..00000000000 --- a/src/v2/test/claim/n7probe/n7_provider.dag +++ /dev/null @@ -1,7 +0,0 @@ -module v2.test.n7probe.n7_provider - -import std.types { Int } - -type N7Leg { - target: Int -} diff --git a/src/v2/test/claim/n7probe/root_children_test.dag b/src/v2/test/claim/n7probe/root_children_test.dag deleted file mode 100644 index 171ea98064b..00000000000 --- a/src/v2/test/claim/n7probe/root_children_test.dag +++ /dev/null @@ -1,26 +0,0 @@ -module v2.test.n7probe.root_children - -import v2.std.node { Edge, Node, Symbol } -import v2.std.diagnostic { Diagnostic } -import v2.test.n7probe.n7_provider { N7Leg } -import v2.std.logic { Bool } -import v2.std.collection { List } -import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -fn n7_leg(l: N7Leg) -> Int { - l.target -} - -fn n7_reason(d: Diagnostic) -> Symbol { - d.reason -} - -fn n7_children(root: Node) -> List { - root.children -} - -test fn n7_probe_trivially_true() -> Bool { - true -} From e59f8a3791377ccd2cb4ff2849f3c9f62d5a0b26 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 3 Oct 2026 19:50:10 +0000 Subject: [PATCH 90/90] fold_lowering: a fold-realized Loop's iteration direction is a declared fact of its realization (map right to left, owned by its synthesis); derived from the realized edge; fold_list vs fold_list_right told apart; controls red under reversal Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ed_loop_carries_no_iteration_direction.dag | 13 +++- src/v2/compiler/fold_lowering.dag | 58 ++++++++++++++++ .../collection_callback_realization_test.dag | 66 ++++++++++++++++++- 3 files changed, 131 insertions(+), 6 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/fold_realized_loop_carries_no_iteration_direction.dag b/dag/gunbc/recurring_failure_mode/fold_realized_loop_carries_no_iteration_direction.dag index 052b95a1e1e..3306a6c1ec8 100644 --- a/dag/gunbc/recurring_failure_mode/fold_realized_loop_carries_no_iteration_direction.dag +++ b/dag/gunbc/recurring_failure_mode/fold_realized_loop_carries_no_iteration_direction.dag @@ -7,13 +7,20 @@ import gunbc.recurring_failure_mode { RecurringFailureMode } data fold_realized_loop_carries_no_iteration_direction: RecurringFailureMode = RecurringFailureMode { identity: "fold_realized_loop_carries_no_iteration_direction" as NonEmptyStr, receipts: [ - "INVALID STATE: a fold-realized Loop carries no iteration direction. v2.compiler.fold_lowering fold_recurrence_encoding builds the identical Loop for the collection fold, fold_list (left) and fold_list_right (right), and since N7-1b for map, whose synthesized step conses onto the carrier and is order-preserving ONLY under right-to-left iteration (all joins with &&, which is order-insensitive). The meaning is implied by the realized declaration (^loop_realized_declaration_edge), and no executor reads it: v2.compiler.eval executes a Loop through the runtime LoopInterpreter (eval_loop_node, step_loop) without consulting the realized edge, and the native route executes the seed's emission of the .dag source, not this encoding. HARM, latent: an executor that ran these encodings left-to-right would return map results reversed, which is silent wrongness, and fold_list vs fold_list_right would be indistinguishable. Today nothing executes them, so no program observes it.", - "DISTINGUISHING FACTS: this is not a cost defect. Each synthesized step is O(1) per member (one Cons onto the carrier, never snoc or append), held structurally by v2.test.claim.compiler.collection_callback_realization ccr_map_step_conses_onto_its_accumulator. It is also why an eval_steps figure for `map` measures the seed path, not this lowering.", - "RUNG FOUND AT: outside the executed path, latent; it becomes silent wrongness the moment an executor consumes the encoding, so it must climb before any does. CEILING: structurally impossible, since direction is decidable from the realized row. NEXT-RUNG TRIGGER, stated as the capability: a per-row iteration-direction fact derived from the template (or the realized declaration) AND an executor of fold-realized Loops that consumes it, SUFFICIENT FOR map, fold_list and fold_list_right encodings to evaluate in their declared order, with a control that reverses the fact and goes red.", + "INVALID STATE: a fold-realized Loop's encoding does not show its iteration direction. v2.compiler.fold_lowering fold_recurrence_encoding builds the identical Loop for the collection fold, fold_list (left), fold_list_right (right) and, since N7-1b, map, whose synthesized step conses onto the carrier and is order-preserving ONLY under right-to-left iteration (all joins with &&, which is order-insensitive). An executor that ran a map encoding left to right would return its results reversed: silent wrongness. CLIMBED IN gunbc#13069 (review 74982 asked that the PR introducing map not ship the reversible encoding with only this row): the direction is now a DECLARED FACT OF THE REALIZATION, v2.compiler.fold_lowering FoldIterationDirection, owned by the step synthesis that needs it (callback_realization_iteration_direction: map right to left) and by the authored forms (an authored fold and fold_list left, fold_list_right right), and derived for any Loop from the realized declaration it already carries (^loop_realized_declaration_edge) by fold_realized_iteration_direction. fold_list and fold_list_right are therefore no longer indistinguishable, and a path that names no collection fold derives no direction rather than a default.", + "WHAT REMAINS: no executor of a fold-realized Loop exists to consume the fact. Both installed Loop handlers REFUSE: v2.extdeps.runtimes.v2_evaluator v2_eval_step_loop and v2.program program_step_loop each return a rejected outcome, so v2.compiler.eval's eval_loop_node reaches a typed refusal, never a value, and the native route executes the seed's emission of the .dag source, not this encoding. So the residual is fail-closed, not latent silent wrongness. The obligation it leaves is on the executor that replaces those refusals: it must read fold_realized_iteration_direction, not assume an order.", + "DISTINGUISHING FACTS: this is not a cost defect. Each synthesized step is O(1) per member (one Cons onto the carrier, never snoc or append), held structurally by v2.test.claim.compiler.collection_callback_realization ccr_map_step_conses_onto_its_accumulator; a snoc step would make map left to right at quadratic cost, which is why the order is declared rather than designed away.", + "RUNG: the fact is structurally carried and its derivation is enforced by discriminating controls (ccr_map_loop_iterates_right_to_left on the real map route; ccr_fold_list_and_fold_list_right_are_told_apart; both red when map or fold_list_right is declared left to right, measured). CEILING: structurally impossible once execution exists. NEXT-RUNG TRIGGER, stated as the capability: an executor of fold-realized Loops replacing the refusing step_loop handlers that consumes fold_realized_iteration_direction, SUFFICIENT FOR map, fold_list and fold_list_right encodings to evaluate in their declared order, with an executing control that reverses the declared direction and goes red.", ], evidence: [ DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "fold_recurrence_encoding", field: WholeDeclaration }, DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "CallbackRealization", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "FoldIterationDirection", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.fold_lowering", decl_name: "fold_realized_iteration_direction", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.extdeps.runtimes.v2_evaluator", decl_name: "v2_eval_step_loop", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.program", decl_name: "program_step_loop", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.compiler.collection_callback_realization", decl_name: "ccr_map_loop_iterates_right_to_left", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.compiler.collection_callback_realization", decl_name: "ccr_fold_list_and_fold_list_right_are_told_apart", field: WholeDeclaration }, DeclarationRef { module_path: "v2.test.claim.compiler.collection_callback_realization", decl_name: "ccr_map_step_conses_onto_its_accumulator", field: WholeDeclaration }, ], } diff --git a/src/v2/compiler/fold_lowering.dag b/src/v2/compiler/fold_lowering.dag index fa583f8c68e..f8a5a4593ec 100644 --- a/src/v2/compiler/fold_lowering.dag +++ b/src/v2/compiler/fold_lowering.dag @@ -941,6 +941,64 @@ fn fold_roster_row_is_at(path: String) -> Bool { any(xs: collection_roster_rows, predicate: fn(r) { r.row.module_path + "." + r.row.decl_name == path }) } +// THE ORDER A FOLD-REALIZED LOOP VISITS ITS DOMAIN IS A FACT OF ITS REALIZATION, and the realization says +// it. The Loop's encoding (fold_recurrence_encoding) is one shape for every realized operation, so the +// order cannot be read off the encoding's structure; it is read off the realized declaration the Loop +// already carries (^loop_realized_declaration_edge), through this one derivation, and an executor of a +// fold-realized Loop must ask it. An authored fold step is a left fold, and fold_list is too; +// fold_list_right is the right fold. A synthesized step's order is owned by the synthesis that built it: +// map conses each result onto the carrier, which is order-preserving ONLY when the members are visited +// right to left, so its row says so; all joins with &&, whose result is the same in either order, and is +// declared left to right with the fold it is written beside. +type FoldIterationDirection + = IteratesLeftToRight + | IteratesRightToLeft + +fn callback_realization_iteration_direction(r: CallbackRealization) -> FoldIterationDirection { + match r { + CallbackMapsToCons => IteratesRightToLeft + CallbackAllByAnd => IteratesLeftToRight + } +} + +fn fold_roster_form_iteration_direction(f: FoldRosterForm) -> FoldIterationDirection { + match f { + RosterAuthoredStep => IteratesLeftToRight + RosterCallbackStep { realization: r } => callback_realization_iteration_direction(r: r) + } +} + +fn collection_roster_row_at(path: String) -> Optional { + fold_list(xs: collection_roster_rows, empty: optional_absent(), cons: fn(acc, r) { + match acc { + Present { value: _ } => acc + Absent => if r.row.module_path + "." + r.row.decl_name == path { optional_present(value: r) } else { acc } + } + }) +} + +// Absent: the path names no collection fold realization (fold_node walks a tree, not a sequence). +fn fold_realized_path_iteration_direction(path: String) -> Optional { + match collection_roster_row_at(path: path) { + Present { value: r } => optional_present(value: fold_roster_form_iteration_direction(f: r.form)) + Absent => + if fold_realization_is_at(head: ^fold_list, path: path) { + optional_present(value: IteratesLeftToRight) + } else if fold_realization_is_at(head: ^fold_list_right, path: path) { + optional_present(value: IteratesRightToLeft) + } else { + optional_absent() + } + } +} + +fn fold_realized_iteration_direction(realized: Node) -> Optional { + match declaration_reference_path_optional(node: realized) { + Absent => optional_absent() + Present { value: path } => fold_realized_path_iteration_direction(path: qualified_name_to_dotted_string(qn: path)) + } +} + // Absent: the realized edge names no fold family declaration, so this Loop is not a fold realization. fn fold_realized_member_role(realized: Node) -> Optional { match declaration_reference_path_optional(node: realized) { diff --git a/src/v2/test/claim/compiler/collection_callback_realization_test.dag b/src/v2/test/claim/compiler/collection_callback_realization_test.dag index 78bc204a603..00803e857f8 100644 --- a/src/v2/test/claim/compiler/collection_callback_realization_test.dag +++ b/src/v2/test/claim/compiler/collection_callback_realization_test.dag @@ -2,7 +2,7 @@ module v2.test.claim.compiler.collection_callback_realization import v2.compiler.resolve { ResolvedTree } import v2.compiler.infer { infer } -import v2.compiler.fold_lowering { fold_loop_step, fold_step_body_node } +import v2.compiler.fold_lowering { FoldIterationDirection, IteratesLeftToRight, IteratesRightToLeft, fold_family_realization, fold_loop_step, fold_realization_declaration_path, fold_realized_iteration_direction, fold_realized_path_iteration_direction, fold_step_body_node } import v2.test.claim.compiler.infer_fold_member_instance { fmi_assemble, fmi_infer, fmi_infer_tree, fmi_no_projection_refusal, fmi_reason } import v2.std.arrow_signature { ArrowParameterOrderAbsent, ArrowParameterOrderDeclared, ArrowParameterOrderMalformed, arrow_declared_parameter_order } import v2.std.collection { List, list_at_optional } @@ -67,6 +67,36 @@ fn ccr_tree_realizes(tree: ResolvedTree, row: String) -> Bool { } } +// The one Loop's direction, derived from its own realized edge through the production reader. +fn ccr_tree_iterates_right_to_left(tree: ResolvedTree) -> Bool { + match ccr_loops(tree: tree) { + Cons { head: l, tail: Empty } => ccr_loop_iterates_right_to_left(l: l) + _ => false + } +} + +fn ccr_loop_iterates_right_to_left(l: Node) -> Bool { + match loop_realized_declaration_target(children: l.children) { + Found { target: realized } => ccr_direction_is(d: fold_realized_iteration_direction(realized: realized), want_right: true) + _ => false + } +} + +fn ccr_direction_is(d: Optional, want_right: Bool) -> Bool { + match d { + Present { value: IteratesRightToLeft } => want_right + Present { value: IteratesLeftToRight } => !want_right + Absent => false + } +} + +fn ccr_head_path(head: Symbol) -> String { + match fold_family_realization(head: head) { + Present { value: r } => qualified_name_to_dotted_string(qn: fold_realization_declaration_path(r: r)) + Absent => "" + } +} + fn ccr_assembled_realizes(src: String, row: String) -> Bool { match fmi_assemble(src: src) { Rejected { diagnostics: _ } => false @@ -118,16 +148,18 @@ type CcrMapRecordsReading { reads_field: Bool binds_row: Bool step_conses: Bool + iterates_right_to_left: Bool } fn ccr_map_records_reading() -> CcrMapRecordsReading { match fmi_assemble(src: ccr_map_records) { - Rejected { diagnostics: _ } => CcrMapRecordsReading { reads_field: false, binds_row: false, step_conses: false } + Rejected { diagnostics: _ } => CcrMapRecordsReading { reads_field: false, binds_row: false, step_conses: false, iterates_right_to_left: false } Accepted { value: tree, diagnostics: _ } => CcrMapRecordsReading { reads_field: fmi_no_projection_refusal(o: fmi_infer_tree(tree: tree)), binds_row: ccr_tree_realizes(tree: tree, row: "std.algebra.collection_map_shape"), - step_conses: ccr_tree_map_step_conses(tree: tree) + step_conses: ccr_tree_map_step_conses(tree: tree), + iterates_right_to_left: ccr_tree_iterates_right_to_left(tree: tree) } } } @@ -199,3 +231,31 @@ test fn ccr_declared_any_stays_an_ordinary_call() -> Bool { test fn ccr_map_step_conses_onto_its_accumulator() -> Bool { ccr_map_records_reading().step_conses } + +// (5) THE LOOP SAYS WHICH WAY IT RUNS. map's synthesized step conses onto the carrier, which preserves +// order only right to left, and the order is a fact of the realization rather than of the encoding +// (v2.compiler.fold_lowering FoldIterationDirection). On the real route the one map Loop's realized edge +// derives IteratesRightToLeft. Red if the reader is deleted or map's synthesis is declared left to right. +test fn ccr_map_loop_iterates_right_to_left() -> Bool { + ccr_map_records_reading().iterates_right_to_left +} + +// The same reader at supplied realized paths: the authored fold and all run left to right; fold_list and +// fold_list_right, which share one encoding, are told apart by their realized declarations. +test fn ccr_fold_and_all_iterate_left_to_right() -> Bool { + ccr_direction_is(d: fold_realized_path_iteration_direction(path: "std.algebra.collection_fold_shape"), want_right: false) + && ccr_direction_is(d: fold_realized_path_iteration_direction(path: "std.algebra.collection_all_shape"), want_right: false) +} + +test fn ccr_fold_list_and_fold_list_right_are_told_apart() -> Bool { + ccr_direction_is(d: fold_realized_path_iteration_direction(path: ccr_head_path(head: ^fold_list)), want_right: false) + && ccr_direction_is(d: fold_realized_path_iteration_direction(path: ccr_head_path(head: ^fold_list_right)), want_right: true) +} + +// A path that names no collection fold derives no direction, rather than a default. +test fn ccr_a_non_fold_path_has_no_direction() -> Bool { + match fold_realized_path_iteration_direction(path: "std.algebra.collection_map_shape_not") { + Absent => true + Present { value: _ } => false + } +}