From 031639a9c3d87b836936d7d60d2992cfcbb02b15 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 20:59:58 +0000 Subject: [PATCH 01/63] Plan: construct tag as a declaration reference (qualified construction/patterns), reader census Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/construct-tag-reference-design.md | 119 +++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 docs/plans/construct-tag-reference-design.md diff --git a/docs/plans/construct-tag-reference-design.md b/docs/plans/construct-tag-reference-design.md new file mode 100644 index 00000000000..f233271b7e2 --- /dev/null +++ b/docs/plans/construct-tag-reference-design.md @@ -0,0 +1,119 @@ +# Construct tag as a declaration reference (qualified variant construction and patterns) + +Status: PR1 of two (model and migration census). PR2 is the cut. Owner lane: lively-eagle-657, parent +gentle-koi-724, reviewer neat-boar-16. + +## Symptom and the chain (DESIGN 6b) + +`src/v2/compiler/01_tokenize.dag` (`v2.compiler.tokenize`) is file-refused at native ingest with +`body_lowering_reason_unsupported_form` at `v2.std.diagnostic.Rejected { diagnostics: .. }` in +`lex_walk_artifact`, so none of its declarations enter the index and every importer refuses +`tokenize` as unbound. That is the single root of the native seven. quiet-hawk-702 established this +by execution on D1: the native resolve walk names only `tokenize` for all seven, while explicit +imports of admitted files bind. + +The chain behind it: + +- **Unqualified `R { f: e }`** lowers through `v2.compiler.body_lowering_fold` + `body_lower_try_record_literal` to `v2.std.node_query` `construct_node(tag: Symbol, ..)`, which + builds `Conj { R: Atom(R), f: e }`. A fielded pattern lowers to the same shape. +- **Dotted head `a.b.R { .. }`** never reaches that route. `body_lower_record_literal_tag_optional` + reads only one branded atom. The postfix read classifies the brace suffix as + `PostfixChainSuffixCarried` and declines it. Its own comments name the reason: a construct tag + is a `Symbol`, so a module-qualified tag has no representation. +- **The earliest unjustified boundary is therefore the construct carrier**, not body lowering. + +The same boundary is already wrong on the unqualified path, one stage later. `v2.compiler.resolve` +`resolve_pattern_node_walk` and `resolve_node_walk` resolve the tag atom through `resolve_atom`, +then `resolved_reference_node`, into `declaration_reference_node(path)`. The edge label stays the +leaf `Symbol`. After resolve, the readers that key on "the label equals the atom's identity" answer +Absent for every user constructor: `construct_tag_optional`, `v2.std.node` +`arrow_body_record_construct_conforms`, and `v2.std.compilers.target_model` +`target_value_expr_record_construct_gate`. The construct shape holds only before resolve and in +hand-built fixtures. + +## The model + +The tag edge stops being a nickname and becomes a reference. + +- **Construct** = `Conj { : , field edges.. }`. + - `construct_tag_marker` is one fixed label declared in `v2.std.node` beside + `declaration_reference_marker`, and distinct from it. If they were the same, a nullary `Rec {}` + would read as a declaration reference under `declaration_reference_body_marked`. + - The label is never the constructor's spelling. That removes the label-as-second-name (§3). +- **Reference form: one form, a qualified_name spine.** Before resolve, `` is the + authored `qualified_name` spine built by `v2.std.qualified_name` `qualified_name_spine_of_atoms` + from the authored segment atoms, so each segment keeps its token occurrence. An unqualified `R` is + the one-segment case of that spine: `fold_list_node` over one atom is the two-edge head/tail Conj + that `qualified_name_spine_shape_present` accepts. Readers do not branch on atom-or-spine. + - The ambiguity of a bare spine (an integer literal is also a cons list) is discharged by the + marker label, not by the spine's shape. +- **After resolve,** `` is `declaration_reference_node(path)` produced by + `resolved_reference_node`, the same carrier every other resolved reference uses. The path is the + VARIANT's own declaration path, never its owning coproduct (quiet-hawk-702's v1 trap). No new tag + type; the shared identity stays the declaration path (`std.decl_ref DeclarationRef` in v1). +- **Lowering never resolves or trims.** The construct producer receives the whole authored path. + Nothing takes the last segment anywhere on the route. +- **Resolution is one route.** A tag spine resolves in both walks through a single function that + reads the path with `qualified_name_from_node` and dispatches by length: + - two or more segments go to the existing `try_resolve_qualified_name_node`; + - one segment goes to the existing bare door, `resolve_atom` on its only atom. + + Both end in `resolved_reference_node`. The one-segment case is needed because + `try_resolve_qualified_name_node` answers Absent for length 1. +- **Nullary qualified values** (`v2.std.diagnostic.None`) are not constructs. They are dotted + references, which already lower to the spine and resolve through `try_resolve_qualified_name_node`. + PR2 adds a control proving they bind to the variant declaration. It changes that route only if + the control goes red. +- **Patterns** use the same producer and the same reference form for the constructor head. + `resolve_pattern_binders` keeps reading only the field edges. + +## Reader and producer census, with each migration + +Established by greps over `src/` and `dag/` (`*.dag` and `*.rs`) for `construct_node`, +`construct_tag`, `construct_field_edges`, `RecordConstructBody`, `record_construct`, +`TargetRecordConstructShape`, and the label-equals-identity comparison. No Rust code reads the v2 +construct shape. `src/v1/stage0` `coproduct_reflection` emits the unrelated v1 +`record_construction_spelling` edge. + +| Module | Symbol | Role | Migration | +|---|---|---|---| +| v2.std.node_query | `construct_tag_edge`, `construct_node` | producer | Take the reference node (a spine) in place of `tag: Symbol`, and emit `Named{construct_tag_marker}` targeting it. | +| v2.std.node_query | `construct_tag_optional` | reader | Key on first-edge label == marker, and answer the reference node (a pre-resolve spine or a post-resolve marked reference). It no longer answers a Symbol. | +| v2.std.node_query | `construct_field_edges` | reader | Unchanged apart from the gate. | +| v2.std.node | `arrow_body_record_construct_conforms` | well_formed gate | First edge is the marker, targeting a spine or a marked reference, and every other edge is Named. Fixes resolved bodies being rejected today. | +| v2.std.node | `classify_arrow_body_form`, `declaration_reference_body_marked` | classifier | Classify a construct by the marker explicitly, not as "any unmarked Conj". | +| v2.compiler.body_lowering_fold | `body_lower_try_record_literal`, `body_lower_record_literal_tag_optional` | producer | Read the head as a qualified-name spine (bare or dotted) and pass it to `construct_node`. The brace suffix leaves `PostfixChainSuffixCarried`; the declined arm and its advisory for this suffix are deleted. | +| v2.compiler.body_lowering_fold | constructor-pattern lowering (the `construct_node` call in the pattern arm) | producer | Same: the spine from the pattern head. | +| v2.compiler.body_lowering_fold | `body_lower_is_core_substrate` | reader | Through the migrated `construct_tag_optional`. Check the order against the `qualified_name_spine_shape_present` test. | +| v2.compiler.resolve | `resolve_pattern_node_walk`, `resolve_node_walk` Conj arm | rewriter | Resolve the marker edge's spine through the one length-dispatching function above. Field edges walk as today. Check the `resolve_ctx_snoc_position` side effect now that the label is the marker. | +| v2.compiler.resolve | `resolve_pattern_binders` | reader | Unchanged apart from the gate. | +| v2.compiler.infer | Conj gather arms (`infer_gather_*`, `infer_product_facts_from_entries`) | reader | The marker edge must not be read as a product field named by the marker. Skip it and type the construct by the referenced declaration where a row exists. Today the tag is `infer_gather_fold_not_derived`. | +| v2.compiler.eval | `eval_callee_body_refusal_reason` | reader | Refusal only; no change. | +| v2.std.compilers.target_model | `target_value_expr_record_construct_gate`, `target_project_record_construct` | reader | Gate on the marker. `type_name` comes from the resolved declaration's path, spelled through the target's binding rows, not from an atom identity. This admits resolved constructs the gate refuses today; that is a behaviour change, and it is named. | +| v2.std.compilers.target_model | `target_value_expr_arrow_has_record_construct_body`, the arm body projection and match-arm slot readers | reader | Follow the gate. | +| v2.std.compilers.target_model | `TargetRecordConstructShape` decode; the `record_construct_form` rows in extdeps.languages rust, typescript, c and dag | token rows | No change. | +| v2.extdeps.languages.dag | `dag_rec_construct_body_node`, `dag_variant_holds_construct_node` and their users | fixture | Rebuild through `construct_node` with a spine. | +| v2.lens.machine_shape | `constructed_tag` | reader | Compare the resolved declaration path of `MachineShape`, not a leaf Symbol. | +| tests: `variant_field_lowering_test`, `wildcard_pattern_form_test`, `arrow_body_form_witness_test` and `arrow_body_form_semantic_helpers`, `record_construct_emit_test`, `rust_record_construct_emit_test`, `rust_record_construct_emit_host_equals_eval_test`, `reference_conservation_test` (occurrence counts), `value_position_whole_read_test`, `declaration_graft_assemble_test` | | test | Rebuild each fixture with the marker, and recount occurrences (segments keep their token occurrences). The unmarked reference-shaped negative must still go red. | +| recurring_failure_mode and rung_drop prose (`postfix_suffix_step_declined_to_the_pre_existing_arms`, `call_expression_erased_at_v2_body_lowering`), `docs/plans/lowering-occurrence-projection-design.md` | | prose | Update the prose. Retire the brace-suffix population of the rung drop by its trigger. | + +Not readers of this shape: `v2.std.data_initializer_identity`, `v2.std.decl_facts_skeleton` and the +v1 `record_construction_census` fixtures all read the v1 reflection edge. The orchestration +`construct_tag:` fields are an unrelated name clash. + +## PR2 (the cut) and its evidence + +All readers migrate in one motion, and no `Symbol`-tag path remains. Controls, fed by production: + +1. `a.b.R { f: e }`, `a.b.C` (nullary) and `a.b.R { f: x } =>` each lower and resolve to the same + node as their unqualified form under an import of the same declaration. This is a content + equality check. +2. Two variants with the same leaf in different modules, constructed qualified, bind to different + declaration paths. +3. Mutations go red: dropping the qualifier binds the other module's variant or refuses as + ambiguous, and dropping the record body changes the node. +4. The variant is bound, not its coproduct. +5. `01_tokenize.dag` is admitted on the native route. A base-vs-head census (the #12550 recipe) + shows the newly admitted files and no unexplained new refusal. quiet-hawk-702 runs the native + seven against the head. From 254cf93844fea8b385c5419f0c13626725731b4a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 21:08:00 +0000 Subject: [PATCH 02/63] Plan: fold in review conditions and landing order; file the live construct-tag misread row (confirmed by execution) Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ality_after_resolve_rewrote_the_target.dag | 23 +++++++++++++ docs/plans/construct-tag-reference-design.md | 32 ++++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/construct_tag_read_by_label_equality_after_resolve_rewrote_the_target.dag diff --git a/dag/gunbc/recurring_failure_mode/construct_tag_read_by_label_equality_after_resolve_rewrote_the_target.dag b/dag/gunbc/recurring_failure_mode/construct_tag_read_by_label_equality_after_resolve_rewrote_the_target.dag new file mode 100644 index 00000000000..9191b34886a --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/construct_tag_read_by_label_equality_after_resolve_rewrote_the_target.dag @@ -0,0 +1,23 @@ +module gunbc.recurring_failure_mode.construct_tag_read_by_label_equality_after_resolve_rewrote_the_target + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data construct_tag_read_by_label_equality_after_resolve_rewrote_the_target: RecurringFailureMode = RecurringFailureMode { + identity: "construct_tag_read_by_label_equality_after_resolve_rewrote_the_target" as NonEmptyStr, + + receipts: [ + "INVALID STATE: a reader recognises a record construct (or a fielded constructor pattern) by comparing the construct's first edge LABEL with the identity of the Atom it targets (v2.std.node_query construct_tag_optional: label == atom identity), while an earlier stage has already replaced that target. body lowering builds Conj { R: Atom(R), fields } (v2.std.node_query construct_node), and v2.compiler.resolve rewrites the tag Atom through resolve_atom and resolved_reference_node into declaration_reference_node(path), leaving the label as the leaf Symbol. After resolve the label names a spelling and the target names a declaration, so the equality the reader keys on can no longer hold for any user constructor.", + "SPECIMEN, measured by execution (2026-09-29, lively-eagle-657): a provider declaring type Pcr = PcrA { n: Bool } | PcrB and a consumer importing it with fn pc_make() -> Pcr { PcrA { n: true } }, ingested and resolved through v2.compiler.name_resolve resolve_with_admission_context_policy. The resolved construct is Conj { PcrA -> v2.test.pc_provider.PcrA, n -> true }: the tag is correctly bound to the VARIANT's own declaration path, and construct_tag_optional answers Absent. The run used the seed built on quiet-hawk-702's D1 tree over this lane's own .dag sources.", + "POPULATION: the readers that key on label == atom identity. construct_tag_optional answers Absent for a resolved user construct (MEASURED). v2.std.node arrow_body_record_construct_conforms requires the first edge to target an Atom, so it answers false and well_formed rejects the body; v2.std.compilers.target_model target_value_expr_record_construct_gate refuses it for the same reason. Those two are INFERRED from their definitions, not measured. Every consumer of construct_tag_optional downstream of resolve inherits the Absent: v2.lens.machine_shape constructed_tag misses a constructed MachineShape, and v2.compiler.body_lowering_fold body_lower_is_core_substrate is reached only before resolve. The readers were correct when written, and the form moved under them.", + "HARM: section 5 silent wrongness where a reader answers Absent and the consumer treats Absent as 'not a construct' (the lens, any fold that dispatches on the tag); a located refusal where the reader is a gate. Neither is a typed statement that a construct was seen and misread.", + "RUNG FOUND AT: below the ladder for the silent readers (a missed construct is indistinguishable from a non-construct), mitigatable for the refusing gates. CEILING: structurally impossible. Which edge of a construct is its tag is a structural role, not a spelling equality, so the invalid pairing need not have a constructor. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the construct's tag edge carries a structural Core marker (the typed EdgeLabel coproduct of gunbc#12473, arm ConstructTag), and its target is an ordinary reference, an authored qualified_name spine before resolve and declaration_reference_node after, so every reader recognises a construct by the marker at every stage and no reader compares a label with a target. Plan and full reader census: docs/plans/construct-tag-reference-design.md (gunbc#12701). The control that is RED on base (a resolved user construct read by construct_tag_optional) lands with the cut and stays enrolled after it.", + ], + + evidence: [ + DeclarationRef { module_path: "v2.std.node_query", decl_name: "construct_tag_optional", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.std.node", decl_name: "arrow_body_record_construct_conforms", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.std.compilers.target_model", decl_name: "target_value_expr_record_construct_gate", field: WholeDeclaration }, + ], +} diff --git a/docs/plans/construct-tag-reference-design.md b/docs/plans/construct-tag-reference-design.md index f233271b7e2..869f8cbafd0 100644 --- a/docs/plans/construct-tag-reference-design.md +++ b/docs/plans/construct-tag-reference-design.md @@ -37,8 +37,16 @@ hand-built fixtures. The tag edge stops being a nickname and becomes a reference. - **Construct** = `Conj { : , field edges.. }`. - - `construct_tag_marker` is one fixed label declared in `v2.std.node` beside - `declaration_reference_marker`, and distinct from it. If they were the same, a nullary `Rec {}` + - `construct_tag_marker` is a STRUCTURAL CORE MARKER in the typed `EdgeLabel` vocabulary of + #12473 (quiet-koi-814, ruling A; review condition from neat-boar-16). It names the edge's role + (constructor tag); the target is an ordinary authored reference. It is counted once in the + #12473 census. Landing order, agreed with quiet-koi-814: this lane's PR2 lands first, because #12473 is the + model document only and its cut has not started. PR2 therefore declares the marker beside + `declaration_reference_marker` in `v2.std.node`, in the same form. It also records the marker once, in + the Core-marker list of `docs/plans/edge-label-coproduct/README.md` (item 1 under "Who owns the + closed set") if #12473 has landed by then, or in #12701 otherwise. The #12473 cut converts it to + the Core arm `ConstructTag` together with the other markers. It is distinct from + `declaration_reference_marker`. If they were the same, a nullary `Rec {}` would read as a declaration reference under `declaration_reference_body_marked`. - The label is never the constructor's spelling. That removes the label-as-second-name (§3). - **Reference form: one form, a qualified_name spine.** Before resolve, `` is the @@ -88,7 +96,7 @@ construct shape. `src/v1/stage0` `coproduct_reflection` emits the unrelated v1 | v2.compiler.body_lowering_fold | `body_lower_is_core_substrate` | reader | Through the migrated `construct_tag_optional`. Check the order against the `qualified_name_spine_shape_present` test. | | v2.compiler.resolve | `resolve_pattern_node_walk`, `resolve_node_walk` Conj arm | rewriter | Resolve the marker edge's spine through the one length-dispatching function above. Field edges walk as today. Check the `resolve_ctx_snoc_position` side effect now that the label is the marker. | | v2.compiler.resolve | `resolve_pattern_binders` | reader | Unchanged apart from the gate. | -| v2.compiler.infer | Conj gather arms (`infer_gather_*`, `infer_product_facts_from_entries`) | reader | The marker edge must not be read as a product field named by the marker. Skip it and type the construct by the referenced declaration where a row exists. Today the tag is `infer_gather_fold_not_derived`. | +| v2.compiler.infer | Conj gather arms (`infer_gather_*`, `infer_product_facts_from_entries`) | reader | The marker edge must not be read as a product field. Skip it by its TYPED edge role, never by the label's spelling, and type the construct by the referenced declaration where a row exists. Today the tag is `infer_gather_fold_not_derived`. | | v2.compiler.eval | `eval_callee_body_refusal_reason` | reader | Refusal only; no change. | | v2.std.compilers.target_model | `target_value_expr_record_construct_gate`, `target_project_record_construct` | reader | Gate on the marker. `type_name` comes from the resolved declaration's path, spelled through the target's binding rows, not from an atom identity. This admits resolved constructs the gate refuses today; that is a behaviour change, and it is named. | | v2.std.compilers.target_model | `target_value_expr_arrow_has_record_construct_body`, the arm body projection and match-arm slot readers | reader | Follow the gate. | @@ -102,6 +110,14 @@ Not readers of this shape: `v2.std.data_initializer_identity`, `v2.std.decl_fact v1 `record_construction_census` fixtures all read the v1 reflection edge. The orchestration `construct_tag:` fields are an unrelated name clash. +## The live defect on main + +Filed as `gunbc.recurring_failure_mode` +`construct_tag_read_by_label_equality_after_resolve_rewrote_the_target`. Confirmed by execution: a +resolved `PcrA { n: true }` is `Conj { PcrA -> v2.test.pc_provider.PcrA, n -> true }`, +and `construct_tag_optional` answers Absent. The well_formed and target_model gate arms are inferred +from their definitions, and the row labels them that way. + ## PR2 (the cut) and its evidence All readers migrate in one motion, and no `Symbol`-tag path remains. Controls, fed by production: @@ -114,6 +130,14 @@ All readers migrate in one motion, and no `Symbol`-tag path remains. Controls, f 3. Mutations go red: dropping the qualifier binds the other module's variant or refuses as ambiguous, and dropping the record body changes the node. 4. The variant is bound, not its coproduct. -5. `01_tokenize.dag` is admitted on the native route. A base-vs-head census (the #12550 recipe) +5. A qualified tag whose last segment names a TYPE rather than a variant, or a variant of a + different type, refuses at a located site and never resolves by spelling (neat-boar-16). +6. The target_model gate is shown by a discriminating pair: a resolved construct is admitted, and + an unresolved or wrong-variant construct still refuses at that gate. +7. The seven's actual form, `v2.std.diagnostic.Rejected { diagnostics: .. }`, is the positive + control on the production route. +8. The live defect below has a control that is RED on main (a resolved user construct read by + `construct_tag_optional`), turns green in PR2, and stays enrolled as the regression control. +9. `01_tokenize.dag` is admitted on the native route. A base-vs-head census (the #12550 recipe) shows the newly admitted files and no unexplained new refusal. quiet-hawk-702 runs the native seven against the head. From db47b854584d8abbed45a9f5a3ea5c42fbafeea7 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 22:45:48 +0000 Subject: [PATCH 03/63] MQ PR1: anonymous record literal as the tag-elided construct, elaborated at a declared-type check site -- model and consumer census Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 docs/plans/anonymous-record-literal-design.md diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md new file mode 100644 index 00000000000..a6f74cbba6d --- /dev/null +++ b/docs/plans/anonymous-record-literal-design.md @@ -0,0 +1,173 @@ +# Anonymous record literals: the tag-absent construct, elaborated from the expected type + +Status: PR1 of two (the model and its consumer census). PR2 is the cut. Owner lane: sleek-fox-423, +parent gentle-koi-724, reviewer neat-boar-16. This builds on the construct carrier of +`docs/plans/construct-tag-reference-design.md` (gunbc#12701, lively-eagle-657) and does not restate +it. + +## Symptom and the chain (DESIGN 6b) + +A headless brace literal, `data x: T = { f: v }` or a nested `f: { g: w }`, file-refuses at native +ingest with `body_lowering_reason_unsupported_form`. A refused file drops its declarations from the +native index, so every importer then fails as unbound. The survey (lively-eagle-657, quiet-hawk-702 +probe on the #12420 tree) names seven files. + +The slice, read from the grammar forward: + +- **Grammar.** `v2.extdeps.languages.dag` `dag_grammar_primary_expr_core` has ONE headless-brace + alternative, `{ field_init_list }`. `dag_grammar_field_init_expr` keys each item on EITHER a + binding name OR a string literal. The record literal `{ f: v }` and the map literal + `{ "k": v }` are the same production. Only the lexical kind of each key tells them apart. +- **Lowering.** `v2.compiler.body_lowering_fold` `body_lower_try_record_literal` needs a head tag + from `body_lower_record_literal_tag_optional`. A headless brace has none, so the primary is + unreadable and refuses at the primary (the XL-2 refusal arm, + `unrecognized_primary_expression_lowers_to_its_first_atom_at_v2_body_lowering`). This refusal is + CORRECT at this link. Lowering runs before resolve and before infer, so it cannot know the + constructor. The earliest unjustified boundary is therefore not lowering. It is the construct + carrier, which has no tag-absent case, together with typing, which has no site that checks a body + against its declared type. +- **Typing has no check site.** `v2.std.inhabitance` `DeclaredTypePosition` declares + `PositionDeclaredReturn`, but nothing constructs it. `v2.compiler.infer` builds only + `position_direct_call_argument` obligations. v2 infer synthesises bottom-up and never pushes a + declared type into a body. So "typing, which checks against the declared type" does not exist + in v2 yet. Form B is the first consumer that needs it. The obligation vocabulary already has a + home for it (`DeclaredTypeObligation`, `declared_type_inhabitance`). The v1 seed carries the + missing positions by name (`src/v1/04_infer.dag` `PositionDataInitializer`, + `PositionVariantPayload`). +- **The seed is not an oracle for the choice.** v1 chooses the struct in its Rust emitter by + matching field names: `v1.compiler.emit_rust` `anonymous_record_struct_candidates`, and it + refuses `AmbiguousAnonymousRecordLiteral` ("shape matches N structs -- add a nominal type"). + That is the selection this lane is forbidden to make. So seed-vs-native agreement on the chosen + tag is not evidence. The oracle is content equality with the authored headed form (control 1). + +## The model + +**The carrier.** Form B is the tag-ABSENT case of the one construct carrier of #12701. It is not a +new node kind. After #12701 a construct is `Conj { : , fields.. }`. +The tag-absent construct is `Conj { : , fields.. }`. + +- The tag edge is always present, so every construct reader keys on the same first edge and asks + only whether its target is a reference or the elision marker. The alternative, a construct with + no tag edge, is a bare `Conj` of Named edges. That is also the anonymous record TYPE + (`body_lower_anonymous_record_type_optional`, `-> { success: Bool }`) and the shape every + "unmarked Conj" reader already means something else by. Presence-by-absence would be a second, + implicit representation. It is refused for the same reason #12701 keeps `construct_tag_marker` + distinct from `declaration_reference_marker`. +- `construct_tag_elided_marker` is a structural core marker declared beside `construct_tag_marker` + in `v2.std.node`, in the same form, and counted once in the #12473 `EdgeLabel` census. It + carries no spelling and no guess. +- **Key kind is recorded, not interpreted.** Each field edge keeps its authored key kind. A name key + is `Named { name }`, as today. A string key has no field-edge form yet (see Scope). Lowering does + not decide "record" versus "map". It records what was written. + +**Where elaboration happens: at a declared-type check site in infer, before synthesis.** This is +the one place where both facts meet: the expected type, which only a declared position carries, +and resolved declarations, which infer's facts carry. The check site is a `DeclaredTypeObligation` +at a named position. A position is where a declared type meets a produced expression: + +- `PositionDataInitializer`: `data x: T = e`. New arm, v1 vocabulary. +- `PositionDeclaredReturn`: `fn f(..) -> T { e }`. The arm exists. This change gives it a + constructor. +- `PositionRecordField`: the expected type of a field init is the declared type of that field on + the (authored or elaborated) constructor. This is how nesting works. `host_transport`'s + `RuntimePrimitive { value: { .. } }` elaborates the inner literal from `RuntimePrimitive.value`'s + declared type, and `compile_stage_memo`'s `key_derivation: { .. }` elaborates from the outer + record's field. New arm. + +At each such site, before the body is synthesised, a tag-elided construct at the root of `e` is +elaborated. Elaboration recurses through field inits under `PositionRecordField`. It is: + +1. Resolve the expected type's HEAD to a declaration. Type arguments (`BoundedLattice`) + do not choose the constructor; they flow into the field expected types by the existing + instantiation (`TypeVariableInstance`). A transparent alias is chased through the existing + alias-identity authority, not re-implemented. +2. The declaration must have exactly ONE record-shaped constructor: a `type R { .. }` record. The + single-variant coproduct form is admitted only if the existing record/coproduct classification + already names it one constructor. +3. Replace `construct_tag_elided_marker` with `declaration_reference_node(path)` of THAT + declaration, through `resolved_reference_node`. This is the same carrier resolve writes for an + authored head. +4. Synthesis then proceeds on an ordinary resolved construct. Field names are checked against the + chosen constructor by the existing construct typing (#12701's infer migration). A surplus or + missing field refuses there, at the field. + +The choice reads the expected type only. The field set is never an input to step 1 or step 2. Its +only role is step 4, which CHECKS a choice already made. + +**Refusal causes** (typed, located at the literal's `{`, each with an ownership row in +`v2.workflow.compile_door_cause_ownership`, per +`refusal_producer_lands_without_its_ownership_row`): + +- `infer_anonymous_record_no_expected_type`: a tag-elided construct reached synthesis without + passing through a check site. Examples: a call argument whose formal is a type variable not yet + instantiated, a `let` without an annotation, a match-arm body. These are not guessed. +- `infer_anonymous_record_expected_type_not_record`: the expected head resolves to a coproduct with + more than one constructor, a primitive, or `Map`/`List` while the literal has name keys. +- `infer_anonymous_record_expected_type_unresolved`: the expected head does not resolve. This is + distinct from the above so that a missing import is not reported as a shape error. + +A tag-elided construct that survives infer is unwritable downstream. The emit, eval and target_model +readers gate on a reference target, and the elision marker is not one. So a missed elaboration +refuses at their existing gates, and cannot emit an anonymous struct. + +## Consumers (producers, readers, rewriters) + +Stated as a delta on #12701's census. Every row there still applies; the rows here are the ones +the elided case adds. + +| Module | Symbol | Role | Change | +|---|---|---|---| +| v2.std.node | `construct_tag_elided_marker` | vocabulary | New structural core marker beside `construct_tag_marker`. | +| v2.std.node_query | `construct_node` | producer | Takes the tag target as the reference OR the elision marker (one parameter; a typed `ConstructTag = Authored { reference } \| Elided` at the call boundary so a caller cannot pass an arbitrary node). | +| v2.std.node_query | `construct_tag_optional` | reader | Answers the reference only. For an elided tag it answers Absent. `construct_tag_is_elided` is the one reader of the elided case. | +| v2.std.node | `arrow_body_record_construct_conforms`, `classify_arrow_body_form` | well_formed gate | Admits the elided marker ONLY pre-infer. The post-infer well_formed gate refuses it. | +| v2.compiler.body_lowering_fold | `body_lower_try_record_literal` | producer | A headless `{ field_init_list }` whose every key is a name lowers to `construct_node(Elided, ..)`. The field inits go through the existing `body_lower_field_init_edge`. | +| v2.compiler.resolve | `resolve_node_walk`, `resolve_pattern_node_walk` Conj arm | rewriter | The elided tag edge is passed through untouched (there is nothing to resolve). Field values walk as today. | +| v2.std.inhabitance | `DeclaredTypePosition` | vocabulary | Add `PositionDataInitializer` and `PositionRecordField`. `PositionDeclaredReturn` gains its constructor. | +| v2.compiler.infer | new `infer_elaborate_expected_construct` | elaborator | Steps 1-4 above, called from the data-initializer, declared-return and record-field check sites. It is the only writer that replaces the elision marker. | +| v2.compiler.infer | Conj gather arms | reader | A tag-elided construct reached here refuses `infer_anonymous_record_no_expected_type` (it is never typed as a bare product). | +| v2.workflow.compile_door_cause_ownership | three rows | ownership | One row per refusal cause. | +| v2.compiler.eval, v2.std.compilers.target_model, emit | construct gates | reader | No change. They already refuse a non-reference tag after #12701. PR2 adds a control proving an elided construct cannot reach them. | + +## Scope: which of the seven Form B admits + +Read from source. PR2's base census will locate each refusal, and this table will be corrected +against it. + +| File | Form | Form B admits it? | +|---|---|---| +| dag/std/primitives | `data c: PrimitiveContract = { .. }` | yes: data initializer | +| dag/std/termination | `data d: BoundedLattice = { .. }` (newline-separated) | yes, via generic instantiation. Separator handling to confirm. | +| dag/extdeps/realization/compile_stage_memo | `data f: CacheInterfaceCatalogFacts = { key_derivation: { .. }, .. }` | yes: data initializer plus record field | +| dag/extdeps/realization/parse_table_memo | same shape | yes | +| src/v2/std/host_transport | `RuntimePrimitive { value: { .. } }` | yes: record field under an authored head | +| dag/std/types | `data s: Map = { "String": true, .. }` | **no.** This is a MAP literal: string keys, and the expected type is `Map`. It needs a modeled map construction, which the XL-2 row already names as an open design question. | +| dag/std/algebra | not located by reading | to be located by the base census | + +So Form B is necessary for the seven but not sufficient: `std/types` needs the map-literal +construction. The expected-type check site modeled here is the same one a map literal elaborates at +(the expected type `Map` chooses map construction; string keys are then the checked +consequence). The map case is therefore a second arm at the same site, not a second site. It is +raised with the parent as scope, not built silently here. + +## PR2 (the cut) and its controls + +1. `data x: T = { f: v }` lowers, resolves and elaborates to a node whose `content_hash` equals that + of `data x: T = T { f: v }`. Content equality with the headed form. The authored-tag token occurrence + is the one expected difference. `v2.std.node` `content_hash` folds only kind, edge labels and + children, so it should fall outside the hash. PR2 confirms this by execution. If it does not, the + control compares the occurrence-erased projection, and says so. +2. A tag-elided literal with no expected type (a `let` without an annotation) refuses + `infer_anonymous_record_no_expected_type`, located at its `{`. +3. The expected type is a two-constructor coproduct: refuses `..._expected_type_not_record`. +4. Fields that do not match the chosen record refuse at the field, through the existing construct + typing, and not as "no constructor". +5. **Mutation: choose by field names.** Two records share the literal's field set. The expected + type names the one declared SECOND. The control asserts the declared type's path is chosen. A + field-name chooser picks the first or refuses as ambiguous, so it goes red either way. +6. Nesting: `host_transport`'s form, and a two-level `compile_stage_memo` form, each equal to their + hand-headed form. +7. An elided construct cannot pass the post-infer well_formed gate or the target_model gate + (a discriminating pair with the headed form). +8. Evidence: the #12550 base-vs-head native census shows each Form B file of the table admitted, + with every other outcome change explained. Seed claims show no true->false. From ab2151388a675309099d399ed9fcb5202f9d5943 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 22:46:47 +0000 Subject: [PATCH 04/63] PR1: map arm refuses located at the same check site, declared population (parent ruling) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index a6f74cbba6d..2f215f646b6 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -141,14 +141,31 @@ against it. | dag/extdeps/realization/compile_stage_memo | `data f: CacheInterfaceCatalogFacts = { key_derivation: { .. }, .. }` | yes: data initializer plus record field | | dag/extdeps/realization/parse_table_memo | same shape | yes | | src/v2/std/host_transport | `RuntimePrimitive { value: { .. } }` | yes: record field under an authored head | -| dag/std/types | `data s: Map = { "String": true, .. }` | **no.** This is a MAP literal: string keys, and the expected type is `Map`. It needs a modeled map construction, which the XL-2 row already names as an open design question. | +| dag/std/types | `data s: Map = { "String": true, .. }` | **no: MAP arm.** Reaches the check site and refuses located `map_literal_construction_not_modeled`, as a declared population (below). | | dag/std/algebra | not located by reading | to be located by the base census | -So Form B is necessary for the seven but not sufficient: `std/types` needs the map-literal -construction. The expected-type check site modeled here is the same one a map literal elaborates at -(the expected type `Map` chooses map construction; string keys are then the checked -consequence). The map case is therefore a second arm at the same site, not a second site. It is -raised with the parent as scope, not built silently here. +**The map arm (ruling, gentle-koi-724).** This lane builds the ONE declared-type check site and its +RECORD arm. At that same site, a headless brace whose expected head is `Map` takes the MAP arm, which +refuses located `map_literal_construction_not_modeled`, with its ownership row. It is not left as +`unsupported_form`, and there is no second check site later: map construction plugs into this arm. + +For lowering to reach the site, a string-keyed item needs a field-edge form. It lowers to the same +tag-elided construct, with each item carried as a `map_literal_entry_marker` edge to a +`(key, value)` pair. Lowering still does not pick map or record; a construct that mixes name keys +and string keys refuses at lowering, located at the first key of the other kind. + +The refusing population is DECLARED, as a `gunbc.rung_drop` row. Population: `dag/std/types` plus +every other map-literal file the base census locates, named. Restoration trigger: map construction +is modeled, so a `Map` expected type elaborates the literal to a value that inhabits it, linked +to the open question on the XL-2 row +(`unrecognized_primary_expression_lowers_to_its_first_atom_at_v2_body_lowering`). + +If `dag/std/types` staying refused still blocks the native seven transitively, the map arm is on the +critical path; the parent dispatches it. + +**The seed.** The seed choosing by field names is not an oracle. If it chooses WRONGLY for a real +file, meaning the chosen struct differs from the declared type, that is a silent seed defect, and PR2 +files a `gunbc.recurring_failure_mode` receipt for it. ## PR2 (the cut) and its controls From 8cb389401b9310f3dc63f0510869d1312b016f9b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 23:15:59 +0000 Subject: [PATCH 05/63] Construct tag as a declaration reference: qualified construction, patterns and nullary values lower through one route The construct tag edge carries construct_tag_marker and targets the authored qualified-name spine (a bare tag is its one-segment case); resolve binds it through the existing doors (qualified door for 2+ segments, bare door for 1) and refuses a non-constructor answer. Every reader migrates in one motion; no Symbol tag remains. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ality_after_resolve_rewrote_the_target.dag | 2 + docs/plans/construct-tag-reference-design.md | 13 + src/v2/compiler/03_resolve.dag | 117 +++++++- src/v2/compiler/body_lowering_fold.dag | 263 +++++++++++------ src/v2/extdeps/languages/dag.dag | 5 +- src/v2/lens/machine_shape.dag | 20 +- src/v2/std/compilers/target_model.dag | 115 ++++---- src/v2/std/node.dag | 51 +++- src/v2/std/node_query.dag | 99 ++++--- .../qualified_construct_test.dag | 266 ++++++++++++++++++ .../parse/variant_field_lowering_test.dag | 9 +- src/v2/workflow/floor_pure_producer_share.dag | 7 + 12 files changed, 759 insertions(+), 208 deletions(-) create mode 100644 src/v2/test/claim/body_lowering/qualified_construct_test.dag diff --git a/dag/gunbc/recurring_failure_mode/construct_tag_read_by_label_equality_after_resolve_rewrote_the_target.dag b/dag/gunbc/recurring_failure_mode/construct_tag_read_by_label_equality_after_resolve_rewrote_the_target.dag index 9191b34886a..31fba323857 100644 --- a/dag/gunbc/recurring_failure_mode/construct_tag_read_by_label_equality_after_resolve_rewrote_the_target.dag +++ b/dag/gunbc/recurring_failure_mode/construct_tag_read_by_label_equality_after_resolve_rewrote_the_target.dag @@ -19,5 +19,7 @@ data construct_tag_read_by_label_equality_after_resolve_rewrote_the_target: Recu DeclarationRef { module_path: "v2.std.node_query", decl_name: "construct_tag_optional", field: WholeDeclaration }, DeclarationRef { module_path: "v2.std.node", decl_name: "arrow_body_record_construct_conforms", field: WholeDeclaration }, DeclarationRef { module_path: "v2.std.compilers.target_model", decl_name: "target_value_expr_record_construct_gate", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.body_lowering.qualified_construct", decl_name: "qc_a_resolved_user_construct_reads_its_tag_holds", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.body_lowering.qualified_construct", decl_name: "qc_the_emission_gate_admits_a_resolved_construct_and_refuses_an_unresolved_one_holds", field: WholeDeclaration }, ], } diff --git a/docs/plans/construct-tag-reference-design.md b/docs/plans/construct-tag-reference-design.md index 869f8cbafd0..46b436823a1 100644 --- a/docs/plans/construct-tag-reference-design.md +++ b/docs/plans/construct-tag-reference-design.md @@ -110,6 +110,19 @@ Not readers of this shape: `v2.std.data_initializer_identity`, `v2.std.decl_fact v1 `record_construction_census` fixtures all read the v1 reflection edge. The orchestration `construct_tag:` fields are an unrelated name clash. +## One decision for a dotted path (ruled by gentle-koi-724, 2026-09-29) + +Whether a dotted path is a qualified name or a field projection on a local is decided in ONE place: +`v2.compiler.resolve` `try_resolve_qualified_name_node`, by `qualified_head_bound_on_chain`, which is +the scope of the first segment. eager-newt-412's lane owns the projection arm there. This lane only +consumes that door. A construct tag whose resolved answer is not a constructor declaration (or a +kernel canonical atom) refuses `resolve_reason_construct_tag_not_a_constructor`. The door's projection arm is gunbc#12506's +`resolve_bound_head_projection`. The LOCAL wins: a bound first segment shadows a whole-path +declaration, so the bound-head check precedes any declaration lookup, and that change is made inside +#12506. Whichever of #12506 and this lane's PR2 lands second adds the control. It is one module with +`artifact.tree` (a match binder), `v2.std.diagnostic.Rejected { .. }` (qualified), and a local +shadowing a module segment. + ## The live defect on main Filed as `gunbc.recurring_failure_mode` diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 664dc19f11e..93ae1e70957 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -58,6 +58,7 @@ import v2.std.diagnostic { diagnostics_merge, diagnostics_fatal_reason, diagnostics_singleton, + bind_outcome, rejected_with_pending } import v2.std.qualified_name { @@ -66,7 +67,8 @@ import v2.std.qualified_name { qualified_name_init, qualified_name_last_segment, qualified_name_snoc, - declaration_reference_node + declaration_reference_node, + declaration_reference_path_optional } import v2.std.resolution_policy { ImportScoped, @@ -120,6 +122,8 @@ import v2.std.node { FirstPositionalFound, edge_label_of, node_with_occurrence_id, + construct_tag_marker, + symbol_eq, well_formed, } 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 } @@ -1584,14 +1588,104 @@ fn resolve_pattern_node_walk(ctx: ResolveContext, pat: Node) -> ResolveNodeWalk } else { match construct_tag_optional(n: pat) { Absent => resolve_node_walk(ctx: ctx, n: pat) - Present { value: _ } => - child_walk_node(n: pat, w: fold(pat.children, init: child_walk_init(), f: fn(acc, e) { - child_walk_step(w: acc, e: e, r: resolve_pattern_node_walk(ctx: ctx, pat: e.target)) - })) + Present { value: reference } => + resolve_construct_walk(ctx: ctx, n: pat, reference: reference, field: fn(e) { + resolve_pattern_node_walk(ctx: ctx, pat: e.target) + }) } } } +// A CONSTRUCT RESOLVES ITS TAG AS A REFERENCE AND ITS FIELDS AS THEIR ROLE DEMANDS. The tag edge +// (v2.std.node construct_tag_marker) targets the constructor's authored path; it is resolved here, +// once, for a construction and a pattern alike, and the field edges are walked by the caller's +// own rule (a value field as an ordinary child, a pattern field as a sub-pattern). The marker label +// is a role, not a declaration segment, so it never extends the lookup position. +fn resolve_construct_walk( + ctx: ResolveContext, + n: Node, + reference: Node, + field: fn(Edge) -> ResolveNodeWalk +) -> ResolveNodeWalk { + child_walk_node(n: n, w: fold(n.children, init: child_walk_init(), f: fn(acc, e) { + match e.label { + Named { name: label } => + if symbol_eq(a: label, b: construct_tag_marker()) { + child_walk_step(w: acc, e: e, r: resolve_construct_tag_walk(ctx: ctx, reference: reference)) + } else { + child_walk_step(w: acc, e: e, r: field(e)) + } + Positional => child_walk_step(w: acc, e: e, r: field(e)) + } + })) +} + +// ONE ROUTE FOR THE TAG, THROUGH THE DOORS EVERY OTHER REFERENCE USES. The authored path is read +// whole (v2.std.qualified_name qualified_name_from_node) and dispatched by its length only: two or +// more segments go to the qualified door (try_resolve_qualified_name_node), one segment to the bare +// door (resolve_atom on that segment, with the spine root's occurrence, which is the tag's locus). +// Both mint through resolved_reference_node, so a qualified and an unqualified constructor naming +// one declaration resolve to one node. The path is never trimmed: `m.R` is looked up as `m.R`. +// +// WHAT IT RESOLVES TO MUST BE A CONSTRUCTOR. A declaration reference whose path is not a declared +// payload -- a coproduct TYPE rather than one of its variants, a function, a data row -- refuses +// resolve_reason_construct_tag_not_a_constructor, located at the tag. The variant is its own +// declaration (symbol_index_declared_payload_at answers VariantArmPayload for the variant, not for +// its coproduct), so the check is by identity, never by spelling. A tag bound to a kernel canonical +// symbol carries no declaration path and is accepted as that symbol, as a bare atom always was. Any +// other answer -- a field projection, if the qualified door reads the first segment as a local value +// (whether a dotted path is a name or a projection is decided in that door, by the first segment's +// scope, and nowhere here) -- is not a constructor and refuses the same way. +fn resolve_construct_tag_walk(ctx: ResolveContext, reference: Node) -> ResolveNodeWalk { + match qualified_name_from_node(root: reference) { + Rejected { diagnostics: r } => resolve_walk_of_outcome(o: Rejected { diagnostics: r }) + Accepted { value: path, diagnostics: _ } => + resolve_walk_of_outcome(o: bind_outcome( + o: resolve_construct_tag_reference(ctx: ctx, reference: reference, path: path), + f: fn(resolved) { resolve_construct_tag_names_constructor(ctx: ctx, reference: reference, resolved: resolved) } + )) + } +} + +fn resolve_construct_tag_reference(ctx: ResolveContext, reference: Node, path: QualifiedName) -> Outcome { + match path { + Cons { head: only, tail: Empty } => + 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 + Absent => Rejected { diagnostics: diagnostics_singleton(d: unbound_symbol_diagnostic(n: reference)) } + } + Empty => Rejected { diagnostics: diagnostics_singleton(d: unbound_symbol_diagnostic(n: reference)) } + } +} + +fn resolve_construct_tag_names_constructor(ctx: ResolveContext, reference: Node, resolved: Node) -> Outcome { + match declaration_reference_path_optional(node: resolved) { + Absent => + match resolved.kind { + TypeNode { connective: Atom { identity: _ } } => Accepted { value: resolved, diagnostics: None } + _ => resolve_construct_tag_not_a_constructor(reference: reference) + } + Present { value: declared } => + if resolve_path_is_declared_payload(ctx: ctx, path: declared) { + Accepted { value: resolved, diagnostics: None } + } else { + resolve_construct_tag_not_a_constructor(reference: reference) + } + } +} + +fn resolve_construct_tag_not_a_constructor(reference: Node) -> Outcome { + Rejected { + diagnostics: diagnostics_singleton(d: Diagnostic { + reason: ^resolve_reason_construct_tag_not_a_constructor, + at: node_locus(node: reference), + correction: Unavailable { reason: UserInputBoundary } + }) + } +} + 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) @@ -1662,9 +1756,16 @@ fn resolve_node_walk( ComputationNode { behavior: Transform } => resolve_transform_children(ctx: ctx, n: n) TypeNode { connective: Conj } => - match try_resolve_qualified_name_node(ctx: ctx, n: n) { - Present { value: outcome } => resolve_walk_of_outcome(o: outcome) - Absent => resolve_children_homogeneous_scope(ctx: ctx, n: n) + match construct_tag_optional(n: n) { + Present { value: reference } => + resolve_construct_walk(ctx: ctx, n: n, reference: reference, field: fn(e) { + resolve_child_edge(ctx: ctx, e: e) + }) + Absent => + match try_resolve_qualified_name_node(ctx: ctx, n: n) { + Present { value: outcome } => resolve_walk_of_outcome(o: outcome) + Absent => resolve_children_homogeneous_scope(ctx: ctx, n: n) + } } TypeNode { connective: _ } => resolve_children_homogeneous_scope(ctx: ctx, n: n) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 171ad929185..433adc262ae 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -4633,8 +4633,8 @@ fn body_lower_postfix_projection_step(repeat_elem: Node) -> Optional { // gunbc.recurring_failure_mode call_expression_erased_at_v2_body_lowering read off the same walk). // A call on a NON-final segment (`a.f().b`) is named by the walk and DECLINED: it is live corpus // idiom the existing arms still erase (the receipt is on the class row; see DottedChainTailRead). -// A brace suffix (`a.b.C { f: v }`) and an `as` suffix answer Absent and the existing arms run -// unchanged for them. +// A brace record suffix (`a.b.C { f: v }`) is a construction, lowered by the chain fold's +// PostfixRecord step, and an `as` suffix is a cast step; neither is read by this head reader. fn body_lower_dotted_head_atom_optional(node: Node) -> Optional { match node_atom_identity_optional(node: node) { Present { value: sym } => @@ -4670,7 +4670,7 @@ fn body_lower_dotted_head_atom_optional(node: Node) -> Optional { // ONE WALK OVER THE CHAIN'S TAIL, READ BY EVERY DOTTED READER. Every repeat element must be a // dot-projection step; its suffix slot is either EMPTY (a bare segment) or a PAREN CALL (an -// argument row). Any other suffix kind -- a brace suffix `a.b.C { f: v }`, an `as` suffix -- is +// argument row). A brace record suffix is the PostfixRecord step, and any other suffix kind is // not this shape and the walk declines so the existing arms run unchanged. // // WHAT CHANGED HERE AND WHY (gunbc#12033's PR2, DESIGN 6b). The walk used to admit a call ONLY on @@ -4698,16 +4698,14 @@ fn body_lower_dotted_head_atom_optional(node: Node) -> Optional { type PostfixStep = PostfixProjection { segment: Node, call_elem: Optional } | PostfixCast { target: Node } + | PostfixRecord { segment: Node, items: List } // THE THIRD ARM CARRIES THE SUFFIX KINDS THIS LOWERING CANNOT READ. A dot-projection step whose -// suffix slot is neither empty nor a paren call -- today the BRACE suffix, the module-qualified -// record construction `a.b.C { f: v }`, and an `as` suffix -- reaches PostfixChainSuffixCarried. -// That shape is NOT lowered by this change: a construct's tag is a Symbol (v2.std.node_query -// construct_node) and a module-qualified tag has no representation in it, so lowering it would -// mean changing the construct shape, which is a wider package than this one. What the door DOES -// with this arm -- decline to the pre-existing arms and emit a counted head-grain advisory rather -// than refuse -- is stated once, on body_lower_postfix_chain_steps_grow below, and is not -// restated here. +// suffix slot is neither empty, a paren call, nor a brace record body reaches PostfixChainSuffixCarried. +// The brace record body is NOT in that arm any more: `a.b.C { f: v }` is a PostfixRecord step, and +// the chain fold builds the construct whose tag is the whole path (v2.std.node_query construct_node), +// the same builder `C { f: v }` reaches from primary_expr -- one production for both heads. What the +// door does with a carried suffix is stated once, on body_lower_postfix_chain_steps_grow below. // // WHETHER EVERY STEP IS BARE IS DECIDED ONCE, BY THE READ (review 70331). The read already knows // each step's suffix kind as it meets it, so it carries that fact in its accumulator @@ -4721,26 +4719,14 @@ type PostfixChainRead | PostfixChainSuffixCarried { at: Node } // AN UNLOWERED SUFFIX KIND DECLINES, IT DOES NOT REFUSE, AND THE PRECEDENT IS #11683's. -// Refusing a step suffix this lowering cannot read is the DESIGN section 5 reading, and it was -// MEASURED against the workload: it turned 32 modules that ACCEPT at base into refusals -- 28 -// directly under the refusal, and 4 more as match_arm_navigation_refused, where the rejection -// propagated out of a match scrutinee and surfaced under a reason this change never touched. -// #11683 faced exactly this choice for the call-before-end class and declined for the same -// reason. So the brace suffix (`a.b.C { f: v }`, the module-qualified record construction) and -// the `as` suffix decline to the pre-existing arms and behave exactly as they do on base. Their -// erasure REMAINS and this package does not claim otherwise: the receipt is on -// gunbc.recurring_failure_mode call_expression_erased_at_v2_body_lowering, and lowering them -// needs a construct tag able to carry a qualified name, which is a wider package than this one. -// -// BUT THE DECLINE IS COUNTED, NOT SILENT, AND THAT IS WHAT KEEPS IT ON THE LADDER. Review 70078 -// on gunbc#12057 found the gap this closes: on base these shapes reached a LOUD located refusal -// (postfix_field_access_refused, deleted here), so declining them silently would have moved the -// class from mitigatable to BELOW the ladder -- DESIGN section 4b's silent wrongness, which is -// forbidden outright rather than droppable. 8 of the 42 base-refusing files carry a brace suffix, -// so the population is real and not hypothetical. The door therefore declines to the existing -// arms AND emits body_lowering_reason_postfix_suffix_carried at HEAD grain, the same shape -// #12033 used for value_carried_unlowered: the file still Accepts, so no module that accepts on -// base regresses, and the residue is located and countable instead of vanishing. +// Refusing a step suffix this lowering cannot read was MEASURED to turn 32 modules that ACCEPT on base +// into refusals, so a carried suffix declines to the pre-existing arms and is COUNTED, not silent: +// the door emits body_lowering_reason_postfix_suffix_carried at HEAD grain (review 70078 on +// gunbc#12057), and the drop from the base refusal is declared as gunbc.rung_drop +// postfix_suffix_step_declined_to_the_pre_existing_arms. The brace record body left this population +// when the construct tag became a reference that can carry a qualified path +// (docs/plans/construct-tag-reference-design.md); what remains is re-derived by the workload +// adjudicate, which is that drop's trigger. fn body_lower_postfix_chain_steps_grow( repeat_capture: Node, grown: PostfixChainAcc @@ -4792,7 +4778,12 @@ fn body_lower_postfix_chain_acc_call_step( // A cast closes a bare spine the way a call does: what follows it is a value, never a path segment. fn body_lower_postfix_chain_acc_cast_step(acc: PostfixChainAcc, target: Node) -> PostfixChainAcc { - let step = PostfixCast { target: target } + body_lower_postfix_chain_acc_closing_step(acc: acc, step: PostfixCast { target: target }) +} + +// A record body closes the spine too: `a.b.C { f: v }` is a construct VALUE whose tag is the whole +// path, and any step after it eliminates that value. +fn body_lower_postfix_chain_acc_closing_step(acc: PostfixChainAcc, step: PostfixStep) -> PostfixChainAcc { match acc { PostfixChainAccBare { segments: segments } => PostfixChainAccCalled { @@ -4847,6 +4838,14 @@ fn body_lower_postfix_chain_steps_read( call_elem: repeat_elem ) ) + PostfixSuffixRecord { items: items } => + body_lower_postfix_chain_steps_grow( + repeat_capture: repeat_capture, + grown: body_lower_postfix_chain_acc_closing_step( + acc: acc, + step: PostfixRecord { segment: segment, items: items } + ) + ) } } } @@ -4977,6 +4976,27 @@ fn body_lower_postfix_chain_fold_step( diagnostics: ad ) } + PostfixRecord { segment: step_segment, items: items } => + match state { + PostfixAccumSpine { segments: segments } => + match body_lower_record_construct( + reference: body_lower_qualified_name_spine_node( + segments: list_snoc_item(xs: segments, item: step_segment), + source: source + ), + items: items, + source: source + ) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: construct, diagnostics: cd } => + outcome_with_diagnostics( + value: PostfixAccumValue { node: construct }, + diagnostics: diagnostics_merge(outer: ad, inner: cd) + ) + } + PostfixAccumValue { node: _ } => + outcome_rejected(d: body_lower_diagnostic(reason: ^body_lowering_reason_unsupported_form, n: step_segment)) + } PostfixProjection { segment: step_segment, call_elem: step_call_elem } => match step_call_elem { Absent => @@ -5152,6 +5172,7 @@ fn body_lower_postfix_chain_head_atom_for_steps(head_node: Node, steps: List optional_absent() PostfixProjection { segment: _, call_elem: _ } => body_lower_dotted_head_atom_optional(node: head_node) + PostfixRecord { segment: _, items: _ } => body_lower_dotted_head_atom_optional(node: head_node) } Empty => body_lower_dotted_head_atom_optional(node: head_node) } @@ -5183,6 +5204,7 @@ fn body_lower_postfix_steps_carry_cast(steps: List) -> Bool { acc || match step { PostfixCast { target: _ } => true PostfixProjection { segment: _, call_elem: _ } => false + PostfixRecord { segment: _, items: _ } => false } }) } @@ -5314,6 +5336,7 @@ type PostfixSuffixRead = PostfixSuffixNone | PostfixSuffixEmpty | PostfixSuffixCall + | PostfixSuffixRecord { items: List } | PostfixSuffixUnlowered // A PAREN CALL IS DECIDED BY THE ONE READER OF A CALL SUFFIX (#12108's comma-list route): @@ -5329,7 +5352,11 @@ fn body_lower_postfix_suffix_read(repeat_elem: Node) -> PostfixSuffixRead { } else { match body_lower_postfix_call_suffix_args(call_suffix: suffix) { Present { value: _ } => PostfixSuffixCall - Absent => PostfixSuffixUnlowered + Absent => + match body_lower_record_brace_items_optional(brace: suffix) { + Present { value: items } => PostfixSuffixRecord { items: items } + Absent => PostfixSuffixUnlowered + } } } } @@ -5589,6 +5616,51 @@ fn body_lower_pattern_field_edges(items: List, braced: Bool, witness: Node } } +// A CONSTRUCTOR PATTERN'S HEAD IS ITS WHOLE qualified_name, the same reference a construction carries: +// every authored segment, lowered from its own token, as the spine. The first-atom reader that stood +// here answered `v2` for `v2.std.diagnostic.Rejected { .. }`, so a qualified pattern silently took +// its first segment as its constructor. A head with no qualified_name is not a constructor pattern +// and refuses located rather than falling back to an atom search. +fn body_lower_pattern_head_segments_optional(head: Node) -> Optional> { + match body_lower_find_captured(root: head, emitted: ^dag_surface_qualified_name) { + ParseSubtreeAbsent => optional_absent() + ParseSubtreeFound { captured: qn } => + match parse_qualified_name_segment_atoms_from_capture(qn_capture: qn, root: head) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: segments, diagnostics: _ } => + match segments { + Empty => optional_absent() + Cons { head: _, tail: _ } => optional_present(value: segments) + } + } + } +} + +fn body_lower_pattern_head_reference_optional(head: Node) -> Optional { + match body_lower_pattern_head_segments_optional(head: head) { + Absent => optional_absent() + Present { value: segments } => + optional_present(value: body_lower_qualified_name_spine_node(segments: segments, source: head)) + } +} + +// A NULLARY PATTERN WITH A QUALIFIED HEAD IS A REFERENCE, NOT A BINDER. `v2.std.diagnostic.None =>` +// names a declaration by its path; only a one-segment head can be a binder, and whether it is one is +// the resolver's to decide, so a one-segment head answers Absent here and stays the atom it always +// was. The first-atom reader answered `v2` for the qualified form -- a binder named `v2` that matched +// everything. +fn body_lower_pattern_qualified_reference_optional(head: Node) -> Optional { + match body_lower_pattern_head_segments_optional(head: head) { + Absent => optional_absent() + Present { value: segments } => + if length(xs: segments) > 1 { + optional_present(value: body_lower_qualified_name_spine_node(segments: segments, source: head)) + } else { + optional_absent() + } + } +} + fn body_lower_pattern_suffix_lowered(head: Node, suffix: Node, pattern_capture: Node) -> Outcome { match sugar_sequence_pair_optional(node: suffix) { Absent => body_lower_pattern_reject(pattern_capture: pattern_capture) @@ -5603,12 +5675,9 @@ fn body_lower_pattern_suffix_lowered(head: Node, suffix: Node, pattern_capture: match body_lower_comma_list_items_optional(list_capture: inner.left) { Absent => body_lower_pattern_reject(pattern_capture: pattern_capture) Present { value: items } => - match body_lower_pattern_leaf_atom_optional(node: head) { + match body_lower_pattern_head_reference_optional(head: head) { Absent => body_lower_pattern_reject(pattern_capture: pattern_capture) - Present { value: tag_atom } => - match node_atom_identity_optional(node: tag_atom) { - Absent => body_lower_pattern_reject(pattern_capture: pattern_capture) - Present { value: tag } => + Present { value: reference } => bind_outcome( o: body_lower_pattern_field_edges( items: items, @@ -5617,11 +5686,10 @@ fn body_lower_pattern_suffix_lowered(head: Node, suffix: Node, pattern_capture: ), f: fn(edges) { outcome_accepted( - value: construct_node(tag: tag, tag_source: tag_atom, field_edges: edges, source: pattern_capture) + value: construct_node(reference: reference, field_edges: edges, source: pattern_capture) ) } ) - } } } } @@ -5649,9 +5717,13 @@ fn body_lower_pattern_lowered(pattern_capture: Node) -> Outcome { match sugar_sequence_pair_optional(node: captured) { Present { value: pair } => if is_empty_conj_root(n: body_lower_deep_unwrap_optional(node: pair.right)) { - match body_lower_pattern_atom_optional(pattern_capture: pair.left) { - Present { value: atom } => outcome_accepted(value: body_lower_pattern_atom_form(atom: atom)) - Absent => body_lower_pattern_reject(pattern_capture: pattern_capture) + match body_lower_pattern_qualified_reference_optional(head: pair.left) { + Present { value: reference } => outcome_accepted(value: reference) + Absent => + match body_lower_pattern_atom_optional(pattern_capture: pair.left) { + Present { value: atom } => outcome_accepted(value: body_lower_pattern_atom_form(atom: atom)) + Absent => body_lower_pattern_reject(pattern_capture: pattern_capture) + } } } else { body_lower_pattern_suffix_lowered( @@ -7627,8 +7699,8 @@ fn body_lower_postfix_expr(shell: Node) -> Outcome { // around its already-lowered value, is seq(name, seq(`:`, value)). The result is // v2.std.node_query construct_node -- Conj { R: Atom(R), f: e, .. } -- the RecordConstructBody // shape v2.std.node admits and the shape a fielded pattern lowers to, so `Rec { n: 1 }` and -// `Rec { n: x } =>` are one shape read in two directions. A dotted head (`m.R { .. }`) is not a -// bare name and falls through to the arms below unchanged. +// `Rec { n: x } =>` are one shape read in two directions. A dotted head (`m.R { .. }`) is a postfix +// chain and reaches the same builder, body_lower_record_construct, through its PostfixRecord step. fn body_lower_field_init_edge(item: Node) -> Outcome { let stripped = body_lower_deep_unwrap_optional(node: item) let captured = match parse_production_captured_child_optional(node: stripped) { @@ -7660,38 +7732,76 @@ fn body_lower_field_init_edge(item: Node) -> Outcome { } } -fn body_lower_record_literal_field_items_optional(captured: Node) -> Optional> { - match sugar_sequence_pair_optional(node: captured) { +// The field items of a brace record body, `{ field_init, .. }`. One reader for both heads: a bare +// name's brace suffix (primary_expr) and a dotted chain's final brace suffix +// (dag_grammar_postfix_dot_suffix_expr takes the same ident suffix), so the qualified and the +// unqualified construct read their fields through one production. +fn body_lower_record_brace_items_optional(brace: Node) -> Optional> { + match sugar_sequence_pair_optional(node: brace) { Absent => Absent - Present { value: pair } => - match sugar_sequence_pair_optional(node: pair.right) { + Present { value: brace_seq } => + match node_atom_identity_optional(node: brace_seq.left) { Absent => Absent - Present { value: brace_seq } => - match node_atom_identity_optional(node: brace_seq.left) { - Absent => Absent - Present { value: open } => - if open == ^dag_token_lbrace { - match sugar_sequence_pair_optional(node: brace_seq.right) { + Present { value: open } => + if open == ^dag_token_lbrace { + match sugar_sequence_pair_optional(node: brace_seq.right) { + Absent => Absent + Present { value: inner } => + match node_atom_identity_optional(node: inner.right) { Absent => Absent - Present { value: inner } => - match node_atom_identity_optional(node: inner.right) { - Absent => Absent - Present { value: close } => - if close == ^dag_token_rbrace { - body_lower_comma_list_items_optional(list_capture: inner.left) - } else { - Absent - } + Present { value: close } => + if close == ^dag_token_rbrace { + body_lower_comma_list_items_optional(list_capture: inner.left) + } else { + Absent } } - } else { - Absent - } + } + } else { + Absent } } } } +fn body_lower_record_literal_field_items_optional(captured: Node) -> Optional> { + match sugar_sequence_pair_optional(node: captured) { + Absent => Absent + Present { value: pair } => body_lower_record_brace_items_optional(brace: pair.right) + } +} + +// THE ONE CONSTRUCT BUILDER. Every record construction reaches it with its constructor REFERENCE +// already built from the authored segments -- a one-segment spine for `R { .. }`, the whole path for +// `a.b.R { .. }` -- so the tag is never trimmed to its last segment and never resolved here; +// v2.compiler.resolve binds it by identity. +fn body_lower_record_construct(reference: Node, items: List, source: Node) -> Outcome { + match fold(items, init: outcome_accepted(value: Empty), f: fn(acc, item) { + bind_outcome(o: acc, f: fn(edges) { + bind_outcome(o: body_lower_field_init_edge(item: item), f: fn(e) { + outcome_accepted(value: list_snoc_item(xs: edges, item: e)) + }) + }) + }) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: edges, diagnostics: d } => + Accepted { + value: construct_node(reference: reference, field_edges: edges, source: source), + diagnostics: d + } + } +} + +// A bare constructor name is the one-segment case of the reference: the segment atom is lowered from +// the tag token, and the spine root carries that token's occurrence, which is the locus a resolved +// reference keeps. +fn body_lower_bare_tag_reference(tag: Symbol, at: Node) -> Node { + body_lower_qualified_name_spine_node( + segments: [node_lowered_from(kind: TypeNode { connective: Atom { identity: tag } }, children: [], source: at)], + source: at + ) +} + // The tag is read with the atom it was read from, so the construct's tag atom can be lowered from // that token rather than from the whole capture (v2.std.node_query construct_tag_edge). type RecordLiteralTag { @@ -7721,19 +7831,14 @@ fn body_lower_try_record_literal(captured: Node) -> Outcome> { match body_lower_record_literal_field_items_optional(captured: captured) { Absent => outcome_accepted(value: optional_absent()) Present { value: items } => - match fold(items, init: outcome_accepted(value: Empty), f: fn(acc, item) { - bind_outcome(o: acc, f: fn(edges) { - bind_outcome(o: body_lower_field_init_edge(item: item), f: fn(e) { - outcome_accepted(value: list_snoc_item(xs: edges, item: e)) - }) - }) - }) { + match body_lower_record_construct( + reference: body_lower_bare_tag_reference(tag: read.tag, at: read.at), + items: items, + source: captured + ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } - Accepted { value: edges, diagnostics: d } => - Accepted { - value: optional_present(value: construct_node(tag: read.tag, tag_source: read.at, field_edges: edges, source: captured)), - diagnostics: d - } + Accepted { value: construct, diagnostics: d } => + Accepted { value: optional_present(value: construct), diagnostics: d } } } } diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 8f7f75cac6d..c7eac2c371c 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -95,6 +95,7 @@ import v2.std.compilers.lexing { } import v2.std.node { symbol_lexeme, + construct_tag_marker, Arrow, Atom, Bind, @@ -4304,7 +4305,7 @@ fn dag_rec_construct_body_node() -> Node { kind: TypeNode { connective: Conj }, children: [ dag_named_edge( - name: ^dag_binding_type_rec, + name: construct_tag_marker(), target: dag_type_atom_node(identity: ^dag_binding_type_rec) ), dag_named_edge( @@ -4333,7 +4334,7 @@ fn dag_variant_holds_construct_node(value_lit: Symbol) -> Node { kind: TypeNode { connective: Conj }, children: [ dag_named_edge( - name: ^dag_binding_type_variant_holds, + name: construct_tag_marker(), target: dag_type_atom_node(identity: ^dag_binding_type_variant_holds) ), dag_named_edge( diff --git a/src/v2/lens/machine_shape.dag b/src/v2/lens/machine_shape.dag index f9fc76a0767..f190b27d040 100644 --- a/src/v2/lens/machine_shape.dag +++ b/src/v2/lens/machine_shape.dag @@ -33,7 +33,7 @@ import v2.std.node { Symbol, fold_node_topdown } -import v2.std.node_query { construct_tag_optional } +import v2.std.node_query { construct_tag_path_optional } import v2.std.compilers.body_lowering { ApplicationHeadFound, ApplicationHeadRefused, @@ -43,7 +43,7 @@ import v2.std.compilers.body_lowering { application_head_read } import std.algebra { Cons, Empty } -import v2.std.qualified_name { QualifiedName, qualified_name_to_dotted_string } +import v2.std.qualified_name { QualifiedName, qualified_name_last_segment, qualified_name_to_dotted_string } import v2.std.witness { Holds, Witness } // Single-constructor discipline (machine-shape-orthogonal-scheduling.md §2 ruling 9 / operator @@ -54,8 +54,8 @@ import v2.std.witness { Holds, Witness } // symbol_index_fill's fill-only discipline (SymbolIndex materializes only through // symbol_index_fill_* / empty_symbol_index, never a parallel index walk). Recognition: // module-context fold_node_topdown over BOTH lowered spellings of a construction: a record literal -// `MachineShape { .. }` lowers to a construct (a Conj whose first edge names the tag, -// v2.std.node_query construct_tag_optional), and a call-spelled `MachineShape(..)` to an +// `MachineShape { .. }` lowers to a construct (a Conj whose marked first edge targets the tag's path, +// v2.std.node_query construct_tag_path_optional), and a call-spelled `MachineShape(..)` to an // application whose head is the atom. Until MQ-2 (gunbc#12202) only the call spelling was read, so // a real record literal passed unseen. @@ -123,9 +123,17 @@ type ConstructedTag | Constructs { tag: Symbol } | ConstructionUnreadable { diagnostics: NonEmptyDiagnostics } +// A construct's tag is its authored path here: this lens reads the lowered tree before resolution, +// so it holds the author's spelling, never a declaration identity, at the same grain as the call +// spelling below. The whole path is read, so `std.machine_shape.MachineShape { .. }` is the +// construction of MachineShape as much as the bare spelling is. fn constructed_tag(n: Node) -> ConstructedTag { - match construct_tag_optional(n: n) { - Present { value: tag } => Constructs { tag: tag } + match construct_tag_path_optional(n: n) { + Present { value: path } => + match qualified_name_last_segment(qn: path) { + Present { value: tag } => Constructs { tag: tag } + Absent => NoConstruction + } Absent => match application_head_read(site: n) { NotApplicationHead => NoConstruction diff --git a/src/v2/std/compilers/target_model.dag b/src/v2/std/compilers/target_model.dag index 36e0d48098d..20f479bf910 100644 --- a/src/v2/std/compilers/target_model.dag +++ b/src/v2/std/compilers/target_model.dag @@ -159,13 +159,16 @@ import v2.std.node_query { node_positional_child_targets, nullary_inhabitant_by_discriminant, is_wildcard_pattern_identity, - pattern_wildcard_name + pattern_wildcard_name, + construct_field_edges, + construct_tag_optional } import v2.std.qualified_name { QualifiedName, qualified_name_from_node, qualified_name_init, - qualified_name_last_segment + qualified_name_last_segment, + declaration_reference_path_optional } import v2.std.logic { Bool @@ -6774,22 +6777,6 @@ fn target_value_expr_record_construct_field_slots_from_wire( } } -fn target_value_expr_labeled_edge_head(labeled: List) -> Optional { - fold(labeled, init: optional_absent(), f: fn(acc, edge) { - match acc { - Present { value: _ } => acc - Absent => optional_present(value: edge) - } - }) -} - -fn target_value_expr_labeled_edges_tail(labeled: List) -> List { - match list_tail(xs: labeled) { - TailFound { tail: rest } => rest - TailAbsent => Empty - } -} - fn target_value_expr_record_construct_field_tokens( slots: List, projection: TargetValueExpressionProjection, @@ -14580,24 +14567,37 @@ fn target_value_expr_project_atom_literal_or( } } -fn target_value_expr_record_construct_gate(node: Node) -> Bool { - match node.kind { - TypeNode { connective: Conj } => - if !all_edges_named(children: node.children) { - false - } else { - match target_value_expr_labeled_edge_head( - labeled: node_labeled_child_edges(node: node) - ) { - Present { value: first_edge } => - match first_edge.target.kind { - TypeNode { connective: Atom { identity: _ } } => true - _ => false - } - Absent => false - } +// A CONSTRUCT REACHES EMISSION RESOLVED. Its tag (v2.std.node_query construct_tag_optional) must be a +// resolved reference -- a declaration reference, whose path names the constructor declaration, or a +// kernel canonical atom -- and the emitted type name is that declaration's own name, spelled by the +// target's binding rows downstream. An authored path that never went through resolution is not an +// identity and refuses here, as does any Conj that is not a construct. Before the tag was a marked +// reference this gate read the first labelled edge's target as an Atom, which a resolved user +// construct never has (gunbc.recurring_failure_mode +// construct_tag_read_by_label_equality_after_resolve_rewrote_the_target), so it refused every one. +fn target_value_expr_record_construct_type_name_optional(node: Node) -> Optional { + match construct_tag_optional(n: node) { + Absent => optional_absent() + Present { value: reference } => + match declaration_reference_path_optional(node: reference) { + Present { value: path } => + match qualified_name_last_segment(qn: path) { + Present { value: name } => optional_present(value: name) + Absent => optional_absent() + } + Absent => + match reference.kind { + TypeNode { connective: Atom { identity: kernel } } => optional_present(value: kernel) + _ => optional_absent() + } } - _ => false + } +} + +fn target_value_expr_record_construct_gate(node: Node) -> Bool { + match target_value_expr_record_construct_type_name_optional(node: node) { + Present { value: _ } => all_edges_named(children: node.children) + Absent => false } } @@ -14668,36 +14668,25 @@ fn target_project_record_construct( ) ) } else { - let labeled = node_labeled_child_edges(node: body) - match target_value_expr_labeled_edge_head(labeled: labeled) { - Present { value: type_edge } => - match type_edge.target.kind { - TypeNode { connective: Atom { identity: type_name } } => - bind_outcome( - o: target_value_expr_record_construct_field_slots_from_edges( - arrow: arrow, - edges: target_value_expr_labeled_edges_tail(labeled: labeled), - target: target, - source_bool_form: source_bool_form, - handle_transform: handle_transform - ), - f: fn(fields) { - outcome_accepted( - target_value_expression_record_construct( - type_name: type_name, - fields: fields - ) - ) - } - ) - _ => - outcome_rejected( - target_value_expr_diagnostic( - reason: ^target_value_expr_reason_transform_shape_invalid, - node: body + match target_value_expr_record_construct_type_name_optional(node: body) { + Present { value: type_name } => + bind_outcome( + o: target_value_expr_record_construct_field_slots_from_edges( + arrow: arrow, + edges: construct_field_edges(n: body), + target: target, + source_bool_form: source_bool_form, + handle_transform: handle_transform + ), + f: fn(fields) { + outcome_accepted( + target_value_expression_record_construct( + type_name: type_name, + fields: fields ) ) - } + } + ) Absent => outcome_rejected( target_value_expr_diagnostic( diff --git a/src/v2/std/node.dag b/src/v2/std/node.dag index f52e6523ab7..cd0e6876208 100644 --- a/src/v2/std/node.dag +++ b/src/v2/std/node.dag @@ -702,6 +702,24 @@ fn declaration_reference_marker() -> Symbol { declaration_reference_marker_symbol } +// A CONSTRUCT'S TAG IS MARKED BY ITS ROLE, NOT RECOGNISED BY SPELLING. A record construct and a +// fielded constructor pattern are one Conj whose first edge carries this marker and targets the +// constructor as an ordinary REFERENCE: the authored qualified-name spine before resolution (a bare +// `R` is its one-segment case) and v2.std.qualified_name declaration_reference_node after it. The +// label used to be the constructor's own spelling, read back by comparing it with the target Atom's +// identity; resolution rewrites that target into a declaration reference, so the comparison failed +// for every resolved user construct (gunbc.recurring_failure_mode +// construct_tag_read_by_label_equality_after_resolve_rewrote_the_target), and a module-qualified +// constructor had no representation at all. It is distinct from the declaration-reference marker, +// so a nullary `R {}` never reads as a reference. It is a Core structural marker of the typed +// EdgeLabel coproduct planned in gunbc#12473 (arm ConstructTag), declared here in the form of its +// sibling until that cut converts both. +data construct_tag_marker_symbol: Symbol = symbol_intern_lexeme(lexeme: "") + +fn construct_tag_marker() -> Symbol { + construct_tag_marker_symbol +} + // A RESOLVED DECLARATION REFERENCE IS NOT A RECORD CONSTRUCT. `data d: D = A` lowers to a nullary // Arrow whose body is the initializer, and when the initializer is a reference that body is the // marked Conj. Before this arm the classifier read every Conj body as a record construct, whose @@ -751,26 +769,43 @@ fn arrow_body_admits_eval_entry(form: ArrowBodyForm) -> Bool { } } +// A Conj body that is not a marked declaration reference is one of two things, and this admits both. +// +// A CONSTRUCT: the tag edge is FIRST and carries v2.std.node construct_tag_marker, and its target is a +// reference, which is a Conj in both of its forms (an authored spine, a marked declaration +// reference); every edge is Named. Which Conj is a well-formed reference is v2.std.qualified_name's to +// read (v2.std.node_query construct_tag_path_optional), not repeated here. +// +// AN AUTHORED PATH BEFORE RESOLUTION: `fn f() -> T { m.C }` has the qualified-name spine as its body +// until resolve replaces it with the marked reference. Its first edge is Named and targets the head +// segment's Atom. This module cannot name the spine's labels -- v2.std.algebra fold_list_node owns +// them and imports this module -- so the admission is the one this function made before the tag +// became a marker, stated as what it is rather than left to accident. A typed arm for an unresolved +// reference body would let the classifier say so; it is a follow-up named in +// docs/plans/construct-tag-reference-design.md, not a change this package makes. fn arrow_body_record_construct_conforms(node: Node) -> Bool { match node.kind { TypeNode { connective: Conj } => if !all_edges_named(children: node.children) { false } else { - fold(node.children, init: false, f: fn(seen_first_named, edge) { - if seen_first_named { - seen_first_named + fold(node.children, init: 0, f: fn(seen, edge) { + if seen != 0 { + seen } else { match edge.label { - Named { name: _ } => + Named { name: label } => match edge.target.kind { - TypeNode { connective: Atom { identity: _ } } => true - _ => false + TypeNode { connective: Conj } => + if symbol_eq(a: label, b: construct_tag_marker()) { 1 } else { 2 } + TypeNode { connective: Atom { identity: _ } } => + if symbol_eq(a: label, b: construct_tag_marker()) { 2 } else { 1 } + _ => 2 } - Positional => false + Positional => 2 } } - }) + }) == 1 } _ => false } diff --git a/src/v2/std/node_query.dag b/src/v2/std/node_query.dag index 183730996c0..d2cf0728a86 100644 --- a/src/v2/std/node_query.dag +++ b/src/v2/std/node_query.dag @@ -42,8 +42,11 @@ import v2.std.node { named_edge_sort_key, node_lowered_from, node_with_occurrence_id, + construct_tag_marker, + symbol_eq, symbol_intern_lexeme, } +import v2.std.qualified_name { QualifiedName, declaration_reference_path_optional, qualified_name_from_node, qualified_name_spine_shape_present } import std.constructors { Cardinality, Required, Optional } import v2.std.optional { Optional, optional_absent, optional_present } @@ -286,62 +289,82 @@ fn declared_field_named(payload: Node, name: Symbol) -> Optional } // A CONSTRUCT is the value-side twin of a payload: the same field edges, each targeting the -// value supplied for that field, behind one leading TAG edge whose label and target both name -// the constructor (`Rec { n: 1 }` is Conj { Rec: Atom(Rec), n: 1 }). It is the shape -// v2.std.node RecordConstructBody admits (arrow_body_record_construct_conforms: every edge Named, -// the first one an Atom) and the shape the language authority's fixtures build -// (v2.extdeps.languages.dag dag_rec_construct_body_node, dag_variant_holds_construct_node). A -// match-arm PATTERN over a fielded variant is the same construct whose field targets are the -// binders, so construction and elimination read one shape. -// THE TAG ATOM STANDS FOR THE TAG TOKEN, so it is lowered from that token, not from the whole -// construct: `tag_source` is the authored terminal the constructor name was read from, and the -// Conj keeps the construct's own occurrence. Lowering the atom from the enclosing capture gave it -// the shell's occurrence, so the tag's locus was a coarser, different span -// (docs/plans/lowering-occurrence-projection-design.md, rank 3). -fn construct_tag_edge(tag: Symbol, tag_source: Node) -> Edge { - Edge { - label: Named { name: tag }, - target: node_lowered_from( - kind: TypeNode { connective: Atom { identity: tag } }, - children: [], - source: tag_source - ) - } -} - -fn construct_node(tag: Symbol, tag_source: Node, field_edges: List, source: Node) -> Node { +// value supplied for that field, behind one leading TAG edge. The tag edge is labelled by its ROLE, +// v2.std.node construct_tag_marker, and targets the constructor as a REFERENCE: the authored +// qualified-name spine before resolution (a bare `Rec` is its one-segment case, a module-qualified +// `m.Rec` its longer one) and v2.std.qualified_name declaration_reference_node after it. So +// `Rec { n: 1 }` is Conj { : spine[Rec], n: 1 }. It is the shape v2.std.node +// RecordConstructBody admits (arrow_body_record_construct_conforms). A match-arm PATTERN over a +// fielded variant is the same construct whose field targets are the binders, so construction and +// elimination read one shape. +// +// THE REFERENCE IS BUILT BY THE CALLER FROM THE AUTHORED SEGMENTS, never trimmed and never resolved +// here: lowering hands in the whole path (v2.std.qualified_name qualified_name_spine_of_atoms over +// atoms lowered from their own tokens), and resolution binds it by identity through the same doors +// every other reference uses. Before this, the label WAS the constructor's spelling and the target an +// Atom of the same identity; resolution rewrote the target, so the label-equals-identity read failed +// for every resolved user construct (gunbc.recurring_failure_mode +// construct_tag_read_by_label_equality_after_resolve_rewrote_the_target), and a qualified head had +// no representation. +fn construct_tag_edge(reference: Node) -> Edge { + Edge { label: Named { name: construct_tag_marker() }, target: reference } +} + +fn construct_node(reference: Node, field_edges: List, source: Node) -> Node { node_with_occurrence_id( kind: TypeNode { connective: Conj }, children: list_append( - left: [construct_tag_edge(tag: tag, tag_source: tag_source)], + left: [construct_tag_edge(reference: reference)], right: field_edges ), occurrence_id: source.occurrence_id ) } -// The tag of a construct: its first edge, when that edge names the atom it targets. Anything -// else -- a payload Conj, a domain Conj, a bare record with no tag -- has no constructor tag. -fn construct_tag_optional(n: Node) -> Optional { +// The tag of a construct: the reference its marked first edge targets. Anything else -- a payload +// Conj, a domain Conj, a declaration reference, a field projection, a bare record -- has none. The +// answer is the reference NODE, at whichever stage the tree is: a spine before resolution, a +// declaration reference after it. +fn construct_tag_optional(n: Node) -> Optional { match n.kind { TypeNode { connective: Conj } => match first(n.children) { Present { value: e } => match e.label { Named { name: label } => - match e.target.kind { - TypeNode { connective: Atom { identity: id } } => - match id == label { - true => Present { value: label } - false => Absent - } - _ => Absent + if symbol_eq(a: label, b: construct_tag_marker()) { + optional_present(value: e.target) + } else { + optional_absent() } - Positional => Absent + Positional => optional_absent() + } + Absent => optional_absent() + } + _ => optional_absent() + } +} + +// The constructor's PATH, read from the tag at either stage: the resolved declaration's path after +// resolution, the authored path before it. Absent when the node is not a construct or its tag is +// not a well-formed reference. A pre-resolution path is what the author wrote, not a declaration +// identity; only a resolved tree's answer names the declaration. +fn construct_tag_path_optional(n: Node) -> Optional { + match construct_tag_optional(n: n) { + Absent => optional_absent() + Present { value: reference } => + match declaration_reference_path_optional(node: reference) { + Present { value: path } => optional_present(value: path) + Absent => + if qualified_name_spine_shape_present(root: reference) { + match qualified_name_from_node(root: reference) { + Accepted { value: path, diagnostics: _ } => optional_present(value: path) + Rejected { diagnostics: _ } => optional_absent() + } + } else { + optional_absent() } - Absent => Absent } - _ => Absent } } diff --git a/src/v2/test/claim/body_lowering/qualified_construct_test.dag b/src/v2/test/claim/body_lowering/qualified_construct_test.dag new file mode 100644 index 00000000000..c9050c24272 --- /dev/null +++ b/src/v2/test/claim/body_lowering/qualified_construct_test.dag @@ -0,0 +1,266 @@ +module v2.test.claim.body_lowering.qualified_construct + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.name_resolve { + Admission, + ResolutionSubject, + resolution_context, + resolve_with_admission_context_policy +} +import v2.std.cross_tree.resolution { source_root_index_empty } +import v2.std.resolution_policy { default_name_resolution_policy } +import v2.compiler.normalized_tree { NormalizedTree, normalized_tree_roots_to_nodes } +import v2.compiler.program_assembly { module_roots_from_source_root_ingest } +import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } +import v2.extdeps.languages.dag { dag_language_model } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.cross_tree.import_model { DagTree } +import v2.std.compilers.target_model { target_value_expr_record_construct_gate } +import v2.std.diagnostic { Accepted, Outcome, Rejected, diagnostics_fatal_reason } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import v2.std.optional { Absent, Present } +import v2.std.node { Hash, Node, Symbol, content_hash, node_subtree_nodes } +import v2.std.node_query { construct_tag_optional, construct_tag_path_optional } +import v2.std.qualified_name { declaration_reference_path_optional, qualified_name_from_dotted_string, qualified_name_to_dotted_string } +import v2.std.algebra { fold_list, length } +import v2.std.text { String } +import v2.std.collection { List } +import v2.std.integer { Int } +import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } +import extdeps.communication.medium { Lossless, Medium } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// A QUALIFIED CONSTRUCTOR IS THE SAME CONSTRUCT AS ITS UNQUALIFIED SPELLING, BY IDENTITY. +// +// The subject is the construct's tag (v2.std.node construct_tag_marker): a record construction, a +// fielded constructor pattern and a nullary constructor value each carry the constructor as a +// REFERENCE -- the authored path before resolution, a declaration reference after it -- so +// `v2.test.qcp.QcRow { n: true }` and an imported `QcRow { n: true }` resolve to one node, and the +// path is never trimmed to its last segment (docs/plans/construct-tag-reference-design.md). +// +// THE SOURCES ARE SUPPLIED AND THE ROUTE IS REAL: every claim below reaches the production fold +// (tokenize, parse, normalize with body lowering, and resolve through +// v2.compiler.name_resolve resolve_with_admission_context_policy) from inline source text. Each +// consumer's resolution is a nullary warm producer, enrolled in v2.workflow.floor_pure_producer_share +// beside the ingest, for the cost reason v2.test.claim.namespace_xl0.cross_module_reference_resolution +// records. +// +// THE RED ON BASE. qc_a_resolved_user_construct_reads_its_tag_holds is the control for +// gunbc.recurring_failure_mode construct_tag_read_by_label_equality_after_resolve_rewrote_the_target: +// on the base this lane started from, construct_tag_optional answered Absent for a resolved user +// construct (measured 2026-09-29, the resolved PcrA specimen in that row), so this claim was red there. +data qc_provider_source: String = "module v2.test.qcp\n\nimport v2.std.logic { Bool }\n\ntype QcShape\n = QcRow { n: Bool }\n | QcNone\n" + +data qc_provider_b_source: String = "module v2.test.qcp_b\n\nimport v2.std.logic { Bool }\n\ntype QcOther\n = QcRow { n: Bool }\n | QcElse\n" + +data qc_bare_source: String = "module v2.test.qc_bare\n\nimport v2.std.logic { Bool }\nimport v2.test.qcp { QcShape, QcRow, QcNone }\n\nfn qc_make() -> QcShape { QcRow { n: true } }\n\nfn qc_empty() -> QcShape { QcNone }\n\nfn qc_read(s: QcShape) -> Bool {\n match s {\n QcRow { n: x } => x\n QcNone => false\n }\n}\n" + +data qc_qualified_source: String = "module v2.test.qc_qualified\n\nimport v2.std.logic { Bool }\nimport v2.test.qcp { QcShape }\n\nfn qc_make() -> QcShape { v2.test.qcp.QcRow { n: true } }\n\nfn qc_empty() -> QcShape { v2.test.qcp.QcNone }\n\nfn qc_read(s: QcShape) -> Bool {\n match s {\n v2.test.qcp.QcRow { n: x } => x\n v2.test.qcp.QcNone => false\n }\n}\n" + +data qc_twins_source: String = "module v2.test.qc_twins\n\nimport v2.std.logic { Bool }\nimport v2.test.qcp { QcShape }\nimport v2.test.qcp_b { QcOther }\n\nfn qc_one() -> QcShape { v2.test.qcp.QcRow { n: true } }\n\nfn qc_two() -> QcOther { v2.test.qcp_b.QcRow { n: false } }\n" + +data qc_type_tag_source: String = "module v2.test.qc_type_tag\n\nimport v2.std.logic { Bool }\nimport v2.test.qcp { QcShape }\n\nfn qc_wrong() -> QcShape { v2.test.qcp.QcShape { n: true } }\n" + +data qc_unqualified_unimported_source: String = "module v2.test.qc_unimported\n\nimport v2.std.logic { Bool }\nimport v2.test.qcp { QcShape }\n\nfn qc_make() -> QcShape { QcRow { n: true } }\n" + +data qc_bodyless_source: String = "module v2.test.qc_bodyless\n\nimport v2.std.logic { Bool }\nimport v2.test.qcp { QcShape }\n\nfn qc_make() -> QcShape { v2.test.qcp.QcRow }\n" + +// The seven's own form, verbatim in its position: the then-arm of v2.compiler.tokenize +// lex_walk_artifact constructs v2.std.diagnostic.Rejected with a record body. This module is +// lowered, not resolved -- v2.std.diagnostic is not in this ingest -- so it establishes that the form +// reaches the construct production with its whole authored path. +data qc_seven_form_source: String = "module v2.test.qc_seven_form\n\nfn qc_refuse() -> v2.std.diagnostic.Outcome {\n v2.std.diagnostic.Rejected { diagnostics: v2.std.diagnostic.None }\n}\n" + +fn qc_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: source, fidelity: Lossless }, + artifact: Artifact { kind: SourceFile, id: id, file_path: path }, + compilation_unit: unit, + source_root: DagTree + } +} + +data qc_ingest: SourceRootIngest = [ + qc_read(source: qc_provider_source, id: ^qc_provider_read, unit: ^qc_provider_cu, path: "src/v2/test/fixture/qualified_construct/provider.dag"), + qc_read(source: qc_provider_b_source, id: ^qc_provider_b_read, unit: ^qc_provider_b_cu, path: "src/v2/test/fixture/qualified_construct/provider_b.dag"), + qc_read(source: qc_bare_source, id: ^qc_bare_read, unit: ^qc_bare_cu, path: "src/v2/test/fixture/qualified_construct/bare.dag"), + qc_read(source: qc_qualified_source, id: ^qc_qualified_read, unit: ^qc_qualified_cu, path: "src/v2/test/fixture/qualified_construct/qualified.dag"), + qc_read(source: qc_twins_source, id: ^qc_twins_read, unit: ^qc_twins_cu, path: "src/v2/test/fixture/qualified_construct/twins.dag"), + qc_read(source: qc_type_tag_source, id: ^qc_type_tag_read, unit: ^qc_type_tag_cu, path: "src/v2/test/fixture/qualified_construct/type_tag.dag"), + qc_read(source: qc_unqualified_unimported_source, id: ^qc_unimported_read, unit: ^qc_unimported_cu, path: "src/v2/test/fixture/qualified_construct/unimported.dag"), + qc_read(source: qc_bodyless_source, id: ^qc_bodyless_read, unit: ^qc_bodyless_cu, path: "src/v2/test/fixture/qualified_construct/bodyless.dag"), + qc_read(source: qc_seven_form_source, id: ^qc_seven_form_read, unit: ^qc_seven_form_cu, path: "src/v2/test/fixture/qualified_construct/seven_form.dag") +] + +fn qc_fixture_normalized() -> Outcome> { + module_roots_from_source_root_ingest(ingest: qc_ingest, lm: dag_language_model()) +} + +fn qc_resolve_subject(subject: String) -> Outcome { + match qc_fixture_normalized() { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: roots, diagnostics: _ } => + resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { + subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: subject) }, + imports: Empty + }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) + } +} + +fn qc_resolved_bare() -> Outcome { qc_resolve_subject(subject: "v2.test.qc_bare") } +fn qc_resolved_qualified() -> Outcome { qc_resolve_subject(subject: "v2.test.qc_qualified") } +fn qc_resolved_twins() -> Outcome { qc_resolve_subject(subject: "v2.test.qc_twins") } +fn qc_resolved_type_tag() -> Outcome { qc_resolve_subject(subject: "v2.test.qc_type_tag") } +fn qc_resolved_unimported() -> Outcome { qc_resolve_subject(subject: "v2.test.qc_unimported") } +fn qc_resolved_bodyless() -> Outcome { qc_resolve_subject(subject: "v2.test.qc_bodyless") } + +fn qc_constructs(root: Node) -> List { + fold(node_subtree_nodes(root: root), init: Empty, f: fn(acc, n) { + match construct_tag_optional(n: n) { + Present { value: _ } => list_snoc_item(xs: acc, item: n) + Absent => acc + } + }) +} + +fn qc_construct_hashes(o: Outcome) -> List { + match o { + Rejected { diagnostics: _ } => Empty + Accepted { value: t, diagnostics: _ } => + fold(qc_constructs(root: t.root), init: Empty, f: fn(acc, n) { list_snoc_item(xs: acc, item: content_hash(n: n)) }) + } +} + +fn qc_construct_paths(o: Outcome) -> List { + match o { + Rejected { diagnostics: _ } => Empty + Accepted { value: t, diagnostics: _ } => + fold(qc_constructs(root: t.root), init: Empty, f: fn(acc, n) { + match construct_tag_path_optional(n: n) { + Present { value: path } => list_snoc_item(xs: acc, item: qualified_name_to_dotted_string(qn: path)) + Absent => list_snoc_item(xs: acc, item: "") + } + }) + } +} + +fn qc_reference_count(o: Outcome, dotted: String) -> Int { + match o { + Rejected { diagnostics: _ } => 0 - 1 + Accepted { value: t, diagnostics: _ } => + fold(node_subtree_nodes(root: t.root), init: 0, f: fn(acc, n) { + match declaration_reference_path_optional(node: n) { + Present { value: path } => if qualified_name_to_dotted_string(qn: path) == dotted { acc + 1 } else { acc } + Absent => acc + } + }) + } +} + +fn qc_pairwise_equal(xs: List, ys: List) -> Bool { + match xs { + Empty => match ys { Empty => true Cons { head: _, tail: _ } => false } + Cons { head: x, tail: xt } => + match ys { + Empty => false + Cons { head: y, tail: yt } => (x == y) && qc_pairwise_equal(xs: xt, ys: yt) + } + } +} + +// (a) and (c): the construction and the fielded pattern, spelled qualified, resolve to the same +// constructs as their imported bare spelling -- two constructs each, equal by content. +test fn qc_qualified_constructs_equal_their_unqualified_form_holds() -> Bool { + let bare = qc_construct_hashes(o: qc_resolved_bare()) + (length(xs: bare) == 2) && qc_pairwise_equal(xs: bare, ys: qc_construct_hashes(o: qc_resolved_qualified())) +} + +// The tag binds the VARIANT's own declaration, never its coproduct, in both spellings. +test fn qc_a_resolved_user_construct_reads_its_tag_holds() -> Bool { + qc_construct_paths(o: qc_resolved_qualified()) == ["v2.test.qcp.QcRow", "v2.test.qcp.QcRow"] + && qc_construct_paths(o: qc_resolved_bare()) == ["v2.test.qcp.QcRow", "v2.test.qcp.QcRow"] +} + +// (b): the nullary value and the nullary pattern, spelled qualified, are references to the variant, +// as the bare spelling is. The first-atom pattern reader answered `v2` for the qualified pattern. +test fn qc_qualified_nullary_binds_the_variant_holds() -> Bool { + (qc_reference_count(o: qc_resolved_qualified(), dotted: "v2.test.qcp.QcNone") == 2) + && (qc_reference_count(o: qc_resolved_bare(), dotted: "v2.test.qcp.QcNone") == 2) +} + +// Identity, not spelling: two variants with one leaf in two modules bind two declarations. +test fn qc_same_leaf_in_two_modules_binds_two_declarations_holds() -> Bool { + qc_construct_paths(o: qc_resolved_twins()) == ["v2.test.qcp.QcRow", "v2.test.qcp_b.QcRow"] +} + +// A tag that resolves to the coproduct TYPE rather than a variant refuses, located, by identity. +test fn qc_a_type_tag_refuses_as_not_a_constructor_holds() -> Bool { + match qc_resolved_type_tag() { + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^resolve_reason_construct_tag_not_a_constructor + Accepted { value: _, diagnostics: _ } => false + } +} + +// MUTATION, THE QUALIFIER DROPPED: without it and without an import the constructor is unbound -- +// the path is what bound it, not a corpus search for its leaf. +test fn qc_dropping_the_qualifier_unbinds_the_constructor_holds() -> Bool { + match qc_resolved_unimported() { + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^resolve_reason_unbound_symbol + Accepted { value: _, diagnostics: _ } => false + } +} + +// MUTATION, THE RECORD BODY DROPPED: the same path with no body is a reference to the variant and +// no construct at all. +test fn qc_dropping_the_record_body_is_not_a_construct_holds() -> Bool { + (length(xs: qc_construct_hashes(o: qc_resolved_bodyless())) == 0) + && (qc_reference_count(o: qc_resolved_bodyless(), dotted: "v2.test.qcp.QcRow") == 1) +} + +fn qc_normalized_constructs_of(module_dotted: String) -> List { + match qc_fixture_normalized() { + Rejected { diagnostics: _ } => Empty + Accepted { value: roots, diagnostics: _ } => + fold(normalized_tree_roots_to_nodes(roots: roots), init: Empty, f: fn(acc, root) { + fold(qc_constructs(root: root), init: acc, f: fn(inner, n) { + match construct_tag_path_optional(n: n) { + Present { value: path } => + if qualified_name_to_dotted_string(qn: path) == module_dotted { list_snoc_item(xs: inner, item: n) } else { inner } + Absent => inner + } + }) + }) + } +} + +// THE SEVEN'S FORM REACHES THE CONSTRUCT PRODUCTION WITH ITS WHOLE PATH (the file refusal that held +// v2.compiler.tokenize out of the index was this form declining to a fatal unsupported_form). +test fn qc_the_sevens_form_lowers_to_a_construct_holds() -> Bool { + length(xs: qc_normalized_constructs_of(module_dotted: "v2.std.diagnostic.Rejected")) == 1 +} + +// THE EMISSION GATE, BOTH WAYS: a resolved construct is admitted, and the same construct before +// resolution -- an authored path, not an identity -- is refused. +test fn qc_the_emission_gate_admits_a_resolved_construct_and_refuses_an_unresolved_one_holds() -> Bool { + let resolved_admitted = match qc_resolved_qualified() { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + match qc_constructs(root: t.root) { + Cons { head: c, tail: _ } => target_value_expr_record_construct_gate(node: c) + Empty => false + } + } + let unresolved_refused = match qc_normalized_constructs_of(module_dotted: "v2.test.qcp.QcRow") { + Cons { head: c, tail: _ } => !target_value_expr_record_construct_gate(node: c) + Empty => false + } + resolved_admitted && unresolved_refused +} diff --git a/src/v2/test/claim/parse/variant_field_lowering_test.dag b/src/v2/test/claim/parse/variant_field_lowering_test.dag index f630173603a..a17367452c0 100644 --- a/src/v2/test/claim/parse/variant_field_lowering_test.dag +++ b/src/v2/test/claim/parse/variant_field_lowering_test.dag @@ -33,7 +33,7 @@ import std.constructors { Cardinality, Required, Optional } import v2.std.node_query { DeclaredField, construct_field_edges, - construct_tag_optional, + construct_tag_path_optional, declared_field_list, declared_field_named, find_arrow_body_child, @@ -44,6 +44,7 @@ import v2.std.node_query { } import v2.std.optional { Absent, Optional, Present } import v2.std.collection { List, list_at_optional } +import v2.std.qualified_name { qualified_name_to_dotted_string } import v2.test.parse.supplied_token_stream_support { supplied_stream_matches_tokenize, supplied_stream_projection } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -327,12 +328,12 @@ test fn vf_positional_construction_carries_its_operand() -> Bool { } } -// A record construction is the construct shape: tag edge R -> Atom(R), then field v -> x. +// A record construction is the construct shape: the marked tag edge -> the authored path R, then field v -> x. test fn vf_record_construction_is_the_construct_shape() -> Bool { match vf_fn_body_of(o: vfs_normalized_5(), fn_lexeme: "mk") { Absent => false Present { value: body } => - (match construct_tag_optional(n: body) { Present { value: tag } => symbol_lexeme(sym: tag) == "R" Absent => false }) + (match construct_tag_path_optional(n: body) { Present { value: path } => qualified_name_to_dotted_string(qn: path) == "R" Absent => false }) && (count(construct_field_edges(n: body)) == 1) && (match list_at_optional(xs: construct_field_edges(n: body), index: 0) { Present { value: e } => @@ -370,7 +371,7 @@ fn vf_first_arm_pattern_from_tree(nt: NormalizedTree, fn_lexeme: String) -> Opti } fn vf_pattern_binds(pat: Node, tag: String, field: String, binder: String) -> Bool { - (match construct_tag_optional(n: pat) { Present { value: t } => symbol_lexeme(sym: t) == tag Absent => false }) + (match construct_tag_path_optional(n: pat) { Present { value: path } => qualified_name_to_dotted_string(qn: path) == tag Absent => false }) && (count(construct_field_edges(n: pat)) == 1) && (match list_at_optional(xs: construct_field_edges(n: pat), index: 0) { Present { value: e } => diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 21fb5ac4787..0b1cf3c070c 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -1023,6 +1023,13 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.body_lowering_block_body_call.body_lowering_block_call_subject", "v2.test.claim.machine_shape_construction_wall.machine_shape_record_literal_subject", "v2.test.claim.machine_shape_construction_wall.machine_shape_record_literal_home_subject", + "v2.test.claim.body_lowering.qualified_construct.qc_fixture_normalized", + "v2.test.claim.body_lowering.qualified_construct.qc_resolved_bare", + "v2.test.claim.body_lowering.qualified_construct.qc_resolved_qualified", + "v2.test.claim.body_lowering.qualified_construct.qc_resolved_twins", + "v2.test.claim.body_lowering.qualified_construct.qc_resolved_type_tag", + "v2.test.claim.body_lowering.qualified_construct.qc_resolved_unimported", + "v2.test.claim.body_lowering.qualified_construct.qc_resolved_bodyless", "v2.test.claim.namespace_xl0.cross_module_reference_resolution.xl0r_fixture_normalized", "v2.test.claim.namespace_xl0.cross_module_reference_resolution.xl0r_resolved_local_consumer", "v2.test.claim.namespace_xl0.cross_module_reference_resolution.xl0r_resolved_declared_consumer", From a24b3c4b537f10f04d548cbb79836183d3bc9e38 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 23:22:43 +0000 Subject: [PATCH 06/63] PR1: enumerate the enrolled map-literal refusal control as a consumer Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index 2f215f646b6..bfa1729bb13 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -126,7 +126,8 @@ the elided case adds. | v2.std.inhabitance | `DeclaredTypePosition` | vocabulary | Add `PositionDataInitializer` and `PositionRecordField`. `PositionDeclaredReturn` gains its constructor. | | v2.compiler.infer | new `infer_elaborate_expected_construct` | elaborator | Steps 1-4 above, called from the data-initializer, declared-return and record-field check sites. It is the only writer that replaces the elision marker. | | v2.compiler.infer | Conj gather arms | reader | A tag-elided construct reached here refuses `infer_anonymous_record_no_expected_type` (it is never typed as a bare product). | -| v2.workflow.compile_door_cause_ownership | three rows | ownership | One row per refusal cause. | +| v2.workflow.compile_door_cause_ownership | three rows, plus `map_literal_construction_not_modeled` | ownership | One row per refusal cause. | +| v2.test.claim.namespace_xl0.reference_conservation_accepted_drops | `a_map_literal_value_refuses_at_the_literal_holds` | control | Today it asserts `unsupported_form` for `Map = { "k": .. }`. It moves to `map_literal_construction_not_modeled` at the check site. It must read the INFER outcome, because the literal now clears lowering and the `.normalized` read would go green-then-false. It stays enrolled as the map arm's refusal control. | | v2.compiler.eval, v2.std.compilers.target_model, emit | construct gates | reader | No change. They already refuse a non-reference tag after #12701. PR2 adds a control proving an elided construct cannot reach them. | ## Scope: which of the seven Form B admits From bc43a418b44929332e6245cbfc021f0c2fcce407 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 01:03:52 +0000 Subject: [PATCH 07/63] Keep the unmarked Conj arrow-body admission unchanged; construct marker is the only new case Narrowing it to the first edge refused field-projection bodies (v2.std.diagnostic diagnostics_fatal_reason) and with them every importer of diagnostic on the native route. The qualified_construct fixture now carries a field-projection body so the shared ingest reds if it narrows again. Also: a construct tag answered by anything but a constructor declaration or a kernel atom refuses; plan records the one-door ruling. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/std/node.dag | 73 +++++++++++-------- .../qualified_construct_test.dag | 6 +- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/v2/std/node.dag b/src/v2/std/node.dag index cd0e6876208..29ad6784e73 100644 --- a/src/v2/std/node.dag +++ b/src/v2/std/node.dag @@ -769,48 +769,63 @@ fn arrow_body_admits_eval_entry(form: ArrowBodyForm) -> Bool { } } -// A Conj body that is not a marked declaration reference is one of two things, and this admits both. +// A Conj body that is not a marked declaration reference is admitted on one of two readings. // // A CONSTRUCT: the tag edge is FIRST and carries v2.std.node construct_tag_marker, and its target is a // reference, which is a Conj in both of its forms (an authored spine, a marked declaration // reference); every edge is Named. Which Conj is a well-formed reference is v2.std.qualified_name's to // read (v2.std.node_query construct_tag_path_optional), not repeated here. // -// AN AUTHORED PATH BEFORE RESOLUTION: `fn f() -> T { m.C }` has the qualified-name spine as its body -// until resolve replaces it with the marked reference. Its first edge is Named and targets the head -// segment's Atom. This module cannot name the spine's labels -- v2.std.algebra fold_list_node owns -// them and imports this module -- so the admission is the one this function made before the tag -// became a marker, stated as what it is rather than left to accident. A typed arm for an unresolved -// reference body would let the classifier say so; it is a follow-up named in -// docs/plans/construct-tag-reference-design.md, not a change this package makes. +// ANY OTHER UNMARKED Conj is admitted exactly as it was before the tag became a marker: every edge +// Named and SOME Named edge targeting an Atom. That population is not constructs -- it is an authored +// path before resolution (`fn f() -> T { m.C }`, the spine whose head targets an Atom) and a field +// projection body (`fn f() -> Symbol { g(x).reason }`, v2.std.node_query field_projection_node, +// whose field edge targets an Atom). This module cannot name either shape's labels -- v2.std.algebra +// and v2.std.node_query own them and import this module -- so the admission is kept unchanged rather +// than narrowed; narrowing it refused v2.std.diagnostic diagnostics_fatal_reason, measured on the +// native route. A typed arm per body form would let the classifier say which one it holds; it is a +// follow-up named in docs/plans/construct-tag-reference-design.md, not a change this package makes. fn arrow_body_record_construct_conforms(node: Node) -> Bool { match node.kind { TypeNode { connective: Conj } => - if !all_edges_named(children: node.children) { - false - } else { - fold(node.children, init: 0, f: fn(seen, edge) { - if seen != 0 { - seen - } else { - match edge.label { - Named { name: label } => - match edge.target.kind { - TypeNode { connective: Conj } => - if symbol_eq(a: label, b: construct_tag_marker()) { 1 } else { 2 } - TypeNode { connective: Atom { identity: _ } } => - if symbol_eq(a: label, b: construct_tag_marker()) { 2 } else { 1 } - _ => 2 - } - Positional => 2 - } - } - }) == 1 - } + all_edges_named(children: node.children) + && (arrow_body_construct_tag_leads(node: node) || arrow_body_some_edge_targets_an_atom(node: node)) _ => false } } +fn arrow_body_construct_tag_leads(node: Node) -> Bool { + fold(node.children, init: 0, f: fn(seen, edge) { + if seen != 0 { + seen + } else { + match edge.label { + Named { name: label } => + if symbol_eq(a: label, b: construct_tag_marker()) + && (match edge.target.kind { + TypeNode { connective: Conj } => true + _ => false + }) { 1 } else { 2 } + Positional => 2 + } + } + }) == 1 +} + +fn arrow_body_some_edge_targets_an_atom(node: Node) -> Bool { + fold(node.children, init: false, f: fn(found, edge) { + found || (match edge.label { + Named { name: label } => + !symbol_eq(a: label, b: construct_tag_marker()) + && (match edge.target.kind { + TypeNode { connective: Atom { identity: _ } } => true + _ => false + }) + Positional => false + }) + }) +} + fn arrow_body_target_is_valid_realization(target: Node) -> Bool { match classify_arrow_body_form(target: target) { UnsupportedArrowBody => false diff --git a/src/v2/test/claim/body_lowering/qualified_construct_test.dag b/src/v2/test/claim/body_lowering/qualified_construct_test.dag index c9050c24272..1ee8791af88 100644 --- a/src/v2/test/claim/body_lowering/qualified_construct_test.dag +++ b/src/v2/test/claim/body_lowering/qualified_construct_test.dag @@ -51,7 +51,11 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // gunbc.recurring_failure_mode construct_tag_read_by_label_equality_after_resolve_rewrote_the_target: // on the base this lane started from, construct_tag_optional answered Absent for a resolved user // construct (measured 2026-09-29, the resolved PcrA specimen in that row), so this claim was red there. -data qc_provider_source: String = "module v2.test.qcp\n\nimport v2.std.logic { Bool }\n\ntype QcShape\n = QcRow { n: Bool }\n | QcNone\n" +// The provider also carries a FIELD PROJECTION body (qcp_read). It is not about constructs: it is the +// unmarked Conj body v2.std.node arrow_body_record_construct_conforms must keep admitting beside the +// construct marker. Narrowing that admission refused v2.std.diagnostic on the native route, and every +// claim below shares this ingest, so the narrowing reddens all of them. +data qc_provider_source: String = "module v2.test.qcp\n\nimport v2.std.logic { Bool }\n\ntype QcShape\n = QcRow { n: Bool }\n | QcNone\n\ntype QcBox {\n n: Bool\n}\n\nfn qcp_box() -> QcBox { QcBox { n: true } }\n\nfn qcp_read() -> Bool { qcp_box().n }\n" data qc_provider_b_source: String = "module v2.test.qcp_b\n\nimport v2.std.logic { Bool }\n\ntype QcOther\n = QcRow { n: Bool }\n | QcElse\n" From 6eb56a277191b346795e1f70583e8f89aed4f695 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 01:52:45 +0000 Subject: [PATCH 08/63] PR1: measured refusal sites for the seven; add PositionListElement (std/algebra); host_transport's first blocker is the caret form Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index bfa1729bb13..1c8ca0782a0 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -73,6 +73,11 @@ at a named position. A position is where a declared type meets a produced expres `RuntimePrimitive { value: { .. } }` elaborates the inner literal from `RuntimePrimitive.value`'s declared type, and `compile_stage_memo`'s `key_derivation: { .. }` elaborates from the outer record's field. New arm. +- `PositionListElement`: the expected type of a list literal's element is the element type of the + list's own expected type (`List` gives `T`). `dag/std/algebra` needs this: its 88 + `AlgebraFieldTemplate` rows are headless literals inside list literals. New arm, v1 vocabulary. + A list literal with no expected type passes none down, so its headless elements refuse + `infer_anonymous_record_no_expected_type`. At each such site, before the body is synthesised, a tag-elided construct at the root of `e` is elaborated. Elaboration recurses through field inits under `PositionRecordField`. It is: @@ -123,8 +128,8 @@ the elided case adds. | v2.std.node | `arrow_body_record_construct_conforms`, `classify_arrow_body_form` | well_formed gate | Admits the elided marker ONLY pre-infer. The post-infer well_formed gate refuses it. | | v2.compiler.body_lowering_fold | `body_lower_try_record_literal` | producer | A headless `{ field_init_list }` whose every key is a name lowers to `construct_node(Elided, ..)`. The field inits go through the existing `body_lower_field_init_edge`. | | v2.compiler.resolve | `resolve_node_walk`, `resolve_pattern_node_walk` Conj arm | rewriter | The elided tag edge is passed through untouched (there is nothing to resolve). Field values walk as today. | -| v2.std.inhabitance | `DeclaredTypePosition` | vocabulary | Add `PositionDataInitializer` and `PositionRecordField`. `PositionDeclaredReturn` gains its constructor. | -| v2.compiler.infer | new `infer_elaborate_expected_construct` | elaborator | Steps 1-4 above, called from the data-initializer, declared-return and record-field check sites. It is the only writer that replaces the elision marker. | +| v2.std.inhabitance | `DeclaredTypePosition` | vocabulary | Add `PositionDataInitializer`, `PositionRecordField` and `PositionListElement`. `PositionDeclaredReturn` gains its constructor. | +| v2.compiler.infer | new `infer_elaborate_expected_construct` | elaborator | Steps 1-4 above, called from the data-initializer, declared-return, record-field and list-element check sites. It is the only writer that replaces the elision marker. | | v2.compiler.infer | Conj gather arms | reader | A tag-elided construct reached here refuses `infer_anonymous_record_no_expected_type` (it is never typed as a bare product). | | v2.workflow.compile_door_cause_ownership | three rows, plus `map_literal_construction_not_modeled` | ownership | One row per refusal cause. | | v2.test.claim.namespace_xl0.reference_conservation_accepted_drops | `a_map_literal_value_refuses_at_the_literal_holds` | control | Today it asserts `unsupported_form` for `Map = { "k": .. }`. It moves to `map_literal_construction_not_modeled` at the check site. It must read the INFER outcome, because the literal now clears lowering and the `.normalized` read would go green-then-false. It stays enrolled as the map arm's refusal control. | @@ -132,18 +137,20 @@ the elided case adds. ## Scope: which of the seven Form B admits -Read from source. PR2's base census will locate each refusal, and this table will be corrected -against it. - -| File | Form | Form B admits it? | -|---|---|---| -| dag/std/primitives | `data c: PrimitiveContract = { .. }` | yes: data initializer | -| dag/std/termination | `data d: BoundedLattice = { .. }` (newline-separated) | yes, via generic instantiation. Separator handling to confirm. | -| dag/extdeps/realization/compile_stage_memo | `data f: CacheInterfaceCatalogFacts = { key_derivation: { .. }, .. }` | yes: data initializer plus record field | -| dag/extdeps/realization/parse_table_memo | same shape | yes | -| src/v2/std/host_transport | `RuntimePrimitive { value: { .. } }` | yes: record field under an authored head | -| dag/std/types | `data s: Map = { "String": true, .. }` | **no: MAP arm.** Reaches the check site and refuses located `map_literal_construction_not_modeled`, as a declared population (below). | -| dag/std/algebra | not located by reading | to be located by the base census | +MEASURED, on main 7f144278c57. Each file's source ran through the same parse and normalize route as +`v2.test.claim.namespace_xl0.reference_conservation` `conservation_subject`. The fatal diagnostic was +rendered through the parse's `SpanIndex`. Only the FIRST fatal refusal is reported per file, so a +file can have later blockers beyond its first. + +| File | First fatal refusal (line) | Form | Form B admits it? | +|---|---|---|---| +| dag/std/algebra | 359, `{ name: "filter", .. }` | the body of `fn collection_filter_shape() -> AlgebraFieldTemplate`; 88 more rows of the same record inside list literals | yes: declared return, and list element | +| dag/std/primitives | 17, `data char_at_contract: PrimitiveContract = {` | data initializer | yes | +| dag/std/termination | 45, `data .. : BoundedLattice = {` | data initializer, generic, newline-separated fields | yes, via generic instantiation. The newline separator is to be confirmed in PR2. | +| dag/extdeps/realization/compile_stage_memo | 39, `key_derivation: {` | record field under a headless data initializer (lowering refuses the inner brace first) | yes: data initializer plus record field | +| dag/extdeps/realization/parse_table_memo | 50, `key_derivation: {` | same | yes | +| dag/std/types | 5, `data kernel_type_set: Map = {` | MAP literal | **no: MAP arm.** Reaches the check site and refuses located `map_literal_construction_not_modeled`, as a declared population (below). | +| src/v2/std/host_transport | about 119, `reason: ^emit_host_runtime_row_..` | **caret symbol literal** (`body_lowering_reason_caret_symbol_not_lowered`), not Form B | **not by Form B alone.** Its first blocker is the caret form. Its Form B site (`RuntimePrimitive { value: { .. } }`, record field) is admitted by this lane, but the file stays refused until caret symbols lower. | **The map arm (ruling, gentle-koi-724).** This lane builds the ONE declared-type check site and its RECORD arm. At that same site, a headless brace whose expected head is `Map` takes the MAP arm, which From f27e59af76eddc2455ea075b4a9e7c3cba9f4288 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 01:55:00 +0000 Subject: [PATCH 09/63] PR1 revision: elaboration lives in resolve's construct-tag writer with a top-down expected-type context, not in infer (infer is a bottom-up fold; resolve is already the one tag writer after #12714) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 157 ++++++++++-------- 1 file changed, 87 insertions(+), 70 deletions(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index 1c8ca0782a0..4b302db3806 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -24,16 +24,15 @@ The slice, read from the grammar forward: `unrecognized_primary_expression_lowers_to_its_first_atom_at_v2_body_lowering`). This refusal is CORRECT at this link. Lowering runs before resolve and before infer, so it cannot know the constructor. The earliest unjustified boundary is therefore not lowering. It is the construct - carrier, which has no tag-absent case, together with typing, which has no site that checks a body - against its declared type. -- **Typing has no check site.** `v2.std.inhabitance` `DeclaredTypePosition` declares - `PositionDeclaredReturn`, but nothing constructs it. `v2.compiler.infer` builds only - `position_direct_call_argument` obligations. v2 infer synthesises bottom-up and never pushes a - declared type into a body. So "typing, which checks against the declared type" does not exist - in v2 yet. Form B is the first consumer that needs it. The obligation vocabulary already has a - home for it (`DeclaredTypeObligation`, `declared_type_inhabitance`). The v1 seed carries the - missing positions by name (`src/v1/04_infer.dag` `PositionDataInitializer`, - `PositionVariantPayload`). + carrier, which has no tag-absent case, together with the absence of any stage that carries a + declared type DOWN into the expression it types. +- **No stage pushes a declared type down.** v2 infer is a bottom-up fold. Its declared-type + judgments run AFTER a body is synthesised: `infer_arrow_body_inhabits_declared_return` compares a + derived body type with a declared return (cause `arrow_body_does_not_inhabit_declared_return`), + and only for a return the language join denotes (`dag_binding_denotation`). Call arguments are + judged the same way (`position_direct_call_argument`). A tag-elided construct cannot be + synthesised at all, because its tag is what types it. So its tag must be supplied BEFORE infer's + fold reaches it, by a stage that walks top-down with the declared type as context. - **The seed is not an oracle for the choice.** v1 chooses the struct in its Rust emitter by matching field names: `v1.compiler.emit_rust` `anonymous_record_struct_candidates`, and it refuses `AmbiguousAnonymousRecordLiteral` ("shape matches N structs -- add a nominal type"). @@ -60,58 +59,75 @@ The tag-absent construct is `Conj { : T { e }`. The arm exists. This change gives it a - constructor. -- `PositionRecordField`: the expected type of a field init is the declared type of that field on - the (authored or elaborated) constructor. This is how nesting works. `host_transport`'s - `RuntimePrimitive { value: { .. } }` elaborates the inner literal from `RuntimePrimitive.value`'s - declared type, and `compile_stage_memo`'s `key_derivation: { .. }` elaborates from the outer - record's field. New arm. -- `PositionListElement`: the expected type of a list literal's element is the element type of the - list's own expected type (`List` gives `T`). `dag/std/algebra` needs this: its 88 - `AlgebraFieldTemplate` rows are headless literals inside list literals. New arm, v1 vocabulary. - A list literal with no expected type passes none down, so its headless elements refuse - `infer_anonymous_record_no_expected_type`. - -At each such site, before the body is synthesised, a tag-elided construct at the root of `e` is -elaborated. Elaboration recurses through field inits under `PositionRecordField`. It is: - -1. Resolve the expected type's HEAD to a declaration. Type arguments (`BoundedLattice`) - do not choose the constructor; they flow into the field expected types by the existing - instantiation (`TypeVariableInstance`). A transparent alias is chased through the existing - alias-identity authority, not re-implemented. -2. The declaration must have exactly ONE record-shaped constructor: a `type R { .. }` record. The - single-variant coproduct form is admitted only if the existing record/coproduct classification - already names it one constructor. -3. Replace `construct_tag_elided_marker` with `declaration_reference_node(path)` of THAT - declaration, through `resolved_reference_node`. This is the same carrier resolve writes for an - authored head. -4. Synthesis then proceeds on an ordinary resolved construct. Field names are checked against the - chosen constructor by the existing construct typing (#12701's infer migration). A surplus or - missing field refuses there, at the field. - -The choice reads the expected type only. The field set is never an input to step 1 or step 2. Its -only role is step 4, which CHECKS a choice already made. +**Where elaboration happens: in resolve, at the construct-tag writer, with an expected-type +context threaded down from declared types.** (Revised after the first review. The first draft +placed it in infer "before synthesis". Infer is a bottom-up fold, so that would have been a second, +top-down pass beside it.) + +Choosing the tag reads exactly two facts, and both are NAME facts that resolve already has: + +- The expected type's HEAD resolves to a declaration. Resolve resolves every type expression. +- Whether that declaration is a single record, and, for nesting, its fields' declared types. Resolve's + `ResolveContext.namespace.symbol_index` returns the declaration node through `symbol_index_lookup`, + and `v2.std.type_binder` `type_decl_view` reads its shape. + +Resolve is also, after #12714, the ONE writer of construct tags: `resolve_construct_walk` rewrites +the authored tag spine into `declaration_reference_node(path)`. Writing the elided tag anywhere else +would give one fact, "the declaration this construct constructs", two writers (§3). So the elided +case is a second arm of that same writer, with the reference derived from the expected type rather +than from an authored spine. Infer then sees only ordinary resolved constructs, and checks them +with its existing construct typing and declared-return judgment. + +Resolve gains an expected-type context: `ResolveContext.expected`, an `Optional` resolved type +expression. It is set at four positions and cleared everywhere else, so a construct reached through +any other edge sees Absent: + +- **data initializer**: `data x: T = e` lowers to `Arrow(, T, T, body: e)`, so the + body edge of an Arrow is walked with `expected = ` its declared return; +- **declared return**: the same Arrow rule covers `fn f(..) -> T { e }`, reaching `e`'s tail + expression through blocks and `let .. in` bodies (the value position, not the statements); +- **record field**: under a construct resolved to record `R` (authored OR elaborated), field `f`'s + init is walked with `expected = ` the declared type of `R.f`, with `R`'s type parameters + substituted by the expected type's arguments. This is how nesting works: `compile_stage_memo`'s + `key_derivation: { .. }`, and `host_transport`'s `RuntimePrimitive { value: { .. } }`; +- **list element**: under a list literal whose expected head is `List` (through its alias + authority), each element is walked with `expected = ` the element type argument. + `dag/std/algebra`'s 88 `AlgebraFieldTemplate` rows need this. + +At a tag-elided construct, the elided arm of the writer: + +1. Takes `expected`. If it is Absent, refuse `resolve_anonymous_record_no_expected_type`. +2. Takes its HEAD's resolved declaration, chasing a transparent alias through the existing alias + authority. Type arguments (`BoundedLattice`) never choose the constructor; they + are substituted into field expected types (step 5). If the head does not resolve, the head's own + resolve refusal stands. No second cause is minted for it. +3. Requires the declaration to be a single record (`type_decl_view` `PlainTypeDecl` whose member is a + product), or `Map` (the MAP arm, which this lane refuses located; see Scope). Anything else, + whether a multi-variant coproduct, a primitive, or `List`, refuses + `resolve_anonymous_record_expected_type_not_record`. +4. Writes `declaration_reference_node(path)` of that declaration through `resolved_reference_node`, + the same carrier the authored arm writes. +5. Walks the fields under the record-field rule above. + +The choice reads `expected` only. The field set is never an input to steps 1-3. Field names are +CHECKED after the choice, by infer's existing construct typing: a surplus or missing field refuses +there, at the field. **Refusal causes** (typed, located at the literal's `{`, each with an ownership row in `v2.workflow.compile_door_cause_ownership`, per `refusal_producer_lands_without_its_ownership_row`): -- `infer_anonymous_record_no_expected_type`: a tag-elided construct reached synthesis without - passing through a check site. Examples: a call argument whose formal is a type variable not yet - instantiated, a `let` without an annotation, a match-arm body. These are not guessed. -- `infer_anonymous_record_expected_type_not_record`: the expected head resolves to a coproduct with - more than one constructor, a primitive, or `Map`/`List` while the literal has name keys. -- `infer_anonymous_record_expected_type_unresolved`: the expected head does not resolve. This is - distinct from the above so that a missing import is not reported as a shape error. - -A tag-elided construct that survives infer is unwritable downstream. The emit, eval and target_model +- `resolve_anonymous_record_no_expected_type`: the construct sits in a position with no declared + type. Examples: a `let` without an annotation, a call argument, a match-arm body, a list literal + that itself has no expected type. These are not guessed. Call arguments stay here on purpose: + their formal's declared type is a fact about the CALLEE, whose resolution this position does not + own. Widening to them is a named follow-up, not an implicit one. +- `resolve_anonymous_record_expected_type_not_record`: the expected head resolves to something other + than a single record or `Map`. +- `map_literal_construction_not_modeled`: the expected head is `Map`. This is the MAP arm, and it + plugs into the same writer. + +A tag-elided construct that survives resolve is unwritable downstream. The emit, eval and target_model readers gate on a reference target, and the elision marker is not one. So a missed elaboration refuses at their existing gates, and cannot emit an anonymous struct. @@ -125,14 +141,15 @@ the elided case adds. | v2.std.node | `construct_tag_elided_marker` | vocabulary | New structural core marker beside `construct_tag_marker`. | | v2.std.node_query | `construct_node` | producer | Takes the tag target as the reference OR the elision marker (one parameter; a typed `ConstructTag = Authored { reference } \| Elided` at the call boundary so a caller cannot pass an arbitrary node). | | v2.std.node_query | `construct_tag_optional` | reader | Answers the reference only. For an elided tag it answers Absent. `construct_tag_is_elided` is the one reader of the elided case. | -| v2.std.node | `arrow_body_record_construct_conforms`, `classify_arrow_body_form` | well_formed gate | Admits the elided marker ONLY pre-infer. The post-infer well_formed gate refuses it. | +| v2.std.node | `arrow_body_record_construct_conforms`, `classify_arrow_body_form` | well_formed gate | Admits the elided marker ONLY before resolve. After resolve the well_formed gate refuses it. | | v2.compiler.body_lowering_fold | `body_lower_try_record_literal` | producer | A headless `{ field_init_list }` whose every key is a name lowers to `construct_node(Elided, ..)`. The field inits go through the existing `body_lower_field_init_edge`. | -| v2.compiler.resolve | `resolve_node_walk`, `resolve_pattern_node_walk` Conj arm | rewriter | The elided tag edge is passed through untouched (there is nothing to resolve). Field values walk as today. | -| v2.std.inhabitance | `DeclaredTypePosition` | vocabulary | Add `PositionDataInitializer`, `PositionRecordField` and `PositionListElement`. `PositionDeclaredReturn` gains its constructor. | -| v2.compiler.infer | new `infer_elaborate_expected_construct` | elaborator | Steps 1-4 above, called from the data-initializer, declared-return, record-field and list-element check sites. It is the only writer that replaces the elision marker. | -| v2.compiler.infer | Conj gather arms | reader | A tag-elided construct reached here refuses `infer_anonymous_record_no_expected_type` (it is never typed as a bare product). | -| v2.workflow.compile_door_cause_ownership | three rows, plus `map_literal_construction_not_modeled` | ownership | One row per refusal cause. | -| v2.test.claim.namespace_xl0.reference_conservation_accepted_drops | `a_map_literal_value_refuses_at_the_literal_holds` | control | Today it asserts `unsupported_form` for `Map = { "k": .. }`. It moves to `map_literal_construction_not_modeled` at the check site. It must read the INFER outcome, because the literal now clears lowering and the `.normalized` read would go green-then-false. It stays enrolled as the map arm's refusal control. | +| v2.compiler.resolve | `ResolveContext` | context | Gains `expected: Optional`. It is set by the four position rules above and cleared by every other child walk (`resolve_ctx_with_scope` and siblings clear it by default, so a new edge kind cannot inherit it silently). | +| v2.compiler.resolve | `resolve_construct_walk` | writer | Gains the elided arm (steps 1-5). The authored arm also sets field expectations, so a headless literal nested under an authored head elaborates. | +| v2.compiler.resolve | `resolve_arrow_node_in`, the list-literal walk | position rules | Set `expected` for the body edge and for list elements. | +| v2.compiler.resolve | `resolve_pattern_node_walk` | reader | A tag-elided PATTERN does not arise (the pattern grammar requires a head). No change. | +| v2.compiler.infer | Conj gather arms | reader | No change. A tag-elided construct cannot reach infer, because resolve either writes its tag or refuses. | +| v2.workflow.compile_door_cause_ownership | three rows | ownership | One row per refusal cause. | +| v2.test.claim.namespace_xl0.reference_conservation_accepted_drops | `a_map_literal_value_refuses_at_the_literal_holds` | control | Today it asserts `unsupported_form` for `Map = { "k": .. }`. It moves to `map_literal_construction_not_modeled` at the elided-tag writer in resolve. It must read the RESOLVE outcome, because the literal now clears lowering, so the `.normalized` read would turn green and the claim false. It stays enrolled as the map arm's refusal control. | | v2.compiler.eval, v2.std.compilers.target_model, emit | construct gates | reader | No change. They already refuse a non-reference tag after #12701. PR2 adds a control proving an elided construct cannot reach them. | ## Scope: which of the seven Form B admits @@ -149,13 +166,13 @@ file can have later blockers beyond its first. | dag/std/termination | 45, `data .. : BoundedLattice = {` | data initializer, generic, newline-separated fields | yes, via generic instantiation. The newline separator is to be confirmed in PR2. | | dag/extdeps/realization/compile_stage_memo | 39, `key_derivation: {` | record field under a headless data initializer (lowering refuses the inner brace first) | yes: data initializer plus record field | | dag/extdeps/realization/parse_table_memo | 50, `key_derivation: {` | same | yes | -| dag/std/types | 5, `data kernel_type_set: Map = {` | MAP literal | **no: MAP arm.** Reaches the check site and refuses located `map_literal_construction_not_modeled`, as a declared population (below). | -| src/v2/std/host_transport | about 119, `reason: ^emit_host_runtime_row_..` | **caret symbol literal** (`body_lowering_reason_caret_symbol_not_lowered`), not Form B | **not by Form B alone.** Its first blocker is the caret form. Its Form B site (`RuntimePrimitive { value: { .. } }`, record field) is admitted by this lane, but the file stays refused until caret symbols lower. | +| dag/std/types | 5, `data kernel_type_set: Map = {` | MAP literal | **no: MAP arm.** Reaches the elided-tag writer in resolve and refuses located `map_literal_construction_not_modeled`, as a declared population (below). | +| src/v2/std/host_transport | about 119, `reason: ^emit_host_runtime_row_..` | **caret symbol literal** (`body_lowering_reason_caret_symbol_not_lowered`), not Form B | **not by Form B alone.** Its first blocker is the caret form. Its Form B site (`RuntimePrimitive { value: { .. } }`, record field) is admitted by this lane, but the file stays refused until caret symbols lower. That is #12420 (vivid-ant-536), so PR2 lists it as depending on #12420. | -**The map arm (ruling, gentle-koi-724).** This lane builds the ONE declared-type check site and its +**The map arm (ruling, gentle-koi-724).** This lane builds the ONE elided-tag writer (in resolve) and its RECORD arm. At that same site, a headless brace whose expected head is `Map` takes the MAP arm, which refuses located `map_literal_construction_not_modeled`, with its ownership row. It is not left as -`unsupported_form`, and there is no second check site later: map construction plugs into this arm. +`unsupported_form`, and there is no second site later: map construction plugs into this arm. For lowering to reach the site, a string-keyed item needs a field-edge form. It lowers to the same tag-elided construct, with each item carried as a `map_literal_entry_marker` edge to a @@ -183,8 +200,8 @@ files a `gunbc.recurring_failure_mode` receipt for it. children, so it should fall outside the hash. PR2 confirms this by execution. If it does not, the control compares the occurrence-erased projection, and says so. 2. A tag-elided literal with no expected type (a `let` without an annotation) refuses - `infer_anonymous_record_no_expected_type`, located at its `{`. -3. The expected type is a two-constructor coproduct: refuses `..._expected_type_not_record`. + `resolve_anonymous_record_no_expected_type`, located at its `{`. +3. The expected type is a two-constructor coproduct: refuses `resolve_anonymous_record_expected_type_not_record`. 4. Fields that do not match the chosen record refuse at the field, through the existing construct typing, and not as "no constructor". 5. **Mutation: choose by field names.** Two records share the literal's field set. The expected From e387c4f21affdc0d2d4ce147c6f5bf2b533695be Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 01:56:07 +0000 Subject: [PATCH 10/63] WIP: construct_tag_elided_marker --- src/v2/std/node.dag | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/v2/std/node.dag b/src/v2/std/node.dag index 29ad6784e73..dea3745c981 100644 --- a/src/v2/std/node.dag +++ b/src/v2/std/node.dag @@ -720,6 +720,20 @@ fn construct_tag_marker() -> Symbol { construct_tag_marker_symbol } +// AN ANONYMOUS RECORD LITERAL'S TAG IS ELIDED, NOT GUESSED. `data x: T = { f: v }` names no +// constructor; the constructor is chosen by the EXPECTED type, which lowering (before resolve) cannot +// know. So lowering writes the ONE construct shape with its tag edge present and targeting this +// marker, and resolve's construct-tag writer replaces it with the declaration reference of the +// expected type's record, or refuses (docs/plans/anonymous-record-literal-design.md). Keeping the +// tag edge rather than omitting it keeps a headless literal distinct from a bare Conj of Named edges, +// which is the anonymous record TYPE. A Core structural marker beside construct_tag_marker, in its +// form, for the same #12473 cut. +data construct_tag_elided_marker_symbol: Symbol = symbol_intern_lexeme(lexeme: "") + +fn construct_tag_elided_marker() -> Symbol { + construct_tag_elided_marker_symbol +} + // A RESOLVED DECLARATION REFERENCE IS NOT A RECORD CONSTRUCT. `data d: D = A` lowers to a nullary // Arrow whose body is the initializer, and when the initializer is a reference that body is the // marked Conj. Before this arm the classifier read every Conj body as a record construct, whose From b97ca8f6e9ae2111a8319fa1b56e7dd7a6b5e7bf Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 01:56:09 +0000 Subject: [PATCH 11/63] PR1: expected is set only from authored annotations; resolve never infers one (parent ruling) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index 4b302db3806..4927cd24884 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -80,7 +80,16 @@ with its existing construct typing and declared-return judgment. Resolve gains an expected-type context: `ResolveContext.expected`, an `Optional` resolved type expression. It is set at four positions and cleared everywhere else, so a construct reached through -any other edge sees Absent: +any other edge sees Absent. + +**`expected` is set ONLY from an AUTHORED type annotation** (ruling, gentle-koi-724): a data +declaration's declared type, a fn's declared return, a record field's declared type, or the element +type of an annotated list. Resolve never infers, unifies or propagates a type it computed. Where no +annotation reaches an elided construct, it refuses located (`resolve_anonymous_record_no_expected_type`) +and never guesses. A record field's declared type is an annotation authored on the record +declaration, so nesting stays within this rule. The type-parameter substitution in the record-field +rule substitutes AUTHORED type arguments from the annotation (`BoundedLattice`), and +never inferred ones. A binder the annotation leaves unsubstituted gives Absent. The four positions: - **data initializer**: `data x: T = e` lowers to `Arrow(, T, T, body: e)`, so the body edge of an Arrow is walked with `expected = ` its declared return; From 30fdd5284a7e5ce84f6ed841b25ba41784ec93e4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 01:56:45 +0000 Subject: [PATCH 12/63] PR1: neat-boar-16 approval conditions -- syntactic peel only (no substitution), infer still checks, expected does not leak; controls 7-9 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index 4927cd24884..211b7aae545 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -87,17 +87,20 @@ declaration's declared type, a fn's declared return, a record field's declared t type of an annotated list. Resolve never infers, unifies or propagates a type it computed. Where no annotation reaches an elided construct, it refuses located (`resolve_anonymous_record_no_expected_type`) and never guesses. A record field's declared type is an annotation authored on the record -declaration, so nesting stays within this rule. The type-parameter substitution in the record-field -rule substitutes AUTHORED type arguments from the annotation (`BoundedLattice`), and -never inferred ones. A binder the annotation leaves unsubstituted gives Absent. The four positions: +declaration, so nesting stays within this rule. **Syntactic peel only** (review condition 1, neat-boar-16, binding both arms). `expected` is peeled +syntactically from the authored annotation. Where reaching a closed record head would need +inference, type-variable instantiation, alias unfolding beyond resolve's existing lookup, or any +head that is not a closed record (or `Map`, for the map arm), the construct REFUSES located. There +is no unification and no substitution in resolve. A record field whose declared type is one of the +record's own binders (`top: T` of `BoundedLattice`) therefore gives no record head, and a headless +literal there refuses. The four positions: - **data initializer**: `data x: T = e` lowers to `Arrow(, T, T, body: e)`, so the body edge of an Arrow is walked with `expected = ` its declared return; - **declared return**: the same Arrow rule covers `fn f(..) -> T { e }`, reaching `e`'s tail expression through blocks and `let .. in` bodies (the value position, not the statements); - **record field**: under a construct resolved to record `R` (authored OR elaborated), field `f`'s - init is walked with `expected = ` the declared type of `R.f`, with `R`'s type parameters - substituted by the expected type's arguments. This is how nesting works: `compile_stage_memo`'s + init is walked with `expected = ` the declared type of `R.f`, as authored (no substitution). This is how nesting works: `compile_stage_memo`'s `key_derivation: { .. }`, and `host_transport`'s `RuntimePrimitive { value: { .. } }`; - **list element**: under a list literal whose expected head is `List` (through its alias authority), each element is walked with `expected = ` the element type argument. @@ -106,10 +109,12 @@ never inferred ones. A binder the annotation leaves unsubstituted gives Absent. At a tag-elided construct, the elided arm of the writer: 1. Takes `expected`. If it is Absent, refuse `resolve_anonymous_record_no_expected_type`. -2. Takes its HEAD's resolved declaration, chasing a transparent alias through the existing alias - authority. Type arguments (`BoundedLattice`) never choose the constructor; they - are substituted into field expected types (step 5). If the head does not resolve, the head's own - resolve refusal stands. No second cause is minted for it. +2. Takes its HEAD's resolved declaration, through resolve's existing lookup only. Type arguments + (`BoundedLattice`) never choose the constructor, and they are not substituted + anywhere. If the head does not resolve, the head's own resolve refusal stands, and no second + cause is minted for it. A head that is a type binder, or that reaches a record only through + alias unfolding the lookup does not already do, refuses + `resolve_anonymous_record_expected_type_not_record`. 3. Requires the declaration to be a single record (`type_decl_view` `PlainTypeDecl` whose member is a product), or `Map` (the MAP arm, which this lane refuses located; see Scope). Anything else, whether a multi-variant coproduct, a primitive, or `List`, refuses @@ -118,9 +123,13 @@ At a tag-elided construct, the elided arm of the writer: the same carrier the authored arm writes. 5. Walks the fields under the record-field rule above. -The choice reads `expected` only. The field set is never an input to steps 1-3. Field names are -CHECKED after the choice, by infer's existing construct typing: a surplus or missing field refuses -there, at the field. +The choice reads `expected` only. The field set is never an input to steps 1-3. + +**Resolve's choice is not proof** (review condition 2). Infer CHECKS the elaborated construct +against the declared type exactly as it checks an authored tag: field names and field value types +through its existing construct typing, and the declared return through +`infer_arrow_body_inhabits_declared_return`. A wrong field or a wrong value type refuses in infer, at +the field. **Refusal causes** (typed, located at the literal's `{`, each with an ownership row in `v2.workflow.compile_door_cause_ownership`, per @@ -218,7 +227,14 @@ files a `gunbc.recurring_failure_mode` receipt for it. field-name chooser picks the first or refuses as ambiguous, so it goes red either way. 6. Nesting: `host_transport`'s form, and a two-level `compile_stage_memo` form, each equal to their hand-headed form. -7. An elided construct cannot pass the post-infer well_formed gate or the target_model gate +7. **Infer still checks** (review condition 2): elaboration succeeds, and infer then refuses a field + value of the wrong type, located at the field. +8. **`expected` does not leak** (review condition 3): an anonymous literal passed as a call argument + under an annotated data declaration refuses `resolve_anonymous_record_no_expected_type`. A second + case puts it in a match-arm body under an annotated fn return. +9. **Syntactic peel** (review condition 1): a headless literal at a record field declared as the + record's own binder (`top: T`) refuses `resolve_anonymous_record_expected_type_not_record`. +10. An elided construct cannot pass the post-resolve well_formed gate or the target_model gate (a discriminating pair with the headed form). -8. Evidence: the #12550 base-vs-head native census shows each Form B file of the table admitted, +11. Evidence: the #12550 base-vs-head native census shows each Form B file of the table admitted, with every other outcome change explained. Seed claims show no true->false. From d03db19d1da22a60c3ffb9c67e49cb78fc0e14e8 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 02:03:39 +0000 Subject: [PATCH 13/63] WIP PR2: elided construct, resolve writer, ownership rows, controls --- src/v2/compiler/03_resolve.dag | 272 +++++++++++++++++- src/v2/compiler/body_lowering_fold.dag | 26 +- src/v2/std/node_query.dag | 59 +++- .../body_lowering/anonymous_record_test.dag | 214 ++++++++++++++ .../workflow/compile_door_cause_ownership.dag | 12 + 5 files changed, 565 insertions(+), 18 deletions(-) create mode 100644 src/v2/test/claim/body_lowering/anonymous_record_test.dag diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 93ae1e70957..49b911097a9 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -30,6 +30,7 @@ import v2.std.collection { Map, PointwisePower, empty_map, + list_at_optional, map_get, map_insert, map_lookup @@ -92,7 +93,8 @@ import v2.std.symbol_index { symbol_index_declared_payload_at, symbol_index_global_unique_lookup, symbol_index_declaring_path_of, - symbol_index_lexical_lookup + symbol_index_lexical_lookup, + symbol_index_lookup } import v2.std.node { named_edges, @@ -123,18 +125,22 @@ import v2.std.node { edge_label_of, node_with_occurrence_id, construct_tag_marker, + Instantiation, symbol_eq, well_formed, } -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.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 { construct_field_edges, + construct_tag_is_elided, construct_tag_optional, + node_positional_child_targets, find_named_child, is_wildcard_pattern, pattern_wildcard_name } import std.occurrence_identity { NodeOccurrenceIdentity, OccurrenceId } +import v2.std.list_introduction { list_introduction_elements_optional, list_introduction_head_path } // 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 @@ -1589,7 +1595,7 @@ fn resolve_pattern_node_walk(ctx: ResolveContext, pat: Node) -> ResolveNodeWalk match construct_tag_optional(n: pat) { Absent => resolve_node_walk(ctx: ctx, n: pat) Present { value: reference } => - resolve_construct_walk(ctx: ctx, n: pat, reference: reference, field: fn(e) { + resolve_construct_walk(ctx: ctx, n: pat, reference: reference, field: fn(e, _) { resolve_pattern_node_walk(ctx: ctx, pat: e.target) }) } @@ -1605,21 +1611,220 @@ fn resolve_construct_walk( ctx: ResolveContext, n: Node, reference: Node, - field: fn(Edge) -> ResolveNodeWalk + field: fn(Edge, Optional) -> ResolveNodeWalk ) -> ResolveNodeWalk { + resolve_construct_walk_with_tag( + n: n, + tag: resolve_construct_tag_walk(ctx: ctx, reference: reference), + field: field + ) +} + +// THE TAG IS WALKED ONCE, BEFORE THE FIELDS, and its resolved declaration path is handed to the +// field rule: a value field under a record construct is walked with that record's authored field +// type as its expectation (resolve_expected_edge), which is how a headless literal nested under an +// authored OR elaborated head elaborates. A tag that did not resolve to a declaration hands Absent, +// and the fields walk as ordinary children. +fn resolve_construct_walk_with_tag( + n: Node, + tag: ResolveNodeWalk, + field: fn(Edge, Optional) -> ResolveNodeWalk +) -> ResolveNodeWalk { + let tag_path = match tag { + ResolveWalkAccepted { value: resolved, diagnostics: _ } => declaration_reference_path_optional(node: resolved) + ResolveWalkRefused { first: _, rest: _, observation: _ } => Absent + } child_walk_node(n: n, w: fold(n.children, init: child_walk_init(), f: fn(acc, e) { match e.label { Named { name: label } => if symbol_eq(a: label, b: construct_tag_marker()) { - child_walk_step(w: acc, e: e, r: resolve_construct_tag_walk(ctx: ctx, reference: reference)) + child_walk_step(w: acc, e: e, r: tag) } else { - child_walk_step(w: acc, e: e, r: field(e)) + child_walk_step(w: acc, e: e, r: field(e, tag_path)) } - Positional => child_walk_step(w: acc, e: e, r: field(e)) + Positional => child_walk_step(w: acc, e: e, r: field(e, tag_path)) } })) } +// AN ANONYMOUS RECORD LITERAL IS ELABORATED FROM AN AUTHORED ANNOTATION, OR REFUSED +// (docs/plans/anonymous-record-literal-design.md). `{ f: v }` lowers to a construct whose tag is +// ELIDED (v2.std.node_query construct_tag_is_elided). Its constructor is the record the EXPECTED +// type names, and the expectation is an explicit parameter handed by exactly four position rules -- +// an Arrow's body under its declared return (which covers a `data` initializer), a record field +// under its authored field type, and a list element under its annotated list's element type -- and +// never ambient context. So a headless literal reached by ANY other edge (a call argument, a let +// without an annotation, a match-arm body) meets resolve_node_walk, which has no expectation to give +// and refuses resolve_anonymous_record_no_expected_type. Nothing is inferred, unified or substituted +// here: the expectation is peeled SYNTACTICALLY from what was written, its head resolved through +// the existing lexical lookup, and the choice is the existing declared-payload authority's +// RecordTypePayload answer. The literal's field set is never an input to the choice; infer checks +// the fields afterwards, exactly as it checks an authored tag. +fn resolve_anonymous_record_refused(n: Node, reason: Symbol) -> ResolveNodeWalk { + resolve_walk_refused_one(chain: diagnostics_singleton(d: Diagnostic { + reason: reason, + at: node_locus(node: n), + correction: Unavailable { reason: UserInputBoundary } + })) +} + +// THE HEAD OF AN AUTHORED TYPE EXPRESSION, peeled syntactically: a bare name, or the head of an +// instantiation `H`. Anything else -- an optional `T?`, a function type, an anonymous record +// type -- has no head a record could be read from, and answers Absent. +fn resolve_expected_head_atom_optional(t: Node) -> Optional { + match t.kind { + TypeNode { connective: Atom { identity: id } } => Present { value: id } + TypeNode { connective: Instantiation } => + match list_at_optional(xs: node_positional_child_targets(node: t), index: 0) { + Present { value: head } => + match head.kind { + TypeNode { connective: Atom { identity: id } } => Present { value: id } + _ => Absent + } + Absent => Absent + } + _ => Absent + } +} + +// The head resolved at `position` through the existing lexical lookup: the literal's own position +// for an annotation written in its module, the record's own path for a field type written on the +// record. A binder (`T`), a kernel symbol, or an unbound or ambiguous name answers Absent. +fn resolve_expected_head_path(ctx: ResolveContext, position: QualifiedName, t: Node) -> Optional { + match resolve_expected_head_atom_optional(t: t) { + Absent => Absent + Present { value: id } => + match symbol_index_lexical_lookup(index: ctx.namespace.symbol_index, position: position, name: id) { + LexicalHit { node: _, path: path } => Present { value: path } + _ => Absent + } + } +} + +// An expected type, and the position its names are read at. +type ResolveExpectation { + expected: Node + position: QualifiedName +} + +// A value edge walked under an expectation. Only three node shapes consume one -- a headless +// literal, an authored construct (whose fields take their own), and a list literal -- and every +// other node is walked exactly as resolve_child_edge would, so the expectation stops here. +fn resolve_expected_edge(ctx: ResolveContext, e: Edge, x: ResolveExpectation) -> ResolveNodeWalk { + if construct_tag_is_elided(n: e.target) { + resolve_elided_construct_walk(ctx: ctx, n: e.target, x: x) + } else { + match list_introduction_elements_optional(node: e.target) { + Present { value: _ } => resolve_list_literal_expected(ctx: ctx, n: e.target, x: x) + Absent => resolve_child_edge(ctx: ctx, e: e) + } + } +} + +fn resolve_elided_construct_walk(ctx: ResolveContext, n: Node, x: ResolveExpectation) -> ResolveNodeWalk { + match resolve_expected_head_path(ctx: ctx, position: x.position, t: x.expected) { + Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_expected_type_not_record) + Present { value: path } => + match symbol_index_declared_payload_at(index: ctx.namespace.symbol_index, qualified_path: path) { + Present { value: RecordTypePayload } => + match construct_tag_optional_edge_target(n: n) { + Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_expected_type_not_record) + Present { value: elided } => + resolve_construct_walk_with_tag( + n: n, + tag: ResolveWalkAccepted { + value: declaration_reference_node(qn: path, occurrence_id: elided.occurrence_id), + diagnostics: None + }, + field: fn(e, record) { resolve_record_field_edge(ctx: ctx, e: e, record: record) } + ) + } + _ => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_expected_type_not_record) + } + } +} + +fn construct_tag_optional_edge_target(n: Node) -> Optional { + match list_at_optional(xs: n.children, index: 0) { + Present { value: e } => Present { value: e.target } + Absent => Absent + } +} + +// A FIELD OF A RECORD CONSTRUCT carries its authored field type as its expectation. The field type +// is read from the record's own declaration (symbol_index_lookup, type_decl_view) and its names are +// resolved at the record's path, where they were written. A field with no declared type found -- +// an unknown field, which infer refuses at the field -- walks as an ordinary child. +fn resolve_record_field_edge(ctx: ResolveContext, e: Edge, record: Optional) -> ResolveNodeWalk { + match record { + Absent => resolve_child_edge(ctx: ctx, e: e) + Present { value: path } => + match e.label { + Positional => resolve_child_edge(ctx: ctx, e: e) + Named { name: field } => + match resolve_record_field_type_optional(ctx: ctx, record: path, field: field) { + Absent => resolve_child_edge(ctx: ctx, e: e) + Present { value: t } => + resolve_expected_edge(ctx: ctx, e: e, x: ResolveExpectation { expected: t, position: path }) + } + } + } +} + +fn resolve_record_field_type_optional(ctx: ResolveContext, record: QualifiedName, field: Symbol) -> Optional { + match symbol_index_lookup(index: ctx.namespace.symbol_index, qualified_path: record) { + Absent => Absent + Present { value: decl } => + let member = match type_decl_view(target: decl) { + PlainTypeDecl { member: m } => Present { value: m } + GenericTypeDecl { binders: _, member: m } => Present { value: m } + _ => Absent + } + match member { + Absent => Absent + Present { value: m } => + fold(m.children, init: Absent, f: fn(acc, fe) { + match acc { + Present { value: _ } => acc + Absent => + match fe.label { + Named { name: sym } => if symbol_eq(a: sym, b: field) { Present { value: fe.target } } else { Absent } + Positional => Absent + } + } + }) + } + } +} + +// A LIST LITERAL UNDER AN ANNOTATED LIST TYPE passes its element type to each element. The list is +// recognised by the ONE authority infer types list literals with (v2.std.list_introduction +// list_introduction_head_path): the annotation's head must resolve to exactly that declaration. A +// head that reaches it only through an alias is not unfolded here; it passes no expectation, and a +// headless element then refuses resolve_anonymous_record_no_expected_type. +fn resolve_list_literal_expected(ctx: ResolveContext, n: Node, x: ResolveExpectation) -> ResolveNodeWalk { + let element_type = match resolve_expected_head_path(ctx: ctx, position: x.position, t: x.expected) { + Present { value: path } => + if path == list_introduction_head_path() { + list_at_optional(xs: node_positional_child_targets(node: x.expected), index: 1) + } else { + Absent + } + Absent => Absent + } + match element_type { + Absent => resolve_node_walk(ctx: ctx, n: n) + Present { value: et } => + child_walk_node(n: n, w: fold(n.children, init: child_walk_init(), f: fn(acc, e) { + if acc.ordinal == 0 { + child_walk_step(w: acc, e: e, r: resolve_child_edge(ctx: ctx, e: e)) + } else { + child_walk_step(w: acc, e: e, r: resolve_expected_edge(ctx: ctx, e: e, x: ResolveExpectation { expected: et, position: x.position })) + } + })) + } +} + // ONE ROUTE FOR THE TAG, THROUGH THE DOORS EVERY OTHER REFERENCE USES. The authored path is read // whole (v2.std.qualified_name qualified_name_from_node) and dispatched by its length only: two or // more segments go to the qualified door (try_resolve_qualified_name_node), one segment to the bare @@ -1756,10 +1961,13 @@ fn resolve_node_walk( ComputationNode { behavior: Transform } => resolve_transform_children(ctx: ctx, n: n) TypeNode { connective: Conj } => + if construct_tag_is_elided(n: n) { + resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_no_expected_type) + } else { match construct_tag_optional(n: n) { Present { value: reference } => - resolve_construct_walk(ctx: ctx, n: n, reference: reference, field: fn(e) { - resolve_child_edge(ctx: ctx, e: e) + resolve_construct_walk(ctx: ctx, n: n, reference: reference, field: fn(e, record) { + resolve_record_field_edge(ctx: ctx, e: e, record: record) }) Absent => match try_resolve_qualified_name_node(ctx: ctx, n: n) { @@ -1767,6 +1975,7 @@ fn resolve_node_walk( Absent => resolve_children_homogeneous_scope(ctx: ctx, n: n) } } + } TypeNode { connective: _ } => resolve_children_homogeneous_scope(ctx: ctx, n: n) ComputationNode { behavior: _ } => @@ -1973,6 +2182,45 @@ fn resolve_arrow_node(ctx: ResolveContext, n: Node) -> ResolveNodeWalk { // under scope_body would resolve each against a parameter or, spelled like a free name, against the // 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. +// THE BODY OF AN ARROW IS WALKED UNDER ITS DECLARED RETURN, as authored: a `data x: T = e` lowers to +// Arrow(, T, T, body: e) and a `fn f(..) -> T { e }` to the same shape, so this one +// rule is both the data-initializer and the declared-return position of +// docs/plans/anonymous-record-literal-design.md. Only a headless literal or a list literal consumes +// the expectation; every other body walks exactly as it did. +fn resolve_arrow_body_or_named_edge(ctx: ResolveContext, arrow: Node, e: Edge) -> ResolveNodeWalk { + if resolve_edge_is_arrow_body(e: e) && resolve_node_consumes_expectation(n: e.target) { + match list_at_optional(xs: node_positional_child_targets(node: arrow), index: 1) { + Present { value: declared } => + resolve_expected_edge( + ctx: ctx, + e: e, + x: ResolveExpectation { expected: declared, position: symbol_index_lookup_position(ctx: ctx) } + ) + Absent => resolve_node_walk(ctx: ctx, n: e.target) + } + } else { + resolve_node_walk(ctx: ctx, n: e.target) + } +} + +fn resolve_edge_is_arrow_body(e: Edge) -> Bool { + match e.label { + Named { name: sym } => symbol_eq(a: sym, b: ^arrow_body_edge) + Positional => false + } +} + +fn resolve_node_consumes_expectation(n: Node) -> Bool { + if construct_tag_is_elided(n: n) { + true + } else { + match list_introduction_elements_optional(node: n) { + Present { value: _ } => true + Absent => false + } + } +} + fn resolve_arrow_node_in(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()) @@ -1989,7 +2237,11 @@ fn resolve_arrow_node_in(outer_ctx: ResolveContext, ctx: ResolveContext, n: Node child_walk_step( w: acc, e: e, - r: resolve_node_walk(ctx: resolve_ctx_with_scope(ctx: ctx, scope: scope_body), n: e.target) + r: resolve_arrow_body_or_named_edge( + ctx: resolve_ctx_with_scope(ctx: ctx, scope: scope_body), + arrow: n, + e: e + ) ) } Positional => diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 433adc262ae..5b382538d04 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -133,6 +133,7 @@ import v2.std.type_binder { cast_target_edge, type_alias_wrapper, type_decl_wrap import v2.std.node_query { node_positional_child_targets, construct_node, + construct_tag_elided_reference, construct_tag_optional, field_projection_node, field_projection_optional, @@ -7824,9 +7825,32 @@ fn body_lower_record_literal_tag_optional(captured: Node) -> Optional Outcome> { + match body_lower_record_brace_items_optional(brace: captured) { + Absent => outcome_accepted(value: optional_absent()) + Present { value: items } => + bind_outcome( + o: body_lower_record_construct( + reference: construct_tag_elided_reference(source: captured), + items: items, + source: captured + ), + f: fn(construct) { outcome_accepted(value: optional_present(value: construct)) } + ) + } +} + fn body_lower_try_record_literal(captured: Node) -> Outcome> { match body_lower_record_literal_tag_optional(captured: captured) { - Absent => outcome_accepted(value: optional_absent()) + Absent => body_lower_try_headless_record_literal(captured: captured) Present { value: read } => match body_lower_record_literal_field_items_optional(captured: captured) { Absent => outcome_accepted(value: optional_absent()) diff --git a/src/v2/std/node_query.dag b/src/v2/std/node_query.dag index d2cf0728a86..8031f462827 100644 --- a/src/v2/std/node_query.dag +++ b/src/v2/std/node_query.dag @@ -43,6 +43,7 @@ import v2.std.node { node_lowered_from, node_with_occurrence_id, construct_tag_marker, + construct_tag_elided_marker, symbol_eq, symbol_intern_lexeme, } @@ -321,11 +322,39 @@ fn construct_node(reference: Node, field_edges: List, source: Node) -> Nod ) } -// The tag of a construct: the reference its marked first edge targets. Anything else -- a payload -// Conj, a domain Conj, a declaration reference, a field projection, a bare record -- has none. The -// answer is the reference NODE, at whichever stage the tree is: a spine before resolution, a -// declaration reference after it. -fn construct_tag_optional(n: Node) -> Optional { +// AN ANONYMOUS RECORD LITERAL IS THE SAME CONSTRUCT WITH ITS TAG ELIDED: the marked tag edge is +// present and targets the v2.std.node construct_tag_elided_marker atom, never a guessed reference. +// Resolve's construct-tag writer replaces that target with the expected type's record reference, or +// refuses (docs/plans/anonymous-record-literal-design.md). +fn construct_tag_elided_reference(source: Node) -> Node { + node_with_occurrence_id( + kind: TypeNode { connective: Atom { identity: construct_tag_elided_marker() } }, + children: [], + occurrence_id: source.occurrence_id + ) +} + +fn construct_node_elided(field_edges: List, source: Node) -> Node { + construct_node(reference: construct_tag_elided_reference(source: source), field_edges: field_edges, source: source) +} + +fn construct_tag_target_is_elided(target: Node) -> Bool { + match target.kind { + TypeNode { connective: Atom { identity: id } } => symbol_eq(a: id, b: construct_tag_elided_marker()) + _ => false + } +} + +// The one reader of the elided case. A construct whose tag is elided answers true; an authored or +// resolved construct, and anything that is not a construct, answers false. +fn construct_tag_is_elided(n: Node) -> Bool { + match construct_tag_edge_target_optional(n: n) { + Present { value: target } => construct_tag_target_is_elided(target: target) + Absent => false + } +} + +fn construct_tag_edge_target_optional(n: Node) -> Optional { match n.kind { TypeNode { connective: Conj } => match first(n.children) { @@ -345,9 +374,25 @@ fn construct_tag_optional(n: Node) -> Optional { } } +// The tag of a construct: the reference its marked first edge targets. Anything else -- a payload +// Conj, a domain Conj, a declaration reference, a field projection, a bare record -- has none. The +// answer is the reference NODE, at whichever stage the tree is: a spine before resolution, a +// declaration reference after it. +fn construct_tag_optional(n: Node) -> Optional { + match construct_tag_edge_target_optional(n: n) { + Present { value: target } => + if construct_tag_target_is_elided(target: target) { + optional_absent() + } else { + optional_present(value: target) + } + Absent => optional_absent() + } +} + // The constructor's PATH, read from the tag at either stage: the resolved declaration's path after -// resolution, the authored path before it. Absent when the node is not a construct or its tag is -// not a well-formed reference. A pre-resolution path is what the author wrote, not a declaration +// resolution, the authored path before it. Absent when the node is not a construct, its tag is +// elided, or its tag is not a well-formed reference. A pre-resolution path is what the author wrote, not a declaration // identity; only a resolved tree's answer names the declaration. fn construct_tag_path_optional(n: Node) -> Optional { match construct_tag_optional(n: n) { diff --git a/src/v2/test/claim/body_lowering/anonymous_record_test.dag b/src/v2/test/claim/body_lowering/anonymous_record_test.dag new file mode 100644 index 00000000000..bb12a0c383b --- /dev/null +++ b/src/v2/test/claim/body_lowering/anonymous_record_test.dag @@ -0,0 +1,214 @@ +module v2.test.claim.body_lowering.anonymous_record + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { infer } +import v2.compiler.name_resolve { + Admission, + ResolutionSubject, + resolution_context, + resolve_with_admission_context_policy +} +import v2.std.cross_tree.resolution { source_root_index_empty } +import v2.std.resolution_policy { default_name_resolution_policy } +import v2.compiler.normalized_tree { NormalizedTree } +import v2.compiler.program_assembly { module_roots_from_source_root_ingest } +import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } +import v2.extdeps.languages.dag { dag_language_model } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.cross_tree.import_model { DagTree } +import v2.std.diagnostic { Accepted, Outcome, Rejected, diagnostics_fatal_reason } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import v2.std.optional { Absent, Present } +import v2.std.node { Hash, Node, Symbol, content_hash, node_subtree_nodes } +import v2.std.node_query { construct_tag_is_elided, construct_tag_optional, construct_tag_path_optional } +import v2.std.qualified_name { qualified_name_from_dotted_string, qualified_name_to_dotted_string } +import v2.std.algebra { length } +import v2.std.text { String } +import v2.std.collection { List } +import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } +import extdeps.communication.medium { Lossless, Medium } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// AN ANONYMOUS RECORD LITERAL ELABORATES FROM ITS AUTHORED ANNOTATION, OR REFUSES. +// +// The subject is v2.compiler.resolve's construct-tag writer for a headless literal `{ f: v }` +// (docs/plans/anonymous-record-literal-design.md): lowering writes the construct with its tag +// ELIDED, and resolve writes the reference of the record the authored expected type names -- at a +// data initializer, a declared return, a record field or an annotated list element -- or refuses +// located. The oracle is CONTENT EQUALITY WITH THE HEADED FORM, never the v1 seed, which chooses by +// field names (v1.compiler.emit_rust anonymous_record_struct_candidates). +// +// THE SOURCES ARE SUPPLIED AND THE ROUTE IS REAL, as in v2.test.claim.body_lowering.qualified_construct: +// tokenize, parse, normalize with body lowering, and resolve through +// v2.compiler.name_resolve resolve_with_admission_context_policy, from inline source text. +// +// ArA and ArB have the SAME field set, and the headless literals are annotated with ArB, the one +// declared SECOND: a chooser that read the field names would pick ArA or refuse as ambiguous, and +// ar_the_annotation_chooses_not_the_field_names_holds reds either way. +data ar_provider_source: String = "module v2.test.arp\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\n\ntype ArA {\n n: Bool\n m: Bool\n}\n\ntype ArB {\n n: Bool\n m: Bool\n}\n\ntype ArOuter {\n inner: ArB\n flag: Bool\n}\n\ntype ArShape\n = ArV { n: Bool }\n | ArW\n\ntype ArBox {\n top: T\n}\n\nfn ar_take(x: ArB) -> Bool { true }\n" + +data ar_headed_source: String = "module v2.test.ar_headed\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\nimport v2.test.arp { ArA, ArB, ArOuter }\n\ndata ar_x: ArB = ArB { n: true, m: false }\n\ndata ar_outer: ArOuter = ArOuter { inner: ArB { n: false, m: true }, flag: true }\n\nfn ar_ret() -> ArB { ArB { n: true, m: true } }\n\nfn ar_list() -> FreeMonoid { [ArB { n: true, m: false }, ArB { n: false, m: false }] }\n" + +data ar_headless_source: String = "module v2.test.ar_headless\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\nimport v2.test.arp { ArA, ArB, ArOuter }\n\ndata ar_x: ArB = { n: true, m: false }\n\ndata ar_outer: ArOuter = { inner: { n: false, m: true }, flag: true }\n\nfn ar_ret() -> ArB { { n: true, m: true } }\n\nfn ar_list() -> FreeMonoid { [{ n: true, m: false }, { n: false, m: false }] }\n" + +// Review condition 3: the expectation does not leak into a call argument. +data ar_call_argument_source: String = "module v2.test.ar_call_argument\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArB, ar_take }\n\nfn ar_call() -> Bool { ar_take(x: { n: true, m: true }) }\n" + +data ar_coproduct_source: String = "module v2.test.ar_coproduct\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArShape }\n\ndata ar_c: ArShape = { n: true }\n" + +// Review condition 1: the outer literal elaborates to ArBox; its field `top: T` is a binder, which +// is not substituted, so the inner literal refuses. +data ar_binder_source: String = "module v2.test.ar_binder\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArB, ArBox }\n\ndata ar_box: ArBox = { top: { n: true, m: true } }\n" + +// Review condition 2: resolve's choice is not proof. `m: 3` is an Int where ArB declares a Bool. +data ar_value_mismatch_source: String = "module v2.test.ar_value_mismatch\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArB }\n\ndata ar_bad: ArB = { n: true, m: 3 }\n" + +fn ar_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: source, fidelity: Lossless }, + artifact: Artifact { kind: SourceFile, id: id, file_path: path }, + compilation_unit: unit, + source_root: DagTree + } +} + +data ar_ingest: SourceRootIngest = [ + ar_read(source: ar_provider_source, id: ^ar_provider_read, unit: ^ar_provider_cu, path: "src/v2/test/fixture/anonymous_record/provider.dag"), + ar_read(source: ar_headed_source, id: ^ar_headed_read, unit: ^ar_headed_cu, path: "src/v2/test/fixture/anonymous_record/headed.dag"), + ar_read(source: ar_headless_source, id: ^ar_headless_read, unit: ^ar_headless_cu, path: "src/v2/test/fixture/anonymous_record/headless.dag"), + ar_read(source: ar_call_argument_source, id: ^ar_call_argument_read, unit: ^ar_call_argument_cu, path: "src/v2/test/fixture/anonymous_record/call_argument.dag"), + ar_read(source: ar_coproduct_source, id: ^ar_coproduct_read, unit: ^ar_coproduct_cu, path: "src/v2/test/fixture/anonymous_record/coproduct.dag"), + ar_read(source: ar_binder_source, id: ^ar_binder_read, unit: ^ar_binder_cu, path: "src/v2/test/fixture/anonymous_record/binder.dag"), + ar_read(source: ar_value_mismatch_source, id: ^ar_value_mismatch_read, unit: ^ar_value_mismatch_cu, path: "src/v2/test/fixture/anonymous_record/value_mismatch.dag") +] + +fn ar_fixture_normalized() -> Outcome> { + module_roots_from_source_root_ingest(ingest: ar_ingest, lm: dag_language_model()) +} + +fn ar_resolve_subject(subject: String) -> Outcome { + match ar_fixture_normalized() { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: roots, diagnostics: _ } => + resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { + subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: subject) }, + imports: Empty + }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) + } +} + +fn ar_resolved_headed() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_headed") } +fn ar_resolved_headless() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_headless") } +fn ar_resolved_call_argument() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_call_argument") } +fn ar_resolved_coproduct() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_coproduct") } +fn ar_resolved_binder() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_binder") } +fn ar_resolved_value_mismatch() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_value_mismatch") } + +// Every construct under the root, authored or elided, in tree order. +fn ar_constructs(root: Node) -> List { + fold(node_subtree_nodes(root: root), init: Empty, f: fn(acc, n) { + if construct_tag_is_elided(n: n) { + list_snoc_item(xs: acc, item: n) + } else { + match construct_tag_optional(n: n) { + Present { value: _ } => list_snoc_item(xs: acc, item: n) + Absent => acc + } + } + }) +} + +fn ar_construct_hashes(o: Outcome) -> List { + match o { + Rejected { diagnostics: _ } => Empty + Accepted { value: t, diagnostics: _ } => + fold(ar_constructs(root: t.root), init: Empty, f: fn(acc, n) { list_snoc_item(xs: acc, item: content_hash(n: n)) }) + } +} + +fn ar_construct_paths(o: Outcome) -> List { + match o { + Rejected { diagnostics: _ } => Empty + Accepted { value: t, diagnostics: _ } => + fold(ar_constructs(root: t.root), init: Empty, f: fn(acc, n) { + match construct_tag_path_optional(n: n) { + Present { value: path } => list_snoc_item(xs: acc, item: qualified_name_to_dotted_string(qn: path)) + Absent => list_snoc_item(xs: acc, item: "") + } + }) + } +} + +fn ar_pairwise_equal(xs: List, ys: List) -> Bool { + match xs { + Empty => match ys { Empty => true Cons { head: _, tail: _ } => false } + Cons { head: x, tail: xt } => + match ys { + Empty => false + Cons { head: y, tail: yt } => (x == y) && ar_pairwise_equal(xs: xt, ys: yt) + } + } +} + +fn ar_refuses_with(o: Outcome, reason: Symbol) -> Bool { + match o { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == reason + } +} + +// Control 1 and 6: every headless literal -- a data initializer, a nested record field, a declared +// return, and two annotated list elements -- resolves to the SAME construct as its headed spelling, +// by content, and there are six of them, so a literal that stayed elided or vanished cannot pass. +test fn ar_headless_literals_equal_their_headed_form_holds() -> Bool { + let headed = ar_construct_hashes(o: ar_resolved_headed()) + (length(xs: headed) == 6) && ar_pairwise_equal(xs: headed, ys: ar_construct_hashes(o: ar_resolved_headless())) +} + +// Control 5, the mutation: ArA and ArB share the field set, and ArA is declared first. The +// annotation says ArB, so every elaborated tag is ArB. A chooser that read the field names would +// answer ArA (or refuse as ambiguous) and this goes red. +test fn ar_the_annotation_chooses_not_the_field_names_holds() -> Bool { + ar_construct_paths(o: ar_resolved_headless()) == [ + "v2.test.arp.ArB", "v2.test.arp.ArOuter", "v2.test.arp.ArB", "v2.test.arp.ArB", "v2.test.arp.ArB", "v2.test.arp.ArB" + ] +} + +// Control 8 (review condition 3): an annotation does not reach a call argument. The argument's +// formal is typed, but that type is a fact about the callee and is not threaded; the literal refuses. +test fn ar_a_call_argument_has_no_expected_type_holds() -> Bool { + ar_refuses_with(o: ar_resolved_call_argument(), reason: ^resolve_anonymous_record_no_expected_type) +} + +// Control 3: a two-constructor coproduct is not a record, and no constructor is picked from it. +test fn ar_a_coproduct_expectation_is_not_a_record_holds() -> Bool { + ar_refuses_with(o: ar_resolved_coproduct(), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +// Control 9 (review condition 1): the field type `T` is a binder and is not substituted, so the +// inner literal has no record head. +test fn ar_a_binder_field_type_is_not_substituted_holds() -> Bool { + ar_refuses_with(o: ar_resolved_binder(), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +// Control 7 (review condition 2): resolve elaborates the literal (it accepts, and the tag is ArB), +// and infer then refuses the Int value where ArB declares a Bool. Resolve's choice is not proof. +test fn ar_infer_still_checks_an_elaborated_construct_holds() -> Bool { + match ar_resolved_value_mismatch() { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: d } => + (ar_construct_paths(o: Accepted { value: t, diagnostics: d }) == ["v2.test.arp.ArB"]) + && match infer(tree: t) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: _ } => true + } + } +} diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index ab0cbb2005e..85ddb2690aa 100644 --- a/src/v2/workflow/compile_door_cause_ownership.dag +++ b/src/v2/workflow/compile_door_cause_ownership.dag @@ -264,6 +264,18 @@ data known_frontier_causes: List = [ lane: SharedSelfHostCriticalPath, flip_trigger: "body lowering's CATCH-ALL refusal: a value, arm capture or postfix/as-suffix shape v2.compiler.body_lowering_fold reads but has no dedicated reason for (gunbc#12245 and gunbc#12315 added producers without this row). Owned as a frontier, not as a class: it retires when each producing site refuses with a cause that names its shape (or lowers), so that no body-lowering path answers with this symbol and the count zeroes. Observed at fatal grain on the native route at main 1457bd81d00 over dag + src/v2 (member rows and file refusals)" }, + CauseOwnership { + cause: ^resolve_anonymous_record_no_expected_type, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a headless record literal `{ f: v }` reached by an edge no authored annotation types -- a call argument, a let without an annotation, a match-arm body, an element of an unannotated list -- so v2.compiler.resolve has no expected type to choose its constructor from, and refuses rather than guess or read the field names (docs/plans/anonymous-record-literal-design.md). Not a frontier to drive to zero: each occurrence is a source that names no type where one is needed, and flips when that source writes the constructor head or an annotation. A position whose type is authored but not yet threaded (a call argument's formal) is a named extension of the position rules, not a reason to widen this one" + }, + CauseOwnership { + cause: ^resolve_anonymous_record_expected_type_not_record, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a headless record literal whose authored expected type does not resolve, by syntactic peel and the existing lexical lookup, to a single record (v2.std.symbol_index RecordTypePayload): a coproduct with several constructors, a primitive, a type binder, an optional, or a head reached only through alias unfolding resolve does not do. Refused rather than guessed (docs/plans/anonymous-record-literal-design.md, review condition 1). Flips per site when the source writes the constructor head; a Map head is the map arm (gunbc#12734), which takes that population when it lands" + }, CauseOwnership { cause: ^body_lowering_reason_function_value_body_unread, grain: FatalGrain, From 0318d66b239e2f0cf0e69b847fdbde2079afc70f Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 02:03:48 +0000 Subject: [PATCH 14/63] PR1: the expectation is an explicit parameter of four position rules, not a ResolveContext field (condition 3 made structural); map-literal control moves with #12734 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index 211b7aae545..c509e13da47 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -78,16 +78,21 @@ case is a second arm of that same writer, with the reference derived from the ex than from an authored spine. Infer then sees only ordinary resolved constructs, and checks them with its existing construct typing and declared-return judgment. -Resolve gains an expected-type context: `ResolveContext.expected`, an `Optional` resolved type -expression. It is set at four positions and cleared everywhere else, so a construct reached through -any other edge sees Absent. - -**`expected` is set ONLY from an AUTHORED type annotation** (ruling, gentle-koi-724): a data +**The expectation is a parameter, not ambient context.** An expectation (`ResolveExpectation`: the +authored type expression, and the position its names are read at) is handed, as an explicit +argument, by exactly the four position rules below to the one child each rule owns +(`resolve_expected_edge`). It is never a `ResolveContext` field. Every other walk goes through +`resolve_node_walk`, which has no expectation to give, and a headless literal there refuses. That +makes review condition 3 structural rather than a clearing discipline: a field on the context would +have been inherited by every child walk that copies the context (a call's arguments, for one), and +each would have had to remember to clear it. + +**The expectation is set ONLY from an AUTHORED type annotation** (ruling, gentle-koi-724): a data declaration's declared type, a fn's declared return, a record field's declared type, or the element type of an annotated list. Resolve never infers, unifies or propagates a type it computed. Where no annotation reaches an elided construct, it refuses located (`resolve_anonymous_record_no_expected_type`) and never guesses. A record field's declared type is an annotation authored on the record -declaration, so nesting stays within this rule. **Syntactic peel only** (review condition 1, neat-boar-16, binding both arms). `expected` is peeled +declaration, so nesting stays within this rule. **Syntactic peel only** (review condition 1, neat-boar-16, binding both arms). The expectation is peeled syntactically from the authored annotation. Where reaching a closed record head would need inference, type-variable instantiation, alias unfolding beyond resolve's existing lookup, or any head that is not a closed record (or `Map`, for the map arm), the construct REFUSES located. There @@ -96,19 +101,19 @@ record's own binders (`top: T` of `BoundedLattice`) therefore gives no record literal there refuses. The four positions: - **data initializer**: `data x: T = e` lowers to `Arrow(, T, T, body: e)`, so the - body edge of an Arrow is walked with `expected = ` its declared return; + body edge of an Arrow is walked with its declared return as the expectation; - **declared return**: the same Arrow rule covers `fn f(..) -> T { e }`, reaching `e`'s tail expression through blocks and `let .. in` bodies (the value position, not the statements); - **record field**: under a construct resolved to record `R` (authored OR elaborated), field `f`'s - init is walked with `expected = ` the declared type of `R.f`, as authored (no substitution). This is how nesting works: `compile_stage_memo`'s + init is walked with the declared type of `R.f` as the expectation, as authored (no substitution). This is how nesting works: `compile_stage_memo`'s `key_derivation: { .. }`, and `host_transport`'s `RuntimePrimitive { value: { .. } }`; - **list element**: under a list literal whose expected head is `List` (through its alias - authority), each element is walked with `expected = ` the element type argument. + authority), each element is walked with the element type argument as the expectation. `dag/std/algebra`'s 88 `AlgebraFieldTemplate` rows need this. At a tag-elided construct, the elided arm of the writer: -1. Takes `expected`. If it is Absent, refuse `resolve_anonymous_record_no_expected_type`. +1. Takes the expectation. If there is none, refuse `resolve_anonymous_record_no_expected_type`. 2. Takes its HEAD's resolved declaration, through resolve's existing lookup only. Type arguments (`BoundedLattice`) never choose the constructor, and they are not substituted anywhere. If the head does not resolve, the head's own resolve refusal stands, and no second @@ -123,7 +128,7 @@ At a tag-elided construct, the elided arm of the writer: the same carrier the authored arm writes. 5. Walks the fields under the record-field rule above. -The choice reads `expected` only. The field set is never an input to steps 1-3. +The choice reads the expectation only. The field set is never an input to steps 1-3. **Resolve's choice is not proof** (review condition 2). Infer CHECKS the elaborated construct against the declared type exactly as it checks an authored tag: field names and field value types @@ -161,13 +166,12 @@ the elided case adds. | v2.std.node_query | `construct_tag_optional` | reader | Answers the reference only. For an elided tag it answers Absent. `construct_tag_is_elided` is the one reader of the elided case. | | v2.std.node | `arrow_body_record_construct_conforms`, `classify_arrow_body_form` | well_formed gate | Admits the elided marker ONLY before resolve. After resolve the well_formed gate refuses it. | | v2.compiler.body_lowering_fold | `body_lower_try_record_literal` | producer | A headless `{ field_init_list }` whose every key is a name lowers to `construct_node(Elided, ..)`. The field inits go through the existing `body_lower_field_init_edge`. | -| v2.compiler.resolve | `ResolveContext` | context | Gains `expected: Optional`. It is set by the four position rules above and cleared by every other child walk (`resolve_ctx_with_scope` and siblings clear it by default, so a new edge kind cannot inherit it silently). | -| v2.compiler.resolve | `resolve_construct_walk` | writer | Gains the elided arm (steps 1-5). The authored arm also sets field expectations, so a headless literal nested under an authored head elaborates. | -| v2.compiler.resolve | `resolve_arrow_node_in`, the list-literal walk | position rules | Set `expected` for the body edge and for list elements. | +| v2.compiler.resolve | `ResolveExpectation`, `resolve_expected_edge`, `resolve_arrow_body_or_named_edge`, `resolve_record_field_edge`, `resolve_list_literal_expected` | position rules | The expectation parameter and the four rules that hand it to one child each. `ResolveContext` is unchanged. | +| v2.compiler.resolve | `resolve_construct_walk`, new `resolve_construct_walk_with_tag`, `resolve_elided_construct_walk` | writer | The tag is walked once before the fields, and its resolved path is handed to the field rule, so a headless literal nested under an authored head elaborates. The elided arm writes the tag from the expectation (steps 1-5). In `resolve_node_walk`, an elided construct refuses `resolve_anonymous_record_no_expected_type`. | | v2.compiler.resolve | `resolve_pattern_node_walk` | reader | A tag-elided PATTERN does not arise (the pattern grammar requires a head). No change. | | v2.compiler.infer | Conj gather arms | reader | No change. A tag-elided construct cannot reach infer, because resolve either writes its tag or refuses. | | v2.workflow.compile_door_cause_ownership | three rows | ownership | One row per refusal cause. | -| v2.test.claim.namespace_xl0.reference_conservation_accepted_drops | `a_map_literal_value_refuses_at_the_literal_holds` | control | Today it asserts `unsupported_form` for `Map = { "k": .. }`. It moves to `map_literal_construction_not_modeled` at the elided-tag writer in resolve. It must read the RESOLVE outcome, because the literal now clears lowering, so the `.normalized` read would turn green and the claim false. It stays enrolled as the map arm's refusal control. | +| v2.test.claim.namespace_xl0.reference_conservation_accepted_drops | `a_map_literal_value_refuses_at_the_literal_holds` | control | Today it asserts `unsupported_form` for `Map = { "k": .. }`. UNCHANGED by this lane: a string-keyed item still refuses at lowering here. It moves with the map arm (gunbc#12734, jolly-boar-246), which gives string keys an edge form and must then read the RESOLVE outcome. | | v2.compiler.eval, v2.std.compilers.target_model, emit | construct gates | reader | No change. They already refuse a non-reference tag after #12701. PR2 adds a control proving an elided construct cannot reach them. | ## Scope: which of the seven Form B admits @@ -229,7 +233,7 @@ files a `gunbc.recurring_failure_mode` receipt for it. hand-headed form. 7. **Infer still checks** (review condition 2): elaboration succeeds, and infer then refuses a field value of the wrong type, located at the field. -8. **`expected` does not leak** (review condition 3): an anonymous literal passed as a call argument +8. **The expectation does not leak** (review condition 3): an anonymous literal passed as a call argument under an annotated data declaration refuses `resolve_anonymous_record_no_expected_type`. A second case puts it in a match-arm body under an annotated fn return. 9. **Syntactic peel** (review condition 1): a headless literal at a record field declared as the From a8553764ef036837571e6cba51afdc8f694e4bfe Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 02:41:51 +0000 Subject: [PATCH 15/63] WIP PR2: optional helpers --- src/v2/compiler/03_resolve.dag | 40 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 49b911097a9..c6d11b87d10 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1673,17 +1673,17 @@ fn resolve_anonymous_record_refused(n: Node, reason: Symbol) -> ResolveNodeWalk // type -- has no head a record could be read from, and answers Absent. fn resolve_expected_head_atom_optional(t: Node) -> Optional { match t.kind { - TypeNode { connective: Atom { identity: id } } => Present { value: id } + TypeNode { connective: Atom { identity: id } } => optional_present(value: id) TypeNode { connective: Instantiation } => match list_at_optional(xs: node_positional_child_targets(node: t), index: 0) { Present { value: head } => match head.kind { - TypeNode { connective: Atom { identity: id } } => Present { value: id } - _ => Absent + TypeNode { connective: Atom { identity: id } } => optional_present(value: id) + _ => optional_absent() } - Absent => Absent + Absent => optional_absent() } - _ => Absent + _ => optional_absent() } } @@ -1692,11 +1692,11 @@ fn resolve_expected_head_atom_optional(t: Node) -> Optional { // record. A binder (`T`), a kernel symbol, or an unbound or ambiguous name answers Absent. fn resolve_expected_head_path(ctx: ResolveContext, position: QualifiedName, t: Node) -> Optional { match resolve_expected_head_atom_optional(t: t) { - Absent => Absent + Absent => optional_absent() Present { value: id } => match symbol_index_lexical_lookup(index: ctx.namespace.symbol_index, position: position, name: id) { - LexicalHit { node: _, path: path } => Present { value: path } - _ => Absent + LexicalHit { node: _, path: path } => optional_present(value: path) + _ => optional_absent() } } } @@ -1746,8 +1746,8 @@ fn resolve_elided_construct_walk(ctx: ResolveContext, n: Node, x: ResolveExpecta fn construct_tag_optional_edge_target(n: Node) -> Optional { match list_at_optional(xs: n.children, index: 0) { - Present { value: e } => Present { value: e.target } - Absent => Absent + Present { value: e } => optional_present(value: e.target) + Absent => optional_absent() } } @@ -1773,23 +1773,23 @@ fn resolve_record_field_edge(ctx: ResolveContext, e: Edge, record: Optional Optional { match symbol_index_lookup(index: ctx.namespace.symbol_index, qualified_path: record) { - Absent => Absent + Absent => optional_absent() Present { value: decl } => let member = match type_decl_view(target: decl) { - PlainTypeDecl { member: m } => Present { value: m } - GenericTypeDecl { binders: _, member: m } => Present { value: m } - _ => Absent + PlainTypeDecl { member: m } => optional_present(value: m) + GenericTypeDecl { binders: _, member: m } => optional_present(value: m) + _ => optional_absent() } match member { - Absent => Absent + Absent => optional_absent() Present { value: m } => - fold(m.children, init: Absent, f: fn(acc, fe) { + fold(m.children, init: optional_absent(), f: fn(acc, fe) { match acc { Present { value: _ } => acc Absent => match fe.label { - Named { name: sym } => if symbol_eq(a: sym, b: field) { Present { value: fe.target } } else { Absent } - Positional => Absent + Named { name: sym } => if symbol_eq(a: sym, b: field) { optional_present(value: fe.target) } else { optional_absent() } + Positional => optional_absent() } } }) @@ -1808,9 +1808,9 @@ fn resolve_list_literal_expected(ctx: ResolveContext, n: Node, x: ResolveExpecta if path == list_introduction_head_path() { list_at_optional(xs: node_positional_child_targets(node: x.expected), index: 1) } else { - Absent + optional_absent() } - Absent => Absent + Absent => optional_absent() } match element_type { Absent => resolve_node_walk(ctx: ctx, n: n) From 4c0f05ee03b2f58c0cde9a3d974e85ceeca7a740 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 02:41:52 +0000 Subject: [PATCH 16/63] PR1: termination is admitted by head lookup, not generic instantiation (review 73019) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index c509e13da47..856b57c7f75 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -185,7 +185,7 @@ file can have later blockers beyond its first. |---|---|---|---| | dag/std/algebra | 359, `{ name: "filter", .. }` | the body of `fn collection_filter_shape() -> AlgebraFieldTemplate`; 88 more rows of the same record inside list literals | yes: declared return, and list element | | dag/std/primitives | 17, `data char_at_contract: PrimitiveContract = {` | data initializer | yes | -| dag/std/termination | 45, `data .. : BoundedLattice = {` | data initializer, generic, newline-separated fields | yes, via generic instantiation. The newline separator is to be confirmed in PR2. | +| dag/std/termination | 45, `data .. : BoundedLattice = {` | data initializer, generic, newline-separated fields | yes, by HEAD lookup: `BoundedLattice` resolves to a single record. Its type argument is not substituted and chooses nothing, and none of its field inits is headless. The newline separator is to be confirmed in PR2. | | dag/extdeps/realization/compile_stage_memo | 39, `key_derivation: {` | record field under a headless data initializer (lowering refuses the inner brace first) | yes: data initializer plus record field | | dag/extdeps/realization/parse_table_memo | 50, `key_derivation: {` | same | yes | | dag/std/types | 5, `data kernel_type_set: Map = {` | MAP literal | **no: MAP arm.** Reaches the elided-tag writer in resolve and refuses located `map_literal_construction_not_modeled`, as a declared population (below). | From bdf00322c2c74f463422f889696d0f8c9eebfb65 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 03:03:50 +0000 Subject: [PATCH 17/63] WIP PR2: fold_list in field type lookup --- src/v2/compiler/03_resolve.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index c6d11b87d10..48f3c0efbc0 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1783,7 +1783,7 @@ fn resolve_record_field_type_optional(ctx: ResolveContext, record: QualifiedName match member { Absent => optional_absent() Present { value: m } => - fold(m.children, init: optional_absent(), f: fn(acc, fe) { + fold_list(xs: m.children, empty: optional_absent(), cons: fn(acc, fe) { match acc { Present { value: _ } => acc Absent => From e9670f65ef6535b0e3468d9294b78efd22fa70ff Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 03:26:58 +0000 Subject: [PATCH 18/63] PR1: the scope table is the scoping reading; the standing instrument is PR2's base-vs-head census (review 73044) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index 856b57c7f75..e6b4ddb1220 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -176,10 +176,13 @@ the elided case adds. ## Scope: which of the seven Form B admits -MEASURED, on main 7f144278c57. Each file's source ran through the same parse and normalize route as -`v2.test.claim.namespace_xl0.reference_conservation` `conservation_subject`. The fatal diagnostic was -rendered through the parse's `SpanIndex`. Only the FIRST fatal refusal is reported per file, so a -file can have later blockers beyond its first. +THE READING THAT SET THIS LANE'S SCOPE, not a standing fact. It comes from a one-off probe on main +7f144278c57: each file's source was run through the parse and normalize route of +`v2.test.claim.namespace_xl0.reference_conservation` `conservation_subject`, and the first fatal was +rendered through the parse's `SpanIndex`. Only the FIRST fatal per file is shown, so a file can have +later blockers. The standing instrument is the #12550 base-vs-head native census, run on PR2's head. +It re-derives which of these files are admitted, and PR2 cites that census rather than this table +(DESIGN §6: name the instrument, never transcribe its output). | File | First fatal refusal (line) | Form | Form B admits it? | |---|---|---|---| From 210f412fd00a8c45e37b8b0a51e085246a038600 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 03:56:27 +0000 Subject: [PATCH 19/63] WIP map arm: introduction, lowering edge, resolve arm, controls Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/std/algebra.dag | 21 ++ src/v2/compiler/03_resolve.dag | 193 +++++++++++++- src/v2/compiler/body_lowering_fold.dag | 88 ++++++- src/v2/extdeps/languages/dag.dag | 46 ++++ src/v2/std/map_introduction.dag | 138 ++++++++++ .../claim/body_lowering/map_literal_test.dag | 238 ++++++++++++++++++ .../workflow/compile_door_cause_ownership.dag | 44 +++- 7 files changed, 762 insertions(+), 6 deletions(-) create mode 100644 src/v2/std/map_introduction.dag create mode 100644 src/v2/test/claim/body_lowering/map_literal_test.dag diff --git a/dag/std/algebra.dag b/dag/std/algebra.dag index 3bb78e5658f..b781c44d1f4 100644 --- a/dag/std/algebra.dag +++ b/dag/std/algebra.dag @@ -237,6 +237,27 @@ type FinitelySupportedFunction { size: fn() -> Int } +// THE MAP-LITERAL INTRODUCTION: the declaration a map literal `{ "k": v, .. }` elaborates to, as +// `FreeMonoid` is the one a list literal elaborates to (v2.std.map_introduction carries its head +// path and constructor; docs/plans/map-literal-introduction-design.md). It is NOT a second +// constructor of the carrier: it is the carrier's own insert (the `map_insert` row of +// `finitely_supported_function_templates`) folded over its own empty (the `empty_map` row), in +// authored order. It lives HERE, below `std.types`, because `std.types`' own map literals elaborate +// to it; a home that imports `std.types` (v2.std.collection) would close an import cycle. +// +// The fold's last-write-wins is unobservable on every accepted program: the one writer that builds +// this call (v2.compiler.resolve, the map arm) refuses a repeated key BEFORE it builds it +// (v2.std.map_introduction map_introduction_duplicate_key). A caller that bypasses that writer and +// calls this directly with a repeated key gets insert's semantics, which is what it asked for. +type MapIntroductionEntry { + key: K + value: V +} + +fn map_from_entries(entries: FreeMonoid>) -> FinitelySupportedFunction { + fold(entries, init: empty_map(), f: fn(acc, entry) { map_insert(acc, entry.key, entry.value) }) +} + // MOVED HERE from `v2.std.collection` by the change that split `PartialFunction`. They are the // total corner of the family above, and leaving them in a different module from their own axis is // where a fork starts. `TotalPolicy` travels with `TotalMap` rather than staying behind: it is the diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index c6d11b87d10..336d95d21a3 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -14,7 +14,9 @@ import v2.std.declaration_marker { import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } import v2.extdeps.languages.dag { + dag_kernel_string_type_spelling, dag_kernel_type_binding_optional, + dag_string_literal_decoded_text, dag_kernel_type_declaration_binding_optional, dag_node_is_float_literal_atom, dag_node_is_int_literal_atom, @@ -25,7 +27,7 @@ import v2.extdeps.languages.dag { qualified_name_from_module_node } import std.algebra { Cons, Empty, FreeMonoid, list_append, list_snoc_item } -import v2.std.algebra { any, fold_list, length } +import v2.std.algebra { any, fold_list, length, skip } import v2.std.collection { Map, PointwisePower, @@ -127,6 +129,7 @@ import v2.std.node { construct_tag_marker, Instantiation, symbol_eq, + symbol_lexeme, well_formed, } 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 } @@ -141,6 +144,15 @@ import v2.std.node_query { } import std.occurrence_identity { NodeOccurrenceIdentity, OccurrenceId } import v2.std.list_introduction { list_introduction_elements_optional, list_introduction_head_path } +import v2.std.map_introduction { + MapIntroductionEntryNodes, + MapIntroductionKeyRepeated, + MapIntroductionKeysDistinct, + edge_is_map_literal_entry, + lower_map_introduction, + map_introduction_duplicate_key, + map_introduction_expected_head_is_map +} // 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 @@ -1723,8 +1735,18 @@ fn resolve_expected_edge(ctx: ResolveContext, e: Edge, x: ResolveExpectation) -> fn resolve_elided_construct_walk(ctx: ResolveContext, n: Node, x: ResolveExpectation) -> ResolveNodeWalk { match resolve_expected_head_path(ctx: ctx, position: x.position, t: x.expected) { - Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_expected_type_not_record) + Absent => + if resolve_elided_construct_is_map_literal(n: n) { + resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_expected_type_not_map) + } else { + resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_expected_type_not_record) + } Present { value: path } => + if map_introduction_expected_head_is_map(path: path) { + resolve_map_literal_walk(ctx: ctx, n: n, x: x) + } else if resolve_elided_construct_is_map_literal(n: n) { + resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_expected_type_not_map) + } else { match symbol_index_declared_payload_at(index: ctx.namespace.symbol_index, qualified_path: path) { Present { value: RecordTypePayload } => match construct_tag_optional_edge_target(n: n) { @@ -1741,9 +1763,170 @@ fn resolve_elided_construct_walk(ctx: ResolveContext, n: Node, x: ResolveExpecta } _ => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_expected_type_not_record) } + } + } +} + +// THE MAP ARM OF THE ONE ELABORATION WRITER (docs/plans/map-literal-introduction-design.md). A +// headless brace whose authored expected head is, by declaration identity, a Map +// (v2.std.map_introduction map_introduction_expected_head_is_map) elaborates to the one +// introduction v2.std.map_introduction lower_map_introduction builds, or refuses located: +// +// - the expectation must be a closed `Map` instantiation, peeled syntactically; else +// resolve_anonymous_map_expected_type_not_closed at the literal; +// - every item must be string-keyed; a name-keyed brace under a Map head refuses +// resolve_anonymous_map_name_key at its first item; +// - K must be the type a string literal denotes: the kernel String, reached unbound, by +// declaration identity (a K resolving to any declaration -- the structural v2.std.text String +// included -- is a text crossing no route declares here); else +// resolve_anonymous_map_key_kind_mismatch at the first key; +// - no key may repeat, judged by the key type's equality -- for string literals, decoded text +// (v2.extdeps.languages.dag dag_string_literal_decoded_text) -- through the ONE key law, +// v2.std.map_introduction map_introduction_duplicate_key; a repeat refuses +// resolve_anonymous_map_duplicate_key at its SECOND occurrence. Never last-wins. +// +// Each value is walked under V (the fifth position rule beside #12711's four), so a nested headless +// value elaborates through this same writer; each key is walked as the ordinary child it is. +// Nothing is unified here, and infer still checks every value against V afterwards. +fn resolve_elided_construct_is_map_literal(n: Node) -> Bool { + any(xs: skip(xs: n.children, n: 1), predicate: fn(e) { edge_is_map_literal_entry(e: e) }) +} + +fn resolve_map_literal_walk(ctx: ResolveContext, n: Node, x: ResolveExpectation) -> ResolveNodeWalk { + let items = skip(xs: n.children, n: 1) + let type_args = node_positional_child_targets(node: x.expected) + match list_at_optional(xs: type_args, index: 1) { + Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_expected_type_not_closed) + Present { value: key_type } => + match list_at_optional(xs: type_args, index: 2) { + Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_expected_type_not_closed) + Present { value: value_type } => + match resolve_map_first_name_keyed_item(items: items) { + Present { value: offending } => resolve_anonymous_record_refused(n: offending, reason: ^resolve_anonymous_map_name_key) + Absent => + if resolve_map_key_type_is_string_literal_type(ctx: ctx, position: x.position, t: key_type) { + resolve_map_literal_entries(ctx: ctx, n: n, items: items, value_x: ResolveExpectation { expected: value_type, position: x.position }) + } else { + match list_at_optional(xs: items, index: 0) { + Present { value: first } => resolve_anonymous_record_refused(n: resolve_map_entry_key(entry: first.target), reason: ^resolve_anonymous_map_key_kind_mismatch) + Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_key_kind_mismatch) + } + } + } + } + } +} + +fn resolve_map_first_name_keyed_item(items: List) -> Optional { + fold(items, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => if edge_is_map_literal_entry(e: e) { optional_absent() } else { optional_present(value: e.target) } + } + }) +} + +fn resolve_map_key_type_is_string_literal_type(ctx: ResolveContext, position: QualifiedName, t: Node) -> Bool { + match t.kind { + TypeNode { connective: Atom { identity: id } } => + match symbol_index_lexical_lookup(index: ctx.namespace.symbol_index, position: position, name: id) { + LexicalUnbound => dag_kernel_string_type_spelling(sym: id) && namespace_has_canonical_symbol(namespace: ctx.namespace, name: id) + _ => false + } + _ => false + } +} + +// An entry edge's target is a tag-elided construct carrying `key` and `value` (body lowering +// body_lower_map_literal_entry_edge); these read them back. +fn resolve_map_entry_field(entry: Node, field: Symbol) -> Optional { + fold(skip(xs: entry.children, n: 1), init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => + match e.label { + Named { name: nm } => if symbol_eq(a: nm, b: field) { optional_present(value: e.target) } else { optional_absent() } + Positional => optional_absent() + } + } + }) +} + +fn resolve_map_entry_key(entry: Node) -> Node { + match resolve_map_entry_field(entry: entry, field: ^key) { + Present { value: k } => k + Absent => entry + } +} + +fn resolve_map_key_text(key: Node) -> String { + match key.kind { + TypeNode { connective: Atom { identity: id } } => dag_string_literal_decoded_text(lexeme: symbol_lexeme(sym: id)) + _ => "" + } +} + +fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, items: List, value_x: ResolveExpectation) -> ResolveNodeWalk { + let keys = fold(items, init: [], f: fn(acc, e) { + list_snoc_item(xs: acc, item: resolve_map_key_text(key: resolve_map_entry_key(entry: e.target))) + }) + match map_introduction_duplicate_key(keys: keys) { + MapIntroductionKeyRepeated { first_index: _, second_index: second } => + match list_at_optional(xs: items, index: second) { + Present { value: e } => resolve_anonymous_record_refused(n: resolve_map_entry_key(entry: e.target), reason: ^resolve_anonymous_map_duplicate_key) + Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_duplicate_key) + } + MapIntroductionKeysDistinct => + let walked = fold(items, init: child_walk_init(), f: fn(acc, e) { + child_walk_step(w: acc, e: e, r: resolve_map_entry_walk(ctx: ctx, entry: e.target, value_x: value_x)) + }) + match child_walk_node(w: walked, n: n) { + ResolveWalkRefused { first: f, rest: r, observation: o } => ResolveWalkRefused { first: f, rest: r, observation: o } + ResolveWalkAccepted { value: resolved, diagnostics: d } => + ResolveWalkAccepted { + value: lower_map_introduction( + entries: fold(resolved.children, init: [], f: fn(acc, e) { + list_snoc_item(xs: acc, item: resolve_map_entry_nodes(entry: e.target)) + }), + source: n + ), + diagnostics: d + } + } } } +fn resolve_map_entry_nodes(entry: Node) -> MapIntroductionEntryNodes { + MapIntroductionEntryNodes { + key: resolve_map_entry_key(entry: entry), + value: match resolve_map_entry_field(entry: entry, field: ^value) { + Present { value: v } => v + Absent => entry + }, + source: entry + } +} + +// One entry: its key walked as an ordinary child, its value under V. The pair keeps its elided tag +// edge untouched; it never leaves this arm. +fn resolve_map_entry_walk(ctx: ResolveContext, entry: Node, value_x: ResolveExpectation) -> ResolveNodeWalk { + child_walk_node(n: entry, w: fold(entry.children, init: child_walk_init(), f: fn(acc, e) { + if acc.ordinal == 0 { + child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None }) + } else { + match e.label { + Named { name: nm } => + if symbol_eq(a: nm, b: ^value) { + child_walk_step(w: acc, e: e, r: resolve_expected_edge(ctx: ctx, e: e, x: value_x)) + } 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 construct_tag_optional_edge_target(n: Node) -> Optional { match list_at_optional(xs: n.children, index: 0) { Present { value: e } => optional_present(value: e.target) @@ -1962,7 +2145,11 @@ fn resolve_node_walk( resolve_transform_children(ctx: ctx, n: n) TypeNode { connective: Conj } => if construct_tag_is_elided(n: n) { - resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_no_expected_type) + if resolve_elided_construct_is_map_literal(n: n) { + resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_no_expected_type) + } else { + resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_no_expected_type) + } } else { match construct_tag_optional(n: n) { Present { value: reference } => diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 5b382538d04..013e18a0297 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -27,6 +27,7 @@ import v2.compiler.body_producer_forward { } import v2.extdeps.languages.dag { ParseSubtreeAbsent, + dag_string_literal_lexeme, ParseSubtreeFind, ParseSubtreeFound, dag_binding_branded_atom_identity, @@ -74,6 +75,7 @@ import v2.std.compilers.body_lowering { lower_unary_prefix } import v2.std.list_introduction { lower_list_introduction } +import v2.std.map_introduction { edge_is_map_literal_entry, map_literal_entry_label } import v2.std.arrow_signature { declared_signature, signature_order_edge } import v2.std.compilers.sugar { SugarSequencePair, sugar_sequence_pair_optional } import v2.std.grammar { GrammarExpr, GrammarExprFold, fold_grammar_expr } @@ -133,7 +135,9 @@ import v2.std.type_binder { cast_target_edge, type_alias_wrapper, type_decl_wrap import v2.std.node_query { node_positional_child_targets, construct_node, + construct_node_elided, construct_tag_elided_reference, + construct_tag_target_is_elided, construct_tag_optional, field_projection_node, field_projection_optional, @@ -7714,6 +7718,9 @@ fn body_lower_field_init_edge(item: Node) -> Outcome { match sugar_sequence_pair_optional(node: captured) { Absent => refuse Present { value: pair } => + match body_lower_string_key_atom_optional(node: pair.left) { + Present { value: key } => body_lower_map_literal_entry_edge(key: key, after_key: pair.right, item: item, refuse: refuse) + Absent => match body_lower_param_binding_optional(node: pair.left) { Absent => refuse Present { value: field_name } => @@ -7730,6 +7737,78 @@ fn body_lower_field_init_edge(item: Node) -> Outcome { } } } + } + } +} + +// A STRING-KEYED ITEM `"k": v` (a map literal's entry). The key is the item's sole atom when that +// atom's lexeme is of the string-literal class (v2.extdeps.languages.dag dag_string_literal_lexeme); +// it is kept as the value the literal lowers to, never read as a field name, so the key's kind +// survives to resolve (docs/plans/map-literal-introduction-design.md). +fn body_lower_string_key_atom_optional(node: Node) -> Optional { + match body_lower_value_sole_atom_optional(node: node) { + Absent => optional_absent() + Present { value: atom } => + match node_atom_identity_optional(node: atom) { + Present { value: id } => + if dag_string_literal_lexeme(lexeme: symbol_lexeme(sym: id)) { optional_present(value: atom) } else { optional_absent() } + Absent => optional_absent() + } + } +} + +// The entry edge: v2.std.map_introduction map_literal_entry_label, targeting a tag-elided construct +// carrying `key` and `value`. Only resolve's map arm reads it. +fn body_lower_map_literal_entry_edge(key: Node, after_key: Node, item: Node, refuse: Outcome) -> Outcome { + match sugar_sequence_pair_optional(node: after_key) { + Absent => refuse + Present { value: after_colon } => + match body_lower_value_read(value: after_colon.right) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: value, diagnostics: d } => + outcome_with_diagnostics( + value: Edge { + label: Named { name: map_literal_entry_label() }, + target: construct_node_elided( + field_edges: [ + Edge { label: Named { name: ^key }, target: key }, + Edge { label: Named { name: ^value }, target: value } + ], + source: item + ) + }, + diagnostics: d + ) + } + } +} + +// ONE KIND OF KEY PER BRACE, AND STRING KEYS ONLY WITHOUT A HEAD. Lowering does not choose record or +// map, but it can see a brace no choice could type: a constructor head over string keys +// (`R { "k": v }`), or a headless brace mixing name keys and string keys. Each refuses at the first +// item of the offending kind. +fn body_lower_brace_key_kinds_refusal_optional(reference: Node, edges: List) -> Optional { + let headless = construct_tag_target_is_elided(target: reference) + let first_entry = fold(edges, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => if edge_is_map_literal_entry(e: e) { optional_present(value: e) } else { optional_absent() } + } + }) + let first_field = fold(edges, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => if edge_is_map_literal_entry(e: e) { optional_absent() } else { optional_present(value: e) } + } + }) + match first_entry { + Absent => optional_absent() + Present { value: entry } => + if headless { + first_field + } else { + optional_present(value: entry) + } } } @@ -7786,10 +7865,15 @@ fn body_lower_record_construct(reference: Node, items: List, source: Node) }) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: edges, diagnostics: d } => + match body_lower_brace_key_kinds_refusal_optional(reference: reference, edges: edges) { + Present { value: offending } => + outcome_rejected(d: body_lower_diagnostic(reason: ^body_lowering_reason_brace_key_kind_mismatch, n: offending.target)) + Absent => Accepted { value: construct_node(reference: reference, field_edges: edges, source: source), diagnostics: d } + } } } @@ -7831,8 +7915,8 @@ fn body_lower_record_literal_tag_optional(captured: Node) -> Optional Outcome> { match body_lower_record_brace_items_optional(brace: captured) { Absent => outcome_accepted(value: optional_absent()) diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index c7eac2c371c..0e18dc57602 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -504,6 +504,43 @@ fn dag_string_escape_char_pattern() -> LexPattern { } } +// THE TEXT A STRING LITERAL DENOTES, read against the escape table above and nowhere else: each +// escape pair collapses to the one scalar it stands for (`\n` and a raw line feed are the same +// scalar, `\"` is `"`), every other scalar stands for itself. Two literals denote the same text +// exactly when their decoded texts are equal, which is what a spelling comparison gets wrong. The +// lexeme is decoded whole, delimiters included; they are identical on every literal, so they cannot +// separate two keys. Consumed by v2.compiler.resolve's map arm (duplicate keys, +// docs/plans/map-literal-introduction-design.md). Linear: scalars are pushed on the persistent +// list and joined once. +type DagStringLiteralDecode { + escaped: Bool + scalars: List +} + +fn dag_string_escape_decoded(c: String) -> String { + if c == "n" { "\n" } else if c == "t" { "\t" } else { c } +} + +// A lexeme of the string-literal class: it opens with the class's delimiter (char 34, the open of +// dag_string_literal_lex_rule). No other token class of this language admits that scalar outside a +// string, so the first scalar decides the class. +fn dag_string_literal_lexeme(lexeme: String) -> Bool { + starts_with(lexeme, "\"") +} + +fn dag_string_literal_decoded_text(lexeme: String) -> String { + let decoded = fold(chars(s: lexeme), init: DagStringLiteralDecode { escaped: false, scalars: [] }, f: fn(acc, c) { + if acc.escaped { + DagStringLiteralDecode { escaped: false, scalars: list_snoc_item(xs: acc.scalars, item: dag_string_escape_decoded(c: c)) } + } else if c == "\\" { + DagStringLiteralDecode { escaped: true, scalars: acc.scalars } + } else { + DagStringLiteralDecode { escaped: false, scalars: list_snoc_item(xs: acc.scalars, item: c) } + } + }) + join(decoded.scalars, "") +} + fn dag_string_escape_sequence_pattern() -> LexPattern { SequencePattern { left: CharPattern { char: 92 }, @@ -3643,6 +3680,15 @@ fn dag_kernel_type_binding_optional(sym: Symbol) -> Optional { } } +// THE KERNEL SPELLING OF THE TYPE A STRING LITERAL DENOTES: host text, the kernel `String` +// (DESIGN section 4, Ruling 3). It has no declaration and no canonical binding in the table above, +// so an unbound `String` is judged by this spelling and nowhere else; a `String` that resolves to a +// declaration (the structural v2.std.text String) is a different type, reached from a literal only +// through the declared unfold. Consumed by v2.compiler.resolve's map arm (the key-kind rule). +fn dag_kernel_string_type_spelling(sym: Symbol) -> Bool { + symbol_lexeme(sym: sym) == "String" +} + // THE DECLARATIONS A KERNEL TYPE SPELLING DENOTES. A kernel spelling that resolves, through an // import or another module's declaration, to a declaration in this table takes its canonical // binding. Resolving to any other declaration is refused rather than captured diff --git a/src/v2/std/map_introduction.dag b/src/v2/std/map_introduction.dag new file mode 100644 index 00000000000..de05bf921ec --- /dev/null +++ b/src/v2/std/map_introduction.dag @@ -0,0 +1,138 @@ +module v2.std.map_introduction + +import std.algebra { list_snoc_item } +import std.types { Map, String } +import v2.std.collection { Absent, List, Present, empty_map, map_insert, map_lookup } +import v2.std.integer { Int } +import v2.std.list_introduction { list_introduction_head_path } +import v2.std.logic { Bool } +import v2.std.node { ComputationNode, Edge, Named, Node, Positional, Symbol, Transform, node_with_occurrence_id } +import v2.std.node_query { construct_node } +import v2.std.qualified_name { QualifiedName, declaration_reference_node } + +// THE MAP-LITERAL INTRODUCTION, ONE AUTHORITY FOR ITS PATHS, ITS CONSTRUCTOR AND ITS KEY LAW +// (docs/plans/map-literal-introduction-design.md). A map literal `{ "k": v, .. }` under an authored +// `Map` is elaborated by v2.compiler.resolve's map arm into ONE ordinary call, +// +// std.algebra.map_from_entries(entries: [std.algebra.MapIntroductionEntry { key: "k", value: v }, ..]) +// +// whose head names the std.algebra declaration exactly as v2.std.list_introduction names +// std.algebra.FreeMonoid. It is a call, not a head with entries as positional children, so infer, +// eval and emit type, evaluate and realize it through the routes they already have for calls, list +// literals and constructs -- no reader arm is added downstream, which is why this module carries no +// reader. The declaration lives in std.algebra, below std.types, because std.types' own map +// literals elaborate to it (an import cycle otherwise). + +// A STRING-KEYED ITEM of a headless brace, as body lowering records it: an edge with this label +// whose target is a tag-elided construct carrying exactly `key` and `value`. Lowering records the +// key's KIND and chooses nothing; the map arm reads these edges. +fn map_literal_entry_label() -> Symbol { + ^map_literal_entry +} + +fn edge_is_map_literal_entry(e: Edge) -> Bool { + match e.label { + Named { name: n } => n == map_literal_entry_label() + Positional => false + } +} + +fn map_introduction_head_path() -> QualifiedName { + [^std, ^algebra, ^map_from_entries] +} + +fn map_introduction_entry_path() -> QualifiedName { + [^std, ^algebra, ^MapIntroductionEntry] +} + +// THE EXPECTED HEADS A MAP LITERAL ELABORATES UNDER, BY DECLARATION IDENTITY: the std.types alias a +// source writes and the carrier it names. Matching a resolved path against this list is identity, +// not alias unfolding; any other head is not a map expectation. +fn map_introduction_expected_head_is_map(path: QualifiedName) -> Bool { + path == [^std, ^types, ^Map] || path == [^std, ^algebra, ^FinitelySupportedFunction] +} + +// THE KEY LAW: the first repeated key, in authored order, as a VALUE -- never a hole, never +// last-wins. `keys` are the entries' keys already reduced to the key type's declared equality (for +// string-literal keys, the decoded text: v2.extdeps.languages.dag dag_string_literal_decoded_text), +// so equal elements are equal keys. One pass over a map from key to its first index: linear. +type MapIntroductionKeys + = MapIntroductionKeysDistinct + | MapIntroductionKeyRepeated { first_index: Int, second_index: Int } + +type MapIntroductionKeyScan { + index: Int + seen: Map + verdict: MapIntroductionKeys +} + +fn map_introduction_duplicate_key(keys: List) -> MapIntroductionKeys { + fold(keys, init: MapIntroductionKeyScan { index: 0, seen: empty_map(), verdict: MapIntroductionKeysDistinct }, f: fn(acc, key) { + match acc.verdict { + MapIntroductionKeyRepeated { first_index: _, second_index: _ } => acc + MapIntroductionKeysDistinct => + match map_lookup(m: acc.seen, key: key) { + Present { value: first } => + MapIntroductionKeyScan { + index: acc.index + 1, + seen: acc.seen, + verdict: MapIntroductionKeyRepeated { first_index: first, second_index: acc.index } + } + Absent => + MapIntroductionKeyScan { + index: acc.index + 1, + seen: map_insert(m: acc.seen, key: key, value: acc.index), + verdict: MapIntroductionKeysDistinct + } + } + } + }).verdict +} + +// One authored entry, its key and value already resolved. +type MapIntroductionEntryNodes { + key: Node + value: Node + source: Node +} + +// The entries argument: the list introduction in its RESOLVED form, its head the declaration +// reference to v2.std.list_introduction's path (the form resolve leaves an authored `[..]` in and +// infer mints for FreeMonoid), since this node is built inside resolve's output. +fn map_introduction_resolved_list(elements: List, source: Node) -> Node { + node_with_occurrence_id( + kind: ComputationNode { behavior: Transform }, + children: fold( + elements, + init: [Edge { label: Positional, target: declaration_reference_node(qn: list_introduction_head_path(), occurrence_id: source.occurrence_id) }], + f: fn(acc, element) { list_snoc_item(xs: acc, item: Edge { label: Positional, target: element }) } + ), + occurrence_id: source.occurrence_id + ) +} + +// THE CONSTRUCTOR: the call above, with the head and entry tag written as declaration references +// (the resolved form), located at the literal. +fn lower_map_introduction(entries: List, source: Node) -> Node { + let entry_nodes = fold(entries, init: [], f: fn(acc, e) { + list_snoc_item( + xs: acc, + item: construct_node( + reference: declaration_reference_node(qn: map_introduction_entry_path(), occurrence_id: e.source.occurrence_id), + field_edges: [ + Edge { label: Named { name: ^key }, target: e.key }, + Edge { label: Named { name: ^value }, target: e.value } + ], + source: e.source + ) + ) + }) + node_with_occurrence_id( + kind: ComputationNode { behavior: Transform }, + children: [ + Edge { label: Positional, target: declaration_reference_node(qn: map_introduction_head_path(), occurrence_id: source.occurrence_id) }, + Edge { label: Named { name: ^entries }, target: map_introduction_resolved_list(elements: entry_nodes, source: source) } + ], + occurrence_id: source.occurrence_id + ) +} diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag new file mode 100644 index 00000000000..913568f8e16 --- /dev/null +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -0,0 +1,238 @@ +module v2.test.claim.body_lowering.map_literal + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { infer } +import v2.compiler.name_resolve { + Admission, + ResolutionSubject, + resolution_context, + resolve_with_admission_context_policy +} +import v2.std.cross_tree.resolution { source_root_index_empty } +import v2.std.resolution_policy { default_name_resolution_policy } +import v2.compiler.normalized_tree { NormalizedTree } +import v2.compiler.program_assembly { module_roots_from_source_root_ingest } +import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } +import v2.extdeps.languages.dag { dag_language_model, dag_string_literal_decoded_text } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.cross_tree.import_model { DagTree } +import v2.std.diagnostic { Accepted, Outcome, Rejected, diagnostics_fatal_reason } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import v2.std.optional { Absent, Present } +import v2.std.node { Hash, Node, Symbol, content_hash, node_subtree_nodes } +import v2.std.node_query { node_positional_child_targets } +import v2.std.qualified_name { declaration_reference_path_optional, qualified_name_from_dotted_string } +import v2.std.map_introduction { MapIntroductionKeyRepeated, MapIntroductionKeysDistinct, map_introduction_duplicate_key, map_introduction_head_path } +import v2.std.algebra { length } +import v2.std.text { String } +import v2.std.collection { List, list_at_optional } +import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } +import extdeps.communication.medium { Lossless, Medium } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// A MAP LITERAL ELABORATES FROM ITS AUTHORED Map, OR REFUSES LOCATED. +// +// The subject is the map arm of v2.compiler.resolve's one elaboration writer +// (docs/plans/map-literal-introduction-design.md): lowering writes a headless string-keyed brace +// with map_literal_entry edges, and resolve elaborates it to the one introduction +// v2.std.map_introduction lower_map_introduction builds, or refuses. The oracle is CONTENT EQUALITY +// WITH THE HEADED SPELLING, `map_from_entries(entries: [MapIntroductionEntry { .. }, ..])`, resolved +// through the ordinary call route. +// +// THE SOURCES ARE SUPPLIED AND THE ROUTE IS REAL (tokenize, parse, normalize with body lowering, +// resolve, and infer for control 7). The providers stand in for std.types and std.algebra with +// exactly the declarations the arm matches by identity -- the Map head, its carrier, the entry +// record and the introduction -- because a fixture ingest carries only the modules it is given. The +// real std.types is the inhabitance claim, and it is the native-route census on the PR, not this +// module. +data ml_types_source: String = "module std.types\n\nimport std.algebra { FinitelySupportedFunction }\n\ntype Map = FinitelySupportedFunction\n" + +data ml_algebra_source: String = "module std.algebra\n\ntype FreeMonoid\n = Empty\n | Cons { head: T, tail: FreeMonoid }\n\ntype FinitelySupportedFunction {\n size: Int\n}\n\ntype MapIntroductionEntry {\n key: K\n value: V\n}\n\nfn map_from_entries(entries: FreeMonoid>) -> FinitelySupportedFunction {\n FinitelySupportedFunction { size: 0 }\n}\n" + +data ml_headed_source: String = "module v2.test.ml_headed\n\nimport std.types { Map }\nimport std.algebra { MapIntroductionEntry, map_from_entries }\nimport v2.std.logic { Bool }\n\ndata ml_x: Map = map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }, MapIntroductionEntry { key: \"b\", value: false }])\n" + +data ml_headless_source: String = "module v2.test.ml_headless\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_x: Map = { \"a\": true, \"b\": false }\n" + +// Distinct under the key law although they differ only in case. +data ml_distinct_source: String = "module v2.test.ml_distinct\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_d: Map = { \"a\": true, \"A\": false }\n" + +data ml_duplicate_source: String = "module v2.test.ml_duplicate\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_dup: Map = { \"a\": true, \"b\": true, \"a\": false }\n" + +// The same key spelled two ways: the escape `\n` and a raw line feed. Equal by decoded text, so a +// duplicate; a spelling comparison would admit both and let the second silently win. +data ml_escape_duplicate_source: String = "module v2.test.ml_escape_duplicate\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_esc: Map = { \"a\\nb\": true, \"a\nb\": false }\n" + +data ml_key_kind_source: String = "module v2.test.ml_key_kind\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_k: Map = { \"k\": true }\n" + +data ml_name_key_source: String = "module v2.test.ml_name_key\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_n: Map = { k: true }\n" + +data ml_call_argument_source: String = "module v2.test.ml_call_argument\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\nfn ml_take(m: Map) -> Bool { true }\n\nfn ml_call() -> Bool { ml_take(m: { \"a\": true }) }\n" + +data ml_value_mismatch_source: String = "module v2.test.ml_value_mismatch\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_bad: Map = { \"a\": 3 }\n" + +// Lowering's refusal is its own ingest: a module that fails body lowering fails the whole ingest. +data ml_mixed_source: String = "module v2.test.ml_mixed\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_m: Map = { \"a\": true, b: false }\n" + +fn ml_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: source, fidelity: Lossless }, + artifact: Artifact { kind: SourceFile, id: id, file_path: path }, + compilation_unit: unit, + source_root: DagTree + } +} + +data ml_ingest: SourceRootIngest = [ + ml_read(source: ml_types_source, id: ^ml_types_read, unit: ^ml_types_cu, path: "src/v2/test/fixture/map_literal/types.dag"), + ml_read(source: ml_algebra_source, id: ^ml_algebra_read, unit: ^ml_algebra_cu, path: "src/v2/test/fixture/map_literal/algebra.dag"), + ml_read(source: ml_headed_source, id: ^ml_headed_read, unit: ^ml_headed_cu, path: "src/v2/test/fixture/map_literal/headed.dag"), + ml_read(source: ml_headless_source, id: ^ml_headless_read, unit: ^ml_headless_cu, path: "src/v2/test/fixture/map_literal/headless.dag"), + ml_read(source: ml_distinct_source, id: ^ml_distinct_read, unit: ^ml_distinct_cu, path: "src/v2/test/fixture/map_literal/distinct.dag"), + ml_read(source: ml_duplicate_source, id: ^ml_duplicate_read, unit: ^ml_duplicate_cu, path: "src/v2/test/fixture/map_literal/duplicate.dag"), + ml_read(source: ml_escape_duplicate_source, id: ^ml_escape_duplicate_read, unit: ^ml_escape_duplicate_cu, path: "src/v2/test/fixture/map_literal/escape_duplicate.dag"), + ml_read(source: ml_key_kind_source, id: ^ml_key_kind_read, unit: ^ml_key_kind_cu, path: "src/v2/test/fixture/map_literal/key_kind.dag"), + ml_read(source: ml_name_key_source, id: ^ml_name_key_read, unit: ^ml_name_key_cu, path: "src/v2/test/fixture/map_literal/name_key.dag"), + ml_read(source: ml_call_argument_source, id: ^ml_call_argument_read, unit: ^ml_call_argument_cu, path: "src/v2/test/fixture/map_literal/call_argument.dag"), + ml_read(source: ml_value_mismatch_source, id: ^ml_value_mismatch_read, unit: ^ml_value_mismatch_cu, path: "src/v2/test/fixture/map_literal/value_mismatch.dag") +] + +data ml_mixed_ingest: SourceRootIngest = [ + ml_read(source: ml_mixed_source, id: ^ml_mixed_read, unit: ^ml_mixed_cu, path: "src/v2/test/fixture/map_literal/mixed.dag") +] + +fn ml_resolve_subject(subject: String) -> Outcome { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: roots, diagnostics: _ } => + resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { + subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: subject) }, + imports: Empty + }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) + } +} + +// Every node under the root that is a call headed by the introduction, in tree order. +fn ml_introductions(root: Node) -> List { + fold(node_subtree_nodes(root: root), init: Empty, f: fn(acc, n) { + match list_at_optional(xs: node_positional_child_targets(node: n), index: 0) { + Present { value: head } => + match declaration_reference_path_optional(node: head) { + Present { value: path } => if path == map_introduction_head_path() { list_snoc_item(xs: acc, item: n) } else { acc } + Absent => acc + } + Absent => acc + } + }) +} + +fn ml_introduction_hashes(o: Outcome) -> List { + match o { + Rejected { diagnostics: _ } => Empty + Accepted { value: t, diagnostics: _ } => + fold(ml_introductions(root: t.root), init: Empty, f: fn(acc, n) { list_snoc_item(xs: acc, item: content_hash(n: n)) }) + } +} + +fn ml_refuses_with(o: Outcome, reason: Symbol) -> Bool { + match o { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == reason + } +} + +fn ml_accepts(o: Outcome) -> Bool { + match o { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +// Control 1: the headless literal elaborates to exactly the introduction the headed call spells -- +// one of it, equal by content. +test fn ml_headless_literal_equals_the_headed_introduction_holds() -> Bool { + let headed = ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headed")) + (length(xs: headed) == 1) && (headed == ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headless"))) +} + +// Control 2: a repeated key refuses at resolve, never last-wins. +test fn ml_a_duplicate_key_refuses_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_duplicate_key) +} + +// Control 2b: the key law compares DECODED text, so an escape and the scalar it stands for collide. +test fn ml_an_escaped_and_a_raw_key_are_one_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_escape_duplicate"), reason: ^resolve_anonymous_map_duplicate_key) +} + +// Control 3: distinct keys are accepted and elaborate. +test fn ml_distinct_keys_are_accepted_holds() -> Bool { + length(xs: ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_distinct"))) == 1 +} + +// The key law itself, as a value: the FIRST repeat, with both occurrences. The mutation this +// discriminates is a law that always answers Distinct -- the fold's last-wins absorbing the repeat -- +// under which this and control 2 go red together. +test fn ml_the_key_law_names_both_occurrences_holds() -> Bool { + match map_introduction_duplicate_key(keys: ["a", "b", "a", "b"]) { + MapIntroductionKeyRepeated { first_index: f, second_index: s } => (f == 0) && (s == 2) + MapIntroductionKeysDistinct => false + } + && match map_introduction_duplicate_key(keys: ["a", "b"]) { + MapIntroductionKeysDistinct => true + MapIntroductionKeyRepeated { first_index: _, second_index: _ } => false + } +} + +// The decoder is the one the key law reads: an escape collapses to its scalar. +test fn ml_the_decoder_collapses_escapes_holds() -> Bool { + (dag_string_literal_decoded_text(lexeme: "\"a\\nb\"") == dag_string_literal_decoded_text(lexeme: "\"a\nb\"")) + && (dag_string_literal_decoded_text(lexeme: "\"a\\\\nb\"") != dag_string_literal_decoded_text(lexeme: "\"a\nb\"")) +} + +// Control 5: K is not the type a string literal denotes, so the key refuses where it is written. +test fn ml_a_key_of_the_wrong_kind_refuses_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_key_kind"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_a_name_keyed_brace_under_a_map_refuses_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_name_key) +} + +// Control 8: the expectation does not leak into a call argument. +test fn ml_a_call_argument_has_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_no_expected_type) +} + +// Control 7: resolve's choice is not proof. The good literal elaborates and infers; the Int value +// elaborates too and then infer refuses it against V = Bool. +test fn ml_infer_still_checks_an_elaborated_map_holds() -> Bool { + let bad = ml_resolve_subject(subject: "v2.test.ml_value_mismatch") + ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_headless")) && ml_accepts(o: bad) && !ml_infers(o: bad) +} + +fn ml_infers(o: Outcome) -> Bool { + match o { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + match infer(tree: t) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } +} + +// Lowering refuses a brace no key-kind choice could type, at the first item of the minority kind. +test fn ml_a_mixed_key_brace_refuses_at_lowering_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_mixed_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch + } +} diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index 85ddb2690aa..95e1bcea5ea 100644 --- a/src/v2/workflow/compile_door_cause_ownership.dag +++ b/src/v2/workflow/compile_door_cause_ownership.dag @@ -274,7 +274,49 @@ data known_frontier_causes: List = [ cause: ^resolve_anonymous_record_expected_type_not_record, grain: FatalGrain, lane: SharedSelfHostCriticalPath, - flip_trigger: "a headless record literal whose authored expected type does not resolve, by syntactic peel and the existing lexical lookup, to a single record (v2.std.symbol_index RecordTypePayload): a coproduct with several constructors, a primitive, a type binder, an optional, or a head reached only through alias unfolding resolve does not do. Refused rather than guessed (docs/plans/anonymous-record-literal-design.md, review condition 1). Flips per site when the source writes the constructor head; a Map head is the map arm (gunbc#12734), which takes that population when it lands" + flip_trigger: "a headless record literal whose authored expected type does not resolve, by syntactic peel and the existing lexical lookup, to a single record (v2.std.symbol_index RecordTypePayload): a coproduct with several constructors, a primitive, a type binder, an optional, or a head reached only through alias unfolding resolve does not do. Refused rather than guessed (docs/plans/anonymous-record-literal-design.md, review condition 1). Flips per site when the source writes the constructor head. A Map head is not in this population: it takes the map arm (resolve_anonymous_map_*)" + }, + CauseOwnership { + cause: ^resolve_anonymous_map_no_expected_type, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a headless string-keyed map literal reached by an edge no authored annotation types (a call argument, an unannotated let, a match-arm body), so v2.compiler.resolve's map arm has no Map to elaborate from and refuses rather than guess (docs/plans/map-literal-introduction-design.md). Not a frontier: each occurrence flips when its source writes an annotation" + }, + CauseOwnership { + cause: ^resolve_anonymous_map_expected_type_not_map, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a headless map literal whose authored expected type does not resolve, by declaration identity, to a Map head (v2.std.map_introduction map_introduction_expected_head_is_map): a record, a coproduct, a binder, or a head reached only by alias unfolding resolve does not do. Refused, never guessed; flips per site when the source writes a Map annotation" + }, + CauseOwnership { + cause: ^resolve_anonymous_map_expected_type_not_closed, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a headless map literal under a Map head that is not syntactically a closed Map instantiation, so K and V cannot be peeled without inference. Refused at the literal; flips per site when the annotation writes both type arguments" + }, + CauseOwnership { + cause: ^resolve_anonymous_map_name_key, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a headless brace of NAME keys under an authored Map head: a record literal where a map was declared. Refused at its first item; flips when the source writes string keys or the declared type is corrected" + }, + CauseOwnership { + cause: ^resolve_anonymous_map_key_kind_mismatch, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a map literal whose declared key type K is not the type a string literal denotes (the unbound kernel String, v2.extdeps.languages.dag dag_kernel_string_type_spelling), so a literal key would cross into K with no declared route (DESIGN section 4). Refused at the first key; flips when K is the kernel String or a declared unfold route is written" + }, + CauseOwnership { + cause: ^resolve_anonymous_map_duplicate_key, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a map literal whose keys repeat under the key type's equality (decoded text: v2.extdeps.languages.dag dag_string_literal_decoded_text), judged by the one key law v2.std.map_introduction map_introduction_duplicate_key and refused at the SECOND occurrence, never last-wins. Not a frontier: each occurrence is a source defect and flips when the repeated entry is removed" + }, + CauseOwnership { + cause: ^body_lowering_reason_brace_key_kind_mismatch, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a brace no key-kind choice could type: a constructor head over string keys, or a headless brace mixing name keys and string keys. Refused at lowering at the first item of the offending kind (docs/plans/map-literal-introduction-design.md); flips per site when the source writes one kind" }, CauseOwnership { cause: ^body_lowering_reason_function_value_body_unread, From 1ffc203e5096ad63ff76c13390fc9814839e377c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 04:00:58 +0000 Subject: [PATCH 20/63] WIP PR2: string-literal field keys refuse (delimiter authority); fixture without std.algebra --- src/v2/compiler/body_lowering_fold.dag | 20 ++++++++++++++++++- src/v2/extdeps/languages/dag.dag | 13 ++++++++++-- .../body_lowering/anonymous_record_test.dag | 16 +++++++-------- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 5b382538d04..4dacff6ac18 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -27,6 +27,7 @@ import v2.compiler.body_producer_forward { } import v2.extdeps.languages.dag { ParseSubtreeAbsent, + dag_lexeme_is_string_literal, ParseSubtreeFind, ParseSubtreeFound, dag_binding_branded_atom_identity, @@ -7702,6 +7703,23 @@ fn body_lower_postfix_expr(shell: Node) -> Outcome { // shape v2.std.node admits and the shape a fielded pattern lowers to, so `Rec { n: 1 }` and // `Rec { n: x } =>` are one shape read in two directions. A dotted head (`m.R { .. }`) is a postfix // chain and reaches the same builder, body_lower_record_construct, through its PostfixRecord step. +// A FIELD INIT'S KEY IS A NAME, read as the grammar's binding-name alternative and nothing else. +// dag_grammar_field_init_expr also admits a STRING-LITERAL key (`{ "k": v }`, a map entry), and the +// general binding reader takes that lexeme's atom for a field name -- erasing the key kind, so +// that `{ "n": true }` under a record type would elaborate as field `n`. A string key therefore +// refuses at the item here; the map arm gives it its own edge (gunbc#12734). +fn body_lower_field_init_name_optional(key: Node) -> Optional { + match body_lower_param_binding_optional(node: key) { + Present { value: name } => + if dag_lexeme_is_string_literal(sym: name) { + optional_absent() + } else { + optional_present(value: name) + } + Absent => optional_absent() + } +} + fn body_lower_field_init_edge(item: Node) -> Outcome { let stripped = body_lower_deep_unwrap_optional(node: item) let captured = match parse_production_captured_child_optional(node: stripped) { @@ -7714,7 +7732,7 @@ fn body_lower_field_init_edge(item: Node) -> Outcome { match sugar_sequence_pair_optional(node: captured) { Absent => refuse Present { value: pair } => - match body_lower_param_binding_optional(node: pair.left) { + match body_lower_field_init_name_optional(key: pair.left) { Absent => refuse Present { value: field_name } => match sugar_sequence_pair_optional(node: pair.right) { diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index c7eac2c371c..419b8a3e88e 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -525,17 +525,26 @@ fn dag_string_body_element_pattern() -> LexPattern { } } +// THE STRING LITERAL'S DELIMITER, held once: the lex rule reads it for both ends, and +// dag_lexeme_is_string_literal reads it to recover a string token's class from its lexeme once parsing +// has carried the token as a plain atom (a field-init key, whose grammar admits a name OR a string). +data dag_string_literal_delimiter_code_point: Int = 34 + fn dag_string_literal_lex_rule() -> LexRule { TokenRule { token_class: ^dag_token_string_literal, pattern: DelimitedPattern { - open: CharPattern { char: 34 }, + open: CharPattern { char: dag_string_literal_delimiter_code_point }, body: RepeatPattern { element: dag_string_body_element_pattern() }, - close: CharPattern { char: 34 } + close: CharPattern { char: dag_string_literal_delimiter_code_point } } } } +fn dag_lexeme_is_string_literal(sym: Symbol) -> Bool { + starts_with(s: symbol_lexeme(sym: sym), prefix: from_code_point(cp: dag_string_literal_delimiter_code_point)) +} + fn dag_whitespace_trivia_rule() -> LexRule { TriviaRule { pattern: OneOrMorePattern { element: CharClassPattern { class: WhitespaceChar } } diff --git a/src/v2/test/claim/body_lowering/anonymous_record_test.dag b/src/v2/test/claim/body_lowering/anonymous_record_test.dag index bb12a0c383b..8d336cd9d05 100644 --- a/src/v2/test/claim/body_lowering/anonymous_record_test.dag +++ b/src/v2/test/claim/body_lowering/anonymous_record_test.dag @@ -47,11 +47,11 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // ArA and ArB have the SAME field set, and the headless literals are annotated with ArB, the one // declared SECOND: a chooser that read the field names would pick ArA or refuse as ambiguous, and // ar_the_annotation_chooses_not_the_field_names_holds reds either way. -data ar_provider_source: String = "module v2.test.arp\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\n\ntype ArA {\n n: Bool\n m: Bool\n}\n\ntype ArB {\n n: Bool\n m: Bool\n}\n\ntype ArOuter {\n inner: ArB\n flag: Bool\n}\n\ntype ArShape\n = ArV { n: Bool }\n | ArW\n\ntype ArBox {\n top: T\n}\n\nfn ar_take(x: ArB) -> Bool { true }\n" +data ar_provider_source: String = "module v2.test.arp\n\nimport v2.std.logic { Bool }\n\ntype ArA {\n n: Bool\n m: Bool\n}\n\ntype ArB {\n n: Bool\n m: Bool\n}\n\ntype ArOuter {\n inner: ArB\n flag: Bool\n}\n\ntype ArShape\n = ArV { n: Bool }\n | ArW\n\ntype ArBox {\n top: T\n}\n\nfn ar_take(x: ArB) -> Bool { true }\n" -data ar_headed_source: String = "module v2.test.ar_headed\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\nimport v2.test.arp { ArA, ArB, ArOuter }\n\ndata ar_x: ArB = ArB { n: true, m: false }\n\ndata ar_outer: ArOuter = ArOuter { inner: ArB { n: false, m: true }, flag: true }\n\nfn ar_ret() -> ArB { ArB { n: true, m: true } }\n\nfn ar_list() -> FreeMonoid { [ArB { n: true, m: false }, ArB { n: false, m: false }] }\n" +data ar_headed_source: String = "module v2.test.ar_headed\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArA, ArB, ArOuter }\n\ndata ar_x: ArB = ArB { n: true, m: false }\n\ndata ar_outer: ArOuter = ArOuter { inner: ArB { n: false, m: true }, flag: true }\n\nfn ar_ret() -> ArB { ArB { n: true, m: true } }\n" -data ar_headless_source: String = "module v2.test.ar_headless\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\nimport v2.test.arp { ArA, ArB, ArOuter }\n\ndata ar_x: ArB = { n: true, m: false }\n\ndata ar_outer: ArOuter = { inner: { n: false, m: true }, flag: true }\n\nfn ar_ret() -> ArB { { n: true, m: true } }\n\nfn ar_list() -> FreeMonoid { [{ n: true, m: false }, { n: false, m: false }] }\n" +data ar_headless_source: String = "module v2.test.ar_headless\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArA, ArB, ArOuter }\n\ndata ar_x: ArB = { n: true, m: false }\n\ndata ar_outer: ArOuter = { inner: { n: false, m: true }, flag: true }\n\nfn ar_ret() -> ArB { { n: true, m: true } }\n" // Review condition 3: the expectation does not leak into a call argument. data ar_call_argument_source: String = "module v2.test.ar_call_argument\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArB, ar_take }\n\nfn ar_call() -> Bool { ar_take(x: { n: true, m: true }) }\n" @@ -165,12 +165,12 @@ fn ar_refuses_with(o: Outcome, reason: Symbol) -> Bool { } } -// Control 1 and 6: every headless literal -- a data initializer, a nested record field, a declared -// return, and two annotated list elements -- resolves to the SAME construct as its headed spelling, -// by content, and there are six of them, so a literal that stayed elided or vanished cannot pass. +// Control 1 and 6: every headless literal -- a data initializer, a nested record field and a declared +// return -- resolves to the SAME construct as its headed spelling, by content, and there are four +// of them, so a literal that stayed elided or vanished cannot pass. test fn ar_headless_literals_equal_their_headed_form_holds() -> Bool { let headed = ar_construct_hashes(o: ar_resolved_headed()) - (length(xs: headed) == 6) && ar_pairwise_equal(xs: headed, ys: ar_construct_hashes(o: ar_resolved_headless())) + (length(xs: headed) == 4) && ar_pairwise_equal(xs: headed, ys: ar_construct_hashes(o: ar_resolved_headless())) } // Control 5, the mutation: ArA and ArB share the field set, and ArA is declared first. The @@ -178,7 +178,7 @@ test fn ar_headless_literals_equal_their_headed_form_holds() -> Bool { // answer ArA (or refuse as ambiguous) and this goes red. test fn ar_the_annotation_chooses_not_the_field_names_holds() -> Bool { ar_construct_paths(o: ar_resolved_headless()) == [ - "v2.test.arp.ArB", "v2.test.arp.ArOuter", "v2.test.arp.ArB", "v2.test.arp.ArB", "v2.test.arp.ArB", "v2.test.arp.ArB" + "v2.test.arp.ArB", "v2.test.arp.ArOuter", "v2.test.arp.ArB", "v2.test.arp.ArB" ] } From 887bfbbfee0737ce71cd89f4b6877ddeb44e8555 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 04:05:50 +0000 Subject: [PATCH 21/63] map arm: decode on code points; shared named-edge reader (fold scope fix) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_resolve.dag | 20 ++++++++------------ src/v2/extdeps/languages/dag.dag | 21 ++++++++------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 336d95d21a3..fec22aff25a 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1840,7 +1840,13 @@ fn resolve_map_key_type_is_string_literal_type(ctx: ResolveContext, position: Qu // An entry edge's target is a tag-elided construct carrying `key` and `value` (body lowering // body_lower_map_literal_entry_edge); these read them back. fn resolve_map_entry_field(entry: Node, field: Symbol) -> Optional { - fold(skip(xs: entry.children, n: 1), init: optional_absent(), f: fn(acc, e) { + resolve_named_edge_target_optional(edges: skip(xs: entry.children, n: 1), field: field) +} + +// The target of the first edge labelled `field`. One reader for a record declaration's field list +// and a map entry's two fields. +fn resolve_named_edge_target_optional(edges: List, field: Symbol) -> Optional { + fold(edges, init: optional_absent(), f: fn(acc, e) { match acc { Present { value: _ } => acc Absent => @@ -1965,17 +1971,7 @@ fn resolve_record_field_type_optional(ctx: ResolveContext, record: QualifiedName } match member { Absent => optional_absent() - Present { value: m } => - fold(m.children, init: optional_absent(), f: fn(acc, fe) { - match acc { - Present { value: _ } => acc - Absent => - match fe.label { - Named { name: sym } => if symbol_eq(a: sym, b: field) { optional_present(value: fe.target) } else { optional_absent() } - Positional => optional_absent() - } - } - }) + Present { value: m } => resolve_named_edge_target_optional(edges: m.children, field: field) } } } diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 0e18dc57602..4f0ebe222ad 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -517,25 +517,20 @@ type DagStringLiteralDecode { scalars: List } -fn dag_string_escape_decoded(c: String) -> String { - if c == "n" { "\n" } else if c == "t" { "\t" } else { c } -} - -// A lexeme of the string-literal class: it opens with the class's delimiter (char 34, the open of -// dag_string_literal_lex_rule). No other token class of this language admits that scalar outside a -// string, so the first scalar decides the class. -fn dag_string_literal_lexeme(lexeme: String) -> Bool { - starts_with(lexeme, "\"") +// The scalar an escape's second code point stands for: `n` (110) is line feed (10), `t` (116) is +// tab (9), and every other escapable scalar (`"`, `\`, `{`, `}`) stands for itself. +fn dag_string_escape_decoded(cp: Int) -> Int { + if cp == 110 { 10 } else if cp == 116 { 9 } else { cp } } fn dag_string_literal_decoded_text(lexeme: String) -> String { - let decoded = fold(chars(s: lexeme), init: DagStringLiteralDecode { escaped: false, scalars: [] }, f: fn(acc, c) { + let decoded = fold(chars(s: lexeme), init: DagStringLiteralDecode { escaped: false, scalars: [] }, f: fn(acc, cp) { if acc.escaped { - DagStringLiteralDecode { escaped: false, scalars: list_snoc_item(xs: acc.scalars, item: dag_string_escape_decoded(c: c)) } - } else if c == "\\" { + DagStringLiteralDecode { escaped: false, scalars: list_snoc_item(xs: acc.scalars, item: from_code_point(cp: dag_string_escape_decoded(cp: cp))) } + } else if cp == 92 { DagStringLiteralDecode { escaped: true, scalars: acc.scalars } } else { - DagStringLiteralDecode { escaped: false, scalars: list_snoc_item(xs: acc.scalars, item: c) } + DagStringLiteralDecode { escaped: false, scalars: list_snoc_item(xs: acc.scalars, item: from_code_point(cp: cp)) } } }) join(decoded.scalars, "") From 31b5a600309b0258054630d1adfb5d4219541e77 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 04:17:07 +0000 Subject: [PATCH 22/63] WIP probes --- .../claim/body_lowering/map_literal_test.dag | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 913568f8e16..0c98c506058 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -236,3 +236,41 @@ test fn ml_a_mixed_key_brace_refuses_at_lowering_holds() -> Bool { Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch } } + +// PROBES (temporary, removed before review): locate where the shared failure is. +test fn ml_probe_ingest_accepts_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +test fn ml_probe_headed_resolves_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headed")) +} + +test fn ml_probe_headless_resolves_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headless")) +} + +test fn ml_probe_headed_has_introduction_holds() -> Bool { + length(xs: ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headed"))) == 1 +} + +test fn ml_probe_headless_has_introduction_holds() -> Bool { + length(xs: ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headless"))) == 1 +} + +test fn ml_probe_ingest_reason_field_init_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_field_init_unlowered + } +} + +test fn ml_probe_ingest_reason_key_kind_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch + } +} From cbe2c1702838270984423f35e9dcc4527499383c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 04:25:56 +0000 Subject: [PATCH 23/63] WIP probes 2 --- .../claim/body_lowering/map_literal_test.dag | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 0c98c506058..ae567309f7b 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -274,3 +274,91 @@ test fn ml_probe_ingest_reason_key_kind_holds() -> Bool { Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch } } + +test fn ml_probe_headed_resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_headless_resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_headed_resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_headless_resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_headed_resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_malformed_tree) +} + +test fn ml_probe_headless_resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_malformed_tree) +} + +test fn ml_probe_headed_resolve_reason_kernel_type_spelling_names_a_foreign_declaration_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_kernel_type_spelling_names_a_foreign_declaration) +} + +test fn ml_probe_headless_resolve_reason_kernel_type_spelling_names_a_foreign_declaration_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_kernel_type_spelling_names_a_foreign_declaration) +} + +test fn ml_probe_headed_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_headless_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_headed_resolve_reason_type_parameter_in_value_position_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_type_parameter_in_value_position) +} + +test fn ml_probe_headless_resolve_reason_type_parameter_in_value_position_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_type_parameter_in_value_position) +} + +test fn ml_probe_headed_resolve_reason_observation_incomplete_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_observation_incomplete) +} + +test fn ml_probe_headless_resolve_reason_observation_incomplete_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_observation_incomplete) +} + +test fn ml_probe_headed_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_headless_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_headed_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_headless_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_headed_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_headless_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_headed_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} + +test fn ml_probe_headless_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} From d1426d85f7f1cdbf318204250478b164dfbd2e7a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 04:44:59 +0000 Subject: [PATCH 24/63] WIP probes 3 --- .../claim/body_lowering/map_literal_test.dag | 189 +++++++++++------- 1 file changed, 120 insertions(+), 69 deletions(-) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index ae567309f7b..867a3647ef4 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -275,90 +275,141 @@ test fn ml_probe_ingest_reason_key_kind_holds() -> Bool { } } -test fn ml_probe_headed_resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_unbound_symbol) -} - -test fn ml_probe_headless_resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_unbound_symbol) -} - -test fn ml_probe_headed_resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_ambiguous_symbol) -} - -test fn ml_probe_headless_resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_ambiguous_symbol) -} - -test fn ml_probe_headed_resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_malformed_tree) -} - -test fn ml_probe_headless_resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_malformed_tree) -} -test fn ml_probe_headed_resolve_reason_kernel_type_spelling_names_a_foreign_declaration_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_kernel_type_spelling_names_a_foreign_declaration) -} - -test fn ml_probe_headless_resolve_reason_kernel_type_spelling_names_a_foreign_declaration_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_kernel_type_spelling_names_a_foreign_declaration) -} +data mlp_int_only_source: String = "module v2.test.mlp_int_only\n\ndata y: Int = 1\n" -test fn ml_probe_headed_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_construct_tag_not_a_constructor) -} - -test fn ml_probe_headless_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_construct_tag_not_a_constructor) -} - -test fn ml_probe_headed_resolve_reason_type_parameter_in_value_position_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_type_parameter_in_value_position) -} - -test fn ml_probe_headless_resolve_reason_type_parameter_in_value_position_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_type_parameter_in_value_position) +test fn ml_probe_v_int_only_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: [ + ml_read(source: ml_types_source, id: ^mlp_types_read_int_only, unit: ^mlp_types_cu_int_only, path: "src/v2/test/fixture/map_literal/p_types_int_only.dag"), + ml_read(source: ml_algebra_source, id: ^mlp_algebra_read_int_only, unit: ^mlp_algebra_cu_int_only, path: "src/v2/test/fixture/map_literal/p_algebra_int_only.dag"), + ml_read(source: mlp_int_only_source, id: ^mlp_int_only_read, unit: ^mlp_int_only_cu, path: "src/v2/test/fixture/map_literal/p_int_only.dag") + ], lm: dag_language_model()) { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.mlp_int_only") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } } -test fn ml_probe_headed_resolve_reason_observation_incomplete_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_observation_incomplete) -} +data mlp_str_only_source: String = "module v2.test.mlp_str_only\n\ndata z: String = \"a\"\n" -test fn ml_probe_headless_resolve_reason_observation_incomplete_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_observation_incomplete) +test fn ml_probe_v_str_only_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: [ + ml_read(source: ml_types_source, id: ^mlp_types_read_str_only, unit: ^mlp_types_cu_str_only, path: "src/v2/test/fixture/map_literal/p_types_str_only.dag"), + ml_read(source: ml_algebra_source, id: ^mlp_algebra_read_str_only, unit: ^mlp_algebra_cu_str_only, path: "src/v2/test/fixture/map_literal/p_algebra_str_only.dag"), + ml_read(source: mlp_str_only_source, id: ^mlp_str_only_read, unit: ^mlp_str_only_cu, path: "src/v2/test/fixture/map_literal/p_str_only.dag") + ], lm: dag_language_model()) { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.mlp_str_only") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } } -test fn ml_probe_headed_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_map) -} +data mlp_bool_import_source: String = "module v2.test.mlp_bool_import\n\nimport v2.std.logic { Bool }\n\ndata b: Bool = true\n" -test fn ml_probe_headless_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_map) +test fn ml_probe_v_bool_import_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: [ + ml_read(source: ml_types_source, id: ^mlp_types_read_bool_import, unit: ^mlp_types_cu_bool_import, path: "src/v2/test/fixture/map_literal/p_types_bool_import.dag"), + ml_read(source: ml_algebra_source, id: ^mlp_algebra_read_bool_import, unit: ^mlp_algebra_cu_bool_import, path: "src/v2/test/fixture/map_literal/p_algebra_bool_import.dag"), + ml_read(source: mlp_bool_import_source, id: ^mlp_bool_import_read, unit: ^mlp_bool_import_cu, path: "src/v2/test/fixture/map_literal/p_bool_import.dag") + ], lm: dag_language_model()) { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.mlp_bool_import") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } } -test fn ml_probe_headed_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_expected_type_not_record) -} +data mlp_bool_bare_source: String = "module v2.test.mlp_bool_bare\n\ndata b: Bool = true\n" -test fn ml_probe_headless_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_expected_type_not_record) +test fn ml_probe_v_bool_bare_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: [ + ml_read(source: ml_types_source, id: ^mlp_types_read_bool_bare, unit: ^mlp_types_cu_bool_bare, path: "src/v2/test/fixture/map_literal/p_types_bool_bare.dag"), + ml_read(source: ml_algebra_source, id: ^mlp_algebra_read_bool_bare, unit: ^mlp_algebra_cu_bool_bare, path: "src/v2/test/fixture/map_literal/p_algebra_bool_bare.dag"), + ml_read(source: mlp_bool_bare_source, id: ^mlp_bool_bare_read, unit: ^mlp_bool_bare_cu, path: "src/v2/test/fixture/map_literal/p_bool_bare.dag") + ], lm: dag_language_model()) { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.mlp_bool_bare") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } } -test fn ml_probe_headed_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_key_kind_mismatch) -} +data mlp_map_int_source: String = "module v2.test.mlp_map_int\n\nimport std.types { Map }\n\ndata m: Map = { \"a\": 1 }\n" -test fn ml_probe_headless_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_key_kind_mismatch) +test fn ml_probe_v_map_int_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: [ + ml_read(source: ml_types_source, id: ^mlp_types_read_map_int, unit: ^mlp_types_cu_map_int, path: "src/v2/test/fixture/map_literal/p_types_map_int.dag"), + ml_read(source: ml_algebra_source, id: ^mlp_algebra_read_map_int, unit: ^mlp_algebra_cu_map_int, path: "src/v2/test/fixture/map_literal/p_algebra_map_int.dag"), + ml_read(source: mlp_map_int_source, id: ^mlp_map_int_read, unit: ^mlp_map_int_cu, path: "src/v2/test/fixture/map_literal/p_map_int.dag") + ], lm: dag_language_model()) { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.mlp_map_int") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } } -test fn ml_probe_headed_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_closed) -} +data mlp_map_type_only_source: String = "module v2.test.mlp_map_type_only\n\nimport std.types { Map }\n\nfn f(m: Map) -> Int { 1 }\n" -test fn ml_probe_headless_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_closed) +test fn ml_probe_v_map_type_only_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: [ + ml_read(source: ml_types_source, id: ^mlp_types_read_map_type_only, unit: ^mlp_types_cu_map_type_only, path: "src/v2/test/fixture/map_literal/p_types_map_type_only.dag"), + ml_read(source: ml_algebra_source, id: ^mlp_algebra_read_map_type_only, unit: ^mlp_algebra_cu_map_type_only, path: "src/v2/test/fixture/map_literal/p_algebra_map_type_only.dag"), + ml_read(source: mlp_map_type_only_source, id: ^mlp_map_type_only_read, unit: ^mlp_map_type_only_cu, path: "src/v2/test/fixture/map_literal/p_map_type_only.dag") + ], lm: dag_language_model()) { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.mlp_map_type_only") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } } From 61a326e38cdf3d52367adbfe72517262975c539d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 04:50:44 +0000 Subject: [PATCH 25/63] map fixtures: no import outside the inline ingest --- .../claim/body_lowering/map_literal_test.dag | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 867a3647ef4..3a95fbc391f 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -51,29 +51,29 @@ data ml_types_source: String = "module std.types\n\nimport std.algebra { Finitel data ml_algebra_source: String = "module std.algebra\n\ntype FreeMonoid\n = Empty\n | Cons { head: T, tail: FreeMonoid }\n\ntype FinitelySupportedFunction {\n size: Int\n}\n\ntype MapIntroductionEntry {\n key: K\n value: V\n}\n\nfn map_from_entries(entries: FreeMonoid>) -> FinitelySupportedFunction {\n FinitelySupportedFunction { size: 0 }\n}\n" -data ml_headed_source: String = "module v2.test.ml_headed\n\nimport std.types { Map }\nimport std.algebra { MapIntroductionEntry, map_from_entries }\nimport v2.std.logic { Bool }\n\ndata ml_x: Map = map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }, MapIntroductionEntry { key: \"b\", value: false }])\n" +data ml_headed_source: String = "module v2.test.ml_headed\n\nimport std.types { Map }\nimport std.algebra { MapIntroductionEntry, map_from_entries }\n\ndata ml_x: Map = map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }, MapIntroductionEntry { key: \"b\", value: false }])\n" -data ml_headless_source: String = "module v2.test.ml_headless\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_x: Map = { \"a\": true, \"b\": false }\n" +data ml_headless_source: String = "module v2.test.ml_headless\n\nimport std.types { Map }\n\ndata ml_x: Map = { \"a\": true, \"b\": false }\n" // Distinct under the key law although they differ only in case. -data ml_distinct_source: String = "module v2.test.ml_distinct\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_d: Map = { \"a\": true, \"A\": false }\n" +data ml_distinct_source: String = "module v2.test.ml_distinct\n\nimport std.types { Map }\n\ndata ml_d: Map = { \"a\": true, \"A\": false }\n" -data ml_duplicate_source: String = "module v2.test.ml_duplicate\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_dup: Map = { \"a\": true, \"b\": true, \"a\": false }\n" +data ml_duplicate_source: String = "module v2.test.ml_duplicate\n\nimport std.types { Map }\n\ndata ml_dup: Map = { \"a\": true, \"b\": true, \"a\": false }\n" // The same key spelled two ways: the escape `\n` and a raw line feed. Equal by decoded text, so a // duplicate; a spelling comparison would admit both and let the second silently win. -data ml_escape_duplicate_source: String = "module v2.test.ml_escape_duplicate\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_esc: Map = { \"a\\nb\": true, \"a\nb\": false }\n" +data ml_escape_duplicate_source: String = "module v2.test.ml_escape_duplicate\n\nimport std.types { Map }\n\ndata ml_esc: Map = { \"a\\nb\": true, \"a\nb\": false }\n" -data ml_key_kind_source: String = "module v2.test.ml_key_kind\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_k: Map = { \"k\": true }\n" +data ml_key_kind_source: String = "module v2.test.ml_key_kind\n\nimport std.types { Map }\n\ndata ml_k: Map = { \"k\": true }\n" -data ml_name_key_source: String = "module v2.test.ml_name_key\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_n: Map = { k: true }\n" +data ml_name_key_source: String = "module v2.test.ml_name_key\n\nimport std.types { Map }\n\ndata ml_n: Map = { k: true }\n" -data ml_call_argument_source: String = "module v2.test.ml_call_argument\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\nfn ml_take(m: Map) -> Bool { true }\n\nfn ml_call() -> Bool { ml_take(m: { \"a\": true }) }\n" +data ml_call_argument_source: String = "module v2.test.ml_call_argument\n\nimport std.types { Map }\n\nfn ml_take(m: Map) -> Bool { true }\n\nfn ml_call() -> Bool { ml_take(m: { \"a\": true }) }\n" -data ml_value_mismatch_source: String = "module v2.test.ml_value_mismatch\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_bad: Map = { \"a\": 3 }\n" +data ml_value_mismatch_source: String = "module v2.test.ml_value_mismatch\n\nimport std.types { Map }\n\ndata ml_bad: Map = { \"a\": 3 }\n" // Lowering's refusal is its own ingest: a module that fails body lowering fails the whole ingest. -data ml_mixed_source: String = "module v2.test.ml_mixed\n\nimport std.types { Map }\nimport v2.std.logic { Bool }\n\ndata ml_m: Map = { \"a\": true, b: false }\n" +data ml_mixed_source: String = "module v2.test.ml_mixed\n\nimport std.types { Map }\n\ndata ml_m: Map = { \"a\": true, b: false }\n" fn ml_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceReadWitness { DagSourceReadWitness { @@ -322,7 +322,7 @@ test fn ml_probe_v_str_only_holds() -> Bool { } } -data mlp_bool_import_source: String = "module v2.test.mlp_bool_import\n\nimport v2.std.logic { Bool }\n\ndata b: Bool = true\n" +data mlp_bool_import_source: String = "module v2.test.mlp_bool_import\n\n\ndata b: Bool = true\n" test fn ml_probe_v_bool_import_holds() -> Bool { match module_roots_from_source_root_ingest(ingest: [ From 568c58ddbada9c3b0870e2b9a9e9a08de0476efa Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 04:50:58 +0000 Subject: [PATCH 26/63] WIP PR2: elision is an edge label, not an atom identity --- src/v2/std/node_query.dag | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/v2/std/node_query.dag b/src/v2/std/node_query.dag index 8031f462827..d8ae05ffbb1 100644 --- a/src/v2/std/node_query.dag +++ b/src/v2/std/node_query.dag @@ -323,13 +323,23 @@ fn construct_node(reference: Node, field_edges: List, source: Node) -> Nod } // AN ANONYMOUS RECORD LITERAL IS THE SAME CONSTRUCT WITH ITS TAG ELIDED: the marked tag edge is -// present and targets the v2.std.node construct_tag_elided_marker atom, never a guessed reference. +// present and targets a reference marked elided (construct_tag_elided_reference), never a guessed one. // Resolve's construct-tag writer replaces that target with the expected type's record reference, or // refuses (docs/plans/anonymous-record-literal-design.md). +// THE ELISION IS AN EDGE LABEL, NOT AN ATOM IDENTITY. The reference is a one-edge Conj whose label is +// v2.std.node construct_tag_elided_marker, targeting an empty Conj -- the same mechanism the tag edge +// itself uses (construct_tag_marker), which survives normalize. An Atom carrying the marker as its +// IDENTITY did not: between lowering and resolve its identity no longer compared equal, the +// elision went unrecognised, and resolve looked the marker up as a name (measured on #12740). fn construct_tag_elided_reference(source: Node) -> Node { node_with_occurrence_id( - kind: TypeNode { connective: Atom { identity: construct_tag_elided_marker() } }, - children: [], + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Named { name: construct_tag_elided_marker() }, + target: node_with_occurrence_id(kind: TypeNode { connective: Conj }, children: [], occurrence_id: source.occurrence_id) + } + ], occurrence_id: source.occurrence_id ) } @@ -340,7 +350,15 @@ fn construct_node_elided(field_edges: List, source: Node) -> Node { fn construct_tag_target_is_elided(target: Node) -> Bool { match target.kind { - TypeNode { connective: Atom { identity: id } } => symbol_eq(a: id, b: construct_tag_elided_marker()) + TypeNode { connective: Conj } => + match first(target.children) { + Present { value: e } => + match e.label { + Named { name: label } => symbol_eq(a: label, b: construct_tag_elided_marker()) + Positional => false + } + Absent => false + } _ => false } } From 4ccfbb6fee2360caa209df8e41ab6406854b1759 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 04:53:46 +0000 Subject: [PATCH 27/63] WIP probes 4 --- .../claim/body_lowering/map_literal_test.dag | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 3a95fbc391f..a868cebe1a7 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -413,3 +413,49 @@ test fn ml_probe_v_map_type_only_holds() -> Bool { } } } + +data mlp_str_type_source: String = "module v2.test.mlp_str_type\n\ndata y: Int = 1\n" + +test fn ml_probe_v_str_type_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: [ + ml_read(source: ml_types_source, id: ^mlp_types_read_str_type, unit: ^mlp_types_cu_str_type, path: "src/v2/test/fixture/map_literal/p_types_str_type.dag"), + ml_read(source: ml_algebra_source, id: ^mlp_algebra_read_str_type, unit: ^mlp_algebra_cu_str_type, path: "src/v2/test/fixture/map_literal/p_algebra_str_type.dag"), + ml_read(source: mlp_str_type_source, id: ^mlp_str_type_read, unit: ^mlp_str_type_cu, path: "src/v2/test/fixture/map_literal/p_str_type.dag") + ], lm: dag_language_model()) { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.mlp_str_type") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } +} + +data mlp_str_lit_source: String = "module v2.test.mlp_str_lit\n\ndata y: Int = 1\n" + +test fn ml_probe_v_str_lit_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: [ + ml_read(source: ml_types_source, id: ^mlp_types_read_str_lit, unit: ^mlp_types_cu_str_lit, path: "src/v2/test/fixture/map_literal/p_types_str_lit.dag"), + ml_read(source: ml_algebra_source, id: ^mlp_algebra_read_str_lit, unit: ^mlp_algebra_cu_str_lit, path: "src/v2/test/fixture/map_literal/p_algebra_str_lit.dag"), + ml_read(source: mlp_str_lit_source, id: ^mlp_str_lit_read, unit: ^mlp_str_lit_cu, path: "src/v2/test/fixture/map_literal/p_str_lit.dag") + ], lm: dag_language_model()) { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.mlp_str_lit") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } +} From 6c1230c16fa566220dea70dcd9ef3bf55d307399 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 05:02:05 +0000 Subject: [PATCH 28/63] WIP probes 4b --- src/v2/test/claim/body_lowering/map_literal_test.dag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index a868cebe1a7..8d5823d1dc5 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -414,7 +414,7 @@ test fn ml_probe_v_map_type_only_holds() -> Bool { } } -data mlp_str_type_source: String = "module v2.test.mlp_str_type\n\ndata y: Int = 1\n" +data mlp_str_type_source: String = "module v2.test.mlp_str_type\n\nfn g(s: String) -> Int { 1 }\n" test fn ml_probe_v_str_type_holds() -> Bool { match module_roots_from_source_root_ingest(ingest: [ @@ -437,7 +437,7 @@ test fn ml_probe_v_str_type_holds() -> Bool { } } -data mlp_str_lit_source: String = "module v2.test.mlp_str_lit\n\ndata y: Int = 1\n" +data mlp_str_lit_source: String = "module v2.test.mlp_str_lit\n\nfn h(x: Int) -> Int { x }\n\ndata w: Int = h(x: \"a\")\n" test fn ml_probe_v_str_lit_holds() -> Bool { match module_roots_from_source_root_ingest(ingest: [ From 0a511723f9e1d99938cf98dd22526d07fb47c882 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 05:10:50 +0000 Subject: [PATCH 29/63] map fixtures: supply the String declaration the real corpus carries --- src/v2/test/claim/body_lowering/map_literal_test.dag | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 8d5823d1dc5..16afb50d6e7 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -51,6 +51,10 @@ data ml_types_source: String = "module std.types\n\nimport std.algebra { Finitel data ml_algebra_source: String = "module std.algebra\n\ntype FreeMonoid\n = Empty\n | Cons { head: T, tail: FreeMonoid }\n\ntype FinitelySupportedFunction {\n size: Int\n}\n\ntype MapIntroductionEntry {\n key: K\n value: V\n}\n\nfn map_from_entries(entries: FreeMonoid>) -> FinitelySupportedFunction {\n FinitelySupportedFunction { size: 0 }\n}\n" +// The kernel String is bound as a canonical symbol only where some module declares it, as +// std.string_type does in the real corpus; without it a fixture's `String` is unbound. +data ml_string_type_source: String = "module std.string_type\n\ntype String = Int\n" + data ml_headed_source: String = "module v2.test.ml_headed\n\nimport std.types { Map }\nimport std.algebra { MapIntroductionEntry, map_from_entries }\n\ndata ml_x: Map = map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }, MapIntroductionEntry { key: \"b\", value: false }])\n" data ml_headless_source: String = "module v2.test.ml_headless\n\nimport std.types { Map }\n\ndata ml_x: Map = { \"a\": true, \"b\": false }\n" @@ -87,6 +91,7 @@ fn ml_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceR data ml_ingest: SourceRootIngest = [ ml_read(source: ml_types_source, id: ^ml_types_read, unit: ^ml_types_cu, path: "src/v2/test/fixture/map_literal/types.dag"), ml_read(source: ml_algebra_source, id: ^ml_algebra_read, unit: ^ml_algebra_cu, path: "src/v2/test/fixture/map_literal/algebra.dag"), + ml_read(source: ml_string_type_source, id: ^ml_string_type_read, unit: ^ml_string_type_cu, path: "src/v2/test/fixture/map_literal/string_type.dag"), ml_read(source: ml_headed_source, id: ^ml_headed_read, unit: ^ml_headed_cu, path: "src/v2/test/fixture/map_literal/headed.dag"), ml_read(source: ml_headless_source, id: ^ml_headless_read, unit: ^ml_headless_cu, path: "src/v2/test/fixture/map_literal/headless.dag"), ml_read(source: ml_distinct_source, id: ^ml_distinct_read, unit: ^ml_distinct_cu, path: "src/v2/test/fixture/map_literal/distinct.dag"), @@ -420,6 +425,7 @@ test fn ml_probe_v_str_type_holds() -> Bool { match module_roots_from_source_root_ingest(ingest: [ ml_read(source: ml_types_source, id: ^mlp_types_read_str_type, unit: ^mlp_types_cu_str_type, path: "src/v2/test/fixture/map_literal/p_types_str_type.dag"), ml_read(source: ml_algebra_source, id: ^mlp_algebra_read_str_type, unit: ^mlp_algebra_cu_str_type, path: "src/v2/test/fixture/map_literal/p_algebra_str_type.dag"), + ml_read(source: ml_string_type_source, id: ^mlp_st_read, unit: ^mlp_st_cu, path: "src/v2/test/fixture/map_literal/p_st.dag"), ml_read(source: mlp_str_type_source, id: ^mlp_str_type_read, unit: ^mlp_str_type_cu, path: "src/v2/test/fixture/map_literal/p_str_type.dag") ], lm: dag_language_model()) { Rejected { diagnostics: _ } => false From 74c1be39164ca1aa196fdc272e0ad90fdd430399 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 05:11:39 +0000 Subject: [PATCH 30/63] map fixtures: opaque String stand-in; Int-key-under-String-map control --- .../claim/body_lowering/map_literal_test.dag | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 16afb50d6e7..72643f65e0a 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -52,8 +52,10 @@ data ml_types_source: String = "module std.types\n\nimport std.algebra { Finitel data ml_algebra_source: String = "module std.algebra\n\ntype FreeMonoid\n = Empty\n | Cons { head: T, tail: FreeMonoid }\n\ntype FinitelySupportedFunction {\n size: Int\n}\n\ntype MapIntroductionEntry {\n key: K\n value: V\n}\n\nfn map_from_entries(entries: FreeMonoid>) -> FinitelySupportedFunction {\n FinitelySupportedFunction { size: 0 }\n}\n" // The kernel String is bound as a canonical symbol only where some module declares it, as -// std.string_type does in the real corpus; without it a fixture's `String` is unbound. -data ml_string_type_source: String = "module std.string_type\n\ntype String = Int\n" +// std.string_type does in the real corpus; without it a fixture's `String` is unbound. The stand-in +// is OPAQUE -- a record aliasing nothing -- so no claim here can pass because String means something +// else (ml_an_int_key_under_a_string_map_refuses_holds reds if it did). +data ml_string_type_source: String = "module std.string_type\n\ntype String {\n opaque_text: Bool\n}\n" data ml_headed_source: String = "module v2.test.ml_headed\n\nimport std.types { Map }\nimport std.algebra { MapIntroductionEntry, map_from_entries }\n\ndata ml_x: Map = map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }, MapIntroductionEntry { key: \"b\", value: false }])\n" @@ -76,6 +78,16 @@ data ml_call_argument_source: String = "module v2.test.ml_call_argument\n\nimpor data ml_value_mismatch_source: String = "module v2.test.ml_value_mismatch\n\nimport std.types { Map }\n\ndata ml_bad: Map = { \"a\": 3 }\n" +// An Int key where K = String: never a map entry, so never admitted. +data ml_int_key_source: String = "module v2.test.ml_int_key\n\nimport std.types { Map }\n\ndata ml_i: Map = { 1: true }\n" + +data ml_int_key_ingest: SourceRootIngest = [ + ml_read(source: ml_types_source, id: ^ml_ik_types_read, unit: ^ml_ik_types_cu, path: "src/v2/test/fixture/map_literal/ik_types.dag"), + ml_read(source: ml_algebra_source, id: ^ml_ik_algebra_read, unit: ^ml_ik_algebra_cu, path: "src/v2/test/fixture/map_literal/ik_algebra.dag"), + ml_read(source: ml_string_type_source, id: ^ml_ik_string_read, unit: ^ml_ik_string_cu, path: "src/v2/test/fixture/map_literal/ik_string.dag"), + ml_read(source: ml_int_key_source, id: ^ml_int_key_read, unit: ^ml_int_key_cu, path: "src/v2/test/fixture/map_literal/int_key.dag") +] + // Lowering's refusal is its own ingest: a module that fails body lowering fails the whole ingest. data ml_mixed_source: String = "module v2.test.ml_mixed\n\nimport std.types { Map }\n\ndata ml_m: Map = { \"a\": true, b: false }\n" @@ -234,6 +246,25 @@ fn ml_infers(o: Outcome) -> Bool { } } +// An Int key under Map is refused somewhere on the route -- at lowering or at resolve -- +// and never elaborates. Were String collapsed onto Int, this is the claim that would go red. +test fn ml_an_int_key_under_a_string_map_refuses_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_int_key_ingest, lm: dag_language_model()) { + Rejected { diagnostics: _ } => true + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.ml_int_key") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: _ } => true + } + } +} + // Lowering refuses a brace no key-kind choice could type, at the first item of the minority kind. test fn ml_a_mixed_key_brace_refuses_at_lowering_holds() -> Bool { match module_roots_from_source_root_ingest(ingest: ml_mixed_ingest, lm: dag_language_model()) { From b8375681c396e1ea48e51c6de0928cbe0fa76ba9 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 05:19:02 +0000 Subject: [PATCH 31/63] map_from_entries: pipe-fold form the seed types empty_map() under --- dag/std/algebra.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/std/algebra.dag b/dag/std/algebra.dag index b781c44d1f4..d9030f58e1d 100644 --- a/dag/std/algebra.dag +++ b/dag/std/algebra.dag @@ -255,7 +255,7 @@ type MapIntroductionEntry { } fn map_from_entries(entries: FreeMonoid>) -> FinitelySupportedFunction { - fold(entries, init: empty_map(), f: fn(acc, entry) { map_insert(acc, entry.key, entry.value) }) + entries |> fold(init: empty_map(), f: (acc, entry) => map_insert(acc, entry.key, entry.value)) } // MOVED HERE from `v2.std.collection` by the change that split `PartialFunction`. They are the From 80b05d1ce29923551bebe4eb8fc77fef33b1faf3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 05:26:12 +0000 Subject: [PATCH 32/63] map_from_entries returns the kernel Map spelling (seed keyed-collection expectation) --- dag/std/algebra.dag | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dag/std/algebra.dag b/dag/std/algebra.dag index d9030f58e1d..73e19cd5883 100644 --- a/dag/std/algebra.dag +++ b/dag/std/algebra.dag @@ -249,12 +249,15 @@ type FinitelySupportedFunction { // this call (v2.compiler.resolve, the map arm) refuses a repeated key BEFORE it builds it // (v2.std.map_introduction map_introduction_duplicate_key). A caller that bypasses that writer and // calls this directly with a repeated key gets insert's semantics, which is what it asked for. +// The result is spelled `Map`, the kernel container spelling this module already uses bare +// (kernel_algebra_profile_value), because the seed types `empty_map()` only under a keyed-collection +// expectation; it names this carrier (std.types Map = FinitelySupportedFunction). type MapIntroductionEntry { key: K value: V } -fn map_from_entries(entries: FreeMonoid>) -> FinitelySupportedFunction { +fn map_from_entries(entries: FreeMonoid>) -> Map { entries |> fold(init: empty_map(), f: (acc, entry) => map_insert(acc, entry.key, entry.value)) } From cfef30a72a2763e3d6acbde23993b3a63e22b84c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 05:32:46 +0000 Subject: [PATCH 33/63] WIP PR2: an elided construct is core substrate (was re-lowered to its first atom) --- src/v2/compiler/body_lowering_fold.dag | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 4dacff6ac18..ad30dbb2f39 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -135,6 +135,7 @@ import v2.std.node_query { node_positional_child_targets, construct_node, construct_tag_elided_reference, + construct_tag_is_elided, construct_tag_optional, field_projection_node, field_projection_optional, @@ -562,6 +563,7 @@ fn body_lower_is_core_substrate(node: Node) -> Bool { (match body_lower_int_literal_with_magnitude_optional(node: node) { Present { value: lit } => lit == node Absent => false }) || qualified_name_spine_shape_present(root: node) || (match construct_tag_optional(n: node) { Present { value: _ } => true Absent => false }) + || construct_tag_is_elided(n: node) || (match field_projection_optional(n: node) { Present { value: _ } => true Absent => false }) } } From 30f6e7e051968da16704ac291d3a2c179834dd7b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 05:47:09 +0000 Subject: [PATCH 34/63] Resolve committed merge marker in body_lowering_fold imports --- src/v2/compiler/body_lowering_fold.dag | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index d202d49371a..273225c61e6 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -137,11 +137,8 @@ import v2.std.node_query { construct_node, construct_node_elided, construct_tag_elided_reference, -<<<<<<< HEAD - construct_tag_target_is_elided, -======= construct_tag_is_elided, ->>>>>>> origin/session/sleek-fox-423-cut + construct_tag_target_is_elided, construct_tag_optional, field_projection_node, field_projection_optional, From 1a3d8583ab78c6b968319a5886184e0f42ffdd39 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 05:55:48 +0000 Subject: [PATCH 35/63] Regenerate the stage0 std_algebra mirror (claim_executor --required-regen candidate) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v1/stage0/src/std_algebra.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/v1/stage0/src/std_algebra.rs b/src/v1/stage0/src/std_algebra.rs index 7552cd847b6..2216f510249 100644 --- a/src/v1/stage0/src/std_algebra.rs +++ b/src/v1/stage0/src/std_algebra.rs @@ -305,6 +305,24 @@ pub struct FinitelySupportedFunction { pub _phantom: std::marker::PhantomData<(K, V)>, } +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct MapIntroductionEntry { + pub key: K, + pub value: V, + pub _phantom: std::marker::PhantomData<(K, V)>, +} + +pub fn map_from_entries( + entries: Rc>>>, +) -> Rc> { + entries.iter().cloned().fold( + v1_rt::rc_empty_map::<_, _>(), + |acc: _, entry: Rc>| { + v1_rt::rc_map_insert(acc, entry.key.clone(), entry.value.clone()) + }, + ) +} + #[derive(Clone)] pub struct TotalMap { pub lookup: Rc V>, From 924656e0e1b321902547ffb13f20e4a3775253ce Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 05:56:11 +0000 Subject: [PATCH 36/63] WIP probes --- .../claim/body_lowering/map_literal_test.dag | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 4d2652bb5a2..b7be2269f6e 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -272,3 +272,149 @@ test fn ml_a_mixed_key_brace_refuses_at_lowering_holds() -> Bool { Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch } } + +// PROBES (temporary) + +test fn ml_probe_headed_accepts_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headed")) +} + +test fn ml_probe_headed_resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_headed_resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_headed_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_headed_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_headed_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_headed_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_headed_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} + +test fn ml_probe_headed_resolve_anonymous_record_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_no_expected_type) +} + +test fn ml_probe_headed_resolve_anonymous_map_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_no_expected_type) +} + +test fn ml_probe_headed_resolve_anonymous_map_name_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_name_key) +} + +test fn ml_probe_headed_resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_malformed_tree) +} + +test fn ml_probe_headless_accepts_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headless")) +} + +test fn ml_probe_headless_resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_headless_resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_headless_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_headless_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_headless_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_headless_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_headless_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} + +test fn ml_probe_headless_resolve_anonymous_record_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_no_expected_type) +} + +test fn ml_probe_headless_resolve_anonymous_map_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_no_expected_type) +} + +test fn ml_probe_headless_resolve_anonymous_map_name_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_name_key) +} + +test fn ml_probe_headless_resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_malformed_tree) +} + +test fn ml_probe_duplicate_accepts_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_duplicate")) +} + +test fn ml_probe_duplicate_resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_duplicate_resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_duplicate_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_duplicate_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_duplicate_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_duplicate_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_duplicate_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} + +test fn ml_probe_duplicate_resolve_anonymous_record_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_record_no_expected_type) +} + +test fn ml_probe_duplicate_resolve_anonymous_map_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_no_expected_type) +} + +test fn ml_probe_duplicate_resolve_anonymous_map_name_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_name_key) +} + +test fn ml_probe_duplicate_resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_malformed_tree) +} From 142560f846d29c6a8df5aa57eb47fa44ca2fd4da Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 06:15:16 +0000 Subject: [PATCH 37/63] WIP probes ingest --- .../claim/body_lowering/map_literal_test.dag | 301 +++++++++++++----- 1 file changed, 229 insertions(+), 72 deletions(-) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index b7be2269f6e..46d5680ff45 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -275,146 +275,303 @@ test fn ml_a_mixed_key_brace_refuses_at_lowering_holds() -> Bool { // PROBES (temporary) -test fn ml_probe_headed_accepts_holds() -> Bool { - ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headed")) +test fn ml_probe_ingest_accepts_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } } -test fn ml_probe_headed_resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_unbound_symbol) +test fn ml_probe_ing_body_lowering_reason_argument_label_repeated_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_argument_label_repeated + } } -test fn ml_probe_headed_resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_ambiguous_symbol) +test fn ml_probe_ing_body_lowering_reason_arrow_domain_param_binding_repeated_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_arrow_domain_param_binding_repeated + } } -test fn ml_probe_headed_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_construct_tag_not_a_constructor) +test fn ml_probe_ing_body_lowering_reason_brace_key_kind_mismatch_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch + } } -test fn ml_probe_headed_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_map) +test fn ml_probe_ing_body_lowering_reason_call_argument_list_shape_unread_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_call_argument_list_shape_unread + } } -test fn ml_probe_headed_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_expected_type_not_record) +test fn ml_probe_ing_body_lowering_reason_call_argument_unread_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_call_argument_unread + } } -test fn ml_probe_headed_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_key_kind_mismatch) +test fn ml_probe_ing_body_lowering_reason_caret_symbol_not_lowered_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_caret_symbol_not_lowered + } } -test fn ml_probe_headed_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_closed) +test fn ml_probe_ing_body_lowering_reason_else_less_if_unlowered_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_else_less_if_unlowered + } } -test fn ml_probe_headed_resolve_anonymous_record_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_no_expected_type) +test fn ml_probe_ing_body_lowering_reason_field_decl_unlowered_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_field_decl_unlowered + } } -test fn ml_probe_headed_resolve_anonymous_map_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_no_expected_type) +test fn ml_probe_ing_body_lowering_reason_field_init_unlowered_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_field_init_unlowered + } } -test fn ml_probe_headed_resolve_anonymous_map_name_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_name_key) +test fn ml_probe_ing_body_lowering_reason_function_value_body_unread_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_function_value_body_unread + } } -test fn ml_probe_headed_resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_malformed_tree) +test fn ml_probe_ing_body_lowering_reason_function_value_shape_unread_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_function_value_shape_unread + } } -test fn ml_probe_headless_accepts_holds() -> Bool { - ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headless")) +test fn ml_probe_ing_body_lowering_reason_if_navigation_refused_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_if_navigation_refused + } } -test fn ml_probe_headless_resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_unbound_symbol) +test fn ml_probe_ing_body_lowering_reason_interface_member_unmodeled_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_interface_member_unmodeled + } } -test fn ml_probe_headless_resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_ambiguous_symbol) +test fn ml_probe_ing_body_lowering_reason_let_binding_name_absent_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_let_binding_name_absent + } } -test fn ml_probe_headless_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_construct_tag_not_a_constructor) +test fn ml_probe_ing_body_lowering_reason_let_body_not_reachable_from_node_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_let_body_not_reachable_from_node + } } -test fn ml_probe_headless_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_map) +test fn ml_probe_ing_body_lowering_reason_let_bound_value_absent_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_let_bound_value_absent + } } -test fn ml_probe_headless_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_expected_type_not_record) +test fn ml_probe_ing_body_lowering_reason_let_navigation_refused_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_let_navigation_refused + } } -test fn ml_probe_headless_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_key_kind_mismatch) +test fn ml_probe_ing_body_lowering_reason_list_element_unlowered_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_list_element_unlowered + } } -test fn ml_probe_headless_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_closed) +test fn ml_probe_ing_body_lowering_reason_list_literal_shape_unread_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_list_literal_shape_unread + } } -test fn ml_probe_headless_resolve_anonymous_record_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_no_expected_type) +test fn ml_probe_ing_body_lowering_reason_loop_navigation_refused_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_loop_navigation_refused + } } -test fn ml_probe_headless_resolve_anonymous_map_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_no_expected_type) +test fn ml_probe_ing_body_lowering_reason_match_arm_navigation_refused_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_match_arm_navigation_refused + } } -test fn ml_probe_headless_resolve_anonymous_map_name_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_name_key) +test fn ml_probe_ing_body_lowering_reason_operator_operand_unread_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_operator_operand_unread + } } -test fn ml_probe_headless_resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_malformed_tree) +test fn ml_probe_ing_body_lowering_reason_operator_tail_unread_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_operator_tail_unread + } +} + +test fn ml_probe_ing_body_lowering_reason_paren_group_unread_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_paren_group_unread + } +} + +test fn ml_probe_ing_body_lowering_reason_pattern_unlowered_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_pattern_unlowered + } +} + +test fn ml_probe_ing_body_lowering_reason_positional_argument_after_named_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_positional_argument_after_named + } +} + +test fn ml_probe_ing_body_lowering_reason_postfix_suffix_carried_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_postfix_suffix_carried + } +} + +test fn ml_probe_ing_body_lowering_reason_realization_member_set_aside_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_realization_member_set_aside + } +} + +test fn ml_probe_ing_body_lowering_reason_service_io_block_repeated_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_service_io_block_repeated + } +} + +test fn ml_probe_ing_body_lowering_reason_service_member_unread_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_service_member_unread + } } -test fn ml_probe_duplicate_accepts_holds() -> Bool { - ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_duplicate")) +test fn ml_probe_ing_body_lowering_reason_service_operation_repeated_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_service_operation_repeated + } } -test fn ml_probe_duplicate_resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_unbound_symbol) +test fn ml_probe_ing_body_lowering_reason_service_realization_unreachable_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_service_realization_unreachable + } } -test fn ml_probe_duplicate_resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_ambiguous_symbol) +test fn ml_probe_ing_body_lowering_reason_statement_precedes_without_binding_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_statement_precedes_without_binding + } } -test fn ml_probe_duplicate_resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_construct_tag_not_a_constructor) +test fn ml_probe_ing_body_lowering_reason_statement_spine_headless_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_statement_spine_headless + } } -test fn ml_probe_duplicate_resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_expected_type_not_map) +test fn ml_probe_ing_body_lowering_reason_test_marker_not_test_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_test_marker_not_test + } } -test fn ml_probe_duplicate_resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_record_expected_type_not_record) +test fn ml_probe_ing_body_lowering_reason_type_annotation_not_carried_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_type_annotation_not_carried + } } -test fn ml_probe_duplicate_resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_key_kind_mismatch) +test fn ml_probe_ing_body_lowering_reason_type_decl_member_uncarried_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_type_decl_member_uncarried + } } -test fn ml_probe_duplicate_resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_expected_type_not_closed) +test fn ml_probe_ing_body_lowering_reason_type_decl_unreadable_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_type_decl_unreadable + } } -test fn ml_probe_duplicate_resolve_anonymous_record_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_record_no_expected_type) +test fn ml_probe_ing_body_lowering_reason_type_params_unreadable_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_type_params_unreadable + } } -test fn ml_probe_duplicate_resolve_anonymous_map_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_no_expected_type) +test fn ml_probe_ing_body_lowering_reason_unsupported_form_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_unsupported_form + } } -test fn ml_probe_duplicate_resolve_anonymous_map_name_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_name_key) +test fn ml_probe_ing_body_lowering_reason_value_unlowered_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_value_unlowered + } } -test fn ml_probe_duplicate_resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_malformed_tree) +test fn ml_probe_ing_normalize_reason_post_normalize_not_well_formed_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^normalize_reason_post_normalize_not_well_formed + } } From d0abac4804f53e1bb426cb2debab94a8e782887d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 06:26:56 +0000 Subject: [PATCH 38/63] map literal: pairs on one map_literal_entries edge (a construct's named edges are distinct) --- src/v2/compiler/03_resolve.dag | 27 +- src/v2/compiler/body_lowering_fold.dag | 20 +- src/v2/std/map_introduction.dag | 35 +- .../claim/body_lowering/map_literal_test.dag | 305 +----------------- 4 files changed, 68 insertions(+), 319 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 47a1faafe8a..19dfe72e33d 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -148,7 +148,8 @@ import v2.std.map_introduction { MapIntroductionEntryNodes, MapIntroductionKeyRepeated, MapIntroductionKeysDistinct, - edge_is_map_literal_entry, + edge_is_map_literal_entries, + map_literal_entries_optional, lower_map_introduction, map_introduction_duplicate_key, map_introduction_expected_head_is_map @@ -1789,11 +1790,20 @@ fn resolve_elided_construct_walk(ctx: ResolveContext, n: Node, x: ResolveExpecta // value elaborates through this same writer; each key is walked as the ordinary child it is. // Nothing is unified here, and infer still checks every value against V afterwards. fn resolve_elided_construct_is_map_literal(n: Node) -> Bool { - any(xs: skip(xs: n.children, n: 1), predicate: fn(e) { edge_is_map_literal_entry(e: e) }) + any(xs: skip(xs: n.children, n: 1), predicate: fn(e) { edge_is_map_literal_entries(e: e) }) +} + +// The pairs as positional edges, in authored order; none for `{}`. +fn resolve_map_literal_items(n: Node) -> List { + match map_literal_entries_optional(n: n) { + Absent => [] + Present { value: pairs } => + fold(pairs, init: [], f: fn(acc, p) { list_snoc_item(xs: acc, item: Edge { label: Positional, target: p }) }) + } } fn resolve_map_literal_walk(ctx: ResolveContext, n: Node, x: ResolveExpectation) -> ResolveNodeWalk { - let items = skip(xs: n.children, n: 1) + let items = resolve_map_literal_items(n: n) let type_args = node_positional_child_targets(node: x.expected) match list_at_optional(xs: type_args, index: 1) { Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_expected_type_not_closed) @@ -1801,7 +1811,7 @@ fn resolve_map_literal_walk(ctx: ResolveContext, n: Node, x: ResolveExpectation) match list_at_optional(xs: type_args, index: 2) { Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_expected_type_not_closed) Present { value: value_type } => - match resolve_map_first_name_keyed_item(items: items) { + match resolve_map_first_name_keyed_item(items: skip(xs: n.children, n: 1)) { Present { value: offending } => resolve_anonymous_record_refused(n: offending, reason: ^resolve_anonymous_map_name_key) Absent => if resolve_map_key_type_is_string_literal_type(ctx: ctx, position: x.position, t: key_type) { @@ -1821,7 +1831,7 @@ fn resolve_map_first_name_keyed_item(items: List) -> Optional { fold(items, init: optional_absent(), f: fn(acc, e) { match acc { Present { value: _ } => acc - Absent => if edge_is_map_literal_entry(e: e) { optional_absent() } else { optional_present(value: e.target) } + Absent => if edge_is_map_literal_entries(e: e) { optional_absent() } else { optional_present(value: e.target) } } }) } @@ -1837,7 +1847,7 @@ fn resolve_map_key_type_is_string_literal_type(ctx: ResolveContext, position: Qu } } -// An entry edge's target is a tag-elided construct carrying `key` and `value` (body lowering +// A pair is a tag-elided construct carrying `key` and `value` (body lowering // body_lower_map_literal_entry_edge); these read them back. fn resolve_map_entry_field(entry: Node, field: Symbol) -> Optional { resolve_named_edge_target_optional(edges: skip(xs: entry.children, n: 1), field: field) @@ -1885,7 +1895,10 @@ fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, items: List, let walked = fold(items, init: child_walk_init(), f: fn(acc, e) { child_walk_step(w: acc, e: e, r: resolve_map_entry_walk(ctx: ctx, entry: e.target, value_x: value_x)) }) - match child_walk_node(w: walked, n: n) { + // The pairs are walked under a Transform carrier (positional edges only) that never leaves + // this arm; only its resolved children are read. + let carrier = node_with_occurrence_id(kind: ComputationNode { behavior: Transform }, children: [], occurrence_id: n.occurrence_id) + match child_walk_node(w: walked, n: carrier) { ResolveWalkRefused { first: f, rest: r, observation: o } => ResolveWalkRefused { first: f, rest: r, observation: o } ResolveWalkAccepted { value: resolved, diagnostics: d } => ResolveWalkAccepted { diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 273225c61e6..b189be5c7cc 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -75,7 +75,7 @@ import v2.std.compilers.body_lowering { lower_unary_prefix } import v2.std.list_introduction { lower_list_introduction } -import v2.std.map_introduction { edge_is_map_literal_entry, map_literal_entry_label } +import v2.std.map_introduction { edge_is_map_literal_entry, map_literal_entries_label, map_literal_entry_label } import v2.std.arrow_signature { declared_signature, signature_order_edge } import v2.std.compilers.sugar { SugarSequencePair, sugar_sequence_pair_optional } import v2.std.grammar { GrammarExpr, GrammarExprFold, fold_grammar_expr } @@ -7802,6 +7802,20 @@ fn body_lower_map_literal_entry_edge(key: Node, after_key: Node, item: Node, ref } } +// A brace already known to be all string keys (body_lower_brace_key_kinds_refusal_optional ran +// first) carries its pairs on ONE edge, a list literal of them in authored order; any other brace +// is returned as it is. +fn body_lower_map_literal_entries_collapsed(edges: List, source: Node) -> List { + let pairs = fold(edges, init: [], f: fn(acc, e) { + if edge_is_map_literal_entry(e: e) { list_snoc_item(xs: acc, item: e.target) } else { acc } + }) + match pairs { + Empty => edges + Cons { head: _, tail: _ } => + [Edge { label: Named { name: map_literal_entries_label() }, target: lower_list_introduction(elements: pairs, source: source) }] + } +} + // ONE KIND OF KEY PER BRACE, AND STRING KEYS ONLY WITHOUT A HEAD. Lowering does not choose record or // map, but it can see a brace no choice could type: a constructor head over string keys // (`R { "k": v }`), or a headless brace mixing name keys and string keys. Each refuses at the first @@ -7889,7 +7903,7 @@ fn body_lower_record_construct(reference: Node, items: List, source: Node) outcome_rejected(d: body_lower_diagnostic(reason: ^body_lowering_reason_brace_key_kind_mismatch, n: offending.target)) Absent => Accepted { - value: construct_node(reference: reference, field_edges: edges, source: source), + value: construct_node(reference: reference, field_edges: body_lower_map_literal_entries_collapsed(edges: edges, source: source), source: source), diagnostics: d } } @@ -7934,7 +7948,7 @@ fn body_lower_record_literal_tag_optional(captured: Node) -> Optional Outcome> { match body_lower_record_brace_items_optional(brace: captured) { diff --git a/src/v2/std/map_introduction.dag b/src/v2/std/map_introduction.dag index de05bf921ec..d0a54766795 100644 --- a/src/v2/std/map_introduction.dag +++ b/src/v2/std/map_introduction.dag @@ -2,9 +2,10 @@ module v2.std.map_introduction import std.algebra { list_snoc_item } import std.types { Map, String } -import v2.std.collection { Absent, List, Present, empty_map, map_insert, map_lookup } +import v2.std.collection { List, empty_map, map_insert, map_lookup } +import v2.std.optional { Absent, Optional, Present, optional_absent } import v2.std.integer { Int } -import v2.std.list_introduction { list_introduction_head_path } +import v2.std.list_introduction { list_introduction_elements_optional, list_introduction_head_path } import v2.std.logic { Bool } import v2.std.node { ComputationNode, Edge, Named, Node, Positional, Symbol, Transform, node_with_occurrence_id } import v2.std.node_query { construct_node } @@ -23,9 +24,12 @@ import v2.std.qualified_name { QualifiedName, declaration_reference_node } // reader. The declaration lives in std.algebra, below std.types, because std.types' own map // literals elaborate to it (an import cycle otherwise). -// A STRING-KEYED ITEM of a headless brace, as body lowering records it: an edge with this label -// whose target is a tag-elided construct carrying exactly `key` and `value`. Lowering records the -// key's KIND and chooses nothing; the map arm reads these edges. +// A STRING-KEYED BRACE, as body lowering records it. Each item is first an edge labelled +// map_literal_entry_label targeting a tag-elided `{ key, value }` construct; once the brace is +// known to be all string keys, lowering collapses those items into ONE edge labelled +// map_literal_entries_label whose target is a list literal of the pairs -- one edge, because a +// construct's named edges must be distinct (v2.std.node well_formed). Lowering records the key's +// KIND and chooses nothing; resolve's map arm reads the list. fn map_literal_entry_label() -> Symbol { ^map_literal_entry } @@ -37,6 +41,27 @@ fn edge_is_map_literal_entry(e: Edge) -> Bool { } } +fn map_literal_entries_label() -> Symbol { + ^map_literal_entries +} + +fn edge_is_map_literal_entries(e: Edge) -> Bool { + match e.label { + Named { name: n } => n == map_literal_entries_label() + Positional => false + } +} + +// The authored pairs of a string-keyed brace, in order; Absent for any node that carries none. +fn map_literal_entries_optional(n: Node) -> Optional> { + fold(n.children, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => if edge_is_map_literal_entries(e: e) { list_introduction_elements_optional(node: e.target) } else { optional_absent() } + } + }) +} + fn map_introduction_head_path() -> QualifiedName { [^std, ^algebra, ^map_from_entries] } diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 46d5680ff45..3ede6156c32 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -36,7 +36,7 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // // The subject is the map arm of v2.compiler.resolve's one elaboration writer // (docs/plans/map-literal-introduction-design.md): lowering writes a headless string-keyed brace -// with map_literal_entry edges, and resolve elaborates it to the one introduction +// with one map_literal_entries edge, and resolve elaborates it to the one introduction // v2.std.map_introduction lower_map_introduction builds, or refuses. The oracle is CONTENT EQUALITY // WITH THE HEADED SPELLING, `map_from_entries(entries: [MapIntroductionEntry { .. }, ..])`, resolved // through the ordinary call route. @@ -272,306 +272,3 @@ test fn ml_a_mixed_key_brace_refuses_at_lowering_holds() -> Bool { Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch } } - -// PROBES (temporary) - -test fn ml_probe_ingest_accepts_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -test fn ml_probe_ing_body_lowering_reason_argument_label_repeated_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_argument_label_repeated - } -} - -test fn ml_probe_ing_body_lowering_reason_arrow_domain_param_binding_repeated_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_arrow_domain_param_binding_repeated - } -} - -test fn ml_probe_ing_body_lowering_reason_brace_key_kind_mismatch_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch - } -} - -test fn ml_probe_ing_body_lowering_reason_call_argument_list_shape_unread_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_call_argument_list_shape_unread - } -} - -test fn ml_probe_ing_body_lowering_reason_call_argument_unread_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_call_argument_unread - } -} - -test fn ml_probe_ing_body_lowering_reason_caret_symbol_not_lowered_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_caret_symbol_not_lowered - } -} - -test fn ml_probe_ing_body_lowering_reason_else_less_if_unlowered_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_else_less_if_unlowered - } -} - -test fn ml_probe_ing_body_lowering_reason_field_decl_unlowered_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_field_decl_unlowered - } -} - -test fn ml_probe_ing_body_lowering_reason_field_init_unlowered_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_field_init_unlowered - } -} - -test fn ml_probe_ing_body_lowering_reason_function_value_body_unread_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_function_value_body_unread - } -} - -test fn ml_probe_ing_body_lowering_reason_function_value_shape_unread_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_function_value_shape_unread - } -} - -test fn ml_probe_ing_body_lowering_reason_if_navigation_refused_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_if_navigation_refused - } -} - -test fn ml_probe_ing_body_lowering_reason_interface_member_unmodeled_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_interface_member_unmodeled - } -} - -test fn ml_probe_ing_body_lowering_reason_let_binding_name_absent_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_let_binding_name_absent - } -} - -test fn ml_probe_ing_body_lowering_reason_let_body_not_reachable_from_node_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_let_body_not_reachable_from_node - } -} - -test fn ml_probe_ing_body_lowering_reason_let_bound_value_absent_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_let_bound_value_absent - } -} - -test fn ml_probe_ing_body_lowering_reason_let_navigation_refused_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_let_navigation_refused - } -} - -test fn ml_probe_ing_body_lowering_reason_list_element_unlowered_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_list_element_unlowered - } -} - -test fn ml_probe_ing_body_lowering_reason_list_literal_shape_unread_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_list_literal_shape_unread - } -} - -test fn ml_probe_ing_body_lowering_reason_loop_navigation_refused_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_loop_navigation_refused - } -} - -test fn ml_probe_ing_body_lowering_reason_match_arm_navigation_refused_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_match_arm_navigation_refused - } -} - -test fn ml_probe_ing_body_lowering_reason_operator_operand_unread_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_operator_operand_unread - } -} - -test fn ml_probe_ing_body_lowering_reason_operator_tail_unread_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_operator_tail_unread - } -} - -test fn ml_probe_ing_body_lowering_reason_paren_group_unread_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_paren_group_unread - } -} - -test fn ml_probe_ing_body_lowering_reason_pattern_unlowered_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_pattern_unlowered - } -} - -test fn ml_probe_ing_body_lowering_reason_positional_argument_after_named_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_positional_argument_after_named - } -} - -test fn ml_probe_ing_body_lowering_reason_postfix_suffix_carried_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_postfix_suffix_carried - } -} - -test fn ml_probe_ing_body_lowering_reason_realization_member_set_aside_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_realization_member_set_aside - } -} - -test fn ml_probe_ing_body_lowering_reason_service_io_block_repeated_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_service_io_block_repeated - } -} - -test fn ml_probe_ing_body_lowering_reason_service_member_unread_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_service_member_unread - } -} - -test fn ml_probe_ing_body_lowering_reason_service_operation_repeated_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_service_operation_repeated - } -} - -test fn ml_probe_ing_body_lowering_reason_service_realization_unreachable_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_service_realization_unreachable - } -} - -test fn ml_probe_ing_body_lowering_reason_statement_precedes_without_binding_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_statement_precedes_without_binding - } -} - -test fn ml_probe_ing_body_lowering_reason_statement_spine_headless_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_statement_spine_headless - } -} - -test fn ml_probe_ing_body_lowering_reason_test_marker_not_test_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_test_marker_not_test - } -} - -test fn ml_probe_ing_body_lowering_reason_type_annotation_not_carried_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_type_annotation_not_carried - } -} - -test fn ml_probe_ing_body_lowering_reason_type_decl_member_uncarried_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_type_decl_member_uncarried - } -} - -test fn ml_probe_ing_body_lowering_reason_type_decl_unreadable_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_type_decl_unreadable - } -} - -test fn ml_probe_ing_body_lowering_reason_type_params_unreadable_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_type_params_unreadable - } -} - -test fn ml_probe_ing_body_lowering_reason_unsupported_form_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_unsupported_form - } -} - -test fn ml_probe_ing_body_lowering_reason_value_unlowered_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_value_unlowered - } -} - -test fn ml_probe_ing_normalize_reason_post_normalize_not_well_formed_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^normalize_reason_post_normalize_not_well_formed - } -} From 83a838e9ad214d7a548ff7a23ecba03e4df8749b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 06:35:53 +0000 Subject: [PATCH 39/63] WIP probes --- .../claim/body_lowering/map_literal_test.dag | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 3ede6156c32..f9f48b48de7 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -272,3 +272,320 @@ test fn ml_a_mixed_key_brace_refuses_at_lowering_holds() -> Bool { Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch } } + +// PROBES (temporary) + +test fn ml_probe_ingest_accepts_holds() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +test fn ml_probe_headed_accepts_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headed")) +} + +test fn ml_probe_headed__resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_headed__resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_headed__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_headed__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_headed__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_headed__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_headed__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} + +test fn ml_probe_headed__resolve_anonymous_record_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_no_expected_type) +} + +test fn ml_probe_headed__resolve_anonymous_map_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_no_expected_type) +} + +test fn ml_probe_headed__resolve_anonymous_map_name_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_name_key) +} + +test fn ml_probe_headed__resolve_anonymous_map_duplicate_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_duplicate_key) +} + +test fn ml_probe_headed__resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_malformed_tree) +} + +test fn ml_probe_headed__resolve_reason_observation_incomplete_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_observation_incomplete) +} + +test fn ml_probe_headed__resolve_reason_type_parameter_in_value_position_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_type_parameter_in_value_position) +} + +test fn ml_probe_headless_accepts_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headless")) +} + +test fn ml_probe_headless__resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_headless__resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_headless__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_headless__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_headless__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_headless__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_headless__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} + +test fn ml_probe_headless__resolve_anonymous_record_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_no_expected_type) +} + +test fn ml_probe_headless__resolve_anonymous_map_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_no_expected_type) +} + +test fn ml_probe_headless__resolve_anonymous_map_name_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_name_key) +} + +test fn ml_probe_headless__resolve_anonymous_map_duplicate_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_duplicate_key) +} + +test fn ml_probe_headless__resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_malformed_tree) +} + +test fn ml_probe_headless__resolve_reason_observation_incomplete_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_observation_incomplete) +} + +test fn ml_probe_headless__resolve_reason_type_parameter_in_value_position_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_type_parameter_in_value_position) +} + +test fn ml_probe_duplicate_accepts_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_duplicate")) +} + +test fn ml_probe_duplicate__resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_duplicate__resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_duplicate__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_duplicate__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_duplicate__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_duplicate__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_duplicate__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} + +test fn ml_probe_duplicate__resolve_anonymous_record_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_record_no_expected_type) +} + +test fn ml_probe_duplicate__resolve_anonymous_map_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_no_expected_type) +} + +test fn ml_probe_duplicate__resolve_anonymous_map_name_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_name_key) +} + +test fn ml_probe_duplicate__resolve_anonymous_map_duplicate_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_duplicate_key) +} + +test fn ml_probe_duplicate__resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_malformed_tree) +} + +test fn ml_probe_duplicate__resolve_reason_observation_incomplete_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_observation_incomplete) +} + +test fn ml_probe_duplicate__resolve_reason_type_parameter_in_value_position_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_type_parameter_in_value_position) +} + +test fn ml_probe_name_key_accepts_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_name_key")) +} + +test fn ml_probe_name_key__resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_name_key__resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_name_key__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_name_key__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_name_key__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_name_key__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_name_key__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} + +test fn ml_probe_name_key__resolve_anonymous_record_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_record_no_expected_type) +} + +test fn ml_probe_name_key__resolve_anonymous_map_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_no_expected_type) +} + +test fn ml_probe_name_key__resolve_anonymous_map_name_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_name_key) +} + +test fn ml_probe_name_key__resolve_anonymous_map_duplicate_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_duplicate_key) +} + +test fn ml_probe_name_key__resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_malformed_tree) +} + +test fn ml_probe_name_key__resolve_reason_observation_incomplete_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_observation_incomplete) +} + +test fn ml_probe_name_key__resolve_reason_type_parameter_in_value_position_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_type_parameter_in_value_position) +} + +test fn ml_probe_call_argument_accepts_holds() -> Bool { + ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_call_argument")) +} + +test fn ml_probe_call_argument__resolve_reason_unbound_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_unbound_symbol) +} + +test fn ml_probe_call_argument__resolve_reason_ambiguous_symbol_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_ambiguous_symbol) +} + +test fn ml_probe_call_argument__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_construct_tag_not_a_constructor) +} + +test fn ml_probe_call_argument__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_expected_type_not_map) +} + +test fn ml_probe_call_argument__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +test fn ml_probe_call_argument__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_key_kind_mismatch) +} + +test fn ml_probe_call_argument__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_expected_type_not_closed) +} + +test fn ml_probe_call_argument__resolve_anonymous_record_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_record_no_expected_type) +} + +test fn ml_probe_call_argument__resolve_anonymous_map_no_expected_type_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_no_expected_type) +} + +test fn ml_probe_call_argument__resolve_anonymous_map_name_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_name_key) +} + +test fn ml_probe_call_argument__resolve_anonymous_map_duplicate_key_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_duplicate_key) +} + +test fn ml_probe_call_argument__resolve_reason_malformed_tree_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_malformed_tree) +} + +test fn ml_probe_call_argument__resolve_reason_observation_incomplete_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_observation_incomplete) +} + +test fn ml_probe_call_argument__resolve_reason_type_parameter_in_value_position_holds() -> Bool { + ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_type_parameter_in_value_position) +} + +test fn ml_probe_headed_intro_count_holds() -> Bool { + length(xs: ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headed"))) == 1 +} + +test fn ml_probe_headless_intro_count_holds() -> Bool { + length(xs: ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headless"))) == 1 +} From ccf96d4ffee97bc3c908c08951f76db0dbef625a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 06:44:36 +0000 Subject: [PATCH 40/63] WIP PR2: condition 2 as a parity claim; rfm row for infer's untyped construct field values; enrol shared producers --- ...t_typed_against_the_record_declaration.dag | 25 ++++ .../body_lowering/anonymous_record_test.dag | 43 +++++-- src/v2/test/zz_probe/ar_debug.dag | 116 ++++++++++++++++++ src/v2/test/zz_probe/probe.dag | 113 +++++++++++++++++ src/v2/workflow/floor_pure_producer_share.dag | 8 ++ 5 files changed, 292 insertions(+), 13 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/construct_field_value_not_typed_against_the_record_declaration.dag create mode 100644 src/v2/test/zz_probe/ar_debug.dag create mode 100644 src/v2/test/zz_probe/probe.dag diff --git a/dag/gunbc/recurring_failure_mode/construct_field_value_not_typed_against_the_record_declaration.dag b/dag/gunbc/recurring_failure_mode/construct_field_value_not_typed_against_the_record_declaration.dag new file mode 100644 index 00000000000..8fc1dcc5948 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/construct_field_value_not_typed_against_the_record_declaration.dag @@ -0,0 +1,25 @@ +module gunbc.recurring_failure_mode.construct_field_value_not_typed_against_the_record_declaration + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data construct_field_value_not_typed_against_the_record_declaration: RecurringFailureMode = RecurringFailureMode { + identity: "construct_field_value_not_typed_against_the_record_declaration" as NonEmptyStr, + + receipts: [ + "INVALID STATE: a record construct whose field value does not inhabit that field's declared type is ACCEPTED by v2.compiler.infer. `type ArB { n: Bool, m: Bool }` with `data ar_bad: ArB = ArB { n: true, m: 3 }` gets no refusal: `m` holds an Int where the record declares a Bool.", + "SPECIMEN, measured by execution (2026-09-30, sleek-fox-423, gunbc#12740 head): the fixture v2.test.claim.body_lowering.anonymous_record ar_value_mismatch_headed_source, resolved through v2.compiler.name_resolve resolve_with_admission_context_policy and handed to v2.compiler.infer infer. Resolve accepts, and the construct's tag is the declaration reference v2.test.arp.ArB. Infer then answers Accepted. The headless spelling `data ar_bad: ArB = { n: true, m: 3 }` gets the same verdict after resolve elaborates its tag, so the gap is in infer's construct typing, and not in how the tag was written.", + "WHY INFER DOES NOT SEE IT: infer is a bottom-up fold. Its declared-type judgments compare an already-derived type with a declaration at two sites only: call arguments (position_direct_call_argument), and an Arrow body against a declared return that the language join denotes (infer_arrow_body_inhabits_declared_return reads dag_binding_denotation, so a user record return type is not judged). No site takes a construct's resolved record declaration and judges each field value against that field's declared type.", + "HARM: below the ordinary compiler floor. DESIGN section 4b names 'values inhabit declared types' among the floor properties whose failure is a below-baseline regression, and a Bool field holding an Int is exactly that failure. It is silent: no diagnostic is produced, and a downstream emitter or evaluator receives a construct its declaration says cannot exist.", + "RUNG FOUND AT: below the ladder (silent wrongness) on the v2 native route. CEILING: structurally guaranteed. The field's declared type and the resolved construct tag are both modeled facts, so infer can derive the refusal from structure. NEXT-RUNG TRIGGER: infer types construct field values against the record declaration. The discriminating RED is the specimen above; the positive control is the same record with `m: false`.", + "RELATED, NOT THE SAME CLASS: zero_field_construction_escapes_the_field_set_check (a missing field, measured on the v1 path) and optional_admitted_where_a_required_value_is_declared (optionality at a Required position). This row is a PRESENT field whose value is of the wrong type, on the v2 route.", + "ENROLLED EVIDENCE: v2.test.claim.body_lowering.anonymous_record ar_infer_verdict_is_the_headed_forms_holds asserts that an elaborated construct gets exactly the headed form's infer verdict, including on this mismatch. It holds today with both Accepted. It keeps holding when the trigger lands and both refuse, so it neither pins the gap nor goes vacuous when the gap closes." + ], + + evidence: [ + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_arrow_body_inhabits_declared_return", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.infer", decl_name: "infer_arrow_declared_return_type", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.body_lowering.anonymous_record", decl_name: "ar_infer_verdict_is_the_headed_forms_holds", field: WholeDeclaration }, + ], +} diff --git a/src/v2/test/claim/body_lowering/anonymous_record_test.dag b/src/v2/test/claim/body_lowering/anonymous_record_test.dag index 8d336cd9d05..cf826f3b070 100644 --- a/src/v2/test/claim/body_lowering/anonymous_record_test.dag +++ b/src/v2/test/claim/body_lowering/anonymous_record_test.dag @@ -20,7 +20,7 @@ import v2.std.diagnostic { Accepted, Outcome, Rejected, diagnostics_fatal_reason import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } import v2.std.optional { Absent, Present } -import v2.std.node { Hash, Node, Symbol, content_hash, node_subtree_nodes } +import v2.std.node { Hash, Node, Symbol, content_hash, node_subtree_nodes, symbol_lexeme } import v2.std.node_query { construct_tag_is_elided, construct_tag_optional, construct_tag_path_optional } import v2.std.qualified_name { qualified_name_from_dotted_string, qualified_name_to_dotted_string } import v2.std.algebra { length } @@ -65,6 +65,8 @@ data ar_binder_source: String = "module v2.test.ar_binder\n\nimport v2.std.logic // Review condition 2: resolve's choice is not proof. `m: 3` is an Int where ArB declares a Bool. data ar_value_mismatch_source: String = "module v2.test.ar_value_mismatch\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArB }\n\ndata ar_bad: ArB = { n: true, m: 3 }\n" +data ar_value_mismatch_headed_source: String = "module v2.test.ar_value_mismatch_headed\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArB }\n\ndata ar_bad: ArB = ArB { n: true, m: 3 }\n" + fn ar_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceReadWitness { DagSourceReadWitness { source: Medium { carried: source, fidelity: Lossless }, @@ -81,7 +83,8 @@ data ar_ingest: SourceRootIngest = [ ar_read(source: ar_call_argument_source, id: ^ar_call_argument_read, unit: ^ar_call_argument_cu, path: "src/v2/test/fixture/anonymous_record/call_argument.dag"), ar_read(source: ar_coproduct_source, id: ^ar_coproduct_read, unit: ^ar_coproduct_cu, path: "src/v2/test/fixture/anonymous_record/coproduct.dag"), ar_read(source: ar_binder_source, id: ^ar_binder_read, unit: ^ar_binder_cu, path: "src/v2/test/fixture/anonymous_record/binder.dag"), - ar_read(source: ar_value_mismatch_source, id: ^ar_value_mismatch_read, unit: ^ar_value_mismatch_cu, path: "src/v2/test/fixture/anonymous_record/value_mismatch.dag") + ar_read(source: ar_value_mismatch_source, id: ^ar_value_mismatch_read, unit: ^ar_value_mismatch_cu, path: "src/v2/test/fixture/anonymous_record/value_mismatch.dag"), + ar_read(source: ar_value_mismatch_headed_source, id: ^ar_value_mismatch_headed_read, unit: ^ar_value_mismatch_headed_cu, path: "src/v2/test/fixture/anonymous_record/value_mismatch_headed.dag") ] fn ar_fixture_normalized() -> Outcome> { @@ -111,6 +114,7 @@ fn ar_resolved_call_argument() -> Outcome { ar_resolve_subject(sub fn ar_resolved_coproduct() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_coproduct") } fn ar_resolved_binder() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_binder") } fn ar_resolved_value_mismatch() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_value_mismatch") } +fn ar_resolved_value_mismatch_headed() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_value_mismatch_headed") } // Every construct under the root, authored or elided, in tree order. fn ar_constructs(root: Node) -> List { @@ -199,16 +203,29 @@ test fn ar_a_binder_field_type_is_not_substituted_holds() -> Bool { ar_refuses_with(o: ar_resolved_binder(), reason: ^resolve_anonymous_record_expected_type_not_record) } -// Control 7 (review condition 2): resolve elaborates the literal (it accepts, and the tag is ArB), -// and infer then refuses the Int value where ArB declares a Bool. Resolve's choice is not proof. -test fn ar_infer_still_checks_an_elaborated_construct_holds() -> Bool { - match ar_resolved_value_mismatch() { - Rejected { diagnostics: _ } => false - Accepted { value: t, diagnostics: d } => - (ar_construct_paths(o: Accepted { value: t, diagnostics: d }) == ["v2.test.arp.ArB"]) - && match infer(tree: t) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: _ } => true - } +// Control 7 (review condition 2, as ruled by gentle-koi-724): resolve's choice is not proof, so an +// elaborated construct gets EXACTLY the infer verdict of its headed spelling -- here on a field +// value of the wrong type (`m: 3` where ArB declares Bool). Both constructs must be tagged ArB, so a +// headless literal that failed to elaborate cannot pass by reaching the same verdict another way. +// Today infer ACCEPTS both: it does not type construct field values against the record declaration +// (gunbc.recurring_failure_mode construct_field_value_not_typed_against_the_record_declaration). +// When that trigger lands both refuse, and this claim still holds: it asserts parity and does not pin +// the gap. +fn ar_infer_verdict(o: Outcome) -> String { + match o { + Rejected { diagnostics: _ } => "resolve-refused" + Accepted { value: t, diagnostics: _ } => + match infer(tree: t) { + Accepted { value: _, diagnostics: _ } => "infer-accepted" + Rejected { diagnostics: d } => symbol_lexeme(sym: diagnostics_fatal_reason(d: d)) + } } } + +test fn ar_infer_verdict_is_the_headed_forms_holds() -> Bool { + let headless = ar_resolved_value_mismatch() + let headed = ar_resolved_value_mismatch_headed() + (ar_construct_paths(o: headless) == ["v2.test.arp.ArB"]) + && (ar_construct_paths(o: headed) == ["v2.test.arp.ArB"]) + && (ar_infer_verdict(o: headless) == ar_infer_verdict(o: headed)) +} diff --git a/src/v2/test/zz_probe/ar_debug.dag b/src/v2/test/zz_probe/ar_debug.dag new file mode 100644 index 00000000000..32cb799c566 --- /dev/null +++ b/src/v2/test/zz_probe/ar_debug.dag @@ -0,0 +1,116 @@ +module v2.test.zz_probe.ar_debug + +import v2.compiler.infer { infer } +import v2.test.claim.body_lowering.anonymous_record { ar_resolved_value_mismatch_headed, ar_resolved_headed, ar_resolved_headless, ar_resolved_call_argument, ar_resolved_coproduct, ar_resolved_binder, ar_resolved_value_mismatch, ar_construct_paths } +import v2.compiler.resolve { ResolvedTree } +import v2.std.diagnostic { Accepted, Outcome, Rejected, Diagnostic } +import v2.std.node { ComputationNode, Arrow, symbol_lexeme, node_synthetic, symbol_eq, construct_tag_elided_marker, Node, TypeNode, Atom, Conj, Named, Positional, node_subtree_nodes } +import v2.std.node_query { construct_tag_is_elided, construct_node_elided, construct_tag_elided_reference } +import v2.std.collection { list_at_optional } +import v2.std.optional { Present, Absent } +import v2.test.claim.namespace_xl0.reference_conservation { conservation_subject } +import v2.test.claim.body_lowering.anonymous_record { ar_headless_source } +import v2.std.lens_verdict { lens_verdict_diagnostic_locus_text } +import v2.std.text { String } +import v2.std.integer { Int } +import std.process { ProcessExit, exit_failure } + +fn ar_dbg(o: Outcome) -> ProcessExit { + match o { + Accepted { value: _, diagnostics: _ } => exit_failure(reason: concat("ACCEPTED paths=", join(ar_construct_paths(o: o), separator: ","))) + Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: concat(concat(concat("REJECTED ", symbol_lexeme(sym: d.head.reason)), " @ "), lens_verdict_diagnostic_locus_text(d: d.head)), f: fn(acc, x) { concat(concat(acc, " | "), symbol_lexeme(sym: x.reason)) })) + } +} + +fn dbg_headed() -> ProcessExit { ar_dbg(o: ar_resolved_headed()) } +fn dbg_headless() -> ProcessExit { ar_dbg(o: ar_resolved_headless()) } +fn dbg_call() -> ProcessExit { ar_dbg(o: ar_resolved_call_argument()) } +fn dbg_coproduct() -> ProcessExit { ar_dbg(o: ar_resolved_coproduct()) } +fn dbg_binder() -> ProcessExit { ar_dbg(o: ar_resolved_binder()) } +fn dbg_mismatch() -> ProcessExit { ar_dbg(o: ar_resolved_value_mismatch()) } + +fn dbg_kind(n: Node) -> String { + match n.kind { + TypeNode { connective: Atom { identity: id } } => concat("Atom:", symbol_lexeme(sym: id)) + TypeNode { connective: Conj } => concat("Conj#", to_string(count(n.children))) + _ => "other" + } +} + +fn dbg_lowered() -> ProcessExit { + match conservation_subject(id_tail: "ar_dbg", source: ar_headless_source).normalized { + Rejected { diagnostics: d } => exit_failure(reason: concat("NORMALIZE REJECTED ", symbol_lexeme(sym: d.head.reason))) + Accepted { value: nt, diagnostics: _ } => + let nodes = node_subtree_nodes(root: nt.root) + exit_failure(reason: concat(concat("nodes=", to_string(count(nodes))), fold(nodes, init: "", f: fn(acc, n) { + fold(n.children, init: acc, f: fn(a2, e) { + match e.label { + Named { name: l } => + if symbol_lexeme(sym: l) == "" { + concat(concat(concat(a2, " | tag->"), dbg_kind(n: e.target)), concat(" elided=", to_string(construct_tag_is_elided(n: n)))) + } else { a2 } + Positional => a2 + } + }) + }))) + } +} + +fn dbg_unit() -> ProcessExit { + let src = node_synthetic(kind: TypeNode { connective: Atom { identity: ^dbg_src } }, children: []) + let c = construct_node_elided(field_edges: [], source: src) + let r = construct_tag_elided_reference(source: src) + exit_failure(reason: concat(concat(concat("elided=", to_string(construct_tag_is_elided(n: c))), concat(" ref=", dbg_kind(n: r))), concat(" eq=", to_string(symbol_eq(a: construct_tag_elided_marker(), b: construct_tag_elided_marker()))))) +} + +fn dbg_sexpr(n: Node, depth: Int) -> String { + let head = match n.kind { + TypeNode { connective: Atom { identity: id } } => concat("A:", symbol_lexeme(sym: id)) + TypeNode { connective: Conj } => "Conj" + TypeNode { connective: Arrow } => "Arrow" + TypeNode { connective: _ } => "Type?" + ComputationNode { behavior: _ } => "Comp" + } + if depth == 0 { + head + } else { + concat(concat(concat(head, "("), fold(n.children, init: "", f: fn(acc, e) { + let lbl = match e.label { + Named { name: l } => symbol_lexeme(sym: l) + Positional => "_" + } + concat(concat(concat(concat(acc, lbl), "="), dbg_sexpr(n: e.target, depth: depth - 1)), " ") + })), ")") + } +} + +fn dbg_find_ar_x(o: Outcome) -> ProcessExit { + match o { + Rejected { diagnostics: d } => exit_failure(reason: "REJECTED") + Accepted { value: t, diagnostics: _ } => + exit_failure(reason: fold(node_subtree_nodes(root: t.root), init: "", f: fn(acc, n) { + fold(n.children, init: acc, f: fn(a2, e) { + match e.label { + Named { name: l } => if symbol_lexeme(sym: l) == "ar_x" { concat(concat(a2, " || "), dbg_sexpr(n: e.target, depth: 5)) } else { a2 } + Positional => a2 + } + }) + })) + } +} + +fn dbg_tree_headless() -> ProcessExit { dbg_find_ar_x(o: ar_resolved_headless()) } +fn dbg_tree_headed() -> ProcessExit { dbg_find_ar_x(o: ar_resolved_headed()) } + +fn dbg_infer(o: Outcome) -> ProcessExit { + match o { + Rejected { diagnostics: d } => exit_failure(reason: concat("RESOLVE REJECTED ", symbol_lexeme(sym: d.head.reason))) + Accepted { value: t, diagnostics: _ } => + match infer(tree: t) { + Accepted { value: _, diagnostics: _ } => exit_failure(reason: concat("INFER ACCEPTED paths=", join(ar_construct_paths(o: o), separator: ","))) + Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: concat("INFER REJECTED ", symbol_lexeme(sym: d.head.reason)), f: fn(acc, x) { concat(concat(acc, " | "), symbol_lexeme(sym: x.reason)) })) + } + } +} +fn dbg_infer_headless() -> ProcessExit { dbg_infer(o: ar_resolved_value_mismatch()) } +fn dbg_infer_headed() -> ProcessExit { dbg_infer(o: ar_resolved_value_mismatch_headed()) } diff --git a/src/v2/test/zz_probe/probe.dag b/src/v2/test/zz_probe/probe.dag new file mode 100644 index 00000000000..c06fe51b44a --- /dev/null +++ b/src/v2/test/zz_probe/probe.dag @@ -0,0 +1,113 @@ +module v2.test.zz_probe.probe + +import v2.test.claim.namespace_xl0.reference_conservation { conservation_subject } +import v2.std.optional { Optional, Present, Absent } +import v2.std.diagnostic { Accepted, Rejected, NonEmptyDiagnostics } +import v2.std.text { String } +import std.process { ProcessExit, ExitSuccess, exit_failure } +import v2.std.lens_verdict { lens_verdict_diagnostic_locus_text_in_spans } +import v2.std.provenance { SpanIndex, span_index_empty } +import v2.std.node { symbol_lexeme } +import v2.std.diagnostic { Diagnostic } + + +data probe_src_0: String = "module std.algebra\n\nimport std.error_primitives { DivError, Result }\n\ntype Magma {\n op: fn(T, T) -> T\n}\n\ntype Semigroup {\n op: fn(T, T) -> T\n\n}\n\ntype Monoid {\n op: fn(T, T) -> T\n identity: T\n}\n\ntype CommutativeMonoid {\n op: fn(T, T) -> T\n identity: T\n\n}\n\ntype Group {\n op: fn(T, T) -> T\n identity: T\n inverse: fn(T) -> T\n}\n\ntype AbelianGroup {\n op: fn(T, T) -> T\n identity: T\n inverse: fn(T) -> T\n\n}\n\ntype GroupCompletion {\n pos: M\n neg: M\n}\n\ntype FieldOfFractions {\n num: R\n denom: R\n}\n\ntype Semiring {\n add: fn(T, T) -> T\n zero: T\n mul: fn(T, T) -> T\n one: T\n}\n\ntype CommutativeSemiring {\n add: fn(T, T) -> T\n zero: T\n mul: fn(T, T) -> T\n one: T\n}\n\ntype Ring {\n add: fn(T, T) -> T\n zero: T\n negate: fn(T) -> T\n mul: fn(T, T) -> T\n one: T\n}\n\ntype OrderedRing {\n add: fn(T, T) -> T\n sub: fn(T, T) -> T\n zero: T\n negate: fn(T) -> T\n mul: fn(T, T) -> T\n div: fn(T, T) -> Result\n one: T\n compare: fn(T, T) -> Ordering\n eq: fn(T, T) -> Bool\n ne: fn(T, T) -> Bool\n lt: fn(T, T) -> Bool\n le: fn(T, T) -> Bool\n gt: fn(T, T) -> Bool\n ge: fn(T, T) -> Bool\n}\n\ntype Field {\n add: fn(T, T) -> T\n zero: T\n negate: fn(T) -> T\n mul: fn(T, T) -> T\n one: T\n reciprocal: fn(T) -> T\n compare: fn(T, T) -> Ordering\n}\n\ntype Lattice {\n meet: fn(T, T) -> T\n join: fn(T, T) -> T\n}\n\ntype BoundedLattice {\n meet: fn(T, T) -> T\n join: fn(T, T) -> T\n top: T\n bottom: T\n}\n\ntype BooleanAlgebra {\n meet: fn(T, T) -> T\n join: fn(T, T) -> T\n complement: fn(T) -> T\n top: T\n bottom: T\n}\n\ntype PointwisePower {\n member: fn(T) -> Bool\n}\n\n// THE FINITE POWER SET -- Pfin, the set of FINITE subsets -- is the enumerable counterpart of\n// `PointwisePower` above, which presents the full power set as a characteristic function. The two\n// sit on the SUPPORT axis exactly as `PartialFunction` and `FinitelySupportedFunction` do below:\n// one answers membership and nothing else, the other can be walked.\n//\n// AUTHORED HERE BECAUSE THE CONCEPT WAS ALREADY IN TREE UNDER A BORROWED NAME. Every language that\n// carries an algebra inhabitant row files the finite-set realization under `\"BooleanAlgebra\"`:\n//\n// rust BTreeSet<{0}> go map[{0}]struct{}\n// python set[{0}] typescript Set<{0}>\n//\n// Four independent realizations, four finite-set containers, not one lattice among them -- while\n// the TYPE `BooleanAlgebra` is `{ meet, join, complement, top, bottom }` and its only\n// construction in the corpus is `v2.std.logic` `bool_boolean_algebra`, a genuine Boolean algebra\n// over `Bool`. The realization roster and the type disagreed about what the name meant, and the\n// rust roster carried the tell outright: the `\"BooleanAlgebra\"` and `\"PointwisePower\"` rows were\n// BYTE-IDENTICAL, two algebra names resolving to one realization with nothing saying which was\n// which. This declaration takes the finite-set concept out from under the lattice's name; the\n// lattice keeps its name, its type and its one construction site unchanged.\ntype FinitePowerSet {\n member: fn(T) -> Bool\n union: fn(FinitePowerSet) -> FinitePowerSet\n intersect: fn(FinitePowerSet) -> FinitePowerSet\n diff: fn(FinitePowerSet) -> FinitePowerSet\n contains: fn(T) -> Bool\n count: fn() -> Int\n length: fn() -> Int\n}\n\ntype FreeMonoid\n = Empty\n | Cons { head: T, tail: FreeMonoid }\n\nfn freemonoid_empty() -> FreeMonoid {\n Empty\n}\n\n// THE COST IS CHARGED IN THE RIGHT OPERAND, AND THE RIGHT-FOLD THIS REPLACED CHARGED THE LEFT.\n// `concat` is the primitive two-list append; `std.primitives` `concat_contract` declares its\n// persistent carrier cost as `log(n + m)`. The `fold_list_right` form that stood here walked\n// its LEFT operand instead, one `Cons` per element, so an accumulate-on-the-left fold paid the\n// whole accumulator at every step and came out quadratic in the accumulated length --\n// `list_snoc_item` below is exactly that shape, which is why it inherited the defect, and it is\n// the shape the corpus uses wherever it builds a list by appending. Routing to the primitive\n// makes the singleton case constant and the general case linear in the appended side only.\n// DESIGN section 6 fixes a proven cost-shape defect at the root rather than per callsite.\nfn list_append(left: FreeMonoid, right: FreeMonoid) -> FreeMonoid {\n left |> concat(right)\n}\n\n// `list_push` is the primitive single-item append (amortized O(log n) on the persistent\n// carrier); routing through list_append paid a full O(n) right-fold per snoc, which made\n// every build-by-appending accumulator quadratic — measured on the first-set union fold in\n// the self-hosted parser (2026-09-09, required-v2-native lane work).\nfn list_snoc_item(xs: FreeMonoid, item: T) -> FreeMonoid {\n xs |> list_push(item)\n}\n\ntype FreeSemigroup {\n head: T\n tail: FreeMonoid\n}\n\n// FreeSemigroup is the free SEMIGROUP over T exactly as FreeMonoid is the free MONOID over T.\n// The head field is the construction wall: an empty inhabitant has no representation.\n\n// TWO AXES, THREE INHABITED CORNERS. The family below is a 2x2 space and the corpus lives in\n// three of its corners, so three carriers are authored and the fourth is named rather than\n// silently omitted.\n//\n// TOTALITY -- does the function answer at every point (`fn(K) -> V`) or may it decline\n// (`fn(K) -> V?`) -- is independent of SUPPORT -- does it differ from the absent value at\n// finitely many points, so that it can be enumerated, or at arbitrarily many, so that it cannot.\n//\n// arbitrary support finite support\n// total TotalMap (uninhabited)\n// partial PartialFunction FinitelySupportedFunction\n//\n// The uninhabited corner is REPRESENTABLE, not impossible: a total function taking a non-default\n// value at finitely many points is a perfectly good object. It is unauthored because no corpus\n// site needs one. The condition that fills it is a site that must both answer everywhere and be\n// enumerated; author it then, and not before.\n//\n// The predicate family sits on the same support axis one row up: `PointwisePower` is the full\n// power set presented as a characteristic function, and its finite counterpart has no carrier\n// yet -- see the `Set` alias in `std.types` for that open half and its trigger.\ntype PartialFunction {\n lookup: fn(K) -> V?\n}\n\n// FINITE SUPPORT IS WHAT MAKES THE ENUMERATION SURFACE HONEST. Everything below `lookup` here --\n// keys, values, size, and the insert/merge that must preserve them -- is answerable only because\n// the support is finite. That is exactly why these rows may not sit on `PartialFunction`: an open\n// partial function cannot produce `map_keys` at all, and until this split every one of the 145\n// `Map { lookup: .. }` literals in the corpus named a carrier promising ten operations it\n// supplied one of. Measured before the split: 0 of those 145 supplied any field beyond `lookup`.\n//\n// `lookup` appears on BOTH carriers rather than being inherited, because the substrate has no\n// subtyping: a finite map really does answer lookup, and saying so twice is the honest encoding\n// of \"this one can do everything that one can, and more\".\n//\n// RECORDED, NOT INTRODUCED, and not dissolved here: `lookup`, `get` and `map_get` are three\n// spellings of one operation, which the template rosters below carry as three rows. That is the\n// same Section 3 fork `algebra_count_length_name_fork_note` records for count/length/size, it\n// predates this change, and collapsing it is a corpus-wide rename touching every call site --\n// its own lane. The split neither widens nor narrows it; the three spellings move together.\ntype FinitelySupportedFunction {\n lookup: fn(K) -> V?\n empty: FinitelySupportedFunction\n get: fn(K) -> V?\n insert: fn(K, V) -> FinitelySupportedFunction\n merge: fn(FinitelySupportedFunction) -> FinitelySupportedFunction\n map_keys: fn() -> FreeMonoid\n map_values: fn() -> FreeMonoid\n map_has: fn(K) -> Bool\n contains_key: fn(K) -> Bool\n size: fn() -> Int\n}\n\n// MOVED HERE from `v2.std.collection` by the change that split `PartialFunction`. They are the\n// total corner of the family above, and leaving them in a different module from their own axis is\n// where a fork starts. `TotalPolicy` travels with `TotalMap` rather than staying behind: it is the\n// same total lookup carrying a context argument, and splitting a pair across two modules is the\n// thing this change exists to stop doing.\n//\n// Both had ZERO code consumers at the time of the move -- one declaration each and two prose cells\n// in `gunbc.plans.self_applying_lenses`, which marks `table_decision_tree` as an Unrealized\n// scaffold and names these as its target substrate rows. So they are carriers authored ahead of a\n// consumer that a plan does declare, which is weaker than an untracked scaffold and stronger than\n// healthy. The home is the cheap part of the answer; the consumer is that plan's to land.\ntype TotalMap {\n lookup: fn(K) -> V\n}\n\ntype TotalPolicy {\n lookup: fn(K, Context) -> RowTemplate\n}\n\ntype Ordering = Less | Equal | Greater\n\ntype OrderingFold {\n less: R\n equal: R\n greater: R\n}\n\nfn ordering_fold(o: Ordering, fold: OrderingFold) -> R {\n match o {\n Less => fold.less\n Equal => fold.equal\n Greater => fold.greater\n }\n}\n\nfn ordering_is_less(o: Ordering) -> Bool {\n ordering_fold(\n o: o,\n fold: OrderingFold {\n less: true,\n equal: false,\n greater: false,\n },\n )\n}\n\ntype AlgebraProfile\n = OrderedRingProfile\n | ApproximateFieldProfile\n | BooleanAlgebraProfile\n | FinitePowerSetProfile\n | PointwisePowerCollectionProfile\n | FreeMonoidScalarProfile\n | FreeMonoidCollectionProfile\n | PartialFunctionProfile\n | FinitelySupportedFunctionProfile\n\ntype ContainerSource\n = SameAsReceiver\n | Named { name: String }\n\ntype AlgebraTypeTemplate\n = ReceiverSelf\n | ReceiverElement\n | ReceiverKey\n | ReceiverValue\n | NamedTemplate { name: String }\n | ContainerOf { source: ContainerSource, element: AlgebraTypeTemplate }\n | OptionalOf { inner: AlgebraTypeTemplate }\n | WitnessOf { inner: AlgebraTypeTemplate }\n | TupleOf { first: AlgebraTypeTemplate, second: AlgebraTypeTemplate }\n | CallableOf { params: List, return_type: AlgebraTypeTemplate }\n | AlgebraTypeVariable { id: String }\n\ntype CollectionSizeEffect\n = ShrinkEffect\n | ProjectionEffect\n | IdentityEffect\n\ntype CostShape\n = ShapeConstant\n | ShapeLinearScan\n | ShapeIterateBody\n | ShapeSortBody\n\n// THE STRUCTURED FORM OF THIS FACT IS NOT DECLARED HERE, AND THE FIRST REVISION OF THIS WORK\n// DECLARED IT ANYWAY. CostShape names a growth RANK and nothing else, so it cannot distinguish an\n// operation whose work is linear in BOTH operands from one linear in only the right operand -- both\n// are \"linear\" -- and that distinction is the whole of the concat specimen in\n// gunbc.recurring_failure_mode lens_prices_the_authority_while_the_lane_runs_the_realization. The\n// repair needs a cost expression over NAMED operands, and this module minted one: a sum-of-products\n// grammar of constant, log, linear and linearithmic factors.\n//\n// THAT WAS A FAILED DECOMPOSITION (DESIGN section 2): std.induction CostBound ALREADY owns exactly\n// that grammar -- AtomicCost PolyCost/LogCost over a named param, ProductBound for a product,\n// SumOfProductsBound for a sum of products -- and owns it more completely, with fractional and\n// log-based exponents this corpus already uses. cost_nlogn IS linearithmic and cost_graph_linear IS\n// the concat specimen, both already written. Minting a second grammar here would have put two names\n// on one concept (section 3) in the same change that argued against exactly that one field over, in\n// the module whose subject is a cost authority disagreeing with itself.\n//\n// So the structured cost of an operation is a std.induction CostBound, and CostShape remains what\n// it always was: a coarse rank for the sites that only need one. The projection between them lives\n// with the growth derivation in std.induction, beside the carrier it reads.\n\ntype AlgebraFieldTemplate {\n name: String\n param_types: List\n return_type: AlgebraTypeTemplate\n size_effect: CollectionSizeEffect?\n cost_shape: CostShape?\n\n callback_element_position: Int?\n}\n\n// Collection filter is this one template, not a second encoding of its fields and not the\n// spelling of `name`. Roster rows and emission membership both consume it; a rename of the\n// method token must not disarm the wall.\nfn collection_filter_shape() -> AlgebraFieldTemplate {\n { name: \"filter\", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: \"Bool\" } }], return_type: ReceiverSelf, size_effect: Present { value: IdentityEffect }, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }\n}\n\n// Collection fold is this one template, declared once for both collection profiles that carry it,\n// and its param_types IS the free-call signature, receiver first, the same convention as `map` and\n// `filter` (and the one v1.compiler.method pair_declared_names reads when it pairs `names: [\"xs\", ..]`\n// with a row). The receiver slot was missing here while every sibling carried it, so the row did not\n// say where the step sits in `fold(xs, init, step)`; v2.compiler.fold_lowering reads that position\n// from this row through template_step_position rather than from a literal index.\nfn collection_fold_shape() -> AlgebraFieldTemplate {\n { 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 } }\n}\n\ntype StepPositionScan {\n next: Int\n found: Int?\n}\n\n// The position of a template's step -- its first callable parameter -- in the free-call signature\n// the row declares. Absent when the row takes no callable.\nfn template_step_position(t: AlgebraFieldTemplate) -> Int? {\n let scan = fold(t.param_types, init: StepPositionScan { next: 0, found: none }, f: fn(acc, p) {\n match acc.found {\n Present { value: _ } => acc\n Absent =>\n match p {\n CallableOf { params: _, return_type: _ } => StepPositionScan { next: acc.next + 1, found: Present { value: acc.next } }\n _ => StepPositionScan { next: acc.next + 1, found: none }\n }\n }\n })\n scan.found\n}\n\nfn is_collection_filter_template(t: AlgebraFieldTemplate) -> Bool {\n let proto = collection_filter_shape()\n t.return_type == proto.return_type\n && t.size_effect == proto.size_effect\n && t.cost_shape == proto.cost_shape\n && t.callback_element_position == proto.callback_element_position\n && t.param_types == proto.param_types\n}\n\n// ONE ROSTER, DECLARED ONCE, AND EVERY SPELLING-KEYED MAP BELOW IS A FOLD OVER IT.\n//\n// What stood here was `kernel_algebra_profile` written out as a nest of ten `map_insert` calls\n// keyed on type-name strings, beside four further hand-authored spelling maps in `std.types`\n// (`container_template_algebra_rows`, `container_template_alias_rows`, `container_type_arity`,\n// `canonical_container_names`). Five maps, one subject: which carrier a spelling names and what\n// that carrier answers for. Nothing joined them, so a carrier existed in a map exactly insofar as\n// an author remembered to write it there -- and the failure mode is not hypothetical, it is what\n// the deleted note above this row recorded: `PartialFunction` had a carrier, a template set and a\n// profile, and no `kernel_algebra_profile` key, so `tree.facts.lookup(node)` refused on a\n// `PartialFunction` receiver while the byte-identical call on `Map` resolved. That repair was\n// three inserted rows. The eleventh carrier forks the same way, in whichever of the five maps its\n// author does not think of.\n//\n// The move is DESIGN section 2-horizontal: model the carrier once, derive every use. `AlgebraCarrier`\n// carries `profile` as a required field, so a carrier row cannot be authored without one --\n// the invalid state (a carrier with no algebra) has no spelling, which is section 4b structural\n// impossibility for that class rather than a check. Each spelling then DECLARES its membership in\n// every derived map, and `RowAbsent` carries a reason, so an omission is an authored answer\n// someone had to write rather than a row nobody wrote. That is the property the five literals\n// could not have: adding a carrier now means answering five questions in one place, and leaving\n// one unanswered is a missing field, not a silent hole.\n//\n// RUNG, honestly: this is *structurally guaranteed* for \"a carrier without a profile\" and\n// \"a carrier absent from a map by oversight\", and NOT a wall on the memberships themselves --\n// an author can still declare `RowAbsent` with a bad reason and get exactly the old behaviour.\n// The next-rung trigger is each membership becoming derivable from a property the carrier already\n// has (arity from `algebra_type_param_names`, roster membership from whether the profile is a\n// collection), which needs the two remaining `std.types` literals this change does not reach --\n// `ordered_element_collections` and the non-carrier `Witness` row -- to be grounded first.\ntype CarrierRowMembership\n = RowPresent\n | RowAbsent { reason: String }\n\n// The five derived maps, one field each. A new membership question means a new field here, and\n// every existing carrier then fails to compile until it answers it -- which is the point.\ntype CarrierSpelling {\n text: String\n\n method_surface: CarrierRowMembership\n container_algebra_row: CarrierRowMembership\n container_alias_row: CarrierRowMembership\n container_roster_name: CarrierRowMembership\n container_arity_row: CarrierRowMembership\n}\n\ntype AlgebraCarrier {\n name: String\n profile: AlgebraProfile\n spellings: List\n}\n\ndata scalar_carrier_not_container_reason: String = \"scalar carrier: names no container, so it holds no container-spelling row\"\n\ndata snake_case_spelling_not_alias_reason: String = \"snake spelling: resolves as an algebra name, not as a declared container alias\"\n\ndata canonical_spelling_sorts_ahead_of_alias_reason: String = \"container_alias_canonical_spelling returns the FIRST sorted key mapping to an algebra, so this key would sort ahead of its own alias and silently become the emitted canonical spelling\"\n\ndata carrier_name_is_not_a_declaration_spelling_reason: String = \"the carrier name is not written in declaration position; its alias carries the profile\"\n\ndata arity_row_is_alias_spellings_only_reason: String = \"container_type_arity is consulted by is_container_type on authored declaration spellings; adding a key here decides containerhood for that spelling\"\n\nfn scalar_carrier_spelling(text: String) -> CarrierSpelling {\n CarrierSpelling {\n text: text,\n method_surface: RowPresent,\n container_algebra_row: RowAbsent { reason: scalar_carrier_not_container_reason },\n container_alias_row: RowAbsent { reason: scalar_carrier_not_container_reason },\n container_roster_name: RowAbsent { reason: scalar_carrier_not_container_reason },\n container_arity_row: RowAbsent { reason: scalar_carrier_not_container_reason },\n }\n}\n\nfn snake_alias_spelling(text: String) -> CarrierSpelling {\n CarrierSpelling {\n text: text,\n method_surface: RowAbsent { reason: carrier_name_is_not_a_declaration_spelling_reason },\n container_algebra_row: RowPresent,\n container_alias_row: RowPresent,\n container_roster_name: RowAbsent { reason: snake_case_spelling_not_alias_reason },\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n }\n}\n\nfn snake_algebra_spelling(text: String) -> CarrierSpelling {\n CarrierSpelling {\n text: text,\n method_surface: RowAbsent { reason: carrier_name_is_not_a_declaration_spelling_reason },\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: snake_case_spelling_not_alias_reason },\n container_roster_name: RowAbsent { reason: snake_case_spelling_not_alias_reason },\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n }\n}\n\n// THE ALIAS SPELLING IS THE ONE WRITTEN IN DECLARATION POSITION -- `List`, `Set`, `Map` -- so it\n// is the one that carries every row: the profile that decides whether a receiver has a method\n// surface, the algebra it names, its own alias row, the container roster, and the arity checked\n// against an authored type expression.\nfn pascal_alias_spelling(text: String) -> CarrierSpelling {\n CarrierSpelling {\n text: text,\n method_surface: RowPresent,\n container_algebra_row: RowPresent,\n container_alias_row: RowPresent,\n container_roster_name: RowPresent,\n container_arity_row: RowPresent,\n }\n}\n\n// TWO ROWS BELOW ARE NOT MECHANICAL, and both are stated here rather than beside the row because\n// a section 4c annotation attaches to a module-scope declaration, not to a line inside a body.\n//\n// `PartialFunction` carries its own alias row, unlike every other carrier name: no alias competes\n// for that algebra, so the carrier name IS the declaration spelling -- which is why\n// `resolve_method_receiver_type` returns a `PartialFunction` receiver as authored, type arguments\n// intact. Every other canonical name declines the alias row because\n// `container_alias_canonical_spelling` returns the first sorted key for an algebra, so\n// `\"FinitelySupportedFunction\"` would sort ahead of `\"Map\"` and silently become the emitted\n// canonical spelling.\n//\n// `PointwisePower` declares `method_surface` ABSENT, and the reason is a measurement not taken\n// rather than a property of the carrier. Its 80 declaration positions predate the carrier split,\n// its templates are a different set from `FinitePowerSet`'s, and `RowPresent` would flip the\n// method-existence wall from UNDECIDED to DECIDED for all 80 at once -- turning any method absent\n// from `pointwise_power_collection_templates` into a hard `MethodNotFound`. Under the five\n// literals this was an omission indistinguishable from forgetting; here it is an authored row\n// with a reason, which is the whole difference this change buys.\nfn algebra_carriers() -> List {\n [\n AlgebraCarrier {\n name: \"Int\",\n profile: OrderedRingProfile,\n spellings: [scalar_carrier_spelling(text: \"Int\")],\n },\n AlgebraCarrier {\n name: \"Float\",\n profile: ApproximateFieldProfile,\n spellings: [scalar_carrier_spelling(text: \"Float\")],\n },\n AlgebraCarrier {\n name: \"BooleanAlgebra\",\n profile: BooleanAlgebraProfile,\n spellings: [\n scalar_carrier_spelling(text: \"Bool\"),\n CarrierSpelling {\n text: \"BooleanAlgebra\",\n method_surface: RowAbsent { reason: carrier_name_is_not_a_declaration_spelling_reason },\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowAbsent { reason: \"the lattice is not a container: its container inhabitant row moved to FinitePowerSet\" },\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"boolean_algebra\"),\n ],\n },\n AlgebraCarrier {\n name: \"String\",\n profile: FreeMonoidScalarProfile,\n spellings: [scalar_carrier_spelling(text: \"String\")],\n },\n AlgebraCarrier {\n name: \"FreeMonoid\",\n profile: FreeMonoidCollectionProfile,\n spellings: [\n pascal_alias_spelling(text: \"List\"),\n snake_alias_spelling(text: \"list\"),\n CarrierSpelling {\n text: \"FreeMonoid\",\n method_surface: RowAbsent { reason: carrier_name_is_not_a_declaration_spelling_reason },\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"free_monoid\"),\n ],\n },\n AlgebraCarrier {\n name: \"FinitePowerSet\",\n profile: FinitePowerSetProfile,\n spellings: [\n pascal_alias_spelling(text: \"Set\"),\n snake_alias_spelling(text: \"set\"),\n CarrierSpelling {\n text: \"FinitePowerSet\",\n method_surface: RowPresent,\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"finite_power_set\"),\n ],\n },\n AlgebraCarrier {\n name: \"FinitelySupportedFunction\",\n profile: FinitelySupportedFunctionProfile,\n spellings: [\n pascal_alias_spelling(text: \"Map\"),\n snake_alias_spelling(text: \"map\"),\n CarrierSpelling {\n text: \"FinitelySupportedFunction\",\n method_surface: RowPresent,\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"finitely_supported_function\"),\n ],\n },\n AlgebraCarrier {\n name: \"PartialFunction\",\n profile: PartialFunctionProfile,\n spellings: [\n CarrierSpelling {\n text: \"PartialFunction\",\n method_surface: RowPresent,\n container_algebra_row: RowPresent,\n container_alias_row: RowPresent,\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_alias_spelling(text: \"partial_function\"),\n ],\n },\n AlgebraCarrier {\n name: \"PointwisePower\",\n profile: PointwisePowerCollectionProfile,\n spellings: [\n CarrierSpelling {\n text: \"PointwisePower\",\n method_surface: RowAbsent { reason: \"80 declaration positions would flip from MethodExistenceUndecided to a hard MethodNotFound for any method absent from pointwise_power_collection_templates; that measurement is not taken\" },\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"pointwise_power\"),\n ],\n },\n ]\n}\n\nfn carrier_spelling_row_present(m: CarrierRowMembership) -> Bool {\n match m {\n RowPresent => true\n RowAbsent { reason: _ } => false\n }\n}\n\nfn kernel_algebra_profile_value() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.method_surface) {\n map_insert(inner, spelling.text, carrier.profile)\n } else {\n inner\n }))\n}\n\nfn carrier_container_algebra_rows() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_algebra_row) {\n map_insert(inner, spelling.text, carrier.name)\n } else {\n inner\n }))\n}\n\nfn carrier_container_alias_rows() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_alias_row) {\n map_insert(inner, spelling.text, carrier.name)\n } else {\n inner\n }))\n}\n\nfn carrier_container_roster_map() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_roster_name) {\n map_insert(inner, spelling.text, true)\n } else {\n inner\n }))\n}\n\n// The arity is DERIVED from the profile's own type parameters rather than restated as a number:\n// `FinitePowerSet` is unary because `algebra_type_param_names` says `[\"T\"]`, so a carrier cannot\n// declare an arity that disagrees with its algebra.\nfn carrier_container_arity_rows() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_arity_row) {\n map_insert(inner, spelling.text, algebra_type_param_names(profile: carrier.profile) |> fold(init: 0, f: (n, _) => n + 1))\n } else {\n inner\n }))\n}\n\n// EQUALITY ADMISSIBILITY IS A CONSEQUENCE OF THE SUPPORT AXIS, declared beside the profiles it\n// follows from rather than re-derived by any consumer. A carrier with finite support (or a scalar\n// carrier) has decidable extensional equality: two values agree exactly when they agree on the\n// finitely many points that exist, so equality LIFTS over the carrier's type arguments and the\n// admission question recurses into them. An open-support carrier (PartialFunction, the pointwise\n// power) cannot answer extensional equality at all -- the agreement set is not enumerable -- so\n// generic equality on it is refused, never approximated. The rows are per-profile because the\n// support axis is a fact about the algebra, not about any spelling that resolves to it.\ntype AlgebraSupportAxis\n = FiniteSupport\n | OpenSupport\n\nfn algebra_profile_support(profile: AlgebraProfile) -> AlgebraSupportAxis {\n match profile {\n OrderedRingProfile => FiniteSupport\n ApproximateFieldProfile => FiniteSupport\n BooleanAlgebraProfile => FiniteSupport\n FinitePowerSetProfile => FiniteSupport\n PointwisePowerCollectionProfile => OpenSupport\n FreeMonoidScalarProfile => FiniteSupport\n FreeMonoidCollectionProfile => FiniteSupport\n PartialFunctionProfile => OpenSupport\n FinitelySupportedFunctionProfile => FiniteSupport\n }\n}\n\nfn algebra_profile_equality_extensional(profile: AlgebraProfile) -> Bool {\n match algebra_profile_support(profile: profile) {\n FiniteSupport => true\n OpenSupport => false\n }\n}\n\nfn carrier_container_equality_rows() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_algebra_row) {\n map_insert(inner, spelling.text, algebra_profile_equality_extensional(profile: carrier.profile))\n } else {\n inner\n }))\n}\n\ndata kernel_algebra_profile: Map = kernel_algebra_profile_value()\n\nfn ordered_ring_templates() -> List {\n [\n { name: \"add\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"zero\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"negate\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"mul\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"one\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"compare\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Ordering\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"clamp\", param_types: [ReceiverSelf, ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeConstant }, callback_element_position: none }\n ]\n}\n\nfn approximate_field_templates() -> List {\n [\n { name: \"add\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"zero\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"negate\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"mul\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"one\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"reciprocal\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"compare\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Ordering\" }, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\nfn boolean_algebra_templates() -> List {\n [\n { name: \"meet\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"join\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"complement\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"top\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"bottom\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\n// A CARDINALITY IS A NATURAL, AND IT NAMES ITS DECLARER. count and length (here and in the\n// free-monoid and function profiles below) return std.nat.Nat, qualified rather than spelled\n// \"Nat\": a bare template name resolves at the CALL SITE, and a src/v2 module resolves it to\n// v2.std.nat.Nat, a Peano Zero | Succ coproduct whose Rust carrier is Rc, not the i64 that\n// len() produces. The spelling then decided the emission -- `count(xs) >= 1` lowered to\n// nat_compare over Rc -- and the self-host build failed with E0308 in v2_compiler_resolve.\nfn finite_power_set_templates() -> List {\n [\n { name: \"union\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"intersect\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"diff\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"member\", param_types: [ReceiverSelf, ReceiverElement], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"contains\", param_types: [ReceiverSelf, ReceiverElement], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n collection_filter_shape(),\n { 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 } },\n { 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 } },\n collection_fold_shape(),\n { 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 } },\n { 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 } },\n { name: \"count\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"length\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none }\n ]\n}\n\nfn pointwise_power_collection_templates() -> List {\n [\n { name: \"member\", param_types: [CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: \"Bool\" } }], return_type: CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: \"Bool\" } }, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\ndata algebra_count_length_name_fork_note: String = \"count and length sit on the same template rows as the SAME operation, and that is a section 3 nicknaming fork being RECORDED rather than introduced (review 45499 raised it, correctly, as a smell). The fork's authority is the interpreter, which dispatches length, count and size to one native arm -- three names, one concept, and that arm predates this change. What was inconsistent was the declared surface: the templates listed only length, so a String receiver piped into count resolved at runtime while having no declared row, which the method-existence wall exposed the moment it began proving absence from the template roster. Reconciling the declaration to the runtime is the honest direction, because the runtime is what programs actually meet; inventing a distinct meaning for count to justify two rows would be the fabrication, and dropping the row would have left a working call refused. So the row is added and the fork is named here rather than silently propagated. Dissolve-on: feature:collection-size-single-spelling -- one name for this operation across the interpreter arm, the templates, and the corpus, a corpus-wide rename touching every call site, which belongs on its own lane rather than inside a P0 live-system fix. THE TRIGGER IS NAMED RATHER THAN DESCRIBED because a deferral whose condition is only prose cannot be looked up, counted, or closed by anyone but its author -- the asymmetry review 45759 caught beside unresolved_method_frontier, which cites feature:primitive-realization-single-authority and is therefore findable. The closing condition is decidable and stated: the interpreter's length/count/size arm dispatches one spelling, these two template rows carry that spelling only, and no other spelling resolves. Until then the count is the measure: three accepted spellings in the interpreter arm, two on these rows, one concept.\"\n\nfn free_monoid_scalar_templates() -> List {\n [\n { name: \"concat\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"empty\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"length\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"count\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"is_empty\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"chars\", param_types: [ReceiverSelf], return_type: ContainerOf { source: Named { name: \"List\" }, element: NamedTemplate { name: \"Int\" } }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"split\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ContainerOf { source: Named { name: \"List\" }, element: ReceiverSelf }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"join\", param_types: [ContainerOf { source: Named { name: \"List\" }, element: ReceiverSelf }, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"contains\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"starts_with\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"ends_with\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"trim\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"to_lower\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"to_upper\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"replace\", param_types: [ReceiverSelf, ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"substring\", param_types: [ReceiverSelf, NamedTemplate { name: \"Int\" }, NamedTemplate { name: \"Int\" }], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"to_int\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"Int\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"to_string\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"reverse\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\nfn free_monoid_collection_templates() -> List {\n [\n { 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 } },\n collection_filter_shape(),\n { 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 } },\n collection_fold_shape(),\n { 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 } },\n { 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 } },\n { name: \"count\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"first\", param_types: [ReceiverSelf], return_type: OptionalOf { inner: ReceiverElement }, size_effect: Present { value: ProjectionEffect }, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"last\", param_types: [ReceiverSelf], return_type: OptionalOf { inner: ReceiverElement }, size_effect: Present { value: ProjectionEffect }, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { 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 },\n { 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 },\n { name: \"take\", param_types: [ReceiverSelf, NamedTemplate { name: \"Int\" }], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: none },\n { 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 } },\n { name: \"append\", param_types: [ReceiverSelf, ReceiverElement], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeConstant }, callback_element_position: none },\n { name: \"contains\", param_types: [ReceiverSelf, ReceiverElement], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { 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 },\n { name: \"reverse\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: Present { value: IdentityEffect }, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"join\", param_types: [ReceiverSelf, NamedTemplate { name: \"String\" }], return_type: NamedTemplate { name: \"String\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"concat\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"list_push\", param_types: [ReceiverSelf, ReceiverElement], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"length\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"is_empty\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none }\n ]\n}\n\nfn partial_function_templates() -> List {\n [\n { name: \"get\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_get\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"lookup\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\nfn finitely_supported_function_templates() -> List {\n [\n { name: \"get\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_get\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"lookup\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_insert\", param_types: [ReceiverSelf, ReceiverKey, ReceiverValue], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_merge\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_has\", param_types: [ReceiverSelf, ReceiverKey], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_contains_key\", param_types: [ReceiverSelf, ReceiverKey], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_keys\", param_types: [ReceiverSelf], return_type: ContainerOf { source: Named { name: \"List\" }, element: ReceiverKey }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_values\", param_types: [ReceiverSelf], return_type: ContainerOf { source: Named { name: \"List\" }, element: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"with\", param_types: [ReceiverSelf, ReceiverKey, ReceiverValue], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"contains\", param_types: [ReceiverSelf, ReceiverKey], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"length\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"count\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\nfn algebra_method_template_name(name: String) -> Bool {\n all_algebra_profiles()\n |> any(profile => algebra_templates_for_profile(profile: profile) |> any(t => t.name == name))\n}\n\nfn algebra_templates_for_profile(profile: AlgebraProfile) -> List {\n match profile {\n OrderedRingProfile => ordered_ring_templates()\n ApproximateFieldProfile => approximate_field_templates()\n BooleanAlgebraProfile => boolean_algebra_templates()\n FinitePowerSetProfile => finite_power_set_templates()\n PointwisePowerCollectionProfile => pointwise_power_collection_templates()\n FreeMonoidScalarProfile => free_monoid_scalar_templates()\n FreeMonoidCollectionProfile => free_monoid_collection_templates()\n PartialFunctionProfile => partial_function_templates()\n FinitelySupportedFunctionProfile => finitely_supported_function_templates()\n }\n}\n\nfn algebra_type_param_names(profile: AlgebraProfile) -> List {\n match profile {\n OrderedRingProfile => []\n ApproximateFieldProfile => []\n BooleanAlgebraProfile => []\n FinitePowerSetProfile => [\"T\"]\n PointwisePowerCollectionProfile => [\"T\"]\n FreeMonoidScalarProfile => []\n FreeMonoidCollectionProfile => [\"T\"]\n PartialFunctionProfile => [\"K\", \"V\"]\n FinitelySupportedFunctionProfile => [\"K\", \"V\"]\n }\n}\n\nfn all_algebra_profiles() -> List {\n [\n OrderedRingProfile,\n ApproximateFieldProfile,\n BooleanAlgebraProfile,\n FinitePowerSetProfile,\n PointwisePowerCollectionProfile,\n FreeMonoidScalarProfile,\n FreeMonoidCollectionProfile,\n PartialFunctionProfile,\n FinitelySupportedFunctionProfile,\n ]\n}\n\nfn all_algebra_field_templates() -> List {\n all_algebra_profiles()\n |> flat_map(profile => algebra_templates_for_profile(profile: profile))\n}\n\nfn all_algebra_template_names() -> List {\n all_algebra_field_templates() |> map(template => template.name)\n}\n\n// trim is the importable free-function authority for whitespace trim on String. The declared\n// substrate body is a pure-dag seam (1 / 0); semantic execution routes through free_call.trim\n// (v1_rt::trim). Host realization authority: std.primitives trim_contract. Emitted host-native\n// std.algebra::trim delegates to v1_rt::trim via rust_host_string_op_fn_emit. The scalar template\n// row trim on FreeMonoid is the method spelling on the same carrier. Free-call trim(s)\n// requires explicit import std.algebra { trim } -- bare trim refuses via\n// closure_independent_bare_free_call registry in v1.compiler.infer_env even when std.algebra is\n// already in the compilation pool (#6985 Class B pool coincidence closed on trim by #8062).\n\n// The seam diverges before producing anything; `s` is read in the condition so the declared\n// input reaches the result (wiring-liveness), mirroring the bytes.dag float-seam pattern --\n// both arms are the same unreached literal, there to give the expression its type.\nfn trim(s: String) -> String {\n if s == s { from_code_point(1 / 0) } else { from_code_point(1 / 0) }\n}\n" +fn probe_0() -> ProcessExit { + let s = conservation_subject(id_tail: "probe_0", source: probe_src_0) + let spans = match s.parsed { + Accepted { value: a, diagnostics: _ } => a.span_index + Rejected { diagnostics: _ } => span_index_empty() + } + match s.normalized { + Accepted { value: _, diagnostics: _ } => ExitSuccess + Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) + } +} + +data probe_src_1: String = "module std.primitives\n\ntype Certainty = Proven | Amortized | Expected | Conservative\n\ntype CarrierCostSensitivity\n = CarrierInsensitive\n | CarrierSensitive { ephemeral_work: String, persistent_work: String }\n\ntype PrimitiveContract {\n name: String\n work: String\n output_size: String\n certainty: Certainty\n carrier_cost: CarrierCostSensitivity\n}\n\ndata char_at_contract: PrimitiveContract = {\n name: \"char_at\",\n work: \"pos\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata string_length_contract: PrimitiveContract = {\n name: \"string_length\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\n// Work is linear in the message, as any MAC over it must be. The output is one bit -- deliberately,\n// since extdeps.crypto.mac exposes no computed tag: the comparison happens inside the bound\n// implementation, in constant time, and a caller that could see the tag could compare it in\n// variable time instead.\ndata hmac_sha256_verify_hex_contract: PrimitiveContract = {\n name: \"hmac_sha256_verify_hex\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\n// Issuance: work linear in the message, output the 64 lowercase hex digits of a 32-octet tag. A\n// SECOND contract rather than a widened verify, because only the key holder mints; a verifier keeps\n// its one-bit answer (hmac_sha256_verify_hex_contract above) and never sees a tag to compare.\ndata hmac_sha256_hex_contract: PrimitiveContract = {\n name: \"hmac_sha256_hex\",\n work: \"n\",\n output_size: \"64\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata substring_contract: PrimitiveContract = {\n name: \"substring\",\n work: \"end\",\n output_size: \"end - start\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata string_contains_contract: PrimitiveContract = {\n name: \"string_contains\",\n work: \"n * m\",\n output_size: \"1\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata starts_with_contract: PrimitiveContract = {\n name: \"starts_with\",\n work: \"m\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata ends_with_contract: PrimitiveContract = {\n name: \"ends_with\",\n work: \"m\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata trim_contract: PrimitiveContract = {\n name: \"trim\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata concat_contract: PrimitiveContract = {\n name: \"concat\",\n work: \"n + m\",\n output_size: \"n + m\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n + m\", persistent_work: \"log(n + m)\" }\n}\n\ndata split_contract: PrimitiveContract = {\n name: \"split\",\n work: \"n\",\n output_size: \"k\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata repeat_contract: PrimitiveContract = {\n name: \"repeat\",\n work: \"m * n\",\n output_size: \"m * n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata chars_contract: PrimitiveContract = {\n name: \"chars\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata to_string_contract: PrimitiveContract = {\n name: \"to_string\",\n work: \"n\",\n output_size: \"n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata parse_int_contract: PrimitiveContract = {\n name: \"parse_int\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata str_eq_contract: PrimitiveContract = {\n name: \"str_eq\",\n work: \"min(n, m)\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata replace_section_contract: PrimitiveContract = {\n name: \"replace_section\",\n work: \"n\",\n output_size: \"n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata code_point_contract: PrimitiveContract = {\n name: \"code_point\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata from_code_point_contract: PrimitiveContract = {\n name: \"from_code_point\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata scan_while_contract: PrimitiveContract = {\n name: \"scan_while\",\n work: \"k\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata skip_horizontal_ws_contract: PrimitiveContract = {\n name: \"skip_horizontal_ws\",\n work: \"k\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata scan_to_eol_contract: PrimitiveContract = {\n name: \"scan_to_eol\",\n work: \"k\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata scan_string_end_contract: PrimitiveContract = {\n name: \"scan_string_end\",\n work: \"k\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata map_contract: PrimitiveContract = {\n name: \"map\",\n work: \"n * f_work\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata filter_contract: PrimitiveContract = {\n name: \"filter\",\n work: \"n * pred_work\",\n output_size: \"k <= n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata filter_map_contract: PrimitiveContract = {\n name: \"filter_map\",\n work: \"n * f_work\",\n output_size: \"k <= n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata flat_map_contract: PrimitiveContract = {\n name: \"flat_map\",\n work: \"n * f_work\",\n output_size: \"sum(f_output_sizes)\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata fold_contract: PrimitiveContract = {\n name: \"fold\",\n work: \"n * f_work\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata any_contract: PrimitiveContract = {\n name: \"any\",\n work: \"k * pred_work\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata all_contract: PrimitiveContract = {\n name: \"all\",\n work: \"k * pred_work\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata contains_contract: PrimitiveContract = {\n name: \"contains\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata join_contract: PrimitiveContract = {\n name: \"join\",\n work: \"n + total_length\",\n output_size: \"total_length + (n - 1) * sep_length\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata sort_contract: PrimitiveContract = {\n name: \"sort\",\n work: \"n * log(n)\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata sort_by_contract: PrimitiveContract = {\n name: \"sort_by\",\n work: \"n * (key_work + log(n))\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata dedup_contract: PrimitiveContract = {\n name: \"dedup\",\n work: \"n * n\",\n output_size: \"k <= n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata len_contract: PrimitiveContract = {\n name: \"len\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata count_contract: PrimitiveContract = {\n name: \"count\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata sum_contract: PrimitiveContract = {\n name: \"sum\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata first_contract: PrimitiveContract = {\n name: \"first\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log n\" }\n}\n\ndata last_contract: PrimitiveContract = {\n name: \"last\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log n\" }\n}\n\ndata get_contract: PrimitiveContract = {\n name: \"get\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log n\" }\n}\n\ndata max_by_contract: PrimitiveContract = {\n name: \"max_by\",\n work: \"n * f_work\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata reverse_contract: PrimitiveContract = {\n name: \"reverse\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata list_push_contract: PrimitiveContract = {\n name: \"list_push\",\n work: \"1\",\n output_size: \"n + 1\",\n certainty: Amortized,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n\", persistent_work: \"log n\" }\n}\n\ndata append_contract: PrimitiveContract = {\n name: \"append\",\n work: \"m\",\n output_size: \"n + m\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n + m\", persistent_work: \"log(n + m)\" }\n}\n\ndata skip_contract: PrimitiveContract = {\n name: \"skip\",\n work: \"n\",\n output_size: \"max(0, len - n)\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata zip_contract: PrimitiveContract = {\n name: \"zip\",\n work: \"min(n, m)\",\n output_size: \"min(n, m)\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata enumerate_contract: PrimitiveContract = {\n name: \"enumerate\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata empty_map_contract: PrimitiveContract = {\n name: \"empty_map\",\n work: \"1\",\n output_size: \"0\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata map_insert_contract: PrimitiveContract = {\n name: \"map_insert\",\n work: \"1\",\n output_size: \"n + 1\",\n certainty: Amortized,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n\", persistent_work: \"log32 n\" }\n}\n\ndata map_get_contract: PrimitiveContract = {\n name: \"map_get\",\n work: \"1\",\n output_size: \"1\",\n certainty: Expected,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log32 n\" }\n}\n\ndata map_contains_key_contract: PrimitiveContract = {\n name: \"map_contains_key\",\n work: \"1\",\n output_size: \"1\",\n certainty: Expected,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log32 n\" }\n}\n\ndata lookup_contract: PrimitiveContract = {\n name: \"lookup\",\n work: \"1\",\n output_size: \"1\",\n certainty: Expected,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log32 n\" }\n}\n\ndata map_merge_contract: PrimitiveContract = {\n name: \"map_merge\",\n work: \"m\",\n output_size: \"n + m\",\n certainty: Amortized,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n + m\", persistent_work: \"m * log32(n + m)\" }\n}\n\ndata map_keys_contract: PrimitiveContract = {\n name: \"map_keys\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata map_values_contract: PrimitiveContract = {\n name: \"map_values\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata index_by_contract: PrimitiveContract = {\n name: \"index_by\",\n work: \"n * key_fn_work\",\n output_size: \"n\",\n certainty: Amortized,\n carrier_cost: CarrierInsensitive\n}\n\ndata with_contract: PrimitiveContract = {\n name: \"with\",\n work: \"n + m\",\n output_size: \"n + m\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n + m\", persistent_work: \"m * log32 n\" }\n}\n\ndata filesystem_read_contract: PrimitiveContract = {\n name: \"filesystem_read\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata to_bytes_contract: PrimitiveContract = {\n name: \"to_bytes\",\n work: \"n\",\n output_size: \"n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata to_json_contract: PrimitiveContract = {\n name: \"to_json\",\n work: \"n\",\n output_size: \"n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata hash_contract: PrimitiveContract = {\n name: \"hash\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata hash_combine_contract: PrimitiveContract = {\n name: \"hash_combine\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata atom_identity_hash_contract: PrimitiveContract = {\n name: \"atom_identity_hash\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata contiguous_loop_elementwise_kernel_contract: PrimitiveContract = {\n name: \"contiguous_loop_elementwise_kernel\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata contiguous_loop_elementwise_float_kernel_contract: PrimitiveContract = {\n name: \"contiguous_loop_elementwise_float_kernel\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\n// Enumerable registry surface names parallel to builtin_function_registry keys. Witness-time census\n// cannot call map_keys on Map because Node is not a runtime Value. Relocated from\n// v1.compiler.infer_method for dag/ compile scope (review 46064) — same parallel-enumeration debt,\n// not a new fork. Dissolve-on: fold builtin_function_registry construction from this list as the\n// single authority.\n// DRIFT IS NOW MEASURED, NOT PREDICTED: gunbc.primitive_egress.census_live reads the authority's\n// key set through the builtin_function_registry_keys compiler query and prints both set\n// differences (hand_roster_missing_registry_rows / hand_roster_rows_absent_from_registry) in every\n// census receipt (`gunbc test //gunbc/instruments:primitive-egress-census-v2`); the receipt, not\n// this note, says whether the list matches the key set.\n\nfn builtin_registry_surface_names() -> List {\n [\n \"hmac_sha256_verify_hex\",\n \"hmac_sha256_hex\",\n \"count\",\n \"string_length\",\n \"code_point\",\n \"to_int\",\n \"scan_while\",\n \"scan_string_end\",\n \"scan_to_eol\",\n \"skip_horizontal_ws\",\n \"parse_int\",\n \"char_at\",\n \"substring\",\n \"from_code_point\",\n \"chars_to_string\",\n \"record_source_chars_index_lookup\",\n \"resolution_silent_pick_is_enabled\",\n \"name_resolution_policy_is_namespace_only\",\n \"resolution_silent_pick_record_global_bare_lcp_pick\",\n \"resolution_silent_pick_record_global_bare_lcp_tie\",\n \"resolution_silent_pick_record_fn_parent_first_hit\",\n \"to_string\",\n \"discriminant\",\n \"concat\",\n \"map_insert\",\n \"map_merge\",\n \"with\",\n \"empty_set\",\n \"set_insert\",\n \"set_union\",\n \"map_contains_key\",\n \"map_has\",\n \"map_is_empty\",\n \"rc_ptr_eq\",\n \"rc_vec_ptr_eq\",\n \"set_contains\",\n \"lookup\",\n \"map_get\",\n \"Some\",\n \"map_keys\",\n \"sorted_map_keys\",\n \"map_values\",\n \"get\",\n \"reverse\",\n \"list_push\",\n \"hash_combine\",\n \"atom_identity_hash\",\n \"trace_mark\",\n \"observed_peak_resident_bytes\",\n \"observed_monotonic_nanos\",\n \"string_contains\",\n \"length\",\n \"starts_with\",\n \"replace\",\n \"filesystem_read\",\n \"emit_host_run_transport\",\n \"emit_host_run_transport_cached\",\n \"emit_host_native_cache_evict\",\n \"contiguous_loop_elementwise_kernel\",\n \"contiguous_loop_elementwise_float_kernel\",\n \"shell_materialize_operation_argv\",\n \"shell_transport_operation_rows\",\n \"extdeps_shape_transport_policy_facts_for_qualified_name\",\n \"extdeps_qualified_name_resolves_in_derived_module_set\",\n \"extdeps_external_authority_facts_for_qualified_name\",\n \"extdeps_external_authority_live_clean_tree_holds\",\n \"extdeps_external_authority_live_roster_module_count\",\n \"seed_runner_bool_false_failure_detail\",\n \"doc_graph_orphan_count\",\n \"doc_graph_admitted_root_count\",\n \"doc_graph_dangling_link_count\",\n \"doc_graph_doc_count\",\n \"compile_dag_rust_emit_check\",\n \"parsed_import_statements\",\n \"floor_discovery_source_inventory\",\n \"namespace_structural_observation_admissions\",\n \"emit_rust_reference_derived_rows_bridge\",\n \"witness_layer_roots_compile_clean_check\",\n \"witness_layer_roots_compile_clean_emit_check\",\n \"install_or_consume_floor_compile_clean_gate_receipt\",\n \"record_generated_artifact_drift_gate_failure_detail\",\n \"record_generated_artifact_drift_gate_clean\",\n \"consume_generated_artifact_drift_gate_receipt\",\n \"witness_compile_clean_cli_floor_verdicts_agree\",\n \"test_migration_debt_module_names\",\n \"test_migration_legacy_behavior_ids\",\n \"test_migration_witness_behavior_ids\",\n \"test_migration_behavior_discovery_holds\",\n \"inert_carrier_names_live\",\n \"inert_carrier_declared_count\",\n \"non_fold_residue_count\",\n \"non_fold_residue_unrostered_count\",\n \"non_fold_residue_stale_roster_count\",\n \"non_fold_residue_coproduct_universe_count\",\n \"commit_witness_claim_roster_unresolvable_count\",\n \"commit_witness_claim_pair_resolvable\",\n \"non_fold_residue_wildcard_red_fixture_holds\",\n \"non_fold_residue_total_fold_green_fixture_holds\",\n \"non_fold_residue_roster_red_fixture_holds\",\n \"non_fold_residue_synthetic_unrostered_red_holds\",\n \"complexity_linearity_syntactic_finding_count\",\n \"complexity_linearity_wildcard_facts\",\n \"complexity_linearity_syntactic_site_fired\",\n \"fallback_arm_census_facts\",\n \"fallback_arm_census_class_count\",\n \"fallback_arm_census_total\",\n \"fallback_arm_census_reconciliation_holds\",\n \"census_corpus_roots_follow_layer_authority\",\n \"transport_script_position_facts_for_path\",\n \"layer_import_facts\",\n \"import_resolution_facts\",\n \"reference_resolution_facts\",\n \"module_declaration_facts\",\n \"module_declaration_facts_at\",\n \"concept_decl_facts\",\n \"decl_facts\",\n \"decl_facts_at\",\n \"export_signature_facts\",\n \"data_decl_type_facts\",\n \"fact_cardinality_decl_facts\",\n \"languages_consumer_census_data_decl_count\",\n \"languages_consumer_census_per_language_row_count\",\n \"languages_consumer_census_format_row_count\",\n \"languages_consumer_census_external_consumer_count\",\n \"languages_consumer_census_is_composition_only\",\n \"languages_consumer_census_has_external_consumer\",\n \"is_xid_start\",\n \"is_xid_continue\",\n \"is_emoji_ident\",\n \"claim_scope_dag_multi_module_fixture\",\n \"class_b_import_closure_gate_not_affected_skip\",\n \"compile_dag_call_form_leaf_guard\",\n \"compile_dag_callsite_resolved_call_edges\",\n \"compile_dag_diagnostic_census\",\n \"compile_dag_importer_resolved_call_edges\",\n \"compile_dag_multi_module_fixture\",\n \"compile_dag_reference_occurrence_binding_census\",\n \"dependency_resolution_facts\",\n \"dependency_resolution_facts_at\",\n \"observe_declared_import_closure_symbol_binding\",\n \"parse_roadmap_acceptance_event_history_jsonl\",\n \"parse_stage0_cargo_manifest_bins\",\n \"project_roadmap_acceptance_event_history_from_authority_text_host\",\n \"toolchain_home_interference_probe\",\n \"type_ref_hit_ne_bind_measure_active\",\n \"builtin_function_registry_keys\",\n \"compile_dag_primitive_call_edges\",\n \"source_root_ingest_module_identities\",\n \"required_floor_nominal_subject_module_identities\",\n \"entry_closure_module_identities\",\n ]\n}\n\ndata primitive_contract_roster_note: String = \"Hand-maintained parallel enumeration of every *_contract data declaration in this module. A new foo_contract row silently under-reports on primitive_surface_census_derived until this roster is updated — the open-denominator class D0 is designed to expose. Dissolve-on: derive the roster by enumerating *_contract declarations once declaration introspection lands, so contract surfaces read the module rather than a second list.\"\n\nfn primitive_contract_roster() -> List {\n [\n char_at_contract,\n string_length_contract,\n hmac_sha256_verify_hex_contract,\n hmac_sha256_hex_contract,\n substring_contract,\n string_contains_contract,\n starts_with_contract,\n ends_with_contract,\n trim_contract,\n concat_contract,\n split_contract,\n repeat_contract,\n chars_contract,\n to_string_contract,\n parse_int_contract,\n str_eq_contract,\n replace_section_contract,\n code_point_contract,\n from_code_point_contract,\n scan_while_contract,\n skip_horizontal_ws_contract,\n scan_to_eol_contract,\n scan_string_end_contract,\n map_contract,\n filter_contract,\n filter_map_contract,\n flat_map_contract,\n fold_contract,\n any_contract,\n all_contract,\n contains_contract,\n join_contract,\n sort_contract,\n sort_by_contract,\n dedup_contract,\n len_contract,\n count_contract,\n sum_contract,\n first_contract,\n last_contract,\n get_contract,\n max_by_contract,\n reverse_contract,\n list_push_contract,\n append_contract,\n skip_contract,\n zip_contract,\n enumerate_contract,\n empty_map_contract,\n map_insert_contract,\n map_get_contract,\n map_contains_key_contract,\n lookup_contract,\n map_merge_contract,\n map_keys_contract,\n map_values_contract,\n index_by_contract,\n with_contract,\n filesystem_read_contract,\n to_bytes_contract,\n to_json_contract,\n hash_contract,\n hash_combine_contract,\n atom_identity_hash_contract,\n contiguous_loop_elementwise_kernel_contract,\n contiguous_loop_elementwise_float_kernel_contract,\n ]\n}\n" +fn probe_1() -> ProcessExit { + let s = conservation_subject(id_tail: "probe_1", source: probe_src_1) + let spans = match s.parsed { + Accepted { value: a, diagnostics: _ } => a.span_index + Rejected { diagnostics: _ } => span_index_empty() + } + match s.normalized { + Accepted { value: _, diagnostics: _ } => ExitSuccess + Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) + } +} + +data probe_src_2: String = "module std.termination\n\nimport std.algebra { BoundedLattice, Ordering }\n\ntype DescentEvidence\n = Strict\n | NonIncreasing\n | DescentUnknown\n\nfn evidence_rank(e: DescentEvidence) -> Int {\n match e {\n Strict => 2\n NonIncreasing => 1\n DescentUnknown => 0\n }\n}\n\nfn descent_evidence_lattice_meet(a: DescentEvidence, b: DescentEvidence) -> DescentEvidence {\n match a {\n Strict => match b {\n Strict => Strict\n NonIncreasing => NonIncreasing\n _ => DescentUnknown\n }\n NonIncreasing => match b {\n Strict => NonIncreasing\n NonIncreasing => NonIncreasing\n _ => DescentUnknown\n }\n _ => DescentUnknown\n }\n}\n\nfn descent_evidence_lattice_join(a: DescentEvidence, b: DescentEvidence) -> DescentEvidence {\n match a {\n DescentUnknown => b\n NonIncreasing => match b {\n Strict => Strict\n _ => NonIncreasing\n }\n Strict => Strict\n }\n}\n\ndata descent_evidence_bounded_lattice: BoundedLattice = {\n meet: descent_evidence_lattice_meet\n join: descent_evidence_lattice_join\n top: Strict\n bottom: DescentUnknown\n}\n\nfn promote_to_strict(evidence: DescentEvidence) -> DescentEvidence {\n match evidence {\n NonIncreasing => NonIncreasing\n Strict => Strict\n _ => DescentUnknown\n }\n}\n\nfn optional_evidence_meet(a: DescentEvidence?, b: DescentEvidence?) -> DescentEvidence? {\n match a {\n Absent => b\n Present { value: va } =>\n match b {\n Absent => a\n Present { value: vb } => Present { value: descent_evidence_lattice_meet(a: va, b: vb) }\n }\n }\n}\n\nfn map_evidence_merge_at(base: Map, key: String, new_val: DescentEvidence) -> Map {\n match map_get(base, key) {\n Present { value: existing } => map_insert(base, key, descent_evidence_lattice_meet(a: existing, b: new_val))\n Absent => map_insert(base, key, new_val)\n }\n}\n\ntype RankingDimension\n = TreeSize { param: String }\n | ListLength { param: String }\n | ArithmeticValue { param: String }\n | TokenPosition { param: String }\n | SetCardinality { param: String }\n\ntype PositiveDescentAmount\n = OneStep\n | AdditionalStep { previous: PositiveDescentAmount }\n\nfn positive_descent_count(steps: PositiveDescentAmount) -> Int {\n match steps {\n OneStep => 1\n AdditionalStep { previous: p } => 1 + positive_descent_count(p)\n }\n}\n\ntype ProportionalDivisor\n = DivideByTwo\n | StrictlyLarger { inner: ProportionalDivisor }\n\nfn proportional_divisor_to_int(d: ProportionalDivisor) -> Int {\n match d {\n DivideByTwo => 2\n StrictlyLarger { inner: p } => 1 + proportional_divisor_to_int(d: p)\n }\n}\n\nfn peano_literal_materialization_cap() -> Int { 256 }\n\nfn positive_descent_amount_from_positive_int(k: Int) -> PositiveDescentAmount? {\n if k <= 0 { none }\n else {\n if k > peano_literal_materialization_cap() { none }\n else {\n if k == 1 { Present { value: OneStep } }\n else {\n match positive_descent_amount_from_positive_int(k: k - 1) {\n Present { value: prev } => Present { value: AdditionalStep { previous: prev } }\n Absent => none\n }\n }\n }\n }\n}\n\nfn proportional_divisor_from_int_at_least_two(k: Int) -> ProportionalDivisor? {\n if k < 2 { none }\n else {\n if k > peano_literal_materialization_cap() { none }\n else {\n if k == 2 { Present { value: DivideByTwo } }\n else {\n match proportional_divisor_from_int_at_least_two(k: k - 1) {\n Present { value: prev } => Present { value: StrictlyLarger { inner: prev } }\n Absent => none\n }\n }\n }\n }\n}\n\ntype DescentSource\n = ChildAccessor { accessor: String }\n | ListShrink { amount: PositiveDescentAmount }\n | ArithmeticSubtractDescent { steps: PositiveDescentAmount }\n | ArithmeticDivideDescent { divisor: ProportionalDivisor }\n | ParserAdvance { witness: String }\n | SetRemoval { element: String }\n | FoldIteration\n\ntype TerminationProof {\n dimensions: List\n}\n\ntype ProofEdge {\n caller: String\n callee: String\n evidence: List\n}\n" +fn probe_2() -> ProcessExit { + let s = conservation_subject(id_tail: "probe_2", source: probe_src_2) + let spans = match s.parsed { + Accepted { value: a, diagnostics: _ } => a.span_index + Rejected { diagnostics: _ } => span_index_empty() + } + match s.normalized { + Accepted { value: _, diagnostics: _ } => ExitSuccess + Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) + } +} + +data probe_src_3: String = "module std.types\n\nimport std.algebra { FinitePowerSet, FinitelySupportedFunction, FreeMonoid, kernel_algebra_profile, algebra_type_param_names, carrier_container_algebra_rows, carrier_container_alias_rows, carrier_container_arity_rows, carrier_container_roster_map }\n\ndata kernel_type_set: Map = {\n \"String\": true, \"Int\": true, \"Bool\": true, \"Float\": true,\n \"Secret\": true, \"Json\": true, \"Unit\": true, \"Bytes\": true\n}\n\nfn is_kernel_type(name: String) -> Bool {\n match map_get(kernel_type_set, name) {\n Present { value: _ } => true\n Absent => false\n }\n}\n\n// DERIVED FROM THE CARRIER ROSTER, not restated. Every row but one is a spelling that declared\n// `container_arity_row` on its carrier in `std.algebra`, and its arity comes from that carrier's\n// own `algebra_type_param_names` -- so a carrier cannot be listed here with an arity its algebra\n// contradicts, and an eleventh carrier reaches this map by declaring the row rather than by\n// someone remembering to edit it.\n//\n// `Witness` is appended because it is NOT an algebra carrier: it has no profile and no template\n// set, and its parameter name is special-cased in `container_param_names_for` directly below.\n// It is the one hand-authored row left in this map, and it is the reason the memberships in\n// `std.algebra` are declared rather than derived from the profile -- grounding `Witness` as a\n// carrier is that class's next-rung trigger.\ndata container_type_arity: Map = map_insert(carrier_container_arity_rows(), \"Witness\", 1)\n\nfn is_container_type(name: String) -> Bool {\n match container_expected_arity(name: name) {\n Present { value: _ } => true\n Absent => false\n }\n}\n\nfn container_expected_arity(name: String) -> Int? {\n map_get(container_type_arity, name)\n}\n\nfn container_param_names_for(kind_name: String) -> List {\n if kind_name == \"Witness\" {\n [\"T\"]\n } else {\n match map_get(kernel_algebra_profile, kind_name) {\n Present { value: p } => algebra_type_param_names(profile: p)\n Absent => []\n }\n }\n}\n\nfn container_param_name(kind_name: String, index: Int) -> String? {\n let names = container_param_names_for(kind_name: kind_name)\n match names |> enumerate |> filter(pair => pair.first == index) |> map(pair => pair.second) |> first {\n Present { value: name } => Present { value: name }\n Absent => none\n }\n}\n\ndata ordered_element_collections: Map = {\n \"List\": true\n}\n\nfn is_ordered_element_collection(name: String) -> Bool {\n map_contains_key(ordered_element_collections, name)\n}\n\n// BOTH MAPS ARE DERIVED FROM THE CARRIER ROSTER in `std.algebra`. What stood here was two\n// spelling-keyed literals whose note explained -- correctly -- why each row was present or\n// absent: `PartialFunction` had to be in the alias map because that map decides what resolves as\n// a container at all, and `FinitelySupportedFunction` had to stay out because\n// `container_alias_canonical_spelling` returns the first sorted key for an algebra and that key\n// would sort ahead of `\"Map\"`. Those reasons are now FIELDS on the spelling rows they govern\n// (`container_alias_row`, with its `RowAbsent` reason), so they travel with the carrier instead\n// of living in prose beside a literal a later author edits without reading it.\n//\n// A self-mapping row is not a nickname: the value is the ALGEBRA and the key is a SPELLING that\n// resolves to it, which is why `List` and `Set` are keys at all.\ndata container_template_algebra_rows: Map = carrier_container_algebra_rows()\n\ndata container_template_alias_rows: Map = carrier_container_alias_rows()\n\nfn container_template_algebra(name: String) -> String? {\n map_get(container_template_algebra_rows, name)\n}\n\nfn container_template_alias_algebra(name: String) -> String? {\n map_get(container_template_alias_rows, name)\n}\n\n// THE COMPLETE ROSTER, DERIVED. This list feeds `v1.compiler.coercion` `inhabitant_test_names`,\n// which used to read `concat(canonical_container_names(), [\"PointwisePower\"])` -- a hand-appended\n// name compensating for one this list omitted. A caller patching its authority's answer is the\n// authority being wrong, not the caller being thorough; the append is gone and the name now\n// arrives from the carrier that declares it.\n//\n// `sorted_map_keys` gives the same ASCII order the hand-written literal held, and `Witness` is\n// appended for the reason `container_type_arity` above states: it is not an algebra carrier.\nfn canonical_container_names() -> List {\n sorted_map_keys(map_insert(carrier_container_roster_map(), \"Witness\", true))\n}\n\ntype Bool = True | False\n\ntype Unit\n\ntype Json\ntype Bytes\n\ntype Char = Int where unicode_scalar, brand(\"Char\")\n\ntype List = FreeMonoid\n// BOTH ALIASES NAME THE FINITE CARRIER, the motion the predicate-half change (`PointwisePower`,\n// landed separately) was waiting for. A `Set` or a `Map` in a declaration position has always\n// LOWERED to a finite container -- `im::OrdSet` and `HashMap` in Rust, likewise in Go, Python and\n// TypeScript -- so pointing the aliases at the finite carriers makes the declaration say what the\n// realization already did. The open carriers keep their own names: `std.algebra` `PointwisePower`\n// for a characteristic function, `PartialFunction` for a partial one.\n//\n// The two rows move together because they are one row set. Retargeting `Map` while `Set` still\n// resolved to the predicate would leave the fork intact on the Set side and make the pair\n// inconsistent for the interval, the dual-authority state the replacement-migration doctrine\n// avoids. The transition is atomic in the AUTHORITY sense -- one motion, no interval in which both\n// answers are live -- whatever the diff size.\n//\n// RUNG: STILL MITIGATABLE. This change does NOT make the bad literal unwritable, and saying so\n// would be the inflation DESIGN 4b calls worse than sitting low. `FinitePowerSet` carries `member`\n// and `FinitelySupportedFunction` carries `lookup`, because a finite set answers membership and a\n// finite map answers lookup -- so `Set { member: .. }` and `Map { lookup: .. }` still typecheck.\n// They must: 145 `Map { lookup: .. }` literals are live in the corpus and are re-authored by the\n// following change, not this one.\n//\n// The correction is that the invalid state was never \"a literal supplying `member` or `lookup`\".\n// It is \"a literal supplying ONLY those, on a carrier whose remaining fields promise an\n// enumeration it cannot perform\" -- a construction-COMPLETENESS question, and record literals in\n// this substrate do not require every field. So the real NEXT-RUNG TRIGGER is field-completeness\n// enforcement on record construction, which would make the deficient literal unwritable at every\n// carrier at once rather than at these two. Until then the class stays mitigatable and only review\n// sees a new one; what this change buys is that each alias now names a carrier that can answer\n// for it -- the single-authority half, not the wall.\n//\n// CARRIED FORWARD FROM THE PREDICATE-HALF CHANGE (#9055), whose note this row replaces: the\n// fusion was measured, not argued -- `extdeps.languages.rust.types` gave `PointwisePower` the\n// inhabitant `BTreeSet<{0}>`, so a declaration position lowered to `im::OrdSet` while a record\n// literal lowered to the algebra struct, and the two met as a rustc refusal wherever one compiled\n// closure held both (`docs/probes/t3_collection_realization_2026-08-22.md`). That change also\n// closed the predicate half: no `Set { member: .. }` literal survives in `src/v2`, so the Set side\n// of the fork is a spelling question only. Its stated NEXT-RUNG TRIGGER was the Map-side\n// decomposition landing, after which both rows retarget in one motion. That is this row.\ntype Set = FinitePowerSet\ntype Map = FinitelySupportedFunction\n\nfn list_length(items: List) -> Int {\n fold(items, init: 0, f: (acc, _) => acc + 1)\n}\n\ntype CommitSha = String\n\n// The located syntax wall for an EXTERNALLY SUPPLIED Git head: lowercase 40-hex. It lives here,\n// beside CommitSha, the one authority every Git-head consumer already names — it was previously\n// homed in std.realization_schedule as scoped_witness_exact_head_text_holds, where a since-deleted\n// receipt family happened to be its first caller, and a second consumer\n// (v2.compiler.self_host.frontier_probe_survey) was already importing it across that seam.\n// CommitSha is presently an unvalidated String alias, so this predicate is a validating CHECK a\n// caller must remember to run, not a construction wall. Dissolve-on: CommitSha gains one validating\n// constructor; callers reach the constructor, the invalid state stops being writable, and this\n// predicate deletes.\n\n// WHAT THIS COMPUTES: forty characters, each a lowercase hex digit. It is spelled as \"remove every\n// hex digit and ask whether nothing is left\" rather than as a test of each character. BOTH SPELLINGS\n// ARE LINEAR in the input, so the per-character one was never a cost-shape defect in the sense\n// DESIGN section 6 makes standing: what this spelling buys is a CONSTANT FACTOR, because the\n// interpreter charges a per-character lambda far more than one whole-string builtin. That is a\n// modeling loss (the alphabet is hard-coded and the word `hex` is gone) taken against an evaluator\n// cost, i.e. a workaround, and it is named as one here.\n//\n// DISSOLVE-ON: the per-character evaluation cost is fixed at its source, in the interpreter, so the\n// per-character spelling costs about what the whole-string one does; this predicate then returns to\n// testing each character against the hex alphabet, and the oracle below becomes its only spelling.\n//\n// THE RESPELLING IS ADMITTED ON ITS OWN COST AND ON NOTHING DOWNSTREAM OF IT (operator ruling,\n// stern-carp-604, 2026-09-20, on gunb-ai/gunbc#11730). A claim budget elsewhere in the tree fitting\n// afterwards is a consequence and was never the reason; anyone reading this as buying margin for a\n// census should read that ruling, which admits a fix to a shared predicate and refuses only an\n// author admitting their own.\n//\n// THE EQUALITY IS THE CONTRACT, not the shape: test.claim.std_commit_sha_text_witness keeps the\n// per-character spelling as an oracle and claims the two answer the same on every input either can\n// be given, with a red per refusal cause -- the lengths either side of forty, uppercase, the\n// characters adjacent to both ranges, and the empty string.\nfn commit_sha_text_holds(head: String) -> Bool {\n (head.length() == 40) && (replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(head, \"0\", \"\"), \"1\", \"\"), \"2\", \"\"), \"3\", \"\"), \"4\", \"\"), \"5\", \"\"), \"6\", \"\"), \"7\", \"\"), \"8\", \"\"), \"9\", \"\"), \"a\", \"\"), \"b\", \"\"), \"c\", \"\"), \"d\", \"\"), \"e\", \"\"), \"f\", \"\") == \"\")\n}\n\ntype Sha256 = String\ntype RetryCount = Int where range(min: 1, max: 5)\ntype HttpStatus = Int where range(min: 100, max: 599)\n\n// THE ONE ADMISSION FOLD FOR AN Int THAT CLAIMS TO BE AN HttpStatus. The refinement above is\n// reported by the compiler as unenforced (predicate deferred), so a value arriving from a parse --\n// curl's --write-out, a wire body, a log line -- has to be admitted somewhere, and that somewhere\n// must be HERE, beside the range, rather than a copied pair of literals at each parse site that\n// drifts silently if the range row ever moves. The bounds are read from the two rows the fold\n// consumes; the refinement keeps the same numbers as literals because a where-clause cannot name\n// a data row. That pairing is a language-layer gap, not a reader check: its next-rung trigger is\n// gunbc.where_refinement_predicate_vocabulary\n// http_status_where_clause_names_declaration_frontier_rows (capability: a where-clause names a\n// declaration). A parse site consumes http_status_of and matches its Absent arm; it does not\n// compare against 100 or 599 itself.\ndata http_status_min: Int = 100\n\ndata http_status_max: Int = 599\n\nfn http_status_of(n: Int) -> HttpStatus? {\n if n < http_status_min || n > http_status_max { none } else { Present { value: n } }\n}\n\ntype Email = String\ntype Port = Int where range(min: 1, max: 65535)\n\ntype GistId = String\ntype Secret nominal_opaque = String\ntype SecretValue = Secret where non_empty\n\ntype SemVer = String\ntype NonEmptyStr = String where non_empty\ntype LanguageId = String where non_empty\ntype SecretName = String where non_empty\n\ntype PathSegment = NonEmptyStr where brand(\"PathSegment\")\n\ndata path_segment_safety_note: String = \"WHAT MAKES A STRING SAFE TO USE AS ONE PATH SEGMENT — the law, held once, so every branded id that becomes a directory name tests the same thing. The brand alone never carried it: a branded NonEmptyStr accepts \\\"..\\\", \\\"a/b\\\", and an embedded newline, so a value that typechecked as a segment could still escape its parent directory, alias a sibling, or split a line-oriented file written under that name. The refused set is exactly the characters that change what a concatenated path MEANS — `/` and `\\\\` introduce a level, `.` and `..` navigate, CR and LF terminate a record in every line-oriented format this repo writes, NUL terminates the string at the syscall boundary. Callers REFUSE on false rather than sanitizing, because a sanitized segment silently denotes something other than what the caller named (§5: a failure arm must refuse, never widen). Percent- or hex-encoding is the reversible alternative and is deliberately not offered until a caller needs a segment it cannot rename.\n\nThis is a PREDICATE, not a constructor, and that is a modeling choice rather than a limitation worked around. A generic `path_segment(raw) -> PathSegment?` would put the branding cast in this module, and each caller would then re-cast the generic segment into its own brand anyway — two casts and two authorities for one law. Holding the law here and letting each branded id (gunbc.merge_admission.walk_attempt_id is the first) construct itself through it keeps one authority for what is hostile and one constructor per brand, which is what §3 asks for. A generic constructor earns its place when a second caller wants a bare PathSegment rather than a brand of its own; none does today.\"\n\nfn path_segment_is_safe(raw: String) -> Bool {\n !((raw == \"\")\n || (raw == \".\")\n || (raw == \"..\")\n || string_contains(s: raw, pattern: \"/\")\n || string_contains(s: raw, pattern: \"\\\\\")\n || string_contains(s: raw, pattern: \"\\n\")\n || string_contains(s: raw, pattern: \"\\x0d\")\n || string_contains(s: raw, pattern: \"\\x00\"))\n}\ntype GlobSegment = NonEmptyStr where brand(\"GlobSegment\")\n\n// TEXT THAT CARRIES NO TERMINAL CONTROL SEQUENCES — the vocabulary two unrelated dependencies meet\n// on so neither has to know the other exists. A terminal-facing tool emits SGR escapes around the\n// values it highlights; a parser reading those values needs them absent. Those are facts about\n// DIFFERENT upstreams, and the wrong fix is for either to mention the other: a note in a CLI's\n// module explaining what a terminal multiplexer strips is filed under the wrong authority, and\n// stops being true the moment a second capture transport appears (operator, 2026-07-30: 'tmux\n// stripping ansi should be a tmux fact, and codex prompts should be a codex fact, and the\n// interaction should be emergent').\n//\n// So the brand is the seam. A capture surface that renders a pane PRODUCES RenderedTerminalText;\n// a prompt parser CONSUMES it; the composition is admissible because the types line up, not\n// because one module was written knowing about the other. A raw stdout pipe cannot inhabit the\n// brand, so feeding one to a parser that requires it is a type error rather than a runtime paste\n// of escape bytes the operator discovers when the far end rejects them.\n//\n// It lives in std because it is a property of text, not of any one tool — the same reason\n// FilePath is not owned by whichever dependency happens to open files.\n\ntype RenderedTerminalText = String where brand(\"RenderedTerminalText\")\ntype FilePathParts {\n segments: List\n}\ntype GlobPattern {\n segments: List\n}\ntype FilePath = String where non_empty\n\ndata file_path_sentinel_scaffold_note: String = \"review 45141. FilePath where non_empty made empty-string absent-file sentinels unwritable; interim re-spellings encode absence in a nominally-non_empty carrier (state-space conflation — path promises a real path while sentinel means absent/synthetic). Sites: emit_rust/go/python emit_*_test_file returns TextFile { path: \\\"\\\", content: \\\"\\\" }, filtered by string_length(content) > 0 not path; 00_core no_span uses SourceSpan.file \\\"\\\" for the null span (make_span, which fabricated that file name for CALLER-SUPPLIED offsets, is deleted -- the fileless constructor now takes no offsets, so a located range inside a nonexistent file has no constructor). Preserves pre-wall behavior. dissolve-on: feature:optional-textfile-and-source-span (lift emit carriers to Option and SourceSpan.file to optional FilePath; delete sentinels and content-length filter).\"\n\ntype SourceSpan {\n file: FilePath\n start: Int\n end: Int\n}\n\ntype Timestamp = String\ntype EpochSecs = Int where range(min: 0)\ntype EpochMs = Int where range(min: 0)\ntype Duration = Int where range(min: 0)\ntype Milliseconds = Int where range(min: 0), brand(\"Milliseconds\")\ntype Seconds = Int where range(min: 0), brand(\"Seconds\")\n\ntype LogicalTime = NonEmptyStr where brand(\"LogicalTime\")\n\ntype IntentId = NonEmptyStr where brand(\"IntentId\")\ntype IssueId = NonEmptyStr where brand(\"IssueId\")\ntype RunKey = NonEmptyStr where brand(\"RunKey\")\ntype ArtifactId = NonEmptyStr where brand(\"ArtifactId\")\ntype LeaseToken = NonEmptyStr where brand(\"LeaseToken\")\ntype WorkerId = NonEmptyStr where brand(\"WorkerId\")\ntype CommentId = NonEmptyStr where brand(\"CommentId\")\ntype SignalKey = NonEmptyStr where brand(\"SignalKey\")\n\ntype WorkflowProducerId = NonEmptyStr where brand(\"WorkflowProducerId\")\ntype WorkflowObserverId = NonEmptyStr where brand(\"WorkflowObserverId\")\ntype WorkflowProverId = NonEmptyStr where brand(\"WorkflowProverId\")\ntype WorkflowRunId = NonEmptyStr where brand(\"WorkflowRunId\")\n\ntype GitRef = String where non_empty\n\ntype TopologyNodeKind = Pure | Transport | SubDag | Env\ntype DocSourceKind = Template | Generated | Static\n\ntype FermiDepth = Xs | S | M | L | Xl\n\ntype TextFilePath = FilePath\n\ntype BinaryFilePath = FilePath\n\ntype MimeType = String\n\ntype AuthScheme\n = Bearer\n | Header { name: String }\n | Basic { username: String }\n | ApiKey\n\ntype AccessToken {\n token: Secret\n scheme: AuthScheme\n expires_at: Timestamp?\n}\n\ntype Credential {\n token: Secret\n scheme: AuthScheme\n header_name: String?\n source_id: String\n required_scopes: List\n expires_in: Int?\n}\n\ntype FilesystemHandle = FilePath where brand(\"FilesystemHandle\")\ntype NetworkHandle = Unit where brand(\"NetworkHandle\")\ntype ToolHandle = String where brand(\"ToolHandle\")\n\ntype FileResponse {\n path: String\n success: Bool\n content: String\n}\n\ntype ShellResponse {\n exit_code: Int\n stdout: String\n stderr: String\n}\n\ntype RestResponse {\n status: Int\n headers: Json\n body: Json\n}\n\ntype TestResult {\n name: String\n ok: Bool\n stdout: String\n stderr: String\n duration_ms: Milliseconds\n}\n\ntype Summary {\n total: Int\n passed: Int\n failed: Int\n}\n\ntype StageResult {\n name: String\n success: Bool\n stdout: String\n stderr: String\n skipped: Bool\n}\n\ntype RenderedTextFile {\n path: String\n content: String\n}\n\ntype ToolEntry {\n name: String\n command: String\n description: String?\n}\n\ntype ToolRegistry {\n tools: List\n}\n\ntype DagTopology {\n nodes: List\n edges: List\n subdag_boundaries: List\n}\n\ntype TopologyNode {\n id: String\n label: String\n kind: TopologyNodeKind\n parent: String?\n}\n\ntype TopologyEdge {\n from: String\n to: String\n port: String?\n}\n\ntype DagDiff {\n added: List\n removed: List\n changed: List\n}\n\ntype PragmaDirective {\n key: String\n value: String\n scope: String?\n}\n\ntype DocSource {\n path: FilePath\n kind: DocSourceKind\n}\n\ntype ReferenceModel\n" +fn probe_3() -> ProcessExit { + let s = conservation_subject(id_tail: "probe_3", source: probe_src_3) + let spans = match s.parsed { + Accepted { value: a, diagnostics: _ } => a.span_index + Rejected { diagnostics: _ } => span_index_empty() + } + match s.normalized { + Accepted { value: _, diagnostics: _ } => ExitSuccess + Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) + } +} + +data probe_src_4: String = "module extdeps.realization.compile_stage_memo\n\nimport extdeps.external_authority { ExternalAuthority }\nimport extdeps.uri { Uri, Https }\nimport v2.std.optional { Absent }\n\nimport extdeps.cache.types { CacheInterfaceCatalogFacts, ContentAddressed, CatalogCasContentEncoding }\nimport std.cache_identity { CacheInterfaceId }\nimport std.cache_interface {\n ContentAddressLookup,\n OverwriteAllowed,\n MissThenCreate,\n StructuredArtifact,\n InProcess,\n PerFile,\n None,\n InProcessNs,\n Strong,\n OperatorObserved,\n ManualPurge,\n Repo,\n CapacityUnbounded,\n ReleasedAtProviderScopeExit,\n}\n\ndata extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {\n uri: Uri {\n scheme: Https\n locator: \"github.com/gunb-ai/gunbc/tree/main/dag/extdeps/realization\"\n }\n}\n\ndata compile_stage_memo_id: CacheInterfaceId = \"compile_stage_memo_cache\"\n\ndata compile_stage_memo_facts: CacheInterfaceCatalogFacts = {\n identity: compile_stage_memo_id\n backing_surface: \"gunbc-v2-compile-stage-in-process\"\n transport_encoding: CatalogCasContentEncoding\n key_derivation: {\n\n key_inputs: ContentAddressed\n overwritable: true\n prefix_fallback_allowed: false\n content_verified_on_read: false\n visibility_scope: Repo\n invalidation_triggers: [ManualPurge]\n }\n io: {\n lookup_semantics: ContentAddressLookup\n write_semantics: OverwriteAllowed\n miss_semantics: MissThenCreate\n }\n placement: {\n value_shape: StructuredArtifact\n locality: InProcess\n retention: { release_policy: ReleasedAtProviderScopeExit, capacity: CapacityUnbounded }\n atomicity: PerFile\n auth: None\n read_latency: InProcessNs\n consistency: Strong\n }\n evidence: [\n {\n field: \"locality\"\n evidence: OperatorObserved {\n note: \"scoped to compile_ingest_staging pipeline; drops with the ingest walk — within-run stage memo (v2.std.staging.cached_stage)\"\n observed_at: Absent\n }\n },\n {\n field: \"key_derivation\"\n evidence: OperatorObserved {\n note: \"stage input content-addressed at the cached_stage lookup site; miss runs the stage, hit skips recompute\"\n observed_at: Absent\n }\n },\n ]\n}\n" +fn probe_4() -> ProcessExit { + let s = conservation_subject(id_tail: "probe_4", source: probe_src_4) + let spans = match s.parsed { + Accepted { value: a, diagnostics: _ } => a.span_index + Rejected { diagnostics: _ } => span_index_empty() + } + match s.normalized { + Accepted { value: _, diagnostics: _ } => ExitSuccess + Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) + } +} + +data probe_src_5: String = "module extdeps.realization.parse_table_memo\n\nimport extdeps.external_authority { ExternalAuthority }\nimport extdeps.uri { Uri, Https }\nimport v2.std.optional { Absent }\n\nimport extdeps.cache.types { CacheInterfaceCatalogFacts, ContentAddressed, CatalogCasContentEncoding }\nimport std.cache_identity { CacheInterfaceId }\nimport std.cache_interface {\n ContentAddressLookup,\n OverwriteAllowed,\n MissThenCreate,\n StructuredArtifact,\n InProcess,\n PerFile,\n None,\n InProcessNs,\n Strong,\n OperatorObserved,\n ManualPurge,\n Repo,\n CapacityUnbounded,\n ReleasedAtProviderScopeExit,\n}\nimport std.disposition { Disposition, Scaffold, RealizationDispatch }\nimport std.decl_ref { DeclarationRef, WholeDeclaration }\n\ndata parse_table_memo_seed_handler_dissolution_trigger: Disposition = Scaffold {\n dissolves_to: RealizationDispatch,\n bind: DeclarationRef {\n module_path: \"v2.compiler.materialization_carriers\",\n decl_name: \"materialization_allows_memo_store\",\n field: WholeDeclaration\n }\n}\n\ndata extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {\n uri: Uri {\n scheme: Https\n locator: \"github.com/gunb-ai/gunbc/tree/main/dag/extdeps/realization\"\n }\n}\n\ndata parse_table_memo_id: CacheInterfaceId = \"parse_table_memo_cache\"\n\ndata parse_table_memo_facts: CacheInterfaceCatalogFacts = {\n identity: parse_table_memo_id\n backing_surface: \"gunbc-parse-table-in-process\"\n transport_encoding: CatalogCasContentEncoding\n key_derivation: {\n\n key_inputs: ContentAddressed\n overwritable: true\n prefix_fallback_allowed: false\n content_verified_on_read: false\n visibility_scope: Repo\n invalidation_triggers: [ManualPurge]\n }\n io: {\n lookup_semantics: ContentAddressLookup\n write_semantics: OverwriteAllowed\n miss_semantics: MissThenCreate\n }\n placement: {\n value_shape: StructuredArtifact\n locality: InProcess\n retention: { release_policy: ReleasedAtProviderScopeExit, capacity: CapacityUnbounded }\n atomicity: PerFile\n auth: None\n read_latency: InProcessNs\n consistency: Strong\n }\n evidence: [\n {\n field: \"locality\"\n evidence: OperatorObserved {\n note: \"scoped to InterpContext; drops with evaluation — within-run memo (design-recompute-memoization §4.1)\"\n observed_at: Absent\n }\n },\n {\n field: \"key_derivation\"\n evidence: OperatorObserved {\n note: \"ParseTableSubject = grammar_digest + token_stream_digest (full stream content-address incl. lexemes) + position + production\"\n observed_at: Absent\n }\n },\n ]\n}\n" +fn probe_5() -> ProcessExit { + let s = conservation_subject(id_tail: "probe_5", source: probe_src_5) + let spans = match s.parsed { + Accepted { value: a, diagnostics: _ } => a.span_index + Rejected { diagnostics: _ } => span_index_empty() + } + match s.normalized { + Accepted { value: _, diagnostics: _ } => ExitSuccess + Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) + } +} + +data probe_src_6: String = "module v2.std.host_transport\n\nimport v2.std.algebra { length }\nimport v2.std.collection { List }\nimport v2.std.integer { Int }\nimport v2.std.diagnostic {\n Accepted,\n Diagnostic,\n Outcome,\n Rejected,\n CorrectionNotModeled,\n None,\n Unavailable,\n diagnostics_singleton,\n outcome_rejected,\n invariant_port_locus\n}\nimport v2.std.machine { Byte }\nimport v2.std.node { Node, Symbol }\nimport v2.std.runtime { RuntimePrimitive, RuntimeValue }\nimport v2.std.text { String } +import std.process { ProcessExit, ExitSuccess, exit_failure } +import v2.std.lens_verdict { lens_verdict_diagnostic_locus_text_in_spans } +import v2.std.provenance { SpanIndex, span_index_empty } +import v2.std.node { symbol_lexeme } +import v2.std.diagnostic { Diagnostic } +\n\ntype ByteString = List\n\ntype RelPath = String\n\ntype WorkspaceContent\n = EmittedSource\n | FixedText { text: String }\n | EmittedWithSuffix { suffix: String }\n\ntype WorkspaceFile {\n path: RelPath\n content: WorkspaceContent\n}\n\ntype MaterializedWorkspaceFile {\n path: RelPath\n text: String\n}\n\ntype InvocationArg\n = LiteralArg { text: String }\n | WorkspacePath { path: RelPath }\n | ProducedArtifact { path: RelPath }\n\ntype ProcessProgram\n = HostToolProgram { executable: String }\n | ProducedProgram { path: RelPath }\n\ntype ProcessInvocation {\n program: ProcessProgram\n args: List\n}\n\ntype RuntimeValueCodec\n = FixedWidthBytes { width: Int }\n | SignedI32Le\n\ntype HostTransportDescriptor {\n identity: Symbol\n workspace: List\n build: List\n run: ProcessInvocation\n runtime_value_codec: RuntimeValueCodec\n build_environment: HostBuildEnvironment\n}\n\n// THE EMITTED PROGRAM'S BUILD ENVIRONMENT IS A ROW ON ITS TARGET'S TRANSPORT, NOT AN\n// INHERITANCE AND NOT ONE GLOBAL LIST. The host transport builds and runs the emitted program\n// under env_clear() plus exactly the ambient variables the descriptor names, admitted by exact\n// name; every other ambient variable is refused by negation. The admitted names are hashed into\n// the build-context identity, so the row is per TARGET: a Rust realization must not carry\n// PYTHONPATH (an unrelated change would perturb its identity) and a Python one must not carry\n// Cargo state. Flag-bearing variables (RUSTFLAGS, CARGO_ENCODED_RUSTFLAGS, CFLAGS, GOFLAGS, ...)\n// are absent BY CONSTRUCTION of each row and deliberately not enumerated as exclusions: a build\n// flag is policy, and a policy a realization wants is a runtime_row field with its own RED,\n// never something the ambient shell exported for compiling the seed. The seed used to admit\n// RUST*/CARGO_*/*FLAGS by prefix (and the uncached transport inherited the whole environment),\n// so the required floor's `RUSTFLAGS=-D warnings` -- exported for the SEED build -- reached the\n// emitted program and four emit_host expected-red rows were red only in CI, on `-D dead-code`\n// (gunbc#9727). CARGO_TARGET_DIR is never a row entry: the seed constructs it at the realization\n// boundary (the workspace's own target dir). The seed carries no veto of its own -- what a row\n// names is admitted, so a row is the whole policy.\ntype HostBuildEnvironment {\n ambient_names: List\n}\n\n// Shared subset, consumed explicitly by each target's row: where the process finds tools and\n// scratch. Nothing applies to a target that does not name it.\nfn host_process_location_names() -> List {\n [\"PATH\", \"HOME\", \"TMPDIR\"]\n}\n\ntype EmitHostRuntimeRow {\n transport: HostTransportDescriptor\n runtime_primitive_type: Node\n}\n\ntype TargetEmitHostRuntimeRow\n = HostRuntimeRowPresent { row: EmitHostRuntimeRow }\n | HostRuntimeRowAbsent\n\ndata target_emit_host_runtime_row_unconfigured: TargetEmitHostRuntimeRow = HostRuntimeRowAbsent\n\nfn emit_host_runtime_row_unconfigured_diagnostic() -> Diagnostic {\n Diagnostic {\n reason: ^emit_host_runtime_row_unconfigured_reason,\n at: invariant_port_locus(\n port: ^emit_host_runtime_row_lookup_port,\n invariant: ^emit_host_runtime_row_unconfigured_reason\n ),\n correction: Unavailable { reason: CorrectionNotModeled }\n }\n}\n\nfn emit_host_runtime_row_parse_byte_width_mismatch_diagnostic() -> Diagnostic {\n Diagnostic {\n reason: ^emit_host_runtime_row_parse_byte_width_mismatch,\n at: invariant_port_locus(\n port: ^emit_host_runtime_row_parse_port,\n invariant: ^emit_host_runtime_row_parse_byte_width_mismatch\n ),\n correction: Unavailable { reason: CorrectionNotModeled }\n }\n}\n\nfn runtime_value_codec_expected_byte_count(codec: RuntimeValueCodec) -> Int {\n match codec {\n FixedWidthBytes { width: w } => w\n SignedI32Le => 4\n }\n}\n\nfn runtime_value_parse_from_row(\n row: EmitHostRuntimeRow,\n bytes: ByteString\n) -> Outcome {\n let expected = runtime_value_codec_expected_byte_count(codec: row.transport.runtime_value_codec)\n if length(xs: bytes) == expected {\n Accepted {\n value: RuntimePrimitive {\n value: {\n primitive_type: row.runtime_primitive_type,\n bytes: bytes\n }\n },\n diagnostics: None\n }\n } else {\n Rejected {\n diagnostics: diagnostics_singleton(\n d: emit_host_runtime_row_parse_byte_width_mismatch_diagnostic()\n )\n }\n }\n}\n\nfn configured_emit_host_runtime_row_from_target_row(\n row: TargetEmitHostRuntimeRow\n) -> Outcome {\n match row {\n HostRuntimeRowPresent { row: configured } =>\n Accepted { value: configured, diagnostics: None }\n HostRuntimeRowAbsent =>\n outcome_rejected(d: emit_host_runtime_row_unconfigured_diagnostic())\n }\n}\n\ntype EmittedEffectFamily\n = FilesystemReadFamily\n | ShellExecRunFamily\n\nfn emitted_family_runtime_row_gated(\n family: EmittedEffectFamily,\n present: EmitHostRuntimeRow\n) -> TargetEmitHostRuntimeRow {\n match family {\n FilesystemReadFamily => HostRuntimeRowPresent { row: present }\n ShellExecRunFamily => HostRuntimeRowPresent { row: present }\n }\n}\n" +fn probe_6() -> ProcessExit { + let s = conservation_subject(id_tail: "probe_6", source: probe_src_6) + let spans = match s.parsed { + Accepted { value: a, diagnostics: _ } => a.span_index + Rejected { diagnostics: _ } => span_index_empty() + } + match s.normalized { + Accepted { value: _, diagnostics: _ } => ExitSuccess + Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) + } +} + +fn probe_diag_text(x: Diagnostic, spans: SpanIndex) -> String { + concat(concat(symbol_lexeme(sym: x.reason), " @ "), lens_verdict_diagnostic_locus_text_in_spans(d: x, index: spans)) +} diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 0b1cf3c070c..93e9dd30c4a 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -1030,6 +1030,14 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.body_lowering.qualified_construct.qc_resolved_type_tag", "v2.test.claim.body_lowering.qualified_construct.qc_resolved_unimported", "v2.test.claim.body_lowering.qualified_construct.qc_resolved_bodyless", + "v2.test.claim.body_lowering.anonymous_record.ar_fixture_normalized", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_headed", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_headless", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_call_argument", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_coproduct", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_binder", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_value_mismatch", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_value_mismatch_headed", "v2.test.claim.namespace_xl0.cross_module_reference_resolution.xl0r_fixture_normalized", "v2.test.claim.namespace_xl0.cross_module_reference_resolution.xl0r_resolved_local_consumer", "v2.test.claim.namespace_xl0.cross_module_reference_resolution.xl0r_resolved_declared_consumer", From d3421f504509772938b3cac170621db7e7beb9e6 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 06:53:41 +0000 Subject: [PATCH 41/63] map arm: key-kind decides against the kernel text binding; K=String controls expected-red (deep-bee-18 lane); name-key and call-argument controls on K=Int Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/explicit_witness_admission.dag | 40 +++ src/v2/compiler/03_resolve.dag | 10 +- src/v2/extdeps/languages/dag.dag | 8 +- .../claim/body_lowering/map_literal_test.dag | 333 +----------------- 4 files changed, 57 insertions(+), 334 deletions(-) diff --git a/dag/gunbc/explicit_witness_admission.dag b/dag/gunbc/explicit_witness_admission.dag index 28bcdd4b36f..42a2a13c51e 100644 --- a/dag/gunbc/explicit_witness_admission.dag +++ b/dag/gunbc/explicit_witness_admission.dag @@ -193,6 +193,46 @@ fn known_red_pre_verdict_probe( data known_red_class_note: String = "WHAT A KNOWN-RED ADMISSION IS: a witness red on main for a feature gap owned by a named lane. It is a RED CONTROL for that lane, not a broken test — deleting it would discard the discriminating input, while leaving it in per-PR discovery would red every wide PR on a debt that PR did not create. Greening is the counted un-quarantine event of std.witness_admission: the row deletes in the SAME change that lands the fix, which promotes the unchanged witness to ordinary DiscoverySelection as a permanent regression control (DESIGN 4b(4) — the climb deletes the quarantine machinery, never the evidence). A DISSOLVE_ON MUST NAME A FACT THAT IS FALSE TODAY AND BECOMES TRUE EXACTLY WHEN THE FIX LANDS, and this requirement is here because two rows failed it in one night. A trigger keyed on a condition already satisfied is not a trigger, it is a deletion licence with a date on it: whoever next reads it dissolves a quarantine standing in for a defect that is still live, and the row reads as dissolved-per-its-own-terms while the wall it substituted for does not exist. RECEIPT 1, another lane's: the #8592 row's trigger read 'the mirrors are regenerated so the compiled harness CONTAINS declared_arg_types_for_method' — that symbol has been present in v1_compiler_infer_lookup.rs all along, so the trigger was satisfiable from the moment it was written, while the defect it quarantines is live (on main the function still takes no TypeEnv, which is precisely the bug). RECEIPT 2, MY OWN, AND IT IS THE MORE INSTRUCTIVE ONE BECAUSE THE DELETION WAS STILL CORRECT: the min-length row's trigger read 'the compiled harness contains the wall'. Measured, where_refinement_predicates_covered has 2 occurrences in that mirror on main and always did, so a reader checking 'the wall' would have dissolved the row at any point in the preceding weeks. It dissolved correctly only because the author happened to grep where_predicate_guaranteed_min_length, which was 0 on main and 2 after the regen. THE TRIGGER DID NOT ENCODE WHICH FACT DECIDED IT; the right answer came from the reader, not the row. So: name the symbol, signature or behaviour whose ABSENCE is the gap — a new parameter, a specific new function, a named repro passing — never a category word like 'the wall' or 'the fix', and never a symbol that predates the change. The test a reviewer applies: run the trigger's check against main TODAY, and if it passes, the trigger is defective and the row is unprotected. The roster must not grow silently: a new admission carries a reason naming the diagnosis and owner plus a dissolve_on, both refused empty by NonEmptyStr, and a row whose witness runs green locally is stale and deletes. Local recipe per row: claim_batch --source-root dag --source-root src/v2 --entry --functions -- AND FOR AN ExecutionWitnessKind ROW, --wet AS WELL, WITHOUT WHICH THE RECIPE CANNOT DO WHAT THE ROW ASKS OF IT. claim_batch is hermetic by default, so a wet row invoked by the recipe as written above never reaches its subject: measured 2026-08-26 on self_host_logic_behavioral_receipt_holds, it returns FAIL in 87 seconds with 'hermetic route has no arm for Dir: the claim never reached its subject, so this is a route gap and not a verdict', naming three undeclared dispatches (Filesystem.Read, git.Inspect.Toplevel, shell.Mktemp.Dir). The refusal is well-modeled and says plainly that it is not a verdict; the defect was in the recipe, which every wet dissolution condition below cites as the way its row greens. A row whose only recorded closing move cannot close it is the shape DESIGN records for a gate whose sole green is the forbidden action -- here arrived at from the other side, with no green reachable at all. CorpusWitnessKind rows run on the hermetic falsifier probe batch; ExecutionWitnessKind rows run on the falsifier wet quarantine batch." data explicit_witness_admissions: List = [ + known_red_probe( + entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", + f: "ml_headless_literal_equals_the_headed_introduction_holds", + kind: CorpusWitnessKind, + budget: FastLaneEvalBudget, + reason: "map arm control under Map (the headless map literal elaborates to exactly the headed introduction), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + ), + known_red_probe( + entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", + f: "ml_a_duplicate_key_refuses_holds", + kind: CorpusWitnessKind, + budget: FastLaneEvalBudget, + reason: "map arm control under Map (a repeated key refuses resolve_anonymous_map_duplicate_key at its second occurrence), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + ), + known_red_probe( + entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", + f: "ml_an_escaped_and_a_raw_key_are_one_key_holds", + kind: CorpusWitnessKind, + budget: FastLaneEvalBudget, + reason: "map arm control under Map (an escaped key and its raw scalar collide under the decoded-text key law), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + ), + known_red_probe( + entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", + f: "ml_distinct_keys_are_accepted_holds", + kind: CorpusWitnessKind, + budget: FastLaneEvalBudget, + reason: "map arm control under Map (distinct keys elaborate), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + ), + known_red_probe( + entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", + f: "ml_infer_still_checks_an_elaborated_map_holds", + kind: CorpusWitnessKind, + budget: FastLaneEvalBudget, + reason: "map arm control under Map (infer checks an elaborated map's values against V), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + ), known_red_probe( entry: "dag/test/claim/declared_type_inhabitance_direct_call_witness_test.dag", f: "w_a_branded_refinement_at_an_unrelated_product_formal_still_refuses", diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 19dfe72e33d..7e276dbaa9e 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1777,8 +1777,8 @@ fn resolve_elided_construct_walk(ctx: ResolveContext, n: Node, x: ResolveExpecta // resolve_anonymous_map_expected_type_not_closed at the literal; // - every item must be string-keyed; a name-keyed brace under a Map head refuses // resolve_anonymous_map_name_key at its first item; -// - K must be the type a string literal denotes: the kernel String, reached unbound, by -// declaration identity (a K resolving to any declaration -- the structural v2.std.text String +// - K must be the type a string literal denotes: the kernel text binding an unimported String +// takes (v2.extdeps.languages.dag dag_kernel_type_binding_optional), never a declaration (a K resolving to any declaration -- the structural v2.std.text String // included -- is a text crossing no route declares here); else // resolve_anonymous_map_key_kind_mismatch at the first key; // - no key may repeat, judged by the key type's equality -- for string literals, decoded text @@ -1840,7 +1840,11 @@ fn resolve_map_key_type_is_string_literal_type(ctx: ResolveContext, position: Qu match t.kind { TypeNode { connective: Atom { identity: id } } => match symbol_index_lexical_lookup(index: ctx.namespace.symbol_index, position: position, name: id) { - LexicalUnbound => dag_kernel_string_type_spelling(sym: id) && namespace_has_canonical_symbol(namespace: ctx.namespace, name: id) + LexicalUnbound => + dag_kernel_string_type_spelling(sym: id) && match dag_kernel_type_binding_optional(sym: id) { + Present { value: _ } => true + Absent => false + } _ => false } _ => false diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 7b16d93039c..fadb7a7d6c0 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -3685,10 +3685,10 @@ fn dag_kernel_type_binding_optional(sym: Symbol) -> Optional { } // THE KERNEL SPELLING OF THE TYPE A STRING LITERAL DENOTES: host text, the kernel `String` -// (DESIGN section 4, Ruling 3). It has no declaration and no canonical binding in the table above, -// so an unbound `String` is judged by this spelling and nowhere else; a `String` that resolves to a -// declaration (the structural v2.std.text String) is a different type, reached from a literal only -// through the declared unfold. Consumed by v2.compiler.resolve's map arm (the key-kind rule). +// (DESIGN section 4, Ruling 3; ruling neat-boar-16: an unimported String denotes the kernel text +// binding, never std.string_type). It is admitted as a key type only where the kernel table above +// binds it, which it does not yet (the deep-bee-18 lane adds it); a `String` that resolves to a +// declaration is a different type. Consumed by v2.compiler.resolve's map arm (the key-kind rule). fn dag_kernel_string_type_spelling(sym: Symbol) -> Bool { symbol_lexeme(sym: sym) == "String" } diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index f9f48b48de7..03e5aec0d62 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -51,12 +51,10 @@ data ml_types_source: String = "module std.types\n\nimport std.algebra { Finitel data ml_algebra_source: String = "module std.algebra\n\ntype FreeMonoid\n = Empty\n | Cons { head: T, tail: FreeMonoid }\n\ntype FinitelySupportedFunction {\n size: Int\n}\n\ntype MapIntroductionEntry {\n key: K\n value: V\n}\n\nfn map_from_entries(entries: FreeMonoid>) -> FinitelySupportedFunction {\n FinitelySupportedFunction { size: 0 }\n}\n" -// The kernel String is bound as a canonical symbol only where some module declares it, as -// std.string_type does in the real corpus; without it a fixture's `String` is unbound. The stand-in -// is OPAQUE -- a record aliasing nothing -- so no claim here can pass because String means something -// else (ml_an_int_key_under_a_string_map_refuses_holds reds if it did). -data ml_string_type_source: String = "module std.string_type\n\ntype String {\n opaque_text: Bool\n}\n" - +// K = String is the KERNEL text binding (ruling, neat-boar-16 via gentle-koi-724): an unimported +// String, not a declaration. It does not bind on the v2 route yet, so every claim below that +// elaborates under Map is an EXPECTED-RED row in gunbc.explicit_witness_admission, owned by +// the deep-bee-18 lane; the name-key and call-argument controls use K = Int and hold today. data ml_headed_source: String = "module v2.test.ml_headed\n\nimport std.types { Map }\nimport std.algebra { MapIntroductionEntry, map_from_entries }\n\ndata ml_x: Map = map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }, MapIntroductionEntry { key: \"b\", value: false }])\n" data ml_headless_source: String = "module v2.test.ml_headless\n\nimport std.types { Map }\n\ndata ml_x: Map = { \"a\": true, \"b\": false }\n" @@ -72,9 +70,9 @@ data ml_escape_duplicate_source: String = "module v2.test.ml_escape_duplicate\n\ data ml_key_kind_source: String = "module v2.test.ml_key_kind\n\nimport std.types { Map }\n\ndata ml_k: Map = { \"k\": true }\n" -data ml_name_key_source: String = "module v2.test.ml_name_key\n\nimport std.types { Map }\n\ndata ml_n: Map = { k: true }\n" +data ml_name_key_source: String = "module v2.test.ml_name_key\n\nimport std.types { Map }\n\ndata ml_n: Map = { k: true }\n" -data ml_call_argument_source: String = "module v2.test.ml_call_argument\n\nimport std.types { Map }\n\nfn ml_take(m: Map) -> Bool { true }\n\nfn ml_call() -> Bool { ml_take(m: { \"a\": true }) }\n" +data ml_call_argument_source: String = "module v2.test.ml_call_argument\n\nimport std.types { Map }\n\nfn ml_take(m: Map) -> Bool { true }\n\nfn ml_call() -> Bool { ml_take(m: { \"a\": true }) }\n" data ml_value_mismatch_source: String = "module v2.test.ml_value_mismatch\n\nimport std.types { Map }\n\ndata ml_bad: Map = { \"a\": 3 }\n" @@ -84,7 +82,6 @@ data ml_int_key_source: String = "module v2.test.ml_int_key\n\nimport std.types data ml_int_key_ingest: SourceRootIngest = [ ml_read(source: ml_types_source, id: ^ml_ik_types_read, unit: ^ml_ik_types_cu, path: "src/v2/test/fixture/map_literal/ik_types.dag"), ml_read(source: ml_algebra_source, id: ^ml_ik_algebra_read, unit: ^ml_ik_algebra_cu, path: "src/v2/test/fixture/map_literal/ik_algebra.dag"), - ml_read(source: ml_string_type_source, id: ^ml_ik_string_read, unit: ^ml_ik_string_cu, path: "src/v2/test/fixture/map_literal/ik_string.dag"), ml_read(source: ml_int_key_source, id: ^ml_int_key_read, unit: ^ml_int_key_cu, path: "src/v2/test/fixture/map_literal/int_key.dag") ] @@ -103,7 +100,6 @@ fn ml_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceR data ml_ingest: SourceRootIngest = [ ml_read(source: ml_types_source, id: ^ml_types_read, unit: ^ml_types_cu, path: "src/v2/test/fixture/map_literal/types.dag"), ml_read(source: ml_algebra_source, id: ^ml_algebra_read, unit: ^ml_algebra_cu, path: "src/v2/test/fixture/map_literal/algebra.dag"), - ml_read(source: ml_string_type_source, id: ^ml_string_type_read, unit: ^ml_string_type_cu, path: "src/v2/test/fixture/map_literal/string_type.dag"), ml_read(source: ml_headed_source, id: ^ml_headed_read, unit: ^ml_headed_cu, path: "src/v2/test/fixture/map_literal/headed.dag"), ml_read(source: ml_headless_source, id: ^ml_headless_read, unit: ^ml_headless_cu, path: "src/v2/test/fixture/map_literal/headless.dag"), ml_read(source: ml_distinct_source, id: ^ml_distinct_read, unit: ^ml_distinct_cu, path: "src/v2/test/fixture/map_literal/distinct.dag"), @@ -272,320 +268,3 @@ test fn ml_a_mixed_key_brace_refuses_at_lowering_holds() -> Bool { Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch } } - -// PROBES (temporary) - -test fn ml_probe_ingest_accepts_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -test fn ml_probe_headed_accepts_holds() -> Bool { - ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headed")) -} - -test fn ml_probe_headed__resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_unbound_symbol) -} - -test fn ml_probe_headed__resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_ambiguous_symbol) -} - -test fn ml_probe_headed__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_construct_tag_not_a_constructor) -} - -test fn ml_probe_headed__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_map) -} - -test fn ml_probe_headed__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_expected_type_not_record) -} - -test fn ml_probe_headed__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_key_kind_mismatch) -} - -test fn ml_probe_headed__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_expected_type_not_closed) -} - -test fn ml_probe_headed__resolve_anonymous_record_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_record_no_expected_type) -} - -test fn ml_probe_headed__resolve_anonymous_map_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_no_expected_type) -} - -test fn ml_probe_headed__resolve_anonymous_map_name_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_name_key) -} - -test fn ml_probe_headed__resolve_anonymous_map_duplicate_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_anonymous_map_duplicate_key) -} - -test fn ml_probe_headed__resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_malformed_tree) -} - -test fn ml_probe_headed__resolve_reason_observation_incomplete_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_observation_incomplete) -} - -test fn ml_probe_headed__resolve_reason_type_parameter_in_value_position_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headed"), reason: ^resolve_reason_type_parameter_in_value_position) -} - -test fn ml_probe_headless_accepts_holds() -> Bool { - ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_headless")) -} - -test fn ml_probe_headless__resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_unbound_symbol) -} - -test fn ml_probe_headless__resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_ambiguous_symbol) -} - -test fn ml_probe_headless__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_construct_tag_not_a_constructor) -} - -test fn ml_probe_headless__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_map) -} - -test fn ml_probe_headless__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_expected_type_not_record) -} - -test fn ml_probe_headless__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_key_kind_mismatch) -} - -test fn ml_probe_headless__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_expected_type_not_closed) -} - -test fn ml_probe_headless__resolve_anonymous_record_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_record_no_expected_type) -} - -test fn ml_probe_headless__resolve_anonymous_map_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_no_expected_type) -} - -test fn ml_probe_headless__resolve_anonymous_map_name_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_name_key) -} - -test fn ml_probe_headless__resolve_anonymous_map_duplicate_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_anonymous_map_duplicate_key) -} - -test fn ml_probe_headless__resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_malformed_tree) -} - -test fn ml_probe_headless__resolve_reason_observation_incomplete_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_observation_incomplete) -} - -test fn ml_probe_headless__resolve_reason_type_parameter_in_value_position_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_headless"), reason: ^resolve_reason_type_parameter_in_value_position) -} - -test fn ml_probe_duplicate_accepts_holds() -> Bool { - ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_duplicate")) -} - -test fn ml_probe_duplicate__resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_unbound_symbol) -} - -test fn ml_probe_duplicate__resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_ambiguous_symbol) -} - -test fn ml_probe_duplicate__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_construct_tag_not_a_constructor) -} - -test fn ml_probe_duplicate__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_expected_type_not_map) -} - -test fn ml_probe_duplicate__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_record_expected_type_not_record) -} - -test fn ml_probe_duplicate__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_key_kind_mismatch) -} - -test fn ml_probe_duplicate__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_expected_type_not_closed) -} - -test fn ml_probe_duplicate__resolve_anonymous_record_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_record_no_expected_type) -} - -test fn ml_probe_duplicate__resolve_anonymous_map_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_no_expected_type) -} - -test fn ml_probe_duplicate__resolve_anonymous_map_name_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_name_key) -} - -test fn ml_probe_duplicate__resolve_anonymous_map_duplicate_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_duplicate_key) -} - -test fn ml_probe_duplicate__resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_malformed_tree) -} - -test fn ml_probe_duplicate__resolve_reason_observation_incomplete_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_observation_incomplete) -} - -test fn ml_probe_duplicate__resolve_reason_type_parameter_in_value_position_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_reason_type_parameter_in_value_position) -} - -test fn ml_probe_name_key_accepts_holds() -> Bool { - ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_name_key")) -} - -test fn ml_probe_name_key__resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_unbound_symbol) -} - -test fn ml_probe_name_key__resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_ambiguous_symbol) -} - -test fn ml_probe_name_key__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_construct_tag_not_a_constructor) -} - -test fn ml_probe_name_key__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_expected_type_not_map) -} - -test fn ml_probe_name_key__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_record_expected_type_not_record) -} - -test fn ml_probe_name_key__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_key_kind_mismatch) -} - -test fn ml_probe_name_key__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_expected_type_not_closed) -} - -test fn ml_probe_name_key__resolve_anonymous_record_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_record_no_expected_type) -} - -test fn ml_probe_name_key__resolve_anonymous_map_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_no_expected_type) -} - -test fn ml_probe_name_key__resolve_anonymous_map_name_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_name_key) -} - -test fn ml_probe_name_key__resolve_anonymous_map_duplicate_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_duplicate_key) -} - -test fn ml_probe_name_key__resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_malformed_tree) -} - -test fn ml_probe_name_key__resolve_reason_observation_incomplete_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_observation_incomplete) -} - -test fn ml_probe_name_key__resolve_reason_type_parameter_in_value_position_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_reason_type_parameter_in_value_position) -} - -test fn ml_probe_call_argument_accepts_holds() -> Bool { - ml_accepts(o: ml_resolve_subject(subject: "v2.test.ml_call_argument")) -} - -test fn ml_probe_call_argument__resolve_reason_unbound_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_unbound_symbol) -} - -test fn ml_probe_call_argument__resolve_reason_ambiguous_symbol_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_ambiguous_symbol) -} - -test fn ml_probe_call_argument__resolve_reason_construct_tag_not_a_constructor_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_construct_tag_not_a_constructor) -} - -test fn ml_probe_call_argument__resolve_anonymous_map_expected_type_not_map_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_expected_type_not_map) -} - -test fn ml_probe_call_argument__resolve_anonymous_record_expected_type_not_record_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_record_expected_type_not_record) -} - -test fn ml_probe_call_argument__resolve_anonymous_map_key_kind_mismatch_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_key_kind_mismatch) -} - -test fn ml_probe_call_argument__resolve_anonymous_map_expected_type_not_closed_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_expected_type_not_closed) -} - -test fn ml_probe_call_argument__resolve_anonymous_record_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_record_no_expected_type) -} - -test fn ml_probe_call_argument__resolve_anonymous_map_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_no_expected_type) -} - -test fn ml_probe_call_argument__resolve_anonymous_map_name_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_name_key) -} - -test fn ml_probe_call_argument__resolve_anonymous_map_duplicate_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_duplicate_key) -} - -test fn ml_probe_call_argument__resolve_reason_malformed_tree_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_malformed_tree) -} - -test fn ml_probe_call_argument__resolve_reason_observation_incomplete_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_observation_incomplete) -} - -test fn ml_probe_call_argument__resolve_reason_type_parameter_in_value_position_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_reason_type_parameter_in_value_position) -} - -test fn ml_probe_headed_intro_count_holds() -> Bool { - length(xs: ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headed"))) == 1 -} - -test fn ml_probe_headless_intro_count_holds() -> Bool { - length(xs: ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headless"))) == 1 -} From 961b3c05d5db4ab22a315b05c1a743e7cb69dfff Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 07:33:22 +0000 Subject: [PATCH 42/63] Move an in-body annotation to module-item grain (DESIGN 4c) --- src/v2/compiler/03_resolve.dag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 7e276dbaa9e..984da20a0a3 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1885,6 +1885,8 @@ fn resolve_map_key_text(key: Node) -> String { } } +// The pairs are walked under a Transform carrier (positional edges only) that never leaves this +// arm; only its resolved children are read. fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, items: List, value_x: ResolveExpectation) -> ResolveNodeWalk { let keys = fold(items, init: [], f: fn(acc, e) { list_snoc_item(xs: acc, item: resolve_map_key_text(key: resolve_map_entry_key(entry: e.target))) @@ -1899,8 +1901,6 @@ fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, items: List, let walked = fold(items, init: child_walk_init(), f: fn(acc, e) { child_walk_step(w: acc, e: e, r: resolve_map_entry_walk(ctx: ctx, entry: e.target, value_x: value_x)) }) - // The pairs are walked under a Transform carrier (positional edges only) that never leaves - // this arm; only its resolved children are read. let carrier = node_with_occurrence_id(kind: ComputationNode { behavior: Transform }, children: [], occurrence_id: n.occurrence_id) match child_walk_node(w: walked, n: carrier) { ResolveWalkRefused { first: f, rest: r, observation: o } => ResolveWalkRefused { first: f, rest: r, observation: o } From f3b4cd1f8a6e1d069df780b531254e2593dc1f81 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 07:54:16 +0000 Subject: [PATCH 43/63] map arm: refuse a key with no string-literal text rather than compare payload-less keys (fabricated duplicates); known-red rows wait on #12760 and #12759 Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/explicit_witness_admission.dag | 20 ++++----- src/v2/compiler/03_resolve.dag | 41 +++++++++++++++++-- .../workflow/compile_door_cause_ownership.dag | 6 +++ 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/dag/gunbc/explicit_witness_admission.dag b/dag/gunbc/explicit_witness_admission.dag index 42a2a13c51e..86404b32906 100644 --- a/dag/gunbc/explicit_witness_admission.dag +++ b/dag/gunbc/explicit_witness_admission.dag @@ -198,40 +198,40 @@ data explicit_witness_admissions: List = [ f: "ml_headless_literal_equals_the_headed_introduction_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (the headless map literal elaborates to exactly the headed introduction), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", - dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + reason: "map arm control under Map (the headless map literal elaborates to exactly the headed introduction), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", f: "ml_a_duplicate_key_refuses_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (a repeated key refuses resolve_anonymous_map_duplicate_key at its second occurrence), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", - dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + reason: "map arm control under Map (a repeated key refuses resolve_anonymous_map_duplicate_key at its second occurrence), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", f: "ml_an_escaped_and_a_raw_key_are_one_key_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (an escaped key and its raw scalar collide under the decoded-text key law), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", - dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + reason: "map arm control under Map (an escaped key and its raw scalar collide under the decoded-text key law), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", f: "ml_distinct_keys_are_accepted_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (distinct keys elaborate), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", - dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + reason: "map arm control under Map (distinct keys elaborate), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", f: "ml_infer_still_checks_an_elaborated_map_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (infer checks an elaborated map's values against V), RED because an unimported String does not bind on the v2 route: resolve refuses it resolve_reason_unbound_symbol before the map arm runs. Per the neat-boar-16 ruling String is the kernel text binding. Owner: the deep-bee-18 lane (neat-ibex-696), after #12549. The arm itself is exercised today by the K = Int controls in the same module.", - dissolution: unbound_dissolution(description: "String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional, with an infer text-literal arm (deep-bee-18 lane): this witness greens and the row deletes in that change, promoting it to ordinary DiscoverySelection") + reason: "map arm control under Map (infer checks an elaborated map's values against V), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( entry: "dag/test/claim/declared_type_inhabitance_direct_call_witness_test.dag", diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 984da20a0a3..1b0350a72ab 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -15,6 +15,7 @@ import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } import v2.extdeps.languages.dag { dag_kernel_string_type_spelling, + dag_lexeme_is_string_literal, dag_kernel_type_binding_optional, dag_string_literal_decoded_text, dag_kernel_type_declaration_binding_optional, @@ -1878,18 +1879,50 @@ fn resolve_map_entry_key(entry: Node) -> Node { } } -fn resolve_map_key_text(key: Node) -> String { +// A KEY'S TEXT, OR NONE. Only a key that carries its own string-literal lexeme has text to compare; +// a key lowered to a class-stamped atom with no payload (gunbc.recurring_failure_mode +// string_literal_lowers_to_one_class_stamped_atom) has none, and reading one would make every pair of +// keys equal -- a fabricated duplicate. So such a key is Absent, and the arm refuses at it. +fn resolve_map_key_text_optional(key: Node) -> Optional { match key.kind { - TypeNode { connective: Atom { identity: id } } => dag_string_literal_decoded_text(lexeme: symbol_lexeme(sym: id)) - _ => "" + TypeNode { connective: Atom { identity: id } } => + if dag_lexeme_is_string_literal(sym: id) { + optional_present(value: dag_string_literal_decoded_text(lexeme: symbol_lexeme(sym: id))) + } else { + optional_absent() + } + _ => optional_absent() } } +fn resolve_map_first_textless_key(items: List) -> Optional { + fold(items, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => + match resolve_map_key_text_optional(key: resolve_map_entry_key(entry: e.target)) { + Present { value: _ } => optional_absent() + Absent => optional_present(value: resolve_map_entry_key(entry: e.target)) + } + } + }) +} + // The pairs are walked under a Transform carrier (positional edges only) that never leaves this // arm; only its resolved children are read. fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, items: List, value_x: ResolveExpectation) -> ResolveNodeWalk { + match resolve_map_first_textless_key(items: items) { + Present { value: key } => resolve_anonymous_record_refused(n: key, reason: ^resolve_anonymous_map_key_text_unavailable) + Absent => resolve_map_literal_distinct_entries(ctx: ctx, n: n, items: items, value_x: value_x) + } +} + +fn resolve_map_literal_distinct_entries(ctx: ResolveContext, n: Node, items: List, value_x: ResolveExpectation) -> ResolveNodeWalk { let keys = fold(items, init: [], f: fn(acc, e) { - list_snoc_item(xs: acc, item: resolve_map_key_text(key: resolve_map_entry_key(entry: e.target))) + match resolve_map_key_text_optional(key: resolve_map_entry_key(entry: e.target)) { + Present { value: t } => list_snoc_item(xs: acc, item: t) + Absent => acc + } }) match map_introduction_duplicate_key(keys: keys) { MapIntroductionKeyRepeated { first_index: _, second_index: second } => diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index 95e1bcea5ea..2d29f57af15 100644 --- a/src/v2/workflow/compile_door_cause_ownership.dag +++ b/src/v2/workflow/compile_door_cause_ownership.dag @@ -306,6 +306,12 @@ data known_frontier_causes: List = [ lane: SharedSelfHostCriticalPath, flip_trigger: "a map literal whose declared key type K is not the type a string literal denotes (the unbound kernel String, v2.extdeps.languages.dag dag_kernel_string_type_spelling), so a literal key would cross into K with no declared route (DESIGN section 4). Refused at the first key; flips when K is the kernel String or a declared unfold route is written" }, + CauseOwnership { + cause: ^resolve_anonymous_map_key_text_unavailable, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a map literal key that carries no string-literal lexeme, so the key law has no text to compare: in v2 today every string literal lowers to one class-stamped atom with no payload (gunbc.recurring_failure_mode string_literal_lowers_to_one_class_stamped_atom). Refused at the key rather than compared, since comparing payload-less keys would call every pair a duplicate. A frontier: flips to zero when string literals lower to lexeme-stamped terminals (sleek-owl-120, gunbc#12759)" + }, CauseOwnership { cause: ^resolve_anonymous_map_duplicate_key, grain: FatalGrain, From 104c3ea6a8f9011f5bfd1ae4e325b7cca5e512dd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 07:59:20 +0000 Subject: [PATCH 44/63] map literal: a key lowers as the ordinary string value, not its raw token atom Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/body_lowering_fold.dag | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index b189be5c7cc..0feae439882 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -7738,7 +7738,11 @@ fn body_lower_field_init_edge(item: Node) -> Outcome { Absent => refuse Present { value: pair } => match body_lower_string_key_atom_optional(node: pair.left) { - Present { value: key } => body_lower_map_literal_entry_edge(key: key, after_key: pair.right, item: item, refuse: refuse) + Present { value: _ } => + match body_lower_value_read(value: pair.left) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: key, diagnostics: _ } => body_lower_map_literal_entry_edge(key: key, after_key: pair.right, item: item, refuse: refuse) + } Absent => match body_lower_field_init_name_optional(key: pair.left) { Absent => refuse @@ -7762,8 +7766,9 @@ fn body_lower_field_init_edge(item: Node) -> Outcome { // A STRING-KEYED ITEM `"k": v` (a map literal's entry). The key is the item's sole atom when that // atom's lexeme is of the string-literal class (v2.extdeps.languages.dag dag_lexeme_is_string_literal); -// it is kept as the value the literal lowers to, never read as a field name, so the key's kind -// survives to resolve (docs/plans/map-literal-introduction-design.md). +// the key is then lowered as the VALUE any string literal lowers to (body_lower_value_read), never +// read as a field name and never kept as its raw token, so the key's kind survives to resolve and +// the key is the same node an ordinary string value is (docs/plans/map-literal-introduction-design.md). fn body_lower_string_key_atom_optional(node: Node) -> Optional { match body_lower_value_sole_atom_optional(node: node) { Absent => optional_absent() From 080c512ae2252d0c759305041f5687abf7c6c169 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 08:32:09 +0000 Subject: [PATCH 45/63] map arm: delete the second string decoder; keys have no text until gunbc#12759, so the arm refuses at the first key Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_resolve.dag | 25 ++++++--------- src/v2/extdeps/languages/dag.dag | 32 ------------------- src/v2/std/map_introduction.dag | 2 +- .../claim/body_lowering/map_literal_test.dag | 8 +---- .../workflow/compile_door_cause_ownership.dag | 2 +- 5 files changed, 12 insertions(+), 57 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 1b0350a72ab..876d32428fd 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -15,9 +15,7 @@ import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } import v2.extdeps.languages.dag { dag_kernel_string_type_spelling, - dag_lexeme_is_string_literal, dag_kernel_type_binding_optional, - dag_string_literal_decoded_text, dag_kernel_type_declaration_binding_optional, dag_node_is_float_literal_atom, dag_node_is_int_literal_atom, @@ -1783,7 +1781,7 @@ fn resolve_elided_construct_walk(ctx: ResolveContext, n: Node, x: ResolveExpecta // included -- is a text crossing no route declares here); else // resolve_anonymous_map_key_kind_mismatch at the first key; // - no key may repeat, judged by the key type's equality -- for string literals, decoded text -// (v2.extdeps.languages.dag dag_string_literal_decoded_text) -- through the ONE key law, +// (the decoder gunbc#12759 lands; until then no key has text and the arm refuses at it) -- through the ONE key law, // v2.std.map_introduction map_introduction_duplicate_key; a repeat refuses // resolve_anonymous_map_duplicate_key at its SECOND occurrence. Never last-wins. // @@ -1879,20 +1877,15 @@ fn resolve_map_entry_key(entry: Node) -> Node { } } -// A KEY'S TEXT, OR NONE. Only a key that carries its own string-literal lexeme has text to compare; -// a key lowered to a class-stamped atom with no payload (gunbc.recurring_failure_mode -// string_literal_lowers_to_one_class_stamped_atom) has none, and reading one would make every pair of -// keys equal -- a fabricated duplicate. So such a key is Absent, and the arm refuses at it. +// A KEY'S TEXT, OR NONE. A string literal lowers today to one class-stamped atom with no payload +// (gunbc.recurring_failure_mode string_literal_lowers_to_one_class_stamped_atom), so NO key has text +// to compare, and comparing payload-less keys would call every pair a duplicate. Every key is +// therefore Absent and the arm refuses at the first one. The text arrives with the lexeme-stamped +// string terminal and its one decoder (sleek-owl-120, gunbc#12759, +// v2.extdeps.languages.dag dag_string_literal_decoded_text, which also refuses a malformed escape); +// this reader is where that decoder is consumed, and no second decoder is written here. fn resolve_map_key_text_optional(key: Node) -> Optional { - match key.kind { - TypeNode { connective: Atom { identity: id } } => - if dag_lexeme_is_string_literal(sym: id) { - optional_present(value: dag_string_literal_decoded_text(lexeme: symbol_lexeme(sym: id))) - } else { - optional_absent() - } - _ => optional_absent() - } + optional_absent() } fn resolve_map_first_textless_key(items: List) -> Optional { diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index fadb7a7d6c0..f962c62665d 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -504,38 +504,6 @@ fn dag_string_escape_char_pattern() -> LexPattern { } } -// THE TEXT A STRING LITERAL DENOTES, read against the escape table above and nowhere else: each -// escape pair collapses to the one scalar it stands for (`\n` and a raw line feed are the same -// scalar, `\"` is `"`), every other scalar stands for itself. Two literals denote the same text -// exactly when their decoded texts are equal, which is what a spelling comparison gets wrong. The -// lexeme is decoded whole, delimiters included; they are identical on every literal, so they cannot -// separate two keys. Consumed by v2.compiler.resolve's map arm (duplicate keys, -// docs/plans/map-literal-introduction-design.md). Linear: scalars are pushed on the persistent -// list and joined once. -type DagStringLiteralDecode { - escaped: Bool - scalars: List -} - -// The scalar an escape's second code point stands for: `n` (110) is line feed (10), `t` (116) is -// tab (9), and every other escapable scalar (`"`, `\`, `{`, `}`) stands for itself. -fn dag_string_escape_decoded(cp: Int) -> Int { - if cp == 110 { 10 } else if cp == 116 { 9 } else { cp } -} - -fn dag_string_literal_decoded_text(lexeme: String) -> String { - let decoded = fold(chars(s: lexeme), init: DagStringLiteralDecode { escaped: false, scalars: [] }, f: fn(acc, cp) { - if acc.escaped { - DagStringLiteralDecode { escaped: false, scalars: list_snoc_item(xs: acc.scalars, item: from_code_point(cp: dag_string_escape_decoded(cp: cp))) } - } else if cp == 92 { - DagStringLiteralDecode { escaped: true, scalars: acc.scalars } - } else { - DagStringLiteralDecode { escaped: false, scalars: list_snoc_item(xs: acc.scalars, item: from_code_point(cp: cp)) } - } - }) - join(decoded.scalars, "") -} - fn dag_string_escape_sequence_pattern() -> LexPattern { SequencePattern { left: CharPattern { char: 92 }, diff --git a/src/v2/std/map_introduction.dag b/src/v2/std/map_introduction.dag index d0a54766795..2458915d5df 100644 --- a/src/v2/std/map_introduction.dag +++ b/src/v2/std/map_introduction.dag @@ -79,7 +79,7 @@ fn map_introduction_expected_head_is_map(path: QualifiedName) -> Bool { // THE KEY LAW: the first repeated key, in authored order, as a VALUE -- never a hole, never // last-wins. `keys` are the entries' keys already reduced to the key type's declared equality (for -// string-literal keys, the decoded text: v2.extdeps.languages.dag dag_string_literal_decoded_text), +// string-literal keys, the decoded text that gunbc#12759's decoder supplies), // so equal elements are equal keys. One pass over a map from key to its first index: linear. type MapIntroductionKeys = MapIntroductionKeysDistinct diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 03e5aec0d62..45c0d5cd271 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -13,7 +13,7 @@ import v2.std.resolution_policy { default_name_resolution_policy } import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.program_assembly { module_roots_from_source_root_ingest } import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } -import v2.extdeps.languages.dag { dag_language_model, dag_string_literal_decoded_text } +import v2.extdeps.languages.dag { dag_language_model } import v2.std.artifact { Artifact, SourceFile } import v2.std.cross_tree.import_model { DagTree } import v2.std.diagnostic { Accepted, Outcome, Rejected, diagnostics_fatal_reason } @@ -204,12 +204,6 @@ test fn ml_the_key_law_names_both_occurrences_holds() -> Bool { } } -// The decoder is the one the key law reads: an escape collapses to its scalar. -test fn ml_the_decoder_collapses_escapes_holds() -> Bool { - (dag_string_literal_decoded_text(lexeme: "\"a\\nb\"") == dag_string_literal_decoded_text(lexeme: "\"a\nb\"")) - && (dag_string_literal_decoded_text(lexeme: "\"a\\\\nb\"") != dag_string_literal_decoded_text(lexeme: "\"a\nb\"")) -} - // Control 5: K is not the type a string literal denotes, so the key refuses where it is written. test fn ml_a_key_of_the_wrong_kind_refuses_holds() -> Bool { ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_key_kind"), reason: ^resolve_anonymous_map_key_kind_mismatch) diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index 2d29f57af15..c2a542c9797 100644 --- a/src/v2/workflow/compile_door_cause_ownership.dag +++ b/src/v2/workflow/compile_door_cause_ownership.dag @@ -316,7 +316,7 @@ data known_frontier_causes: List = [ cause: ^resolve_anonymous_map_duplicate_key, grain: FatalGrain, lane: SharedSelfHostCriticalPath, - flip_trigger: "a map literal whose keys repeat under the key type's equality (decoded text: v2.extdeps.languages.dag dag_string_literal_decoded_text), judged by the one key law v2.std.map_introduction map_introduction_duplicate_key and refused at the SECOND occurrence, never last-wins. Not a frontier: each occurrence is a source defect and flips when the repeated entry is removed" + flip_trigger: "a map literal whose keys repeat under the key type's equality (decoded text, via the decoder gunbc#12759 lands), judged by the one key law v2.std.map_introduction map_introduction_duplicate_key and refused at the SECOND occurrence, never last-wins. Not a frontier: each occurrence is a source defect and flips when the repeated entry is removed" }, CauseOwnership { cause: ^body_lowering_reason_brace_key_kind_mismatch, From 951973ca3e4cdb365f1d6a093a327b3439e5353a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 08:51:50 +0000 Subject: [PATCH 46/63] PR2: remove the committed debug probes; the comment counts three position rules (review 73128) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_resolve.dag | 2 +- src/v2/test/zz_probe/ar_debug.dag | 116 ------------------------------ src/v2/test/zz_probe/probe.dag | 113 ----------------------------- 3 files changed, 1 insertion(+), 230 deletions(-) delete mode 100644 src/v2/test/zz_probe/ar_debug.dag delete mode 100644 src/v2/test/zz_probe/probe.dag diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 48f3c0efbc0..fb431f12723 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1650,7 +1650,7 @@ fn resolve_construct_walk_with_tag( // AN ANONYMOUS RECORD LITERAL IS ELABORATED FROM AN AUTHORED ANNOTATION, OR REFUSED // (docs/plans/anonymous-record-literal-design.md). `{ f: v }` lowers to a construct whose tag is // ELIDED (v2.std.node_query construct_tag_is_elided). Its constructor is the record the EXPECTED -// type names, and the expectation is an explicit parameter handed by exactly four position rules -- +// type names, and the expectation is an explicit parameter handed by three position rules -- // an Arrow's body under its declared return (which covers a `data` initializer), a record field // under its authored field type, and a list element under its annotated list's element type -- and // never ambient context. So a headless literal reached by ANY other edge (a call argument, a let diff --git a/src/v2/test/zz_probe/ar_debug.dag b/src/v2/test/zz_probe/ar_debug.dag deleted file mode 100644 index 32cb799c566..00000000000 --- a/src/v2/test/zz_probe/ar_debug.dag +++ /dev/null @@ -1,116 +0,0 @@ -module v2.test.zz_probe.ar_debug - -import v2.compiler.infer { infer } -import v2.test.claim.body_lowering.anonymous_record { ar_resolved_value_mismatch_headed, ar_resolved_headed, ar_resolved_headless, ar_resolved_call_argument, ar_resolved_coproduct, ar_resolved_binder, ar_resolved_value_mismatch, ar_construct_paths } -import v2.compiler.resolve { ResolvedTree } -import v2.std.diagnostic { Accepted, Outcome, Rejected, Diagnostic } -import v2.std.node { ComputationNode, Arrow, symbol_lexeme, node_synthetic, symbol_eq, construct_tag_elided_marker, Node, TypeNode, Atom, Conj, Named, Positional, node_subtree_nodes } -import v2.std.node_query { construct_tag_is_elided, construct_node_elided, construct_tag_elided_reference } -import v2.std.collection { list_at_optional } -import v2.std.optional { Present, Absent } -import v2.test.claim.namespace_xl0.reference_conservation { conservation_subject } -import v2.test.claim.body_lowering.anonymous_record { ar_headless_source } -import v2.std.lens_verdict { lens_verdict_diagnostic_locus_text } -import v2.std.text { String } -import v2.std.integer { Int } -import std.process { ProcessExit, exit_failure } - -fn ar_dbg(o: Outcome) -> ProcessExit { - match o { - Accepted { value: _, diagnostics: _ } => exit_failure(reason: concat("ACCEPTED paths=", join(ar_construct_paths(o: o), separator: ","))) - Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: concat(concat(concat("REJECTED ", symbol_lexeme(sym: d.head.reason)), " @ "), lens_verdict_diagnostic_locus_text(d: d.head)), f: fn(acc, x) { concat(concat(acc, " | "), symbol_lexeme(sym: x.reason)) })) - } -} - -fn dbg_headed() -> ProcessExit { ar_dbg(o: ar_resolved_headed()) } -fn dbg_headless() -> ProcessExit { ar_dbg(o: ar_resolved_headless()) } -fn dbg_call() -> ProcessExit { ar_dbg(o: ar_resolved_call_argument()) } -fn dbg_coproduct() -> ProcessExit { ar_dbg(o: ar_resolved_coproduct()) } -fn dbg_binder() -> ProcessExit { ar_dbg(o: ar_resolved_binder()) } -fn dbg_mismatch() -> ProcessExit { ar_dbg(o: ar_resolved_value_mismatch()) } - -fn dbg_kind(n: Node) -> String { - match n.kind { - TypeNode { connective: Atom { identity: id } } => concat("Atom:", symbol_lexeme(sym: id)) - TypeNode { connective: Conj } => concat("Conj#", to_string(count(n.children))) - _ => "other" - } -} - -fn dbg_lowered() -> ProcessExit { - match conservation_subject(id_tail: "ar_dbg", source: ar_headless_source).normalized { - Rejected { diagnostics: d } => exit_failure(reason: concat("NORMALIZE REJECTED ", symbol_lexeme(sym: d.head.reason))) - Accepted { value: nt, diagnostics: _ } => - let nodes = node_subtree_nodes(root: nt.root) - exit_failure(reason: concat(concat("nodes=", to_string(count(nodes))), fold(nodes, init: "", f: fn(acc, n) { - fold(n.children, init: acc, f: fn(a2, e) { - match e.label { - Named { name: l } => - if symbol_lexeme(sym: l) == "" { - concat(concat(concat(a2, " | tag->"), dbg_kind(n: e.target)), concat(" elided=", to_string(construct_tag_is_elided(n: n)))) - } else { a2 } - Positional => a2 - } - }) - }))) - } -} - -fn dbg_unit() -> ProcessExit { - let src = node_synthetic(kind: TypeNode { connective: Atom { identity: ^dbg_src } }, children: []) - let c = construct_node_elided(field_edges: [], source: src) - let r = construct_tag_elided_reference(source: src) - exit_failure(reason: concat(concat(concat("elided=", to_string(construct_tag_is_elided(n: c))), concat(" ref=", dbg_kind(n: r))), concat(" eq=", to_string(symbol_eq(a: construct_tag_elided_marker(), b: construct_tag_elided_marker()))))) -} - -fn dbg_sexpr(n: Node, depth: Int) -> String { - let head = match n.kind { - TypeNode { connective: Atom { identity: id } } => concat("A:", symbol_lexeme(sym: id)) - TypeNode { connective: Conj } => "Conj" - TypeNode { connective: Arrow } => "Arrow" - TypeNode { connective: _ } => "Type?" - ComputationNode { behavior: _ } => "Comp" - } - if depth == 0 { - head - } else { - concat(concat(concat(head, "("), fold(n.children, init: "", f: fn(acc, e) { - let lbl = match e.label { - Named { name: l } => symbol_lexeme(sym: l) - Positional => "_" - } - concat(concat(concat(concat(acc, lbl), "="), dbg_sexpr(n: e.target, depth: depth - 1)), " ") - })), ")") - } -} - -fn dbg_find_ar_x(o: Outcome) -> ProcessExit { - match o { - Rejected { diagnostics: d } => exit_failure(reason: "REJECTED") - Accepted { value: t, diagnostics: _ } => - exit_failure(reason: fold(node_subtree_nodes(root: t.root), init: "", f: fn(acc, n) { - fold(n.children, init: acc, f: fn(a2, e) { - match e.label { - Named { name: l } => if symbol_lexeme(sym: l) == "ar_x" { concat(concat(a2, " || "), dbg_sexpr(n: e.target, depth: 5)) } else { a2 } - Positional => a2 - } - }) - })) - } -} - -fn dbg_tree_headless() -> ProcessExit { dbg_find_ar_x(o: ar_resolved_headless()) } -fn dbg_tree_headed() -> ProcessExit { dbg_find_ar_x(o: ar_resolved_headed()) } - -fn dbg_infer(o: Outcome) -> ProcessExit { - match o { - Rejected { diagnostics: d } => exit_failure(reason: concat("RESOLVE REJECTED ", symbol_lexeme(sym: d.head.reason))) - Accepted { value: t, diagnostics: _ } => - match infer(tree: t) { - Accepted { value: _, diagnostics: _ } => exit_failure(reason: concat("INFER ACCEPTED paths=", join(ar_construct_paths(o: o), separator: ","))) - Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: concat("INFER REJECTED ", symbol_lexeme(sym: d.head.reason)), f: fn(acc, x) { concat(concat(acc, " | "), symbol_lexeme(sym: x.reason)) })) - } - } -} -fn dbg_infer_headless() -> ProcessExit { dbg_infer(o: ar_resolved_value_mismatch()) } -fn dbg_infer_headed() -> ProcessExit { dbg_infer(o: ar_resolved_value_mismatch_headed()) } diff --git a/src/v2/test/zz_probe/probe.dag b/src/v2/test/zz_probe/probe.dag deleted file mode 100644 index c06fe51b44a..00000000000 --- a/src/v2/test/zz_probe/probe.dag +++ /dev/null @@ -1,113 +0,0 @@ -module v2.test.zz_probe.probe - -import v2.test.claim.namespace_xl0.reference_conservation { conservation_subject } -import v2.std.optional { Optional, Present, Absent } -import v2.std.diagnostic { Accepted, Rejected, NonEmptyDiagnostics } -import v2.std.text { String } -import std.process { ProcessExit, ExitSuccess, exit_failure } -import v2.std.lens_verdict { lens_verdict_diagnostic_locus_text_in_spans } -import v2.std.provenance { SpanIndex, span_index_empty } -import v2.std.node { symbol_lexeme } -import v2.std.diagnostic { Diagnostic } - - -data probe_src_0: String = "module std.algebra\n\nimport std.error_primitives { DivError, Result }\n\ntype Magma {\n op: fn(T, T) -> T\n}\n\ntype Semigroup {\n op: fn(T, T) -> T\n\n}\n\ntype Monoid {\n op: fn(T, T) -> T\n identity: T\n}\n\ntype CommutativeMonoid {\n op: fn(T, T) -> T\n identity: T\n\n}\n\ntype Group {\n op: fn(T, T) -> T\n identity: T\n inverse: fn(T) -> T\n}\n\ntype AbelianGroup {\n op: fn(T, T) -> T\n identity: T\n inverse: fn(T) -> T\n\n}\n\ntype GroupCompletion {\n pos: M\n neg: M\n}\n\ntype FieldOfFractions {\n num: R\n denom: R\n}\n\ntype Semiring {\n add: fn(T, T) -> T\n zero: T\n mul: fn(T, T) -> T\n one: T\n}\n\ntype CommutativeSemiring {\n add: fn(T, T) -> T\n zero: T\n mul: fn(T, T) -> T\n one: T\n}\n\ntype Ring {\n add: fn(T, T) -> T\n zero: T\n negate: fn(T) -> T\n mul: fn(T, T) -> T\n one: T\n}\n\ntype OrderedRing {\n add: fn(T, T) -> T\n sub: fn(T, T) -> T\n zero: T\n negate: fn(T) -> T\n mul: fn(T, T) -> T\n div: fn(T, T) -> Result\n one: T\n compare: fn(T, T) -> Ordering\n eq: fn(T, T) -> Bool\n ne: fn(T, T) -> Bool\n lt: fn(T, T) -> Bool\n le: fn(T, T) -> Bool\n gt: fn(T, T) -> Bool\n ge: fn(T, T) -> Bool\n}\n\ntype Field {\n add: fn(T, T) -> T\n zero: T\n negate: fn(T) -> T\n mul: fn(T, T) -> T\n one: T\n reciprocal: fn(T) -> T\n compare: fn(T, T) -> Ordering\n}\n\ntype Lattice {\n meet: fn(T, T) -> T\n join: fn(T, T) -> T\n}\n\ntype BoundedLattice {\n meet: fn(T, T) -> T\n join: fn(T, T) -> T\n top: T\n bottom: T\n}\n\ntype BooleanAlgebra {\n meet: fn(T, T) -> T\n join: fn(T, T) -> T\n complement: fn(T) -> T\n top: T\n bottom: T\n}\n\ntype PointwisePower {\n member: fn(T) -> Bool\n}\n\n// THE FINITE POWER SET -- Pfin, the set of FINITE subsets -- is the enumerable counterpart of\n// `PointwisePower` above, which presents the full power set as a characteristic function. The two\n// sit on the SUPPORT axis exactly as `PartialFunction` and `FinitelySupportedFunction` do below:\n// one answers membership and nothing else, the other can be walked.\n//\n// AUTHORED HERE BECAUSE THE CONCEPT WAS ALREADY IN TREE UNDER A BORROWED NAME. Every language that\n// carries an algebra inhabitant row files the finite-set realization under `\"BooleanAlgebra\"`:\n//\n// rust BTreeSet<{0}> go map[{0}]struct{}\n// python set[{0}] typescript Set<{0}>\n//\n// Four independent realizations, four finite-set containers, not one lattice among them -- while\n// the TYPE `BooleanAlgebra` is `{ meet, join, complement, top, bottom }` and its only\n// construction in the corpus is `v2.std.logic` `bool_boolean_algebra`, a genuine Boolean algebra\n// over `Bool`. The realization roster and the type disagreed about what the name meant, and the\n// rust roster carried the tell outright: the `\"BooleanAlgebra\"` and `\"PointwisePower\"` rows were\n// BYTE-IDENTICAL, two algebra names resolving to one realization with nothing saying which was\n// which. This declaration takes the finite-set concept out from under the lattice's name; the\n// lattice keeps its name, its type and its one construction site unchanged.\ntype FinitePowerSet {\n member: fn(T) -> Bool\n union: fn(FinitePowerSet) -> FinitePowerSet\n intersect: fn(FinitePowerSet) -> FinitePowerSet\n diff: fn(FinitePowerSet) -> FinitePowerSet\n contains: fn(T) -> Bool\n count: fn() -> Int\n length: fn() -> Int\n}\n\ntype FreeMonoid\n = Empty\n | Cons { head: T, tail: FreeMonoid }\n\nfn freemonoid_empty() -> FreeMonoid {\n Empty\n}\n\n// THE COST IS CHARGED IN THE RIGHT OPERAND, AND THE RIGHT-FOLD THIS REPLACED CHARGED THE LEFT.\n// `concat` is the primitive two-list append; `std.primitives` `concat_contract` declares its\n// persistent carrier cost as `log(n + m)`. The `fold_list_right` form that stood here walked\n// its LEFT operand instead, one `Cons` per element, so an accumulate-on-the-left fold paid the\n// whole accumulator at every step and came out quadratic in the accumulated length --\n// `list_snoc_item` below is exactly that shape, which is why it inherited the defect, and it is\n// the shape the corpus uses wherever it builds a list by appending. Routing to the primitive\n// makes the singleton case constant and the general case linear in the appended side only.\n// DESIGN section 6 fixes a proven cost-shape defect at the root rather than per callsite.\nfn list_append(left: FreeMonoid, right: FreeMonoid) -> FreeMonoid {\n left |> concat(right)\n}\n\n// `list_push` is the primitive single-item append (amortized O(log n) on the persistent\n// carrier); routing through list_append paid a full O(n) right-fold per snoc, which made\n// every build-by-appending accumulator quadratic — measured on the first-set union fold in\n// the self-hosted parser (2026-09-09, required-v2-native lane work).\nfn list_snoc_item(xs: FreeMonoid, item: T) -> FreeMonoid {\n xs |> list_push(item)\n}\n\ntype FreeSemigroup {\n head: T\n tail: FreeMonoid\n}\n\n// FreeSemigroup is the free SEMIGROUP over T exactly as FreeMonoid is the free MONOID over T.\n// The head field is the construction wall: an empty inhabitant has no representation.\n\n// TWO AXES, THREE INHABITED CORNERS. The family below is a 2x2 space and the corpus lives in\n// three of its corners, so three carriers are authored and the fourth is named rather than\n// silently omitted.\n//\n// TOTALITY -- does the function answer at every point (`fn(K) -> V`) or may it decline\n// (`fn(K) -> V?`) -- is independent of SUPPORT -- does it differ from the absent value at\n// finitely many points, so that it can be enumerated, or at arbitrarily many, so that it cannot.\n//\n// arbitrary support finite support\n// total TotalMap (uninhabited)\n// partial PartialFunction FinitelySupportedFunction\n//\n// The uninhabited corner is REPRESENTABLE, not impossible: a total function taking a non-default\n// value at finitely many points is a perfectly good object. It is unauthored because no corpus\n// site needs one. The condition that fills it is a site that must both answer everywhere and be\n// enumerated; author it then, and not before.\n//\n// The predicate family sits on the same support axis one row up: `PointwisePower` is the full\n// power set presented as a characteristic function, and its finite counterpart has no carrier\n// yet -- see the `Set` alias in `std.types` for that open half and its trigger.\ntype PartialFunction {\n lookup: fn(K) -> V?\n}\n\n// FINITE SUPPORT IS WHAT MAKES THE ENUMERATION SURFACE HONEST. Everything below `lookup` here --\n// keys, values, size, and the insert/merge that must preserve them -- is answerable only because\n// the support is finite. That is exactly why these rows may not sit on `PartialFunction`: an open\n// partial function cannot produce `map_keys` at all, and until this split every one of the 145\n// `Map { lookup: .. }` literals in the corpus named a carrier promising ten operations it\n// supplied one of. Measured before the split: 0 of those 145 supplied any field beyond `lookup`.\n//\n// `lookup` appears on BOTH carriers rather than being inherited, because the substrate has no\n// subtyping: a finite map really does answer lookup, and saying so twice is the honest encoding\n// of \"this one can do everything that one can, and more\".\n//\n// RECORDED, NOT INTRODUCED, and not dissolved here: `lookup`, `get` and `map_get` are three\n// spellings of one operation, which the template rosters below carry as three rows. That is the\n// same Section 3 fork `algebra_count_length_name_fork_note` records for count/length/size, it\n// predates this change, and collapsing it is a corpus-wide rename touching every call site --\n// its own lane. The split neither widens nor narrows it; the three spellings move together.\ntype FinitelySupportedFunction {\n lookup: fn(K) -> V?\n empty: FinitelySupportedFunction\n get: fn(K) -> V?\n insert: fn(K, V) -> FinitelySupportedFunction\n merge: fn(FinitelySupportedFunction) -> FinitelySupportedFunction\n map_keys: fn() -> FreeMonoid\n map_values: fn() -> FreeMonoid\n map_has: fn(K) -> Bool\n contains_key: fn(K) -> Bool\n size: fn() -> Int\n}\n\n// MOVED HERE from `v2.std.collection` by the change that split `PartialFunction`. They are the\n// total corner of the family above, and leaving them in a different module from their own axis is\n// where a fork starts. `TotalPolicy` travels with `TotalMap` rather than staying behind: it is the\n// same total lookup carrying a context argument, and splitting a pair across two modules is the\n// thing this change exists to stop doing.\n//\n// Both had ZERO code consumers at the time of the move -- one declaration each and two prose cells\n// in `gunbc.plans.self_applying_lenses`, which marks `table_decision_tree` as an Unrealized\n// scaffold and names these as its target substrate rows. So they are carriers authored ahead of a\n// consumer that a plan does declare, which is weaker than an untracked scaffold and stronger than\n// healthy. The home is the cheap part of the answer; the consumer is that plan's to land.\ntype TotalMap {\n lookup: fn(K) -> V\n}\n\ntype TotalPolicy {\n lookup: fn(K, Context) -> RowTemplate\n}\n\ntype Ordering = Less | Equal | Greater\n\ntype OrderingFold {\n less: R\n equal: R\n greater: R\n}\n\nfn ordering_fold(o: Ordering, fold: OrderingFold) -> R {\n match o {\n Less => fold.less\n Equal => fold.equal\n Greater => fold.greater\n }\n}\n\nfn ordering_is_less(o: Ordering) -> Bool {\n ordering_fold(\n o: o,\n fold: OrderingFold {\n less: true,\n equal: false,\n greater: false,\n },\n )\n}\n\ntype AlgebraProfile\n = OrderedRingProfile\n | ApproximateFieldProfile\n | BooleanAlgebraProfile\n | FinitePowerSetProfile\n | PointwisePowerCollectionProfile\n | FreeMonoidScalarProfile\n | FreeMonoidCollectionProfile\n | PartialFunctionProfile\n | FinitelySupportedFunctionProfile\n\ntype ContainerSource\n = SameAsReceiver\n | Named { name: String }\n\ntype AlgebraTypeTemplate\n = ReceiverSelf\n | ReceiverElement\n | ReceiverKey\n | ReceiverValue\n | NamedTemplate { name: String }\n | ContainerOf { source: ContainerSource, element: AlgebraTypeTemplate }\n | OptionalOf { inner: AlgebraTypeTemplate }\n | WitnessOf { inner: AlgebraTypeTemplate }\n | TupleOf { first: AlgebraTypeTemplate, second: AlgebraTypeTemplate }\n | CallableOf { params: List, return_type: AlgebraTypeTemplate }\n | AlgebraTypeVariable { id: String }\n\ntype CollectionSizeEffect\n = ShrinkEffect\n | ProjectionEffect\n | IdentityEffect\n\ntype CostShape\n = ShapeConstant\n | ShapeLinearScan\n | ShapeIterateBody\n | ShapeSortBody\n\n// THE STRUCTURED FORM OF THIS FACT IS NOT DECLARED HERE, AND THE FIRST REVISION OF THIS WORK\n// DECLARED IT ANYWAY. CostShape names a growth RANK and nothing else, so it cannot distinguish an\n// operation whose work is linear in BOTH operands from one linear in only the right operand -- both\n// are \"linear\" -- and that distinction is the whole of the concat specimen in\n// gunbc.recurring_failure_mode lens_prices_the_authority_while_the_lane_runs_the_realization. The\n// repair needs a cost expression over NAMED operands, and this module minted one: a sum-of-products\n// grammar of constant, log, linear and linearithmic factors.\n//\n// THAT WAS A FAILED DECOMPOSITION (DESIGN section 2): std.induction CostBound ALREADY owns exactly\n// that grammar -- AtomicCost PolyCost/LogCost over a named param, ProductBound for a product,\n// SumOfProductsBound for a sum of products -- and owns it more completely, with fractional and\n// log-based exponents this corpus already uses. cost_nlogn IS linearithmic and cost_graph_linear IS\n// the concat specimen, both already written. Minting a second grammar here would have put two names\n// on one concept (section 3) in the same change that argued against exactly that one field over, in\n// the module whose subject is a cost authority disagreeing with itself.\n//\n// So the structured cost of an operation is a std.induction CostBound, and CostShape remains what\n// it always was: a coarse rank for the sites that only need one. The projection between them lives\n// with the growth derivation in std.induction, beside the carrier it reads.\n\ntype AlgebraFieldTemplate {\n name: String\n param_types: List\n return_type: AlgebraTypeTemplate\n size_effect: CollectionSizeEffect?\n cost_shape: CostShape?\n\n callback_element_position: Int?\n}\n\n// Collection filter is this one template, not a second encoding of its fields and not the\n// spelling of `name`. Roster rows and emission membership both consume it; a rename of the\n// method token must not disarm the wall.\nfn collection_filter_shape() -> AlgebraFieldTemplate {\n { name: \"filter\", param_types: [ReceiverSelf, CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: \"Bool\" } }], return_type: ReceiverSelf, size_effect: Present { value: IdentityEffect }, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: Present { value: 0 } }\n}\n\n// Collection fold is this one template, declared once for both collection profiles that carry it,\n// and its param_types IS the free-call signature, receiver first, the same convention as `map` and\n// `filter` (and the one v1.compiler.method pair_declared_names reads when it pairs `names: [\"xs\", ..]`\n// with a row). The receiver slot was missing here while every sibling carried it, so the row did not\n// say where the step sits in `fold(xs, init, step)`; v2.compiler.fold_lowering reads that position\n// from this row through template_step_position rather than from a literal index.\nfn collection_fold_shape() -> AlgebraFieldTemplate {\n { 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 } }\n}\n\ntype StepPositionScan {\n next: Int\n found: Int?\n}\n\n// The position of a template's step -- its first callable parameter -- in the free-call signature\n// the row declares. Absent when the row takes no callable.\nfn template_step_position(t: AlgebraFieldTemplate) -> Int? {\n let scan = fold(t.param_types, init: StepPositionScan { next: 0, found: none }, f: fn(acc, p) {\n match acc.found {\n Present { value: _ } => acc\n Absent =>\n match p {\n CallableOf { params: _, return_type: _ } => StepPositionScan { next: acc.next + 1, found: Present { value: acc.next } }\n _ => StepPositionScan { next: acc.next + 1, found: none }\n }\n }\n })\n scan.found\n}\n\nfn is_collection_filter_template(t: AlgebraFieldTemplate) -> Bool {\n let proto = collection_filter_shape()\n t.return_type == proto.return_type\n && t.size_effect == proto.size_effect\n && t.cost_shape == proto.cost_shape\n && t.callback_element_position == proto.callback_element_position\n && t.param_types == proto.param_types\n}\n\n// ONE ROSTER, DECLARED ONCE, AND EVERY SPELLING-KEYED MAP BELOW IS A FOLD OVER IT.\n//\n// What stood here was `kernel_algebra_profile` written out as a nest of ten `map_insert` calls\n// keyed on type-name strings, beside four further hand-authored spelling maps in `std.types`\n// (`container_template_algebra_rows`, `container_template_alias_rows`, `container_type_arity`,\n// `canonical_container_names`). Five maps, one subject: which carrier a spelling names and what\n// that carrier answers for. Nothing joined them, so a carrier existed in a map exactly insofar as\n// an author remembered to write it there -- and the failure mode is not hypothetical, it is what\n// the deleted note above this row recorded: `PartialFunction` had a carrier, a template set and a\n// profile, and no `kernel_algebra_profile` key, so `tree.facts.lookup(node)` refused on a\n// `PartialFunction` receiver while the byte-identical call on `Map` resolved. That repair was\n// three inserted rows. The eleventh carrier forks the same way, in whichever of the five maps its\n// author does not think of.\n//\n// The move is DESIGN section 2-horizontal: model the carrier once, derive every use. `AlgebraCarrier`\n// carries `profile` as a required field, so a carrier row cannot be authored without one --\n// the invalid state (a carrier with no algebra) has no spelling, which is section 4b structural\n// impossibility for that class rather than a check. Each spelling then DECLARES its membership in\n// every derived map, and `RowAbsent` carries a reason, so an omission is an authored answer\n// someone had to write rather than a row nobody wrote. That is the property the five literals\n// could not have: adding a carrier now means answering five questions in one place, and leaving\n// one unanswered is a missing field, not a silent hole.\n//\n// RUNG, honestly: this is *structurally guaranteed* for \"a carrier without a profile\" and\n// \"a carrier absent from a map by oversight\", and NOT a wall on the memberships themselves --\n// an author can still declare `RowAbsent` with a bad reason and get exactly the old behaviour.\n// The next-rung trigger is each membership becoming derivable from a property the carrier already\n// has (arity from `algebra_type_param_names`, roster membership from whether the profile is a\n// collection), which needs the two remaining `std.types` literals this change does not reach --\n// `ordered_element_collections` and the non-carrier `Witness` row -- to be grounded first.\ntype CarrierRowMembership\n = RowPresent\n | RowAbsent { reason: String }\n\n// The five derived maps, one field each. A new membership question means a new field here, and\n// every existing carrier then fails to compile until it answers it -- which is the point.\ntype CarrierSpelling {\n text: String\n\n method_surface: CarrierRowMembership\n container_algebra_row: CarrierRowMembership\n container_alias_row: CarrierRowMembership\n container_roster_name: CarrierRowMembership\n container_arity_row: CarrierRowMembership\n}\n\ntype AlgebraCarrier {\n name: String\n profile: AlgebraProfile\n spellings: List\n}\n\ndata scalar_carrier_not_container_reason: String = \"scalar carrier: names no container, so it holds no container-spelling row\"\n\ndata snake_case_spelling_not_alias_reason: String = \"snake spelling: resolves as an algebra name, not as a declared container alias\"\n\ndata canonical_spelling_sorts_ahead_of_alias_reason: String = \"container_alias_canonical_spelling returns the FIRST sorted key mapping to an algebra, so this key would sort ahead of its own alias and silently become the emitted canonical spelling\"\n\ndata carrier_name_is_not_a_declaration_spelling_reason: String = \"the carrier name is not written in declaration position; its alias carries the profile\"\n\ndata arity_row_is_alias_spellings_only_reason: String = \"container_type_arity is consulted by is_container_type on authored declaration spellings; adding a key here decides containerhood for that spelling\"\n\nfn scalar_carrier_spelling(text: String) -> CarrierSpelling {\n CarrierSpelling {\n text: text,\n method_surface: RowPresent,\n container_algebra_row: RowAbsent { reason: scalar_carrier_not_container_reason },\n container_alias_row: RowAbsent { reason: scalar_carrier_not_container_reason },\n container_roster_name: RowAbsent { reason: scalar_carrier_not_container_reason },\n container_arity_row: RowAbsent { reason: scalar_carrier_not_container_reason },\n }\n}\n\nfn snake_alias_spelling(text: String) -> CarrierSpelling {\n CarrierSpelling {\n text: text,\n method_surface: RowAbsent { reason: carrier_name_is_not_a_declaration_spelling_reason },\n container_algebra_row: RowPresent,\n container_alias_row: RowPresent,\n container_roster_name: RowAbsent { reason: snake_case_spelling_not_alias_reason },\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n }\n}\n\nfn snake_algebra_spelling(text: String) -> CarrierSpelling {\n CarrierSpelling {\n text: text,\n method_surface: RowAbsent { reason: carrier_name_is_not_a_declaration_spelling_reason },\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: snake_case_spelling_not_alias_reason },\n container_roster_name: RowAbsent { reason: snake_case_spelling_not_alias_reason },\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n }\n}\n\n// THE ALIAS SPELLING IS THE ONE WRITTEN IN DECLARATION POSITION -- `List`, `Set`, `Map` -- so it\n// is the one that carries every row: the profile that decides whether a receiver has a method\n// surface, the algebra it names, its own alias row, the container roster, and the arity checked\n// against an authored type expression.\nfn pascal_alias_spelling(text: String) -> CarrierSpelling {\n CarrierSpelling {\n text: text,\n method_surface: RowPresent,\n container_algebra_row: RowPresent,\n container_alias_row: RowPresent,\n container_roster_name: RowPresent,\n container_arity_row: RowPresent,\n }\n}\n\n// TWO ROWS BELOW ARE NOT MECHANICAL, and both are stated here rather than beside the row because\n// a section 4c annotation attaches to a module-scope declaration, not to a line inside a body.\n//\n// `PartialFunction` carries its own alias row, unlike every other carrier name: no alias competes\n// for that algebra, so the carrier name IS the declaration spelling -- which is why\n// `resolve_method_receiver_type` returns a `PartialFunction` receiver as authored, type arguments\n// intact. Every other canonical name declines the alias row because\n// `container_alias_canonical_spelling` returns the first sorted key for an algebra, so\n// `\"FinitelySupportedFunction\"` would sort ahead of `\"Map\"` and silently become the emitted\n// canonical spelling.\n//\n// `PointwisePower` declares `method_surface` ABSENT, and the reason is a measurement not taken\n// rather than a property of the carrier. Its 80 declaration positions predate the carrier split,\n// its templates are a different set from `FinitePowerSet`'s, and `RowPresent` would flip the\n// method-existence wall from UNDECIDED to DECIDED for all 80 at once -- turning any method absent\n// from `pointwise_power_collection_templates` into a hard `MethodNotFound`. Under the five\n// literals this was an omission indistinguishable from forgetting; here it is an authored row\n// with a reason, which is the whole difference this change buys.\nfn algebra_carriers() -> List {\n [\n AlgebraCarrier {\n name: \"Int\",\n profile: OrderedRingProfile,\n spellings: [scalar_carrier_spelling(text: \"Int\")],\n },\n AlgebraCarrier {\n name: \"Float\",\n profile: ApproximateFieldProfile,\n spellings: [scalar_carrier_spelling(text: \"Float\")],\n },\n AlgebraCarrier {\n name: \"BooleanAlgebra\",\n profile: BooleanAlgebraProfile,\n spellings: [\n scalar_carrier_spelling(text: \"Bool\"),\n CarrierSpelling {\n text: \"BooleanAlgebra\",\n method_surface: RowAbsent { reason: carrier_name_is_not_a_declaration_spelling_reason },\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowAbsent { reason: \"the lattice is not a container: its container inhabitant row moved to FinitePowerSet\" },\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"boolean_algebra\"),\n ],\n },\n AlgebraCarrier {\n name: \"String\",\n profile: FreeMonoidScalarProfile,\n spellings: [scalar_carrier_spelling(text: \"String\")],\n },\n AlgebraCarrier {\n name: \"FreeMonoid\",\n profile: FreeMonoidCollectionProfile,\n spellings: [\n pascal_alias_spelling(text: \"List\"),\n snake_alias_spelling(text: \"list\"),\n CarrierSpelling {\n text: \"FreeMonoid\",\n method_surface: RowAbsent { reason: carrier_name_is_not_a_declaration_spelling_reason },\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"free_monoid\"),\n ],\n },\n AlgebraCarrier {\n name: \"FinitePowerSet\",\n profile: FinitePowerSetProfile,\n spellings: [\n pascal_alias_spelling(text: \"Set\"),\n snake_alias_spelling(text: \"set\"),\n CarrierSpelling {\n text: \"FinitePowerSet\",\n method_surface: RowPresent,\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"finite_power_set\"),\n ],\n },\n AlgebraCarrier {\n name: \"FinitelySupportedFunction\",\n profile: FinitelySupportedFunctionProfile,\n spellings: [\n pascal_alias_spelling(text: \"Map\"),\n snake_alias_spelling(text: \"map\"),\n CarrierSpelling {\n text: \"FinitelySupportedFunction\",\n method_surface: RowPresent,\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"finitely_supported_function\"),\n ],\n },\n AlgebraCarrier {\n name: \"PartialFunction\",\n profile: PartialFunctionProfile,\n spellings: [\n CarrierSpelling {\n text: \"PartialFunction\",\n method_surface: RowPresent,\n container_algebra_row: RowPresent,\n container_alias_row: RowPresent,\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_alias_spelling(text: \"partial_function\"),\n ],\n },\n AlgebraCarrier {\n name: \"PointwisePower\",\n profile: PointwisePowerCollectionProfile,\n spellings: [\n CarrierSpelling {\n text: \"PointwisePower\",\n method_surface: RowAbsent { reason: \"80 declaration positions would flip from MethodExistenceUndecided to a hard MethodNotFound for any method absent from pointwise_power_collection_templates; that measurement is not taken\" },\n container_algebra_row: RowPresent,\n container_alias_row: RowAbsent { reason: canonical_spelling_sorts_ahead_of_alias_reason },\n container_roster_name: RowPresent,\n container_arity_row: RowAbsent { reason: arity_row_is_alias_spellings_only_reason },\n },\n snake_algebra_spelling(text: \"pointwise_power\"),\n ],\n },\n ]\n}\n\nfn carrier_spelling_row_present(m: CarrierRowMembership) -> Bool {\n match m {\n RowPresent => true\n RowAbsent { reason: _ } => false\n }\n}\n\nfn kernel_algebra_profile_value() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.method_surface) {\n map_insert(inner, spelling.text, carrier.profile)\n } else {\n inner\n }))\n}\n\nfn carrier_container_algebra_rows() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_algebra_row) {\n map_insert(inner, spelling.text, carrier.name)\n } else {\n inner\n }))\n}\n\nfn carrier_container_alias_rows() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_alias_row) {\n map_insert(inner, spelling.text, carrier.name)\n } else {\n inner\n }))\n}\n\nfn carrier_container_roster_map() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_roster_name) {\n map_insert(inner, spelling.text, true)\n } else {\n inner\n }))\n}\n\n// The arity is DERIVED from the profile's own type parameters rather than restated as a number:\n// `FinitePowerSet` is unary because `algebra_type_param_names` says `[\"T\"]`, so a carrier cannot\n// declare an arity that disagrees with its algebra.\nfn carrier_container_arity_rows() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_arity_row) {\n map_insert(inner, spelling.text, algebra_type_param_names(profile: carrier.profile) |> fold(init: 0, f: (n, _) => n + 1))\n } else {\n inner\n }))\n}\n\n// EQUALITY ADMISSIBILITY IS A CONSEQUENCE OF THE SUPPORT AXIS, declared beside the profiles it\n// follows from rather than re-derived by any consumer. A carrier with finite support (or a scalar\n// carrier) has decidable extensional equality: two values agree exactly when they agree on the\n// finitely many points that exist, so equality LIFTS over the carrier's type arguments and the\n// admission question recurses into them. An open-support carrier (PartialFunction, the pointwise\n// power) cannot answer extensional equality at all -- the agreement set is not enumerable -- so\n// generic equality on it is refused, never approximated. The rows are per-profile because the\n// support axis is a fact about the algebra, not about any spelling that resolves to it.\ntype AlgebraSupportAxis\n = FiniteSupport\n | OpenSupport\n\nfn algebra_profile_support(profile: AlgebraProfile) -> AlgebraSupportAxis {\n match profile {\n OrderedRingProfile => FiniteSupport\n ApproximateFieldProfile => FiniteSupport\n BooleanAlgebraProfile => FiniteSupport\n FinitePowerSetProfile => FiniteSupport\n PointwisePowerCollectionProfile => OpenSupport\n FreeMonoidScalarProfile => FiniteSupport\n FreeMonoidCollectionProfile => FiniteSupport\n PartialFunctionProfile => OpenSupport\n FinitelySupportedFunctionProfile => FiniteSupport\n }\n}\n\nfn algebra_profile_equality_extensional(profile: AlgebraProfile) -> Bool {\n match algebra_profile_support(profile: profile) {\n FiniteSupport => true\n OpenSupport => false\n }\n}\n\nfn carrier_container_equality_rows() -> Map {\n algebra_carriers() |> fold(init: empty_map(), f: (acc, carrier) =>\n carrier.spellings |> fold(init: acc, f: (inner, spelling) =>\n if carrier_spelling_row_present(m: spelling.container_algebra_row) {\n map_insert(inner, spelling.text, algebra_profile_equality_extensional(profile: carrier.profile))\n } else {\n inner\n }))\n}\n\ndata kernel_algebra_profile: Map = kernel_algebra_profile_value()\n\nfn ordered_ring_templates() -> List {\n [\n { name: \"add\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"zero\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"negate\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"mul\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"one\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"compare\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Ordering\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"clamp\", param_types: [ReceiverSelf, ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeConstant }, callback_element_position: none }\n ]\n}\n\nfn approximate_field_templates() -> List {\n [\n { name: \"add\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"zero\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"negate\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"mul\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"one\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"reciprocal\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"compare\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Ordering\" }, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\nfn boolean_algebra_templates() -> List {\n [\n { name: \"meet\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"join\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"complement\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"top\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"bottom\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\n// A CARDINALITY IS A NATURAL, AND IT NAMES ITS DECLARER. count and length (here and in the\n// free-monoid and function profiles below) return std.nat.Nat, qualified rather than spelled\n// \"Nat\": a bare template name resolves at the CALL SITE, and a src/v2 module resolves it to\n// v2.std.nat.Nat, a Peano Zero | Succ coproduct whose Rust carrier is Rc, not the i64 that\n// len() produces. The spelling then decided the emission -- `count(xs) >= 1` lowered to\n// nat_compare over Rc -- and the self-host build failed with E0308 in v2_compiler_resolve.\nfn finite_power_set_templates() -> List {\n [\n { name: \"union\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"intersect\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"diff\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"member\", param_types: [ReceiverSelf, ReceiverElement], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"contains\", param_types: [ReceiverSelf, ReceiverElement], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n collection_filter_shape(),\n { 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 } },\n { 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 } },\n collection_fold_shape(),\n { 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 } },\n { 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 } },\n { name: \"count\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"length\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none }\n ]\n}\n\nfn pointwise_power_collection_templates() -> List {\n [\n { name: \"member\", param_types: [CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: \"Bool\" } }], return_type: CallableOf { params: [ReceiverElement], return_type: NamedTemplate { name: \"Bool\" } }, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\ndata algebra_count_length_name_fork_note: String = \"count and length sit on the same template rows as the SAME operation, and that is a section 3 nicknaming fork being RECORDED rather than introduced (review 45499 raised it, correctly, as a smell). The fork's authority is the interpreter, which dispatches length, count and size to one native arm -- three names, one concept, and that arm predates this change. What was inconsistent was the declared surface: the templates listed only length, so a String receiver piped into count resolved at runtime while having no declared row, which the method-existence wall exposed the moment it began proving absence from the template roster. Reconciling the declaration to the runtime is the honest direction, because the runtime is what programs actually meet; inventing a distinct meaning for count to justify two rows would be the fabrication, and dropping the row would have left a working call refused. So the row is added and the fork is named here rather than silently propagated. Dissolve-on: feature:collection-size-single-spelling -- one name for this operation across the interpreter arm, the templates, and the corpus, a corpus-wide rename touching every call site, which belongs on its own lane rather than inside a P0 live-system fix. THE TRIGGER IS NAMED RATHER THAN DESCRIBED because a deferral whose condition is only prose cannot be looked up, counted, or closed by anyone but its author -- the asymmetry review 45759 caught beside unresolved_method_frontier, which cites feature:primitive-realization-single-authority and is therefore findable. The closing condition is decidable and stated: the interpreter's length/count/size arm dispatches one spelling, these two template rows carry that spelling only, and no other spelling resolves. Until then the count is the measure: three accepted spellings in the interpreter arm, two on these rows, one concept.\"\n\nfn free_monoid_scalar_templates() -> List {\n [\n { name: \"concat\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"empty\", param_types: [], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"length\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"count\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"is_empty\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"chars\", param_types: [ReceiverSelf], return_type: ContainerOf { source: Named { name: \"List\" }, element: NamedTemplate { name: \"Int\" } }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"split\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ContainerOf { source: Named { name: \"List\" }, element: ReceiverSelf }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"join\", param_types: [ContainerOf { source: Named { name: \"List\" }, element: ReceiverSelf }, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"contains\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"starts_with\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"ends_with\", param_types: [ReceiverSelf, ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"trim\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"to_lower\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"to_upper\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"replace\", param_types: [ReceiverSelf, ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"substring\", param_types: [ReceiverSelf, NamedTemplate { name: \"Int\" }, NamedTemplate { name: \"Int\" }], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"to_int\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"Int\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"to_string\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"reverse\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\nfn free_monoid_collection_templates() -> List {\n [\n { 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 } },\n collection_filter_shape(),\n { 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 } },\n collection_fold_shape(),\n { 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 } },\n { 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 } },\n { name: \"count\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"first\", param_types: [ReceiverSelf], return_type: OptionalOf { inner: ReceiverElement }, size_effect: Present { value: ProjectionEffect }, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"last\", param_types: [ReceiverSelf], return_type: OptionalOf { inner: ReceiverElement }, size_effect: Present { value: ProjectionEffect }, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { 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 },\n { 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 },\n { name: \"take\", param_types: [ReceiverSelf, NamedTemplate { name: \"Int\" }], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeIterateBody }, callback_element_position: none },\n { 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 } },\n { name: \"append\", param_types: [ReceiverSelf, ReceiverElement], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeConstant }, callback_element_position: none },\n { name: \"contains\", param_types: [ReceiverSelf, ReceiverElement], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { 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 },\n { name: \"reverse\", param_types: [ReceiverSelf], return_type: ReceiverSelf, size_effect: Present { value: IdentityEffect }, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"join\", param_types: [ReceiverSelf, NamedTemplate { name: \"String\" }], return_type: NamedTemplate { name: \"String\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"concat\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"list_push\", param_types: [ReceiverSelf, ReceiverElement], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"length\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none },\n { name: \"is_empty\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: Present { value: ShapeLinearScan }, callback_element_position: none }\n ]\n}\n\nfn partial_function_templates() -> List {\n [\n { name: \"get\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_get\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"lookup\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\nfn finitely_supported_function_templates() -> List {\n [\n { name: \"get\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_get\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"lookup\", param_types: [ReceiverSelf, ReceiverKey], return_type: OptionalOf { inner: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_insert\", param_types: [ReceiverSelf, ReceiverKey, ReceiverValue], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_merge\", param_types: [ReceiverSelf, ReceiverSelf], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_has\", param_types: [ReceiverSelf, ReceiverKey], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_contains_key\", param_types: [ReceiverSelf, ReceiverKey], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_keys\", param_types: [ReceiverSelf], return_type: ContainerOf { source: Named { name: \"List\" }, element: ReceiverKey }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"map_values\", param_types: [ReceiverSelf], return_type: ContainerOf { source: Named { name: \"List\" }, element: ReceiverValue }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"with\", param_types: [ReceiverSelf, ReceiverKey, ReceiverValue], return_type: ReceiverSelf, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"contains\", param_types: [ReceiverSelf, ReceiverKey], return_type: NamedTemplate { name: \"Bool\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"length\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: none, callback_element_position: none },\n { name: \"count\", param_types: [ReceiverSelf], return_type: NamedTemplate { name: \"std.nat.Nat\" }, size_effect: none, cost_shape: none, callback_element_position: none }\n ]\n}\n\nfn algebra_method_template_name(name: String) -> Bool {\n all_algebra_profiles()\n |> any(profile => algebra_templates_for_profile(profile: profile) |> any(t => t.name == name))\n}\n\nfn algebra_templates_for_profile(profile: AlgebraProfile) -> List {\n match profile {\n OrderedRingProfile => ordered_ring_templates()\n ApproximateFieldProfile => approximate_field_templates()\n BooleanAlgebraProfile => boolean_algebra_templates()\n FinitePowerSetProfile => finite_power_set_templates()\n PointwisePowerCollectionProfile => pointwise_power_collection_templates()\n FreeMonoidScalarProfile => free_monoid_scalar_templates()\n FreeMonoidCollectionProfile => free_monoid_collection_templates()\n PartialFunctionProfile => partial_function_templates()\n FinitelySupportedFunctionProfile => finitely_supported_function_templates()\n }\n}\n\nfn algebra_type_param_names(profile: AlgebraProfile) -> List {\n match profile {\n OrderedRingProfile => []\n ApproximateFieldProfile => []\n BooleanAlgebraProfile => []\n FinitePowerSetProfile => [\"T\"]\n PointwisePowerCollectionProfile => [\"T\"]\n FreeMonoidScalarProfile => []\n FreeMonoidCollectionProfile => [\"T\"]\n PartialFunctionProfile => [\"K\", \"V\"]\n FinitelySupportedFunctionProfile => [\"K\", \"V\"]\n }\n}\n\nfn all_algebra_profiles() -> List {\n [\n OrderedRingProfile,\n ApproximateFieldProfile,\n BooleanAlgebraProfile,\n FinitePowerSetProfile,\n PointwisePowerCollectionProfile,\n FreeMonoidScalarProfile,\n FreeMonoidCollectionProfile,\n PartialFunctionProfile,\n FinitelySupportedFunctionProfile,\n ]\n}\n\nfn all_algebra_field_templates() -> List {\n all_algebra_profiles()\n |> flat_map(profile => algebra_templates_for_profile(profile: profile))\n}\n\nfn all_algebra_template_names() -> List {\n all_algebra_field_templates() |> map(template => template.name)\n}\n\n// trim is the importable free-function authority for whitespace trim on String. The declared\n// substrate body is a pure-dag seam (1 / 0); semantic execution routes through free_call.trim\n// (v1_rt::trim). Host realization authority: std.primitives trim_contract. Emitted host-native\n// std.algebra::trim delegates to v1_rt::trim via rust_host_string_op_fn_emit. The scalar template\n// row trim on FreeMonoid is the method spelling on the same carrier. Free-call trim(s)\n// requires explicit import std.algebra { trim } -- bare trim refuses via\n// closure_independent_bare_free_call registry in v1.compiler.infer_env even when std.algebra is\n// already in the compilation pool (#6985 Class B pool coincidence closed on trim by #8062).\n\n// The seam diverges before producing anything; `s` is read in the condition so the declared\n// input reaches the result (wiring-liveness), mirroring the bytes.dag float-seam pattern --\n// both arms are the same unreached literal, there to give the expression its type.\nfn trim(s: String) -> String {\n if s == s { from_code_point(1 / 0) } else { from_code_point(1 / 0) }\n}\n" -fn probe_0() -> ProcessExit { - let s = conservation_subject(id_tail: "probe_0", source: probe_src_0) - let spans = match s.parsed { - Accepted { value: a, diagnostics: _ } => a.span_index - Rejected { diagnostics: _ } => span_index_empty() - } - match s.normalized { - Accepted { value: _, diagnostics: _ } => ExitSuccess - Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) - } -} - -data probe_src_1: String = "module std.primitives\n\ntype Certainty = Proven | Amortized | Expected | Conservative\n\ntype CarrierCostSensitivity\n = CarrierInsensitive\n | CarrierSensitive { ephemeral_work: String, persistent_work: String }\n\ntype PrimitiveContract {\n name: String\n work: String\n output_size: String\n certainty: Certainty\n carrier_cost: CarrierCostSensitivity\n}\n\ndata char_at_contract: PrimitiveContract = {\n name: \"char_at\",\n work: \"pos\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata string_length_contract: PrimitiveContract = {\n name: \"string_length\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\n// Work is linear in the message, as any MAC over it must be. The output is one bit -- deliberately,\n// since extdeps.crypto.mac exposes no computed tag: the comparison happens inside the bound\n// implementation, in constant time, and a caller that could see the tag could compare it in\n// variable time instead.\ndata hmac_sha256_verify_hex_contract: PrimitiveContract = {\n name: \"hmac_sha256_verify_hex\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\n// Issuance: work linear in the message, output the 64 lowercase hex digits of a 32-octet tag. A\n// SECOND contract rather than a widened verify, because only the key holder mints; a verifier keeps\n// its one-bit answer (hmac_sha256_verify_hex_contract above) and never sees a tag to compare.\ndata hmac_sha256_hex_contract: PrimitiveContract = {\n name: \"hmac_sha256_hex\",\n work: \"n\",\n output_size: \"64\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata substring_contract: PrimitiveContract = {\n name: \"substring\",\n work: \"end\",\n output_size: \"end - start\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata string_contains_contract: PrimitiveContract = {\n name: \"string_contains\",\n work: \"n * m\",\n output_size: \"1\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata starts_with_contract: PrimitiveContract = {\n name: \"starts_with\",\n work: \"m\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata ends_with_contract: PrimitiveContract = {\n name: \"ends_with\",\n work: \"m\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata trim_contract: PrimitiveContract = {\n name: \"trim\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata concat_contract: PrimitiveContract = {\n name: \"concat\",\n work: \"n + m\",\n output_size: \"n + m\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n + m\", persistent_work: \"log(n + m)\" }\n}\n\ndata split_contract: PrimitiveContract = {\n name: \"split\",\n work: \"n\",\n output_size: \"k\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata repeat_contract: PrimitiveContract = {\n name: \"repeat\",\n work: \"m * n\",\n output_size: \"m * n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata chars_contract: PrimitiveContract = {\n name: \"chars\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata to_string_contract: PrimitiveContract = {\n name: \"to_string\",\n work: \"n\",\n output_size: \"n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata parse_int_contract: PrimitiveContract = {\n name: \"parse_int\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata str_eq_contract: PrimitiveContract = {\n name: \"str_eq\",\n work: \"min(n, m)\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata replace_section_contract: PrimitiveContract = {\n name: \"replace_section\",\n work: \"n\",\n output_size: \"n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata code_point_contract: PrimitiveContract = {\n name: \"code_point\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata from_code_point_contract: PrimitiveContract = {\n name: \"from_code_point\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata scan_while_contract: PrimitiveContract = {\n name: \"scan_while\",\n work: \"k\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata skip_horizontal_ws_contract: PrimitiveContract = {\n name: \"skip_horizontal_ws\",\n work: \"k\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata scan_to_eol_contract: PrimitiveContract = {\n name: \"scan_to_eol\",\n work: \"k\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata scan_string_end_contract: PrimitiveContract = {\n name: \"scan_string_end\",\n work: \"k\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata map_contract: PrimitiveContract = {\n name: \"map\",\n work: \"n * f_work\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata filter_contract: PrimitiveContract = {\n name: \"filter\",\n work: \"n * pred_work\",\n output_size: \"k <= n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata filter_map_contract: PrimitiveContract = {\n name: \"filter_map\",\n work: \"n * f_work\",\n output_size: \"k <= n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata flat_map_contract: PrimitiveContract = {\n name: \"flat_map\",\n work: \"n * f_work\",\n output_size: \"sum(f_output_sizes)\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata fold_contract: PrimitiveContract = {\n name: \"fold\",\n work: \"n * f_work\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata any_contract: PrimitiveContract = {\n name: \"any\",\n work: \"k * pred_work\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata all_contract: PrimitiveContract = {\n name: \"all\",\n work: \"k * pred_work\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata contains_contract: PrimitiveContract = {\n name: \"contains\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata join_contract: PrimitiveContract = {\n name: \"join\",\n work: \"n + total_length\",\n output_size: \"total_length + (n - 1) * sep_length\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata sort_contract: PrimitiveContract = {\n name: \"sort\",\n work: \"n * log(n)\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata sort_by_contract: PrimitiveContract = {\n name: \"sort_by\",\n work: \"n * (key_work + log(n))\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata dedup_contract: PrimitiveContract = {\n name: \"dedup\",\n work: \"n * n\",\n output_size: \"k <= n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata len_contract: PrimitiveContract = {\n name: \"len\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata count_contract: PrimitiveContract = {\n name: \"count\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata sum_contract: PrimitiveContract = {\n name: \"sum\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata first_contract: PrimitiveContract = {\n name: \"first\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log n\" }\n}\n\ndata last_contract: PrimitiveContract = {\n name: \"last\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log n\" }\n}\n\ndata get_contract: PrimitiveContract = {\n name: \"get\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log n\" }\n}\n\ndata max_by_contract: PrimitiveContract = {\n name: \"max_by\",\n work: \"n * f_work\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata reverse_contract: PrimitiveContract = {\n name: \"reverse\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata list_push_contract: PrimitiveContract = {\n name: \"list_push\",\n work: \"1\",\n output_size: \"n + 1\",\n certainty: Amortized,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n\", persistent_work: \"log n\" }\n}\n\ndata append_contract: PrimitiveContract = {\n name: \"append\",\n work: \"m\",\n output_size: \"n + m\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n + m\", persistent_work: \"log(n + m)\" }\n}\n\ndata skip_contract: PrimitiveContract = {\n name: \"skip\",\n work: \"n\",\n output_size: \"max(0, len - n)\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata zip_contract: PrimitiveContract = {\n name: \"zip\",\n work: \"min(n, m)\",\n output_size: \"min(n, m)\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata enumerate_contract: PrimitiveContract = {\n name: \"enumerate\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata empty_map_contract: PrimitiveContract = {\n name: \"empty_map\",\n work: \"1\",\n output_size: \"0\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata map_insert_contract: PrimitiveContract = {\n name: \"map_insert\",\n work: \"1\",\n output_size: \"n + 1\",\n certainty: Amortized,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n\", persistent_work: \"log32 n\" }\n}\n\ndata map_get_contract: PrimitiveContract = {\n name: \"map_get\",\n work: \"1\",\n output_size: \"1\",\n certainty: Expected,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log32 n\" }\n}\n\ndata map_contains_key_contract: PrimitiveContract = {\n name: \"map_contains_key\",\n work: \"1\",\n output_size: \"1\",\n certainty: Expected,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log32 n\" }\n}\n\ndata lookup_contract: PrimitiveContract = {\n name: \"lookup\",\n work: \"1\",\n output_size: \"1\",\n certainty: Expected,\n carrier_cost: CarrierSensitive { ephemeral_work: \"1\", persistent_work: \"log32 n\" }\n}\n\ndata map_merge_contract: PrimitiveContract = {\n name: \"map_merge\",\n work: \"m\",\n output_size: \"n + m\",\n certainty: Amortized,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n + m\", persistent_work: \"m * log32(n + m)\" }\n}\n\ndata map_keys_contract: PrimitiveContract = {\n name: \"map_keys\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata map_values_contract: PrimitiveContract = {\n name: \"map_values\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata index_by_contract: PrimitiveContract = {\n name: \"index_by\",\n work: \"n * key_fn_work\",\n output_size: \"n\",\n certainty: Amortized,\n carrier_cost: CarrierInsensitive\n}\n\ndata with_contract: PrimitiveContract = {\n name: \"with\",\n work: \"n + m\",\n output_size: \"n + m\",\n certainty: Proven,\n carrier_cost: CarrierSensitive { ephemeral_work: \"n + m\", persistent_work: \"m * log32 n\" }\n}\n\ndata filesystem_read_contract: PrimitiveContract = {\n name: \"filesystem_read\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata to_bytes_contract: PrimitiveContract = {\n name: \"to_bytes\",\n work: \"n\",\n output_size: \"n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata to_json_contract: PrimitiveContract = {\n name: \"to_json\",\n work: \"n\",\n output_size: \"n\",\n certainty: Conservative,\n carrier_cost: CarrierInsensitive\n}\n\ndata hash_contract: PrimitiveContract = {\n name: \"hash\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata hash_combine_contract: PrimitiveContract = {\n name: \"hash_combine\",\n work: \"1\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata atom_identity_hash_contract: PrimitiveContract = {\n name: \"atom_identity_hash\",\n work: \"n\",\n output_size: \"1\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata contiguous_loop_elementwise_kernel_contract: PrimitiveContract = {\n name: \"contiguous_loop_elementwise_kernel\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\ndata contiguous_loop_elementwise_float_kernel_contract: PrimitiveContract = {\n name: \"contiguous_loop_elementwise_float_kernel\",\n work: \"n\",\n output_size: \"n\",\n certainty: Proven,\n carrier_cost: CarrierInsensitive\n}\n\n// Enumerable registry surface names parallel to builtin_function_registry keys. Witness-time census\n// cannot call map_keys on Map because Node is not a runtime Value. Relocated from\n// v1.compiler.infer_method for dag/ compile scope (review 46064) — same parallel-enumeration debt,\n// not a new fork. Dissolve-on: fold builtin_function_registry construction from this list as the\n// single authority.\n// DRIFT IS NOW MEASURED, NOT PREDICTED: gunbc.primitive_egress.census_live reads the authority's\n// key set through the builtin_function_registry_keys compiler query and prints both set\n// differences (hand_roster_missing_registry_rows / hand_roster_rows_absent_from_registry) in every\n// census receipt (`gunbc test //gunbc/instruments:primitive-egress-census-v2`); the receipt, not\n// this note, says whether the list matches the key set.\n\nfn builtin_registry_surface_names() -> List {\n [\n \"hmac_sha256_verify_hex\",\n \"hmac_sha256_hex\",\n \"count\",\n \"string_length\",\n \"code_point\",\n \"to_int\",\n \"scan_while\",\n \"scan_string_end\",\n \"scan_to_eol\",\n \"skip_horizontal_ws\",\n \"parse_int\",\n \"char_at\",\n \"substring\",\n \"from_code_point\",\n \"chars_to_string\",\n \"record_source_chars_index_lookup\",\n \"resolution_silent_pick_is_enabled\",\n \"name_resolution_policy_is_namespace_only\",\n \"resolution_silent_pick_record_global_bare_lcp_pick\",\n \"resolution_silent_pick_record_global_bare_lcp_tie\",\n \"resolution_silent_pick_record_fn_parent_first_hit\",\n \"to_string\",\n \"discriminant\",\n \"concat\",\n \"map_insert\",\n \"map_merge\",\n \"with\",\n \"empty_set\",\n \"set_insert\",\n \"set_union\",\n \"map_contains_key\",\n \"map_has\",\n \"map_is_empty\",\n \"rc_ptr_eq\",\n \"rc_vec_ptr_eq\",\n \"set_contains\",\n \"lookup\",\n \"map_get\",\n \"Some\",\n \"map_keys\",\n \"sorted_map_keys\",\n \"map_values\",\n \"get\",\n \"reverse\",\n \"list_push\",\n \"hash_combine\",\n \"atom_identity_hash\",\n \"trace_mark\",\n \"observed_peak_resident_bytes\",\n \"observed_monotonic_nanos\",\n \"string_contains\",\n \"length\",\n \"starts_with\",\n \"replace\",\n \"filesystem_read\",\n \"emit_host_run_transport\",\n \"emit_host_run_transport_cached\",\n \"emit_host_native_cache_evict\",\n \"contiguous_loop_elementwise_kernel\",\n \"contiguous_loop_elementwise_float_kernel\",\n \"shell_materialize_operation_argv\",\n \"shell_transport_operation_rows\",\n \"extdeps_shape_transport_policy_facts_for_qualified_name\",\n \"extdeps_qualified_name_resolves_in_derived_module_set\",\n \"extdeps_external_authority_facts_for_qualified_name\",\n \"extdeps_external_authority_live_clean_tree_holds\",\n \"extdeps_external_authority_live_roster_module_count\",\n \"seed_runner_bool_false_failure_detail\",\n \"doc_graph_orphan_count\",\n \"doc_graph_admitted_root_count\",\n \"doc_graph_dangling_link_count\",\n \"doc_graph_doc_count\",\n \"compile_dag_rust_emit_check\",\n \"parsed_import_statements\",\n \"floor_discovery_source_inventory\",\n \"namespace_structural_observation_admissions\",\n \"emit_rust_reference_derived_rows_bridge\",\n \"witness_layer_roots_compile_clean_check\",\n \"witness_layer_roots_compile_clean_emit_check\",\n \"install_or_consume_floor_compile_clean_gate_receipt\",\n \"record_generated_artifact_drift_gate_failure_detail\",\n \"record_generated_artifact_drift_gate_clean\",\n \"consume_generated_artifact_drift_gate_receipt\",\n \"witness_compile_clean_cli_floor_verdicts_agree\",\n \"test_migration_debt_module_names\",\n \"test_migration_legacy_behavior_ids\",\n \"test_migration_witness_behavior_ids\",\n \"test_migration_behavior_discovery_holds\",\n \"inert_carrier_names_live\",\n \"inert_carrier_declared_count\",\n \"non_fold_residue_count\",\n \"non_fold_residue_unrostered_count\",\n \"non_fold_residue_stale_roster_count\",\n \"non_fold_residue_coproduct_universe_count\",\n \"commit_witness_claim_roster_unresolvable_count\",\n \"commit_witness_claim_pair_resolvable\",\n \"non_fold_residue_wildcard_red_fixture_holds\",\n \"non_fold_residue_total_fold_green_fixture_holds\",\n \"non_fold_residue_roster_red_fixture_holds\",\n \"non_fold_residue_synthetic_unrostered_red_holds\",\n \"complexity_linearity_syntactic_finding_count\",\n \"complexity_linearity_wildcard_facts\",\n \"complexity_linearity_syntactic_site_fired\",\n \"fallback_arm_census_facts\",\n \"fallback_arm_census_class_count\",\n \"fallback_arm_census_total\",\n \"fallback_arm_census_reconciliation_holds\",\n \"census_corpus_roots_follow_layer_authority\",\n \"transport_script_position_facts_for_path\",\n \"layer_import_facts\",\n \"import_resolution_facts\",\n \"reference_resolution_facts\",\n \"module_declaration_facts\",\n \"module_declaration_facts_at\",\n \"concept_decl_facts\",\n \"decl_facts\",\n \"decl_facts_at\",\n \"export_signature_facts\",\n \"data_decl_type_facts\",\n \"fact_cardinality_decl_facts\",\n \"languages_consumer_census_data_decl_count\",\n \"languages_consumer_census_per_language_row_count\",\n \"languages_consumer_census_format_row_count\",\n \"languages_consumer_census_external_consumer_count\",\n \"languages_consumer_census_is_composition_only\",\n \"languages_consumer_census_has_external_consumer\",\n \"is_xid_start\",\n \"is_xid_continue\",\n \"is_emoji_ident\",\n \"claim_scope_dag_multi_module_fixture\",\n \"class_b_import_closure_gate_not_affected_skip\",\n \"compile_dag_call_form_leaf_guard\",\n \"compile_dag_callsite_resolved_call_edges\",\n \"compile_dag_diagnostic_census\",\n \"compile_dag_importer_resolved_call_edges\",\n \"compile_dag_multi_module_fixture\",\n \"compile_dag_reference_occurrence_binding_census\",\n \"dependency_resolution_facts\",\n \"dependency_resolution_facts_at\",\n \"observe_declared_import_closure_symbol_binding\",\n \"parse_roadmap_acceptance_event_history_jsonl\",\n \"parse_stage0_cargo_manifest_bins\",\n \"project_roadmap_acceptance_event_history_from_authority_text_host\",\n \"toolchain_home_interference_probe\",\n \"type_ref_hit_ne_bind_measure_active\",\n \"builtin_function_registry_keys\",\n \"compile_dag_primitive_call_edges\",\n \"source_root_ingest_module_identities\",\n \"required_floor_nominal_subject_module_identities\",\n \"entry_closure_module_identities\",\n ]\n}\n\ndata primitive_contract_roster_note: String = \"Hand-maintained parallel enumeration of every *_contract data declaration in this module. A new foo_contract row silently under-reports on primitive_surface_census_derived until this roster is updated — the open-denominator class D0 is designed to expose. Dissolve-on: derive the roster by enumerating *_contract declarations once declaration introspection lands, so contract surfaces read the module rather than a second list.\"\n\nfn primitive_contract_roster() -> List {\n [\n char_at_contract,\n string_length_contract,\n hmac_sha256_verify_hex_contract,\n hmac_sha256_hex_contract,\n substring_contract,\n string_contains_contract,\n starts_with_contract,\n ends_with_contract,\n trim_contract,\n concat_contract,\n split_contract,\n repeat_contract,\n chars_contract,\n to_string_contract,\n parse_int_contract,\n str_eq_contract,\n replace_section_contract,\n code_point_contract,\n from_code_point_contract,\n scan_while_contract,\n skip_horizontal_ws_contract,\n scan_to_eol_contract,\n scan_string_end_contract,\n map_contract,\n filter_contract,\n filter_map_contract,\n flat_map_contract,\n fold_contract,\n any_contract,\n all_contract,\n contains_contract,\n join_contract,\n sort_contract,\n sort_by_contract,\n dedup_contract,\n len_contract,\n count_contract,\n sum_contract,\n first_contract,\n last_contract,\n get_contract,\n max_by_contract,\n reverse_contract,\n list_push_contract,\n append_contract,\n skip_contract,\n zip_contract,\n enumerate_contract,\n empty_map_contract,\n map_insert_contract,\n map_get_contract,\n map_contains_key_contract,\n lookup_contract,\n map_merge_contract,\n map_keys_contract,\n map_values_contract,\n index_by_contract,\n with_contract,\n filesystem_read_contract,\n to_bytes_contract,\n to_json_contract,\n hash_contract,\n hash_combine_contract,\n atom_identity_hash_contract,\n contiguous_loop_elementwise_kernel_contract,\n contiguous_loop_elementwise_float_kernel_contract,\n ]\n}\n" -fn probe_1() -> ProcessExit { - let s = conservation_subject(id_tail: "probe_1", source: probe_src_1) - let spans = match s.parsed { - Accepted { value: a, diagnostics: _ } => a.span_index - Rejected { diagnostics: _ } => span_index_empty() - } - match s.normalized { - Accepted { value: _, diagnostics: _ } => ExitSuccess - Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) - } -} - -data probe_src_2: String = "module std.termination\n\nimport std.algebra { BoundedLattice, Ordering }\n\ntype DescentEvidence\n = Strict\n | NonIncreasing\n | DescentUnknown\n\nfn evidence_rank(e: DescentEvidence) -> Int {\n match e {\n Strict => 2\n NonIncreasing => 1\n DescentUnknown => 0\n }\n}\n\nfn descent_evidence_lattice_meet(a: DescentEvidence, b: DescentEvidence) -> DescentEvidence {\n match a {\n Strict => match b {\n Strict => Strict\n NonIncreasing => NonIncreasing\n _ => DescentUnknown\n }\n NonIncreasing => match b {\n Strict => NonIncreasing\n NonIncreasing => NonIncreasing\n _ => DescentUnknown\n }\n _ => DescentUnknown\n }\n}\n\nfn descent_evidence_lattice_join(a: DescentEvidence, b: DescentEvidence) -> DescentEvidence {\n match a {\n DescentUnknown => b\n NonIncreasing => match b {\n Strict => Strict\n _ => NonIncreasing\n }\n Strict => Strict\n }\n}\n\ndata descent_evidence_bounded_lattice: BoundedLattice = {\n meet: descent_evidence_lattice_meet\n join: descent_evidence_lattice_join\n top: Strict\n bottom: DescentUnknown\n}\n\nfn promote_to_strict(evidence: DescentEvidence) -> DescentEvidence {\n match evidence {\n NonIncreasing => NonIncreasing\n Strict => Strict\n _ => DescentUnknown\n }\n}\n\nfn optional_evidence_meet(a: DescentEvidence?, b: DescentEvidence?) -> DescentEvidence? {\n match a {\n Absent => b\n Present { value: va } =>\n match b {\n Absent => a\n Present { value: vb } => Present { value: descent_evidence_lattice_meet(a: va, b: vb) }\n }\n }\n}\n\nfn map_evidence_merge_at(base: Map, key: String, new_val: DescentEvidence) -> Map {\n match map_get(base, key) {\n Present { value: existing } => map_insert(base, key, descent_evidence_lattice_meet(a: existing, b: new_val))\n Absent => map_insert(base, key, new_val)\n }\n}\n\ntype RankingDimension\n = TreeSize { param: String }\n | ListLength { param: String }\n | ArithmeticValue { param: String }\n | TokenPosition { param: String }\n | SetCardinality { param: String }\n\ntype PositiveDescentAmount\n = OneStep\n | AdditionalStep { previous: PositiveDescentAmount }\n\nfn positive_descent_count(steps: PositiveDescentAmount) -> Int {\n match steps {\n OneStep => 1\n AdditionalStep { previous: p } => 1 + positive_descent_count(p)\n }\n}\n\ntype ProportionalDivisor\n = DivideByTwo\n | StrictlyLarger { inner: ProportionalDivisor }\n\nfn proportional_divisor_to_int(d: ProportionalDivisor) -> Int {\n match d {\n DivideByTwo => 2\n StrictlyLarger { inner: p } => 1 + proportional_divisor_to_int(d: p)\n }\n}\n\nfn peano_literal_materialization_cap() -> Int { 256 }\n\nfn positive_descent_amount_from_positive_int(k: Int) -> PositiveDescentAmount? {\n if k <= 0 { none }\n else {\n if k > peano_literal_materialization_cap() { none }\n else {\n if k == 1 { Present { value: OneStep } }\n else {\n match positive_descent_amount_from_positive_int(k: k - 1) {\n Present { value: prev } => Present { value: AdditionalStep { previous: prev } }\n Absent => none\n }\n }\n }\n }\n}\n\nfn proportional_divisor_from_int_at_least_two(k: Int) -> ProportionalDivisor? {\n if k < 2 { none }\n else {\n if k > peano_literal_materialization_cap() { none }\n else {\n if k == 2 { Present { value: DivideByTwo } }\n else {\n match proportional_divisor_from_int_at_least_two(k: k - 1) {\n Present { value: prev } => Present { value: StrictlyLarger { inner: prev } }\n Absent => none\n }\n }\n }\n }\n}\n\ntype DescentSource\n = ChildAccessor { accessor: String }\n | ListShrink { amount: PositiveDescentAmount }\n | ArithmeticSubtractDescent { steps: PositiveDescentAmount }\n | ArithmeticDivideDescent { divisor: ProportionalDivisor }\n | ParserAdvance { witness: String }\n | SetRemoval { element: String }\n | FoldIteration\n\ntype TerminationProof {\n dimensions: List\n}\n\ntype ProofEdge {\n caller: String\n callee: String\n evidence: List\n}\n" -fn probe_2() -> ProcessExit { - let s = conservation_subject(id_tail: "probe_2", source: probe_src_2) - let spans = match s.parsed { - Accepted { value: a, diagnostics: _ } => a.span_index - Rejected { diagnostics: _ } => span_index_empty() - } - match s.normalized { - Accepted { value: _, diagnostics: _ } => ExitSuccess - Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) - } -} - -data probe_src_3: String = "module std.types\n\nimport std.algebra { FinitePowerSet, FinitelySupportedFunction, FreeMonoid, kernel_algebra_profile, algebra_type_param_names, carrier_container_algebra_rows, carrier_container_alias_rows, carrier_container_arity_rows, carrier_container_roster_map }\n\ndata kernel_type_set: Map = {\n \"String\": true, \"Int\": true, \"Bool\": true, \"Float\": true,\n \"Secret\": true, \"Json\": true, \"Unit\": true, \"Bytes\": true\n}\n\nfn is_kernel_type(name: String) -> Bool {\n match map_get(kernel_type_set, name) {\n Present { value: _ } => true\n Absent => false\n }\n}\n\n// DERIVED FROM THE CARRIER ROSTER, not restated. Every row but one is a spelling that declared\n// `container_arity_row` on its carrier in `std.algebra`, and its arity comes from that carrier's\n// own `algebra_type_param_names` -- so a carrier cannot be listed here with an arity its algebra\n// contradicts, and an eleventh carrier reaches this map by declaring the row rather than by\n// someone remembering to edit it.\n//\n// `Witness` is appended because it is NOT an algebra carrier: it has no profile and no template\n// set, and its parameter name is special-cased in `container_param_names_for` directly below.\n// It is the one hand-authored row left in this map, and it is the reason the memberships in\n// `std.algebra` are declared rather than derived from the profile -- grounding `Witness` as a\n// carrier is that class's next-rung trigger.\ndata container_type_arity: Map = map_insert(carrier_container_arity_rows(), \"Witness\", 1)\n\nfn is_container_type(name: String) -> Bool {\n match container_expected_arity(name: name) {\n Present { value: _ } => true\n Absent => false\n }\n}\n\nfn container_expected_arity(name: String) -> Int? {\n map_get(container_type_arity, name)\n}\n\nfn container_param_names_for(kind_name: String) -> List {\n if kind_name == \"Witness\" {\n [\"T\"]\n } else {\n match map_get(kernel_algebra_profile, kind_name) {\n Present { value: p } => algebra_type_param_names(profile: p)\n Absent => []\n }\n }\n}\n\nfn container_param_name(kind_name: String, index: Int) -> String? {\n let names = container_param_names_for(kind_name: kind_name)\n match names |> enumerate |> filter(pair => pair.first == index) |> map(pair => pair.second) |> first {\n Present { value: name } => Present { value: name }\n Absent => none\n }\n}\n\ndata ordered_element_collections: Map = {\n \"List\": true\n}\n\nfn is_ordered_element_collection(name: String) -> Bool {\n map_contains_key(ordered_element_collections, name)\n}\n\n// BOTH MAPS ARE DERIVED FROM THE CARRIER ROSTER in `std.algebra`. What stood here was two\n// spelling-keyed literals whose note explained -- correctly -- why each row was present or\n// absent: `PartialFunction` had to be in the alias map because that map decides what resolves as\n// a container at all, and `FinitelySupportedFunction` had to stay out because\n// `container_alias_canonical_spelling` returns the first sorted key for an algebra and that key\n// would sort ahead of `\"Map\"`. Those reasons are now FIELDS on the spelling rows they govern\n// (`container_alias_row`, with its `RowAbsent` reason), so they travel with the carrier instead\n// of living in prose beside a literal a later author edits without reading it.\n//\n// A self-mapping row is not a nickname: the value is the ALGEBRA and the key is a SPELLING that\n// resolves to it, which is why `List` and `Set` are keys at all.\ndata container_template_algebra_rows: Map = carrier_container_algebra_rows()\n\ndata container_template_alias_rows: Map = carrier_container_alias_rows()\n\nfn container_template_algebra(name: String) -> String? {\n map_get(container_template_algebra_rows, name)\n}\n\nfn container_template_alias_algebra(name: String) -> String? {\n map_get(container_template_alias_rows, name)\n}\n\n// THE COMPLETE ROSTER, DERIVED. This list feeds `v1.compiler.coercion` `inhabitant_test_names`,\n// which used to read `concat(canonical_container_names(), [\"PointwisePower\"])` -- a hand-appended\n// name compensating for one this list omitted. A caller patching its authority's answer is the\n// authority being wrong, not the caller being thorough; the append is gone and the name now\n// arrives from the carrier that declares it.\n//\n// `sorted_map_keys` gives the same ASCII order the hand-written literal held, and `Witness` is\n// appended for the reason `container_type_arity` above states: it is not an algebra carrier.\nfn canonical_container_names() -> List {\n sorted_map_keys(map_insert(carrier_container_roster_map(), \"Witness\", true))\n}\n\ntype Bool = True | False\n\ntype Unit\n\ntype Json\ntype Bytes\n\ntype Char = Int where unicode_scalar, brand(\"Char\")\n\ntype List = FreeMonoid\n// BOTH ALIASES NAME THE FINITE CARRIER, the motion the predicate-half change (`PointwisePower`,\n// landed separately) was waiting for. A `Set` or a `Map` in a declaration position has always\n// LOWERED to a finite container -- `im::OrdSet` and `HashMap` in Rust, likewise in Go, Python and\n// TypeScript -- so pointing the aliases at the finite carriers makes the declaration say what the\n// realization already did. The open carriers keep their own names: `std.algebra` `PointwisePower`\n// for a characteristic function, `PartialFunction` for a partial one.\n//\n// The two rows move together because they are one row set. Retargeting `Map` while `Set` still\n// resolved to the predicate would leave the fork intact on the Set side and make the pair\n// inconsistent for the interval, the dual-authority state the replacement-migration doctrine\n// avoids. The transition is atomic in the AUTHORITY sense -- one motion, no interval in which both\n// answers are live -- whatever the diff size.\n//\n// RUNG: STILL MITIGATABLE. This change does NOT make the bad literal unwritable, and saying so\n// would be the inflation DESIGN 4b calls worse than sitting low. `FinitePowerSet` carries `member`\n// and `FinitelySupportedFunction` carries `lookup`, because a finite set answers membership and a\n// finite map answers lookup -- so `Set { member: .. }` and `Map { lookup: .. }` still typecheck.\n// They must: 145 `Map { lookup: .. }` literals are live in the corpus and are re-authored by the\n// following change, not this one.\n//\n// The correction is that the invalid state was never \"a literal supplying `member` or `lookup`\".\n// It is \"a literal supplying ONLY those, on a carrier whose remaining fields promise an\n// enumeration it cannot perform\" -- a construction-COMPLETENESS question, and record literals in\n// this substrate do not require every field. So the real NEXT-RUNG TRIGGER is field-completeness\n// enforcement on record construction, which would make the deficient literal unwritable at every\n// carrier at once rather than at these two. Until then the class stays mitigatable and only review\n// sees a new one; what this change buys is that each alias now names a carrier that can answer\n// for it -- the single-authority half, not the wall.\n//\n// CARRIED FORWARD FROM THE PREDICATE-HALF CHANGE (#9055), whose note this row replaces: the\n// fusion was measured, not argued -- `extdeps.languages.rust.types` gave `PointwisePower` the\n// inhabitant `BTreeSet<{0}>`, so a declaration position lowered to `im::OrdSet` while a record\n// literal lowered to the algebra struct, and the two met as a rustc refusal wherever one compiled\n// closure held both (`docs/probes/t3_collection_realization_2026-08-22.md`). That change also\n// closed the predicate half: no `Set { member: .. }` literal survives in `src/v2`, so the Set side\n// of the fork is a spelling question only. Its stated NEXT-RUNG TRIGGER was the Map-side\n// decomposition landing, after which both rows retarget in one motion. That is this row.\ntype Set = FinitePowerSet\ntype Map = FinitelySupportedFunction\n\nfn list_length(items: List) -> Int {\n fold(items, init: 0, f: (acc, _) => acc + 1)\n}\n\ntype CommitSha = String\n\n// The located syntax wall for an EXTERNALLY SUPPLIED Git head: lowercase 40-hex. It lives here,\n// beside CommitSha, the one authority every Git-head consumer already names — it was previously\n// homed in std.realization_schedule as scoped_witness_exact_head_text_holds, where a since-deleted\n// receipt family happened to be its first caller, and a second consumer\n// (v2.compiler.self_host.frontier_probe_survey) was already importing it across that seam.\n// CommitSha is presently an unvalidated String alias, so this predicate is a validating CHECK a\n// caller must remember to run, not a construction wall. Dissolve-on: CommitSha gains one validating\n// constructor; callers reach the constructor, the invalid state stops being writable, and this\n// predicate deletes.\n\n// WHAT THIS COMPUTES: forty characters, each a lowercase hex digit. It is spelled as \"remove every\n// hex digit and ask whether nothing is left\" rather than as a test of each character. BOTH SPELLINGS\n// ARE LINEAR in the input, so the per-character one was never a cost-shape defect in the sense\n// DESIGN section 6 makes standing: what this spelling buys is a CONSTANT FACTOR, because the\n// interpreter charges a per-character lambda far more than one whole-string builtin. That is a\n// modeling loss (the alphabet is hard-coded and the word `hex` is gone) taken against an evaluator\n// cost, i.e. a workaround, and it is named as one here.\n//\n// DISSOLVE-ON: the per-character evaluation cost is fixed at its source, in the interpreter, so the\n// per-character spelling costs about what the whole-string one does; this predicate then returns to\n// testing each character against the hex alphabet, and the oracle below becomes its only spelling.\n//\n// THE RESPELLING IS ADMITTED ON ITS OWN COST AND ON NOTHING DOWNSTREAM OF IT (operator ruling,\n// stern-carp-604, 2026-09-20, on gunb-ai/gunbc#11730). A claim budget elsewhere in the tree fitting\n// afterwards is a consequence and was never the reason; anyone reading this as buying margin for a\n// census should read that ruling, which admits a fix to a shared predicate and refuses only an\n// author admitting their own.\n//\n// THE EQUALITY IS THE CONTRACT, not the shape: test.claim.std_commit_sha_text_witness keeps the\n// per-character spelling as an oracle and claims the two answer the same on every input either can\n// be given, with a red per refusal cause -- the lengths either side of forty, uppercase, the\n// characters adjacent to both ranges, and the empty string.\nfn commit_sha_text_holds(head: String) -> Bool {\n (head.length() == 40) && (replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(head, \"0\", \"\"), \"1\", \"\"), \"2\", \"\"), \"3\", \"\"), \"4\", \"\"), \"5\", \"\"), \"6\", \"\"), \"7\", \"\"), \"8\", \"\"), \"9\", \"\"), \"a\", \"\"), \"b\", \"\"), \"c\", \"\"), \"d\", \"\"), \"e\", \"\"), \"f\", \"\") == \"\")\n}\n\ntype Sha256 = String\ntype RetryCount = Int where range(min: 1, max: 5)\ntype HttpStatus = Int where range(min: 100, max: 599)\n\n// THE ONE ADMISSION FOLD FOR AN Int THAT CLAIMS TO BE AN HttpStatus. The refinement above is\n// reported by the compiler as unenforced (predicate deferred), so a value arriving from a parse --\n// curl's --write-out, a wire body, a log line -- has to be admitted somewhere, and that somewhere\n// must be HERE, beside the range, rather than a copied pair of literals at each parse site that\n// drifts silently if the range row ever moves. The bounds are read from the two rows the fold\n// consumes; the refinement keeps the same numbers as literals because a where-clause cannot name\n// a data row. That pairing is a language-layer gap, not a reader check: its next-rung trigger is\n// gunbc.where_refinement_predicate_vocabulary\n// http_status_where_clause_names_declaration_frontier_rows (capability: a where-clause names a\n// declaration). A parse site consumes http_status_of and matches its Absent arm; it does not\n// compare against 100 or 599 itself.\ndata http_status_min: Int = 100\n\ndata http_status_max: Int = 599\n\nfn http_status_of(n: Int) -> HttpStatus? {\n if n < http_status_min || n > http_status_max { none } else { Present { value: n } }\n}\n\ntype Email = String\ntype Port = Int where range(min: 1, max: 65535)\n\ntype GistId = String\ntype Secret nominal_opaque = String\ntype SecretValue = Secret where non_empty\n\ntype SemVer = String\ntype NonEmptyStr = String where non_empty\ntype LanguageId = String where non_empty\ntype SecretName = String where non_empty\n\ntype PathSegment = NonEmptyStr where brand(\"PathSegment\")\n\ndata path_segment_safety_note: String = \"WHAT MAKES A STRING SAFE TO USE AS ONE PATH SEGMENT — the law, held once, so every branded id that becomes a directory name tests the same thing. The brand alone never carried it: a branded NonEmptyStr accepts \\\"..\\\", \\\"a/b\\\", and an embedded newline, so a value that typechecked as a segment could still escape its parent directory, alias a sibling, or split a line-oriented file written under that name. The refused set is exactly the characters that change what a concatenated path MEANS — `/` and `\\\\` introduce a level, `.` and `..` navigate, CR and LF terminate a record in every line-oriented format this repo writes, NUL terminates the string at the syscall boundary. Callers REFUSE on false rather than sanitizing, because a sanitized segment silently denotes something other than what the caller named (§5: a failure arm must refuse, never widen). Percent- or hex-encoding is the reversible alternative and is deliberately not offered until a caller needs a segment it cannot rename.\n\nThis is a PREDICATE, not a constructor, and that is a modeling choice rather than a limitation worked around. A generic `path_segment(raw) -> PathSegment?` would put the branding cast in this module, and each caller would then re-cast the generic segment into its own brand anyway — two casts and two authorities for one law. Holding the law here and letting each branded id (gunbc.merge_admission.walk_attempt_id is the first) construct itself through it keeps one authority for what is hostile and one constructor per brand, which is what §3 asks for. A generic constructor earns its place when a second caller wants a bare PathSegment rather than a brand of its own; none does today.\"\n\nfn path_segment_is_safe(raw: String) -> Bool {\n !((raw == \"\")\n || (raw == \".\")\n || (raw == \"..\")\n || string_contains(s: raw, pattern: \"/\")\n || string_contains(s: raw, pattern: \"\\\\\")\n || string_contains(s: raw, pattern: \"\\n\")\n || string_contains(s: raw, pattern: \"\\x0d\")\n || string_contains(s: raw, pattern: \"\\x00\"))\n}\ntype GlobSegment = NonEmptyStr where brand(\"GlobSegment\")\n\n// TEXT THAT CARRIES NO TERMINAL CONTROL SEQUENCES — the vocabulary two unrelated dependencies meet\n// on so neither has to know the other exists. A terminal-facing tool emits SGR escapes around the\n// values it highlights; a parser reading those values needs them absent. Those are facts about\n// DIFFERENT upstreams, and the wrong fix is for either to mention the other: a note in a CLI's\n// module explaining what a terminal multiplexer strips is filed under the wrong authority, and\n// stops being true the moment a second capture transport appears (operator, 2026-07-30: 'tmux\n// stripping ansi should be a tmux fact, and codex prompts should be a codex fact, and the\n// interaction should be emergent').\n//\n// So the brand is the seam. A capture surface that renders a pane PRODUCES RenderedTerminalText;\n// a prompt parser CONSUMES it; the composition is admissible because the types line up, not\n// because one module was written knowing about the other. A raw stdout pipe cannot inhabit the\n// brand, so feeding one to a parser that requires it is a type error rather than a runtime paste\n// of escape bytes the operator discovers when the far end rejects them.\n//\n// It lives in std because it is a property of text, not of any one tool — the same reason\n// FilePath is not owned by whichever dependency happens to open files.\n\ntype RenderedTerminalText = String where brand(\"RenderedTerminalText\")\ntype FilePathParts {\n segments: List\n}\ntype GlobPattern {\n segments: List\n}\ntype FilePath = String where non_empty\n\ndata file_path_sentinel_scaffold_note: String = \"review 45141. FilePath where non_empty made empty-string absent-file sentinels unwritable; interim re-spellings encode absence in a nominally-non_empty carrier (state-space conflation — path promises a real path while sentinel means absent/synthetic). Sites: emit_rust/go/python emit_*_test_file returns TextFile { path: \\\"\\\", content: \\\"\\\" }, filtered by string_length(content) > 0 not path; 00_core no_span uses SourceSpan.file \\\"\\\" for the null span (make_span, which fabricated that file name for CALLER-SUPPLIED offsets, is deleted -- the fileless constructor now takes no offsets, so a located range inside a nonexistent file has no constructor). Preserves pre-wall behavior. dissolve-on: feature:optional-textfile-and-source-span (lift emit carriers to Option and SourceSpan.file to optional FilePath; delete sentinels and content-length filter).\"\n\ntype SourceSpan {\n file: FilePath\n start: Int\n end: Int\n}\n\ntype Timestamp = String\ntype EpochSecs = Int where range(min: 0)\ntype EpochMs = Int where range(min: 0)\ntype Duration = Int where range(min: 0)\ntype Milliseconds = Int where range(min: 0), brand(\"Milliseconds\")\ntype Seconds = Int where range(min: 0), brand(\"Seconds\")\n\ntype LogicalTime = NonEmptyStr where brand(\"LogicalTime\")\n\ntype IntentId = NonEmptyStr where brand(\"IntentId\")\ntype IssueId = NonEmptyStr where brand(\"IssueId\")\ntype RunKey = NonEmptyStr where brand(\"RunKey\")\ntype ArtifactId = NonEmptyStr where brand(\"ArtifactId\")\ntype LeaseToken = NonEmptyStr where brand(\"LeaseToken\")\ntype WorkerId = NonEmptyStr where brand(\"WorkerId\")\ntype CommentId = NonEmptyStr where brand(\"CommentId\")\ntype SignalKey = NonEmptyStr where brand(\"SignalKey\")\n\ntype WorkflowProducerId = NonEmptyStr where brand(\"WorkflowProducerId\")\ntype WorkflowObserverId = NonEmptyStr where brand(\"WorkflowObserverId\")\ntype WorkflowProverId = NonEmptyStr where brand(\"WorkflowProverId\")\ntype WorkflowRunId = NonEmptyStr where brand(\"WorkflowRunId\")\n\ntype GitRef = String where non_empty\n\ntype TopologyNodeKind = Pure | Transport | SubDag | Env\ntype DocSourceKind = Template | Generated | Static\n\ntype FermiDepth = Xs | S | M | L | Xl\n\ntype TextFilePath = FilePath\n\ntype BinaryFilePath = FilePath\n\ntype MimeType = String\n\ntype AuthScheme\n = Bearer\n | Header { name: String }\n | Basic { username: String }\n | ApiKey\n\ntype AccessToken {\n token: Secret\n scheme: AuthScheme\n expires_at: Timestamp?\n}\n\ntype Credential {\n token: Secret\n scheme: AuthScheme\n header_name: String?\n source_id: String\n required_scopes: List\n expires_in: Int?\n}\n\ntype FilesystemHandle = FilePath where brand(\"FilesystemHandle\")\ntype NetworkHandle = Unit where brand(\"NetworkHandle\")\ntype ToolHandle = String where brand(\"ToolHandle\")\n\ntype FileResponse {\n path: String\n success: Bool\n content: String\n}\n\ntype ShellResponse {\n exit_code: Int\n stdout: String\n stderr: String\n}\n\ntype RestResponse {\n status: Int\n headers: Json\n body: Json\n}\n\ntype TestResult {\n name: String\n ok: Bool\n stdout: String\n stderr: String\n duration_ms: Milliseconds\n}\n\ntype Summary {\n total: Int\n passed: Int\n failed: Int\n}\n\ntype StageResult {\n name: String\n success: Bool\n stdout: String\n stderr: String\n skipped: Bool\n}\n\ntype RenderedTextFile {\n path: String\n content: String\n}\n\ntype ToolEntry {\n name: String\n command: String\n description: String?\n}\n\ntype ToolRegistry {\n tools: List\n}\n\ntype DagTopology {\n nodes: List\n edges: List\n subdag_boundaries: List\n}\n\ntype TopologyNode {\n id: String\n label: String\n kind: TopologyNodeKind\n parent: String?\n}\n\ntype TopologyEdge {\n from: String\n to: String\n port: String?\n}\n\ntype DagDiff {\n added: List\n removed: List\n changed: List\n}\n\ntype PragmaDirective {\n key: String\n value: String\n scope: String?\n}\n\ntype DocSource {\n path: FilePath\n kind: DocSourceKind\n}\n\ntype ReferenceModel\n" -fn probe_3() -> ProcessExit { - let s = conservation_subject(id_tail: "probe_3", source: probe_src_3) - let spans = match s.parsed { - Accepted { value: a, diagnostics: _ } => a.span_index - Rejected { diagnostics: _ } => span_index_empty() - } - match s.normalized { - Accepted { value: _, diagnostics: _ } => ExitSuccess - Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) - } -} - -data probe_src_4: String = "module extdeps.realization.compile_stage_memo\n\nimport extdeps.external_authority { ExternalAuthority }\nimport extdeps.uri { Uri, Https }\nimport v2.std.optional { Absent }\n\nimport extdeps.cache.types { CacheInterfaceCatalogFacts, ContentAddressed, CatalogCasContentEncoding }\nimport std.cache_identity { CacheInterfaceId }\nimport std.cache_interface {\n ContentAddressLookup,\n OverwriteAllowed,\n MissThenCreate,\n StructuredArtifact,\n InProcess,\n PerFile,\n None,\n InProcessNs,\n Strong,\n OperatorObserved,\n ManualPurge,\n Repo,\n CapacityUnbounded,\n ReleasedAtProviderScopeExit,\n}\n\ndata extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {\n uri: Uri {\n scheme: Https\n locator: \"github.com/gunb-ai/gunbc/tree/main/dag/extdeps/realization\"\n }\n}\n\ndata compile_stage_memo_id: CacheInterfaceId = \"compile_stage_memo_cache\"\n\ndata compile_stage_memo_facts: CacheInterfaceCatalogFacts = {\n identity: compile_stage_memo_id\n backing_surface: \"gunbc-v2-compile-stage-in-process\"\n transport_encoding: CatalogCasContentEncoding\n key_derivation: {\n\n key_inputs: ContentAddressed\n overwritable: true\n prefix_fallback_allowed: false\n content_verified_on_read: false\n visibility_scope: Repo\n invalidation_triggers: [ManualPurge]\n }\n io: {\n lookup_semantics: ContentAddressLookup\n write_semantics: OverwriteAllowed\n miss_semantics: MissThenCreate\n }\n placement: {\n value_shape: StructuredArtifact\n locality: InProcess\n retention: { release_policy: ReleasedAtProviderScopeExit, capacity: CapacityUnbounded }\n atomicity: PerFile\n auth: None\n read_latency: InProcessNs\n consistency: Strong\n }\n evidence: [\n {\n field: \"locality\"\n evidence: OperatorObserved {\n note: \"scoped to compile_ingest_staging pipeline; drops with the ingest walk — within-run stage memo (v2.std.staging.cached_stage)\"\n observed_at: Absent\n }\n },\n {\n field: \"key_derivation\"\n evidence: OperatorObserved {\n note: \"stage input content-addressed at the cached_stage lookup site; miss runs the stage, hit skips recompute\"\n observed_at: Absent\n }\n },\n ]\n}\n" -fn probe_4() -> ProcessExit { - let s = conservation_subject(id_tail: "probe_4", source: probe_src_4) - let spans = match s.parsed { - Accepted { value: a, diagnostics: _ } => a.span_index - Rejected { diagnostics: _ } => span_index_empty() - } - match s.normalized { - Accepted { value: _, diagnostics: _ } => ExitSuccess - Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) - } -} - -data probe_src_5: String = "module extdeps.realization.parse_table_memo\n\nimport extdeps.external_authority { ExternalAuthority }\nimport extdeps.uri { Uri, Https }\nimport v2.std.optional { Absent }\n\nimport extdeps.cache.types { CacheInterfaceCatalogFacts, ContentAddressed, CatalogCasContentEncoding }\nimport std.cache_identity { CacheInterfaceId }\nimport std.cache_interface {\n ContentAddressLookup,\n OverwriteAllowed,\n MissThenCreate,\n StructuredArtifact,\n InProcess,\n PerFile,\n None,\n InProcessNs,\n Strong,\n OperatorObserved,\n ManualPurge,\n Repo,\n CapacityUnbounded,\n ReleasedAtProviderScopeExit,\n}\nimport std.disposition { Disposition, Scaffold, RealizationDispatch }\nimport std.decl_ref { DeclarationRef, WholeDeclaration }\n\ndata parse_table_memo_seed_handler_dissolution_trigger: Disposition = Scaffold {\n dissolves_to: RealizationDispatch,\n bind: DeclarationRef {\n module_path: \"v2.compiler.materialization_carriers\",\n decl_name: \"materialization_allows_memo_store\",\n field: WholeDeclaration\n }\n}\n\ndata extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {\n uri: Uri {\n scheme: Https\n locator: \"github.com/gunb-ai/gunbc/tree/main/dag/extdeps/realization\"\n }\n}\n\ndata parse_table_memo_id: CacheInterfaceId = \"parse_table_memo_cache\"\n\ndata parse_table_memo_facts: CacheInterfaceCatalogFacts = {\n identity: parse_table_memo_id\n backing_surface: \"gunbc-parse-table-in-process\"\n transport_encoding: CatalogCasContentEncoding\n key_derivation: {\n\n key_inputs: ContentAddressed\n overwritable: true\n prefix_fallback_allowed: false\n content_verified_on_read: false\n visibility_scope: Repo\n invalidation_triggers: [ManualPurge]\n }\n io: {\n lookup_semantics: ContentAddressLookup\n write_semantics: OverwriteAllowed\n miss_semantics: MissThenCreate\n }\n placement: {\n value_shape: StructuredArtifact\n locality: InProcess\n retention: { release_policy: ReleasedAtProviderScopeExit, capacity: CapacityUnbounded }\n atomicity: PerFile\n auth: None\n read_latency: InProcessNs\n consistency: Strong\n }\n evidence: [\n {\n field: \"locality\"\n evidence: OperatorObserved {\n note: \"scoped to InterpContext; drops with evaluation — within-run memo (design-recompute-memoization §4.1)\"\n observed_at: Absent\n }\n },\n {\n field: \"key_derivation\"\n evidence: OperatorObserved {\n note: \"ParseTableSubject = grammar_digest + token_stream_digest (full stream content-address incl. lexemes) + position + production\"\n observed_at: Absent\n }\n },\n ]\n}\n" -fn probe_5() -> ProcessExit { - let s = conservation_subject(id_tail: "probe_5", source: probe_src_5) - let spans = match s.parsed { - Accepted { value: a, diagnostics: _ } => a.span_index - Rejected { diagnostics: _ } => span_index_empty() - } - match s.normalized { - Accepted { value: _, diagnostics: _ } => ExitSuccess - Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) - } -} - -data probe_src_6: String = "module v2.std.host_transport\n\nimport v2.std.algebra { length }\nimport v2.std.collection { List }\nimport v2.std.integer { Int }\nimport v2.std.diagnostic {\n Accepted,\n Diagnostic,\n Outcome,\n Rejected,\n CorrectionNotModeled,\n None,\n Unavailable,\n diagnostics_singleton,\n outcome_rejected,\n invariant_port_locus\n}\nimport v2.std.machine { Byte }\nimport v2.std.node { Node, Symbol }\nimport v2.std.runtime { RuntimePrimitive, RuntimeValue }\nimport v2.std.text { String } -import std.process { ProcessExit, ExitSuccess, exit_failure } -import v2.std.lens_verdict { lens_verdict_diagnostic_locus_text_in_spans } -import v2.std.provenance { SpanIndex, span_index_empty } -import v2.std.node { symbol_lexeme } -import v2.std.diagnostic { Diagnostic } -\n\ntype ByteString = List\n\ntype RelPath = String\n\ntype WorkspaceContent\n = EmittedSource\n | FixedText { text: String }\n | EmittedWithSuffix { suffix: String }\n\ntype WorkspaceFile {\n path: RelPath\n content: WorkspaceContent\n}\n\ntype MaterializedWorkspaceFile {\n path: RelPath\n text: String\n}\n\ntype InvocationArg\n = LiteralArg { text: String }\n | WorkspacePath { path: RelPath }\n | ProducedArtifact { path: RelPath }\n\ntype ProcessProgram\n = HostToolProgram { executable: String }\n | ProducedProgram { path: RelPath }\n\ntype ProcessInvocation {\n program: ProcessProgram\n args: List\n}\n\ntype RuntimeValueCodec\n = FixedWidthBytes { width: Int }\n | SignedI32Le\n\ntype HostTransportDescriptor {\n identity: Symbol\n workspace: List\n build: List\n run: ProcessInvocation\n runtime_value_codec: RuntimeValueCodec\n build_environment: HostBuildEnvironment\n}\n\n// THE EMITTED PROGRAM'S BUILD ENVIRONMENT IS A ROW ON ITS TARGET'S TRANSPORT, NOT AN\n// INHERITANCE AND NOT ONE GLOBAL LIST. The host transport builds and runs the emitted program\n// under env_clear() plus exactly the ambient variables the descriptor names, admitted by exact\n// name; every other ambient variable is refused by negation. The admitted names are hashed into\n// the build-context identity, so the row is per TARGET: a Rust realization must not carry\n// PYTHONPATH (an unrelated change would perturb its identity) and a Python one must not carry\n// Cargo state. Flag-bearing variables (RUSTFLAGS, CARGO_ENCODED_RUSTFLAGS, CFLAGS, GOFLAGS, ...)\n// are absent BY CONSTRUCTION of each row and deliberately not enumerated as exclusions: a build\n// flag is policy, and a policy a realization wants is a runtime_row field with its own RED,\n// never something the ambient shell exported for compiling the seed. The seed used to admit\n// RUST*/CARGO_*/*FLAGS by prefix (and the uncached transport inherited the whole environment),\n// so the required floor's `RUSTFLAGS=-D warnings` -- exported for the SEED build -- reached the\n// emitted program and four emit_host expected-red rows were red only in CI, on `-D dead-code`\n// (gunbc#9727). CARGO_TARGET_DIR is never a row entry: the seed constructs it at the realization\n// boundary (the workspace's own target dir). The seed carries no veto of its own -- what a row\n// names is admitted, so a row is the whole policy.\ntype HostBuildEnvironment {\n ambient_names: List\n}\n\n// Shared subset, consumed explicitly by each target's row: where the process finds tools and\n// scratch. Nothing applies to a target that does not name it.\nfn host_process_location_names() -> List {\n [\"PATH\", \"HOME\", \"TMPDIR\"]\n}\n\ntype EmitHostRuntimeRow {\n transport: HostTransportDescriptor\n runtime_primitive_type: Node\n}\n\ntype TargetEmitHostRuntimeRow\n = HostRuntimeRowPresent { row: EmitHostRuntimeRow }\n | HostRuntimeRowAbsent\n\ndata target_emit_host_runtime_row_unconfigured: TargetEmitHostRuntimeRow = HostRuntimeRowAbsent\n\nfn emit_host_runtime_row_unconfigured_diagnostic() -> Diagnostic {\n Diagnostic {\n reason: ^emit_host_runtime_row_unconfigured_reason,\n at: invariant_port_locus(\n port: ^emit_host_runtime_row_lookup_port,\n invariant: ^emit_host_runtime_row_unconfigured_reason\n ),\n correction: Unavailable { reason: CorrectionNotModeled }\n }\n}\n\nfn emit_host_runtime_row_parse_byte_width_mismatch_diagnostic() -> Diagnostic {\n Diagnostic {\n reason: ^emit_host_runtime_row_parse_byte_width_mismatch,\n at: invariant_port_locus(\n port: ^emit_host_runtime_row_parse_port,\n invariant: ^emit_host_runtime_row_parse_byte_width_mismatch\n ),\n correction: Unavailable { reason: CorrectionNotModeled }\n }\n}\n\nfn runtime_value_codec_expected_byte_count(codec: RuntimeValueCodec) -> Int {\n match codec {\n FixedWidthBytes { width: w } => w\n SignedI32Le => 4\n }\n}\n\nfn runtime_value_parse_from_row(\n row: EmitHostRuntimeRow,\n bytes: ByteString\n) -> Outcome {\n let expected = runtime_value_codec_expected_byte_count(codec: row.transport.runtime_value_codec)\n if length(xs: bytes) == expected {\n Accepted {\n value: RuntimePrimitive {\n value: {\n primitive_type: row.runtime_primitive_type,\n bytes: bytes\n }\n },\n diagnostics: None\n }\n } else {\n Rejected {\n diagnostics: diagnostics_singleton(\n d: emit_host_runtime_row_parse_byte_width_mismatch_diagnostic()\n )\n }\n }\n}\n\nfn configured_emit_host_runtime_row_from_target_row(\n row: TargetEmitHostRuntimeRow\n) -> Outcome {\n match row {\n HostRuntimeRowPresent { row: configured } =>\n Accepted { value: configured, diagnostics: None }\n HostRuntimeRowAbsent =>\n outcome_rejected(d: emit_host_runtime_row_unconfigured_diagnostic())\n }\n}\n\ntype EmittedEffectFamily\n = FilesystemReadFamily\n | ShellExecRunFamily\n\nfn emitted_family_runtime_row_gated(\n family: EmittedEffectFamily,\n present: EmitHostRuntimeRow\n) -> TargetEmitHostRuntimeRow {\n match family {\n FilesystemReadFamily => HostRuntimeRowPresent { row: present }\n ShellExecRunFamily => HostRuntimeRowPresent { row: present }\n }\n}\n" -fn probe_6() -> ProcessExit { - let s = conservation_subject(id_tail: "probe_6", source: probe_src_6) - let spans = match s.parsed { - Accepted { value: a, diagnostics: _ } => a.span_index - Rejected { diagnostics: _ } => span_index_empty() - } - match s.normalized { - Accepted { value: _, diagnostics: _ } => ExitSuccess - Rejected { diagnostics: d } => exit_failure(reason: fold(d.tail, init: probe_diag_text(x: d.head, spans: spans), f: fn(acc, x) { concat(concat(acc, " || "), probe_diag_text(x: x, spans: spans)) })) - } -} - -fn probe_diag_text(x: Diagnostic, spans: SpanIndex) -> String { - concat(concat(symbol_lexeme(sym: x.reason), " @ "), lens_verdict_diagnostic_locus_text_in_spans(d: x, index: spans)) -} From d50f2984d6b04a2b404f83aae084175b60f682cb Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 09:21:57 +0000 Subject: [PATCH 47/63] Merge main; migrate vf_wildcard_pattern_keeps_its_field (added by #12650) to the reference tag Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/test/claim/parse/variant_field_lowering_test.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/test/claim/parse/variant_field_lowering_test.dag b/src/v2/test/claim/parse/variant_field_lowering_test.dag index bca1231727e..3f290f8fe35 100644 --- a/src/v2/test/claim/parse/variant_field_lowering_test.dag +++ b/src/v2/test/claim/parse/variant_field_lowering_test.dag @@ -420,7 +420,7 @@ test fn vf_wildcard_pattern_keeps_its_field() -> Bool { match vf_first_arm_pattern_of(o: vfs_normalized_8(), fn_lexeme: "tag") { Absent => false Present { value: pat } => - (match construct_tag_optional(n: pat) { Present { value: t } => symbol_lexeme(sym: t) == "A" Absent => false }) + (match construct_tag_path_optional(n: pat) { Present { value: path } => qualified_name_to_dotted_string(qn: path) == "A" Absent => false }) && (count(construct_field_edges(n: pat)) == 1) && (match list_at_optional(xs: construct_field_edges(n: pat), index: 0) { Present { value: e } => From 43b1fb7896ef350f79817dd5b11fc927a6c6f625 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 10:12:58 +0000 Subject: [PATCH 48/63] map_literal controls: plain forms the v2 parser reads (no leading && continuation, no prefix !) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../claim/body_lowering/map_literal_test.dag | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 45c0d5cd271..97ad18e3f30 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -194,14 +194,21 @@ test fn ml_distinct_keys_are_accepted_holds() -> Bool { // discriminates is a law that always answers Distinct -- the fold's last-wins absorbing the repeat -- // under which this and control 2 go red together. test fn ml_the_key_law_names_both_occurrences_holds() -> Bool { - match map_introduction_duplicate_key(keys: ["a", "b", "a", "b"]) { - MapIntroductionKeyRepeated { first_index: f, second_index: s } => (f == 0) && (s == 2) + ml_key_law_repeat_is(keys: ["a", "b", "a", "b"], first: 0, second: 2) && ml_key_law_distinct(keys: ["a", "b"]) +} + +fn ml_key_law_repeat_is(keys: List, first: Int, second: Int) -> Bool { + match map_introduction_duplicate_key(keys: keys) { + MapIntroductionKeyRepeated { first_index: f, second_index: s } => (f == first) && (s == second) MapIntroductionKeysDistinct => false } - && match map_introduction_duplicate_key(keys: ["a", "b"]) { - MapIntroductionKeysDistinct => true - MapIntroductionKeyRepeated { first_index: _, second_index: _ } => false - } +} + +fn ml_key_law_distinct(keys: List) -> Bool { + match map_introduction_duplicate_key(keys: keys) { + MapIntroductionKeysDistinct => true + MapIntroductionKeyRepeated { first_index: _, second_index: _ } => false + } } // Control 5: K is not the type a string literal denotes, so the key refuses where it is written. @@ -222,7 +229,7 @@ test fn ml_a_call_argument_has_no_expected_type_holds() -> Bool { // elaborates too and then infer refuses it against V = Bool. test fn ml_infer_still_checks_an_elaborated_map_holds() -> Bool { let bad = ml_resolve_subject(subject: "v2.test.ml_value_mismatch") - ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_headless")) && ml_accepts(o: bad) && !ml_infers(o: bad) + ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_headless")) && ml_accepts(o: bad) && (ml_infers(o: bad) == false) } fn ml_infers(o: Outcome) -> Bool { From e97899755504d35ad798f8ca1914ebacabd81299 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 10:14:34 +0000 Subject: [PATCH 49/63] Revert "map_literal controls: plain forms the v2 parser reads (no leading && continuation, no prefix !)" This reverts commit 43b1fb7896ef350f79817dd5b11fc927a6c6f625. --- .../claim/body_lowering/map_literal_test.dag | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 97ad18e3f30..45c0d5cd271 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -194,21 +194,14 @@ test fn ml_distinct_keys_are_accepted_holds() -> Bool { // discriminates is a law that always answers Distinct -- the fold's last-wins absorbing the repeat -- // under which this and control 2 go red together. test fn ml_the_key_law_names_both_occurrences_holds() -> Bool { - ml_key_law_repeat_is(keys: ["a", "b", "a", "b"], first: 0, second: 2) && ml_key_law_distinct(keys: ["a", "b"]) -} - -fn ml_key_law_repeat_is(keys: List, first: Int, second: Int) -> Bool { - match map_introduction_duplicate_key(keys: keys) { - MapIntroductionKeyRepeated { first_index: f, second_index: s } => (f == first) && (s == second) + match map_introduction_duplicate_key(keys: ["a", "b", "a", "b"]) { + MapIntroductionKeyRepeated { first_index: f, second_index: s } => (f == 0) && (s == 2) MapIntroductionKeysDistinct => false } -} - -fn ml_key_law_distinct(keys: List) -> Bool { - match map_introduction_duplicate_key(keys: keys) { - MapIntroductionKeysDistinct => true - MapIntroductionKeyRepeated { first_index: _, second_index: _ } => false - } + && match map_introduction_duplicate_key(keys: ["a", "b"]) { + MapIntroductionKeysDistinct => true + MapIntroductionKeyRepeated { first_index: _, second_index: _ } => false + } } // Control 5: K is not the type a string literal denotes, so the key refuses where it is written. @@ -229,7 +222,7 @@ test fn ml_a_call_argument_has_no_expected_type_holds() -> Bool { // elaborates too and then infer refuses it against V = Bool. test fn ml_infer_still_checks_an_elaborated_map_holds() -> Bool { let bad = ml_resolve_subject(subject: "v2.test.ml_value_mismatch") - ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_headless")) && ml_accepts(o: bad) && (ml_infers(o: bad) == false) + ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_headless")) && ml_accepts(o: bad) && !ml_infers(o: bad) } fn ml_infers(o: Outcome) -> Bool { From 7f729cf452b7f6c93dfb1a063bc27ae8aaba1ec2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 10:23:04 +0000 Subject: [PATCH 50/63] RFM receipt: block-headed left operand refused by v2, accepted by the seed; located specimen in #12758's controls module (kept as witness) Co-Authored-By: Claude Opus 5.5 (1M context) --- ...k_headed_operand_lowers_to_that_block_at_v2_body_lowering.dag | 1 + 1 file changed, 1 insertion(+) diff --git a/dag/gunbc/recurring_failure_mode/expression_enclosing_a_block_headed_operand_lowers_to_that_block_at_v2_body_lowering.dag b/dag/gunbc/recurring_failure_mode/expression_enclosing_a_block_headed_operand_lowers_to_that_block_at_v2_body_lowering.dag index b90e358013b..d610a3ba452 100644 --- a/dag/gunbc/recurring_failure_mode/expression_enclosing_a_block_headed_operand_lowers_to_that_block_at_v2_body_lowering.dag +++ b/dag/gunbc/recurring_failure_mode/expression_enclosing_a_block_headed_operand_lowers_to_that_block_at_v2_body_lowering.dag @@ -14,6 +14,7 @@ data expression_enclosing_a_block_headed_operand_lowers_to_that_block_at_v2_body "DISTINGUISHING FACTS. Not `lowering_accessor_collapses_a_sequence_operand`, which keeps the LEFT element of an operand; here the block is kept and the enclosing expression is lost. Not gunbc#11911, a parse-order defect; here the parse accepts. Not `match_arms_after_the_second_are_dropped_at_v2_body_lowering`, which loses arms INSIDE a match; this loses what is AROUND it.", "RUNG FOUND AT: below the floor (silent wrongness). CEILING: structurally guaranteed. An expression lowers to its own operator, construct or application over every constituent, or refuses at the constituent it could not read. A control form is lowered as the whole value only where it IS the value. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: v2 body lowering has no reader that answers a value by searching its subtree for a control form, so an enclosing expression the fold left unlowered refuses instead of being replaced. Owner: bright-boar-848's fold queue (quiet-seal-543, 2026-09-25). The fall-back reader is that lane's subject. Pinned drops, each holding while the drop happens and redding when the repair lands, in `v2.test.claim.namespace_xl0.reference_conservation_accepted_drops`: `a_binary_operand_beside_a_match_is_reported_dropped_holds`, `a_binary_operand_beside_an_if_is_reported_dropped_holds`, `a_record_field_beside_a_match_valued_field_is_reported_dropped_holds`, `a_call_argument_beside_a_match_valued_argument_is_reported_dropped_holds`. The census's clean control, `v2.test.claim.namespace_xl0.reference_conservation` `the_clean_control_conserves_every_authored_atom_holds`, is the neighbour that conserves.", "CLIMBED, THE TRIGGER CAPABILITY MET (bright-boar-848, XL-2). Two links, both in `v2.compiler.body_lowering_fold`. (1) `body_lower_find_control_form_optional` no longer searches a value's subtree: it descends only through what does not change the expression -- a production to its captured child, a sequence whose right side is an empty tail, a lone opening or closing delimiter -- so a control form is lowered as the whole value only where it IS the value, and an enclosing operator, call or record goes whole to the binary/postfix route. (2) That route then exposed the second link: `body_lower_primary_expr` ran its raw-syntax strategy readers over a capture the bottom-up fold had ALREADY lowered (a Match or Branch) and one answered it with its first child, so `t && match w {..}` became `t && w`. Measured by calling `body_lower_value_read` at each production level of the operand: correct at the match_expr node, collapsed from its primary parent up. A primary whose capture is core substrate is now that node, the rule `body_lower_postfix_lowered_primary_optional` already states one level up. The four pinned drops flip to conservation controls (accepted AND the enclosing constituent not dropped), red against the base fold and green after. The label and let-binder atoms that stay absent in the same fixtures are the separately declared NamedArgumentLabel and StatementLetBinder drops. RUNG NOW: structurally guaranteed for the enclosing expression.", + "THE LEFT-OPERAND HALF IS STILL OPEN, and it is valid dag the corpus writes: a block-headed expression as the LEFT operand of a binary operator (`match w {..} && t`, `match w {..} || t`) at expression start. Measured occurrence count 179 (a scan of dag/ and src/v2 for a statement-initial `match .. {` whose closing brace is followed by a `&&`/`||` line); all sit under dag/test, which the native route does not admit into its population, so the native census shows none of them. The seed parser accepts the form and v2 refuses it, so the two parsers disagree on the same source. LOCATED SPECIMEN (gunbc#12758): v2.test.claim.body_lowering.map_literal `ml_the_key_law_names_both_occurrences_holds` is `match map_introduction_duplicate_key(..) { .. } && match .. { .. }`; claim_batch (seed) runs it PASS, and the v2 native route file-refuses src/v2/test/claim/body_lowering/map_literal_test.dag with parse_g0_tokens_remain (WholeFile). Bisected by feeding each top-level declaration to the v2 parser alone: only that declaration refuses. The specimen keeps its spelling as the witness; it is not rewritten around the gap. NEXT-RUNG TRIGGER: the v2 grammar admits a block-headed primary as the left operand of a binary operator, and this specimen's file clears parse on the native route.", ], evidence: [ From 8cafe0d7749febb80f541249f6c2246f361a4d63 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 10:44:43 +0000 Subject: [PATCH 51/63] PR2: string key is a grammar production (review 73139); one typed ConstructTagReading; pure-renaming aliases decided on the declaration and followed; algebra native control Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_resolve.dag | 157 +++++++++++++----- src/v2/compiler/body_lowering_fold.dag | 39 ++--- src/v2/compiler/occurrence_role.dag | 20 ++- src/v2/extdeps/languages/dag.dag | 43 +++-- src/v2/std/node_query.dag | 69 ++++---- src/v2/std/symbol_index.dag | 36 +++- src/v2/std/type_binder.dag | 72 +++++++- .../anonymous_record_algebra_test.dag | 16 ++ .../body_lowering/anonymous_record_test.dag | 130 ++++++++++++++- .../workflow/compile_door_cause_ownership.dag | 6 + src/v2/workflow/floor_pure_producer_share.dag | 8 + 11 files changed, 465 insertions(+), 131 deletions(-) create mode 100644 src/v2/test/claim/body_lowering/anonymous_record_algebra_test.dag diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index fb431f12723..ce54d4ec75d 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -94,7 +94,8 @@ import v2.std.symbol_index { symbol_index_global_unique_lookup, symbol_index_declaring_path_of, symbol_index_lexical_lookup, - symbol_index_lookup + symbol_index_lookup, + symbol_index_renaming_alias_at } import v2.std.node { named_edges, @@ -132,7 +133,10 @@ 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 { construct_field_edges, - construct_tag_is_elided, + construct_tag_reading, + ConstructTagAuthored, + ConstructTagElided, + NotAConstruct, construct_tag_optional, node_positional_child_targets, find_named_child, @@ -1649,7 +1653,7 @@ fn resolve_construct_walk_with_tag( // AN ANONYMOUS RECORD LITERAL IS ELABORATED FROM AN AUTHORED ANNOTATION, OR REFUSED // (docs/plans/anonymous-record-literal-design.md). `{ f: v }` lowers to a construct whose tag is -// ELIDED (v2.std.node_query construct_tag_is_elided). Its constructor is the record the EXPECTED +// ELIDED (v2.std.node_query construct_tag_reading answers ConstructTagElided). Its constructor is the record the EXPECTED // type names, and the expectation is an explicit parameter handed by three position rules -- // an Arrow's body under its declared return (which covers a `data` initializer), a record field // under its authored field type, and a list element under its annotated list's element type -- and @@ -1671,14 +1675,14 @@ fn resolve_anonymous_record_refused(n: Node, reason: Symbol) -> ResolveNodeWalk // THE HEAD OF AN AUTHORED TYPE EXPRESSION, peeled syntactically: a bare name, or the head of an // instantiation `H`. Anything else -- an optional `T?`, a function type, an anonymous record // type -- has no head a record could be read from, and answers Absent. -fn resolve_expected_head_atom_optional(t: Node) -> Optional { +fn resolve_expected_head_node_optional(t: Node) -> Optional { match t.kind { - TypeNode { connective: Atom { identity: id } } => optional_present(value: id) + TypeNode { connective: Atom { identity: _ } } => optional_present(value: t) TypeNode { connective: Instantiation } => match list_at_optional(xs: node_positional_child_targets(node: t), index: 0) { Present { value: head } => match head.kind { - TypeNode { connective: Atom { identity: id } } => optional_present(value: id) + TypeNode { connective: Atom { identity: _ } } => optional_present(value: head) _ => optional_absent() } Absent => optional_absent() @@ -1687,15 +1691,70 @@ fn resolve_expected_head_atom_optional(t: Node) -> Optional { } } -// The head resolved at `position` through the existing lexical lookup: the literal's own position -// for an annotation written in its module, the record's own path for a field type written on the -// record. A binder (`T`), a kernel symbol, or an unbound or ambiguous name answers Absent. -fn resolve_expected_head_path(ctx: ResolveContext, position: QualifiedName, t: Node) -> Optional { - match resolve_expected_head_atom_optional(t: t) { +// THE HEAD IS RESOLVED BY THE SAME ROUTE THE ANNOTATION ITSELF IS. An annotation written in the +// literal's own module (a data declaration's type, a fn's return, a list element type taken from one) +// resolves its head through resolve_atom in this context -- scope chain, root bindings, the index, +// the kernel -- so the expectation can never bind a head to a different declaration than the one +// the annotation denotes. A field type is written in the RECORD's module, whose scope and bindings +// this context does not carry, so it is looked up at the record's own position through the index's +// lexical lookup (NamespaceOnlyY: lookup by containment position). A head that is a binder, a kernel +// symbol, or unbound gives no declaration path, and answers Absent. +// WHERE AN EXPECTED TYPE'S HEAD LEADS: a declaration, nowhere, or round a cycle of aliases. +type ExpectedHead + = ExpectedHeadAt { path: QualifiedName } + | ExpectedHeadAbsent + | ExpectedHeadAliasCycle + +fn resolve_expected_head_path(ctx: ResolveContext, x: ResolveExpectation) -> ExpectedHead { + match resolve_expected_head_path_as_written(ctx: ctx, x: x) { + Absent => ExpectedHeadAbsent + Present { value: path } => resolve_expected_rename_chain(ctx: ctx, path: path, visited: []) + } +} + +// A PURE-RENAMING ALIAS IS FOLLOWED; NO OTHER ALIAS IS (ruling, neat-boar-16). Whether an alias is a +// pure renaming -- `type A = H`, or `type A = H` -- was decided once, on its +// declaration, and recorded in the index (v2.std.type_binder type_alias_renaming, +// v2.std.symbol_index symbol_index_renaming_alias_at); this reads that fact and does not re-derive +// it. Following a renaming substitutes nothing: the annotation's own arguments stay in the positions +// they were written. This is how `List` (type List = FreeMonoid) reaches the list +// head. Chains are followed transitively, each renamed head looked up at the alias's own position +// through the existing lexical lookup, and a path met twice is a cycle, refused located rather than +// followed. Any other alias is not recorded as a renaming and is the declaration the head names, +// which is not a record, so the literal refuses. +fn resolve_expected_rename_chain(ctx: ResolveContext, path: QualifiedName, visited: List) -> ExpectedHead { + if any(xs: visited, predicate: fn(v) { v == path }) { + ExpectedHeadAliasCycle + } else { + match symbol_index_renaming_alias_at(index: ctx.namespace.symbol_index, qualified_path: path) { + Absent => ExpectedHeadAt { path: path } + Present { value: head } => + match symbol_index_lexical_lookup(index: ctx.namespace.symbol_index, position: path, name: head) { + LexicalHit { node: _, path: renamed } => + resolve_expected_rename_chain(ctx: ctx, path: renamed, visited: list_snoc_item(xs: visited, item: path)) + _ => ExpectedHeadAbsent + } + } + } +} + +fn resolve_expected_head_path_as_written(ctx: ResolveContext, x: ResolveExpectation) -> Optional { + match resolve_expected_head_node_optional(t: x.expected) { Absent => optional_absent() - Present { value: id } => - match symbol_index_lexical_lookup(index: ctx.namespace.symbol_index, position: position, name: id) { - LexicalHit { node: _, path: path } => optional_present(value: path) + Present { value: head } => + match head.kind { + TypeNode { connective: Atom { identity: id } } => + if x.at_use_site { + match resolve_atom(ctx: resolve_ctx_with_scope(ctx: ctx, scope: ctx.type_scope), n: head, identity: id) { + Accepted { value: resolved, diagnostics: _ } => declaration_reference_path_optional(node: resolved) + Rejected { diagnostics: _ } => optional_absent() + } + } else { + match symbol_index_lexical_lookup(index: ctx.namespace.symbol_index, position: x.position, name: id) { + LexicalHit { node: _, path: path } => optional_present(value: path) + _ => optional_absent() + } + } _ => optional_absent() } } @@ -1705,26 +1764,29 @@ fn resolve_expected_head_path(ctx: ResolveContext, position: QualifiedName, t: N type ResolveExpectation { expected: Node position: QualifiedName + at_use_site: Bool } // A value edge walked under an expectation. Only three node shapes consume one -- a headless // literal, an authored construct (whose fields take their own), and a list literal -- and every // other node is walked exactly as resolve_child_edge would, so the expectation stops here. fn resolve_expected_edge(ctx: ResolveContext, e: Edge, x: ResolveExpectation) -> ResolveNodeWalk { - if construct_tag_is_elided(n: e.target) { - resolve_elided_construct_walk(ctx: ctx, n: e.target, x: x) - } else { - match list_introduction_elements_optional(node: e.target) { - Present { value: _ } => resolve_list_literal_expected(ctx: ctx, n: e.target, x: x) - Absent => resolve_child_edge(ctx: ctx, e: e) - } + match construct_tag_reading(n: e.target) { + ConstructTagElided => resolve_elided_construct_walk(ctx: ctx, n: e.target, x: x) + ConstructTagAuthored { reference: _ } => resolve_child_edge(ctx: ctx, e: e) + NotAConstruct => + match list_introduction_elements_optional(node: e.target) { + Present { value: _ } => resolve_list_literal_expected(ctx: ctx, n: e.target, x: x) + Absent => resolve_child_edge(ctx: ctx, e: e) + } } } fn resolve_elided_construct_walk(ctx: ResolveContext, n: Node, x: ResolveExpectation) -> ResolveNodeWalk { - match resolve_expected_head_path(ctx: ctx, position: x.position, t: x.expected) { - Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_expected_type_not_record) - Present { value: path } => + match resolve_expected_head_path(ctx: ctx, x: x) { + ExpectedHeadAbsent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_expected_type_not_record) + ExpectedHeadAliasCycle => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_alias_cycle) + ExpectedHeadAt { path: path } => match symbol_index_declared_payload_at(index: ctx.namespace.symbol_index, qualified_path: path) { Present { value: RecordTypePayload } => match construct_tag_optional_edge_target(n: n) { @@ -1765,7 +1827,7 @@ fn resolve_record_field_edge(ctx: ResolveContext, e: Edge, record: Optional resolve_child_edge(ctx: ctx, e: e) Present { value: t } => - resolve_expected_edge(ctx: ctx, e: e, x: ResolveExpectation { expected: t, position: path }) + resolve_expected_edge(ctx: ctx, e: e, x: ResolveExpectation { expected: t, position: path, at_use_site: false }) } } } @@ -1803,15 +1865,19 @@ fn resolve_record_field_type_optional(ctx: ResolveContext, record: QualifiedName // head that reaches it only through an alias is not unfolded here; it passes no expectation, and a // headless element then refuses resolve_anonymous_record_no_expected_type. fn resolve_list_literal_expected(ctx: ResolveContext, n: Node, x: ResolveExpectation) -> ResolveNodeWalk { - let element_type = match resolve_expected_head_path(ctx: ctx, position: x.position, t: x.expected) { - Present { value: path } => + match resolve_expected_head_path(ctx: ctx, x: x) { + ExpectedHeadAliasCycle => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_alias_cycle) + ExpectedHeadAbsent => resolve_node_walk(ctx: ctx, n: n) + ExpectedHeadAt { path: path } => if path == list_introduction_head_path() { - list_at_optional(xs: node_positional_child_targets(node: x.expected), index: 1) + resolve_list_elements_expected(ctx: ctx, n: n, x: x, element_type: list_at_optional(xs: node_positional_child_targets(node: x.expected), index: 1)) } else { - optional_absent() + resolve_node_walk(ctx: ctx, n: n) } - Absent => optional_absent() } +} + +fn resolve_list_elements_expected(ctx: ResolveContext, n: Node, x: ResolveExpectation, element_type: Optional) -> ResolveNodeWalk { match element_type { Absent => resolve_node_walk(ctx: ctx, n: n) Present { value: et } => @@ -1819,7 +1885,7 @@ fn resolve_list_literal_expected(ctx: ResolveContext, n: Node, x: ResolveExpecta if acc.ordinal == 0 { child_walk_step(w: acc, e: e, r: resolve_child_edge(ctx: ctx, e: e)) } else { - child_walk_step(w: acc, e: e, r: resolve_expected_edge(ctx: ctx, e: e, x: ResolveExpectation { expected: et, position: x.position })) + child_walk_step(w: acc, e: e, r: resolve_expected_edge(ctx: ctx, e: e, x: ResolveExpectation { expected: et, position: x.position, at_use_site: x.at_use_site })) } })) } @@ -1961,21 +2027,19 @@ fn resolve_node_walk( ComputationNode { behavior: Transform } => resolve_transform_children(ctx: ctx, n: n) TypeNode { connective: Conj } => - if construct_tag_is_elided(n: n) { - resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_no_expected_type) - } else { - match construct_tag_optional(n: n) { - Present { value: reference } => + match construct_tag_reading(n: n) { + ConstructTagElided => + resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_record_no_expected_type) + ConstructTagAuthored { reference: reference } => resolve_construct_walk(ctx: ctx, n: n, reference: reference, field: fn(e, record) { resolve_record_field_edge(ctx: ctx, e: e, record: record) }) - Absent => + NotAConstruct => match try_resolve_qualified_name_node(ctx: ctx, n: n) { Present { value: outcome } => resolve_walk_of_outcome(o: outcome) Absent => resolve_children_homogeneous_scope(ctx: ctx, n: n) } } - } TypeNode { connective: _ } => resolve_children_homogeneous_scope(ctx: ctx, n: n) ComputationNode { behavior: _ } => @@ -2194,7 +2258,7 @@ fn resolve_arrow_body_or_named_edge(ctx: ResolveContext, arrow: Node, e: Edge) - resolve_expected_edge( ctx: ctx, e: e, - x: ResolveExpectation { expected: declared, position: symbol_index_lookup_position(ctx: ctx) } + x: ResolveExpectation { expected: declared, position: symbol_index_lookup_position(ctx: ctx), at_use_site: true } ) Absent => resolve_node_walk(ctx: ctx, n: e.target) } @@ -2211,13 +2275,14 @@ fn resolve_edge_is_arrow_body(e: Edge) -> Bool { } fn resolve_node_consumes_expectation(n: Node) -> Bool { - if construct_tag_is_elided(n: n) { - true - } else { - match list_introduction_elements_optional(node: n) { - Present { value: _ } => true - Absent => false - } + match construct_tag_reading(n: n) { + ConstructTagElided => true + ConstructTagAuthored { reference: _ } => false + NotAConstruct => + match list_introduction_elements_optional(node: n) { + Present { value: _ } => true + Absent => false + } } } diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index ad30dbb2f39..9b3a25abbeb 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -27,7 +27,6 @@ import v2.compiler.body_producer_forward { } import v2.extdeps.languages.dag { ParseSubtreeAbsent, - dag_lexeme_is_string_literal, ParseSubtreeFind, ParseSubtreeFound, dag_binding_branded_atom_identity, @@ -135,7 +134,10 @@ import v2.std.node_query { node_positional_child_targets, construct_node, construct_tag_elided_reference, - construct_tag_is_elided, + construct_tag_reading, + ConstructTagAuthored, + ConstructTagElided, + NotAConstruct, construct_tag_optional, field_projection_node, field_projection_optional, @@ -562,8 +564,7 @@ fn body_lower_is_core_substrate(node: Node) -> Bool { _ => (match body_lower_int_literal_with_magnitude_optional(node: node) { Present { value: lit } => lit == node Absent => false }) || qualified_name_spine_shape_present(root: node) - || (match construct_tag_optional(n: node) { Present { value: _ } => true Absent => false }) - || construct_tag_is_elided(n: node) + || (match construct_tag_reading(n: node) { ConstructTagAuthored { reference: _ } => true ConstructTagElided => true NotAConstruct => false }) || (match field_projection_optional(n: node) { Present { value: _ } => true Absent => false }) } } @@ -7705,20 +7706,16 @@ fn body_lower_postfix_expr(shell: Node) -> Outcome { // shape v2.std.node admits and the shape a fielded pattern lowers to, so `Rec { n: 1 }` and // `Rec { n: x } =>` are one shape read in two directions. A dotted head (`m.R { .. }`) is a postfix // chain and reaches the same builder, body_lower_record_construct, through its PostfixRecord step. -// A FIELD INIT'S KEY IS A NAME, read as the grammar's binding-name alternative and nothing else. -// dag_grammar_field_init_expr also admits a STRING-LITERAL key (`{ "k": v }`, a map entry), and the -// general binding reader takes that lexeme's atom for a field name -- erasing the key kind, so -// that `{ "n": true }` under a record type would elaborate as field `n`. A string key therefore -// refuses at the item here; the map arm gives it its own edge (gunbc#12734). -fn body_lower_field_init_name_optional(key: Node) -> Optional { - match body_lower_param_binding_optional(node: key) { - Present { value: name } => - if dag_lexeme_is_string_literal(sym: name) { - optional_absent() - } else { - optional_present(value: name) - } - Absent => optional_absent() +// A STRING-KEYED INIT IS NOT A RECORD FIELD, AND THE PARSE SAYS SO. The key's kind is the grammar's +// choice, carried as the production identity ^dag_surface_field_init_string_key +// (v2.extdeps.languages.dag dag_grammar_field_init_expr), so a record literal refuses it at the item +// rather than reading `{ "n": true }` as field `n`. Admitting a quoted key whose decoded text names a +// declared field is a declared frontier (gunbc#12740); a map literal's entry is the map arm's +// (gunbc#12758), which dispatches on this same identity. +fn body_lower_field_init_is_string_keyed(item: Node) -> Bool { + match parse_production_emitted_identity_optional(node: body_lower_deep_unwrap_optional(node: item)) { + Present { value: id } => id == ^dag_surface_field_init_string_key + Absent => false } } @@ -7731,10 +7728,13 @@ fn body_lower_field_init_edge(item: Node) -> Outcome { let refuse = outcome_rejected( d: body_lower_diagnostic(reason: ^body_lowering_reason_field_init_unlowered, n: item) ) + if body_lower_field_init_is_string_keyed(item: captured) { + refuse + } else { match sugar_sequence_pair_optional(node: captured) { Absent => refuse Present { value: pair } => - match body_lower_field_init_name_optional(key: pair.left) { + match body_lower_param_binding_optional(node: pair.left) { Absent => refuse Present { value: field_name } => match sugar_sequence_pair_optional(node: pair.right) { @@ -7751,6 +7751,7 @@ fn body_lower_field_init_edge(item: Node) -> Outcome { } } } + } } // The field items of a brace record body, `{ field_init, .. }`. One reader for both heads: a bare diff --git a/src/v2/compiler/occurrence_role.dag b/src/v2/compiler/occurrence_role.dag index 16caa54f1a2..aab6b3506e3 100644 --- a/src/v2/compiler/occurrence_role.dag +++ b/src/v2/compiler/occurrence_role.dag @@ -132,8 +132,25 @@ fn read_kw_then_ident(captured: Node) -> NameRead { } // `name: value` in a record literal: the field label. +// A STRING-KEYED INIT NAMES NO FIELD: its key is a literal, not a FieldOccurrence, and the choice's +// string alternative is its own production (^dag_surface_field_init_string_key), whose value's names +// are read as references by that production's own row. So that alternative lawfully has no name here, +// as a positional argument has none below. fn read_field_init_label(captured: Node) -> NameRead { - match sugar_sequence_pair_optional(node: body_lower_deep_unwrap_optional(node: captured)) { + let item = body_lower_deep_unwrap_optional(node: captured) + match parse_production_emitted_identity_optional(node: item) { + Present { value: id } => + if id == ^dag_surface_field_init_string_key { + NoNameHere + } else { + read_field_init_name_label(item: item) + } + Absent => read_field_init_name_label(item: item) + } +} + +fn read_field_init_name_label(item: Node) -> NameRead { + match sugar_sequence_pair_optional(node: item) { Absent => NameReadRefused Present { value: pair } => one_name(atom: body_lower_param_binding_atom_optional(node: pair.left)) } @@ -175,6 +192,7 @@ fn dag_name_production_rows() -> List { name_row(production: ^dag_production_type_decl, disposition: NameRoleRead { role: DeclarationRole, category: TypeOccurrence, reader: ReadKwThenIdent }), name_row(production: ^dag_production_alias_decl, disposition: NameRoleRead { role: DeclarationRole, category: TypeOccurrence, reader: ReadKwThenIdent }), name_row(production: ^dag_production_field_init, disposition: NameRoleRead { role: ReferenceRole, category: FieldOccurrence, reader: ReadFieldInitLabel }), + name_row(production: ^dag_production_field_init_string_key, disposition: NamesAreReferences), name_row(production: ^dag_production_arg, disposition: NameRoleRead { role: ReferenceRole, category: LexicalValueOccurrence, reader: ReadNamedArgLabel }), name_row(production: ^dag_production_field_pattern, disposition: NameRoleRead { role: DeclarationRole, category: FieldOccurrence, reader: ReadFieldPatternName }), name_row(production: ^dag_production_test_fn_decl, disposition: NamesAreMarkers), diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 419b8a3e88e..c3d729fafc3 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -525,26 +525,17 @@ fn dag_string_body_element_pattern() -> LexPattern { } } -// THE STRING LITERAL'S DELIMITER, held once: the lex rule reads it for both ends, and -// dag_lexeme_is_string_literal reads it to recover a string token's class from its lexeme once parsing -// has carried the token as a plain atom (a field-init key, whose grammar admits a name OR a string). -data dag_string_literal_delimiter_code_point: Int = 34 - fn dag_string_literal_lex_rule() -> LexRule { TokenRule { token_class: ^dag_token_string_literal, pattern: DelimitedPattern { - open: CharPattern { char: dag_string_literal_delimiter_code_point }, + open: CharPattern { char: 34 }, body: RepeatPattern { element: dag_string_body_element_pattern() }, - close: CharPattern { char: dag_string_literal_delimiter_code_point } + close: CharPattern { char: 34 } } } } -fn dag_lexeme_is_string_literal(sym: Symbol) -> Bool { - starts_with(s: symbol_lexeme(sym: sym), prefix: from_code_point(cp: dag_string_literal_delimiter_code_point)) -} - fn dag_whitespace_trivia_rule() -> LexRule { TriviaRule { pattern: OneOrMorePattern { element: CharClassPattern { class: WhitespaceChar } } @@ -1553,12 +1544,30 @@ fn dag_grammar_arg_expr() -> GrammarExpr { ) } +// A FIELD INIT IS KEYED BY A NAME OR BY A STRING, AND THE PARSE SAYS WHICH. The two keys are two +// alternatives of one choice, and the string alternative is its own production +// (^dag_production_field_init_string_key, emitted ^dag_surface_field_init_string_key), so the key's +// kind is carried by the parse tree as a production identity -- the same fact every reader +// dispatches on -- rather than recovered later from a key atom's spelling. A record literal's field +// is the name alternative; a map literal's entry is the string alternative. fn dag_grammar_field_init_expr() -> GrammarExpr { - dag_grammar_sequence( - left: dag_grammar_choice( + dag_grammar_choice( + left: dag_grammar_sequence( left: dag_grammar_binding_name_terminal(), - right: dag_grammar_terminal_lexeme(token_class: ^dag_token_string_literal) + right: dag_grammar_sequence( + left: dag_grammar_terminal(token_class: ^dag_token_colon), + right: dag_grammar_nonterminal(production: ^dag_production_expr) + ) ), + right: dag_grammar_nonterminal(production: ^dag_production_field_init_string_key) + ) +} + +// THE STRING KEY IS THE CLASS-STAMPED STRING TERMINAL -- the one string terminal, whose node carries +// the literal's decoded value (gunbc#12759) -- never a second, spelling-stamped one. +fn dag_grammar_field_init_string_key_expr() -> GrammarExpr { + dag_grammar_sequence( + left: dag_grammar_terminal(token_class: ^dag_token_string_literal), right: dag_grammar_sequence( left: dag_grammar_terminal(token_class: ^dag_token_colon), right: dag_grammar_nonterminal(production: ^dag_production_expr) @@ -2784,6 +2793,11 @@ fn dag_grammar_root() -> GrammarRoot { expression: dag_grammar_field_init_expr(), emitted: ^dag_surface_field_init ) + let field_init_string_key = dag_grammar_production( + name: ^dag_production_field_init_string_key, + expression: dag_grammar_field_init_string_key_expr(), + emitted: ^dag_surface_field_init_string_key + ) let match_expr = dag_grammar_production( name: ^dag_production_match_expr, expression: dag_grammar_match_expr_expr(), @@ -2885,6 +2899,7 @@ fn dag_grammar_root() -> GrammarRoot { block_expr, arg, field_init, + field_init_string_key, match_expr, match_arm_stmt_body, match_arm, diff --git a/src/v2/std/node_query.dag b/src/v2/std/node_query.dag index d8ae05ffbb1..215841c4bf5 100644 --- a/src/v2/std/node_query.dag +++ b/src/v2/std/node_query.dag @@ -348,47 +348,54 @@ fn construct_node_elided(field_edges: List, source: Node) -> Node { construct_node(reference: construct_tag_elided_reference(source: source), field_edges: field_edges, source: source) } -fn construct_tag_target_is_elided(target: Node) -> Bool { - match target.kind { +// WHAT A NODE'S CONSTRUCT TAG IS, READ ONCE. Every reader of the tag -- the reference reader below, +// body lowering's core-substrate test, resolve's construct-tag writer -- matches on this one answer, +// so "is this a construct, and is its tag authored or elided" has a single classifier rather than a +// Bool predicate beside a reader that re-reads the tag. +type ConstructTagReading + = ConstructTagAuthored { reference: Node } + | ConstructTagElided + | NotAConstruct + +fn construct_tag_reading(n: Node) -> ConstructTagReading { + match n.kind { TypeNode { connective: Conj } => - match first(target.children) { + match first(n.children) { Present { value: e } => match e.label { - Named { name: label } => symbol_eq(a: label, b: construct_tag_elided_marker()) - Positional => false + Named { name: label } => + if symbol_eq(a: label, b: construct_tag_marker()) { + construct_tag_reading_of_target(target: e.target) + } else { + NotAConstruct + } + Positional => NotAConstruct } - Absent => false + Absent => NotAConstruct } - _ => false - } -} - -// The one reader of the elided case. A construct whose tag is elided answers true; an authored or -// resolved construct, and anything that is not a construct, answers false. -fn construct_tag_is_elided(n: Node) -> Bool { - match construct_tag_edge_target_optional(n: n) { - Present { value: target } => construct_tag_target_is_elided(target: target) - Absent => false + _ => NotAConstruct } } -fn construct_tag_edge_target_optional(n: Node) -> Optional { - match n.kind { +// The tag edge's target is either the elided reference (its first edge carries +// v2.std.node construct_tag_elided_marker) or an authored/resolved reference. +fn construct_tag_reading_of_target(target: Node) -> ConstructTagReading { + match target.kind { TypeNode { connective: Conj } => - match first(n.children) { + match first(target.children) { Present { value: e } => match e.label { Named { name: label } => - if symbol_eq(a: label, b: construct_tag_marker()) { - optional_present(value: e.target) + if symbol_eq(a: label, b: construct_tag_elided_marker()) { + ConstructTagElided } else { - optional_absent() + ConstructTagAuthored { reference: target } } - Positional => optional_absent() + Positional => ConstructTagAuthored { reference: target } } - Absent => optional_absent() + Absent => ConstructTagAuthored { reference: target } } - _ => optional_absent() + _ => ConstructTagAuthored { reference: target } } } @@ -397,14 +404,10 @@ fn construct_tag_edge_target_optional(n: Node) -> Optional { // answer is the reference NODE, at whichever stage the tree is: a spine before resolution, a // declaration reference after it. fn construct_tag_optional(n: Node) -> Optional { - match construct_tag_edge_target_optional(n: n) { - Present { value: target } => - if construct_tag_target_is_elided(target: target) { - optional_absent() - } else { - optional_present(value: target) - } - Absent => optional_absent() + match construct_tag_reading(n: n) { + ConstructTagAuthored { reference: reference } => optional_present(value: reference) + ConstructTagElided => optional_absent() + NotAConstruct => optional_absent() } } diff --git a/src/v2/std/symbol_index.dag b/src/v2/std/symbol_index.dag index 8521c50b076..f4594f319c5 100644 --- a/src/v2/std/symbol_index.dag +++ b/src/v2/std/symbol_index.dag @@ -28,6 +28,7 @@ import v2.std.optional { import v2.std.diagnostic { Accepted, Rejected } import v2.std.logic { Bool } import v2.std.node { Node, Symbol } +import v2.std.type_binder { AliasRenaming, NotARenaming, PureRenamingOf, type_alias_renaming } import v2.std.qualified_name { QualifiedName, qualified_name_init, @@ -84,6 +85,7 @@ type SymbolIndex { global_bare: Map bound_declarings: Map> declared_payloads: Map + renaming_aliases: Map } // WHICH KIND OF DECLARED PAYLOAD A PATH DENOTES, stored as the map's value so the map cannot hold a @@ -101,7 +103,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(), + renaming_aliases: empty_map() } } @@ -176,7 +179,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, + renaming_aliases: index.renaming_aliases } Present { value: GlobalBareUnique { node: existing, path: existing_path } } => if existing_path == qualified_path && existing == resolved { @@ -192,7 +196,8 @@ fn symbol_index_track_global_bare( } ), bound_declarings: index.bound_declarings, - declared_payloads: index.declared_payloads + declared_payloads: index.declared_payloads, + renaming_aliases: index.renaming_aliases } } Absent => @@ -204,7 +209,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, + renaming_aliases: index.renaming_aliases } } Rejected { diagnostics: _ } => index @@ -212,6 +218,13 @@ fn symbol_index_track_global_bare( } } +// THE RENAMED HEAD OF A PURE-RENAMING ALIAS, as recorded when the declaration was recorded +// (v2.std.type_binder type_alias_renaming, decided once on the declaration). Absent for every other +// path, including an alias that reorders, drops or fixes its binders. +fn symbol_index_renaming_alias_at(index: SymbolIndex, qualified_path: QualifiedName) -> Optional { + map_lookup(m: index.renaming_aliases, key: qualified_path) +} + fn symbol_index_insert(index: SymbolIndex, qualified_path: QualifiedName, resolved: Node) -> SymbolIndex { let with_entry = SymbolIndex { entries: map_insert( @@ -221,7 +234,11 @@ 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, + renaming_aliases: match type_alias_renaming(target: resolved) { + PureRenamingOf { head: head } => map_insert(m: index.renaming_aliases, key: qualified_path, value: head) + NotARenaming => index.renaming_aliases + } } symbol_index_track_global_bare( index: with_entry, @@ -247,7 +264,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), + renaming_aliases: index.renaming_aliases } } @@ -366,14 +384,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, + renaming_aliases: index.renaming_aliases } 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, + renaming_aliases: recorded.renaming_aliases } } else { recorded diff --git a/src/v2/std/type_binder.dag b/src/v2/std/type_binder.dag index 637dc8e6b11..8b333aade7f 100644 --- a/src/v2/std/type_binder.dag +++ b/src/v2/std/type_binder.dag @@ -1,11 +1,13 @@ module v2.std.type_binder -import std.algebra { Empty, list_snoc_item } +import std.algebra { Cons, Empty, list_snoc_item } import v2.std.collection { List } import v2.std.logic { Bool } import v2.std.optional { Optional, Present, Absent, optional_present, optional_absent } import v2.std.node { Arrow, + Atom, + Instantiation, Behavior, Bind, ComputationNode, @@ -423,3 +425,71 @@ fn type_binder_first_mislabelled(root: Node) -> Optional { } }) } + +// WHETHER AN ALIAS DECLARATION IS A PURE RENAMING, decided once, on the declaration (ruling, +// neat-boar-16, gunbc#12740). `type A = H` -- the aliased expression is a head applied +// to exactly the alias's own binders, in declared order, or `type A = H` with none -- denotes H with +// the same arguments in the same positions, so a reader that follows it substitutes nothing. Every +// other declaration is not a renaming: a reordering, constant or partially applied alias, a +// generic record, a plain type. v2.std.symbol_index records the answer when it records the +// declaration, and readers take it from there rather than re-deriving it at each use. +type AliasRenaming + = PureRenamingOf { head: Symbol } + | NotARenaming + +fn type_alias_renaming(target: Node) -> AliasRenaming { + match type_decl_view(target: target) { + TypeAlias { binders: binders, aliased: aliased } => + type_alias_renaming_of(binders: binder_conj_names(conj: binders), aliased: aliased) + _ => NotARenaming + } +} + +fn type_alias_renaming_of(binders: List, aliased: Node) -> AliasRenaming { + match aliased.kind { + TypeNode { connective: Atom { identity: id } } => + if count(binders) == 0 { PureRenamingOf { head: id } } else { NotARenaming } + TypeNode { connective: Instantiation } => + let scan = type_binder_instantiation_scan(n: aliased) + match scan.head { + Present { value: head } => + if scan.args_are_atoms && (scan.arg_ids == binders) { PureRenamingOf { head: head } } else { NotARenaming } + Absent => NotARenaming + } + _ => NotARenaming + } +} + +// An instantiation read once, positionally: its head atom (if the head is one), the identities of +// its argument atoms in order, and whether every argument is an atom. +type TypeBinderInstantiationScan { + seen: Int + head: Optional + arg_ids: List + args_are_atoms: Bool +} + +fn type_binder_instantiation_scan(n: Node) -> TypeBinderInstantiationScan { + fold(n.children, init: TypeBinderInstantiationScan { seen: 0, head: optional_absent(), arg_ids: Empty, args_are_atoms: true }, f: fn(acc, e) { + match e.label { + Positional => + let id = type_binder_atom_identity_optional(n: e.target) + if acc.seen == 0 { + TypeBinderInstantiationScan { seen: 1, head: id, arg_ids: acc.arg_ids, args_are_atoms: acc.args_are_atoms } + } else { + match id { + Present { value: a } => TypeBinderInstantiationScan { seen: acc.seen + 1, head: acc.head, arg_ids: list_snoc_item(xs: acc.arg_ids, item: a), args_are_atoms: acc.args_are_atoms } + Absent => TypeBinderInstantiationScan { seen: acc.seen + 1, head: acc.head, arg_ids: acc.arg_ids, args_are_atoms: false } + } + } + _ => acc + } + }) +} + +fn type_binder_atom_identity_optional(n: Node) -> Optional { + match n.kind { + TypeNode { connective: Atom { identity: id } } => optional_present(value: id) + _ => optional_absent() + } +} diff --git a/src/v2/test/claim/body_lowering/anonymous_record_algebra_test.dag b/src/v2/test/claim/body_lowering/anonymous_record_algebra_test.dag new file mode 100644 index 00000000000..f8dc93c58ec --- /dev/null +++ b/src/v2/test/claim/body_lowering/anonymous_record_algebra_test.dag @@ -0,0 +1,16 @@ +module v2.test.claim.body_lowering.anonymous_record_algebra + +import std.algebra { ordered_ring_templates, free_monoid_scalar_templates } +import v2.std.algebra { length } +import v2.std.logic { Bool } + +// THE REAL CORPUS'S HEADLESS LIST ROWS ELABORATE ON THE NATIVE ROUTE (ruling, neat-boar-16, for +// gunbc#12740). std.algebra writes its algebra templates as headless records inside list literals +// under `-> List`, and List is the pure-renaming alias +// `type List = FreeMonoid`. On the native route this module's closure resolves +// std.algebra, so if those rows did not elaborate -- no expectation through the alias, or a wrong +// constructor -- the module refuses at prepare with that cause, and this claim is not green. The +// counts are the rows each function authors, so a row lost in lowering also reds it. +test fn std_algebra_list_rows_elaborate_on_the_native_route_holds() -> Bool { + (length(xs: ordered_ring_templates()) == 7) && (length(xs: free_monoid_scalar_templates()) == 19) +} diff --git a/src/v2/test/claim/body_lowering/anonymous_record_test.dag b/src/v2/test/claim/body_lowering/anonymous_record_test.dag index cf826f3b070..a756f534ee2 100644 --- a/src/v2/test/claim/body_lowering/anonymous_record_test.dag +++ b/src/v2/test/claim/body_lowering/anonymous_record_test.dag @@ -21,13 +21,16 @@ import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } import v2.std.optional { Absent, Present } import v2.std.node { Hash, Node, Symbol, content_hash, node_subtree_nodes, symbol_lexeme } -import v2.std.node_query { construct_tag_is_elided, construct_tag_optional, construct_tag_path_optional } +import v2.std.node_query { ConstructTagAuthored, ConstructTagElided, NotAConstruct, construct_tag_reading, construct_tag_path_optional } import v2.std.qualified_name { qualified_name_from_dotted_string, qualified_name_to_dotted_string } import v2.std.algebra { length } +import v2.std.integer { Int } import v2.std.text { String } import v2.std.collection { List } import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } import extdeps.communication.medium { Lossless, Medium } +import v2.test.claim.namespace_xl0.reference_conservation { conservation_subject } +import v2.extdeps.languages.dag { parse_production_emitted_identity_optional } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -67,6 +70,35 @@ data ar_value_mismatch_source: String = "module v2.test.ar_value_mismatch\n\nimp data ar_value_mismatch_headed_source: String = "module v2.test.ar_value_mismatch_headed\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArB }\n\ndata ar_bad: ArB = ArB { n: true, m: 3 }\n" +// THE LIST-ELEMENT RULE AND WHERE CONDITION 1 DRAWS ITS LINE. A list literal's element type is +// passed to its elements only when the annotation's head resolves to the one declaration infer types +// list literals with (v2.std.list_introduction list_introduction_head_path, std.algebra.FreeMonoid). +// The fixture declares that module so the path exists in this ingest. A head reaches it directly, +// or through PURE-RENAMING aliases only (ruling, neat-boar-16; see the renaming claims below). +data ar_algebra_source: String = "module std.algebra\n\ntype FreeMonoid\n = Empty\n | Cons { head: T, tail: FreeMonoid }\n" + +data ar_seq_source: String = "module v2.test.arseq\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\n\ntype ArSeq = FreeMonoid\n\ntype List = FreeMonoid\n\ntype ArPair {\n first: x\n second: y\n}\n\ntype ArPairS = ArPair\n\ntype ArPairR = ArPair\n\ntype ArPairC = ArPair\n\ntype ArPairP = ArPair\n\ntype ArCycA = ArCycB\n\ntype ArCycB = ArCycA\n" + +// PURE RENAMINGS ELABORATE; ANY OTHER ALIAS REFUSES (ruling, neat-boar-16). ArSeq and List rename +// FreeMonoid; ArPairS renames ArPair with its binders written b, a -- a renaming because its +// arguments are its binders IN ITS OWN ORDER, which also reds if binder order were ever sorted away. +// ArPairR reorders, ArPairC is constant, ArPairP is partially applied, and ArCycA/ArCycB are a cycle. +data ar_rename_headed_source: String = "module v2.test.ar_rename_headed\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArB }\nimport v2.test.arseq { ArSeq, List, ArPair, ArPairS }\n\nfn ar_seq() -> ArSeq { [ArB { n: true, m: false }] }\n\nfn ar_list() -> List { [ArB { n: false, m: true }] }\n\ndata ar_pair: ArPairS = ArPair { first: true, second: false }\n" + +data ar_rename_headless_source: String = "module v2.test.ar_rename_headless\n\nimport v2.std.logic { Bool }\nimport v2.test.arp { ArB }\nimport v2.test.arseq { ArSeq, List, ArPair, ArPairS }\n\nfn ar_seq() -> ArSeq { [{ n: true, m: false }] }\n\nfn ar_list() -> List { [{ n: false, m: true }] }\n\ndata ar_pair: ArPairS = { first: true, second: false }\n" + +data ar_reorder_source: String = "module v2.test.ar_reorder\n\nimport v2.std.logic { Bool }\nimport v2.test.arseq { ArPairR }\n\ndata ar_r: ArPairR = { first: true, second: false }\n" + +data ar_constant_source: String = "module v2.test.ar_constant\n\nimport v2.std.logic { Bool }\nimport v2.test.arseq { ArPairC }\n\ndata ar_c: ArPairC = { first: true, second: false }\n" + +data ar_partial_source: String = "module v2.test.ar_partial\n\nimport v2.std.logic { Bool }\nimport v2.test.arseq { ArPairP }\n\ndata ar_p: ArPairP = { first: true, second: false }\n" + +data ar_cycle_source: String = "module v2.test.ar_cycle\n\nimport v2.std.logic { Bool }\nimport v2.test.arseq { ArCycA }\n\ndata ar_y: ArCycA = { first: true }\n" + +data ar_list_headed_source: String = "module v2.test.ar_list_headed\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\nimport v2.test.arp { ArB }\n\nfn ar_list() -> FreeMonoid { [ArB { n: true, m: false }, ArB { n: false, m: false }] }\n" + +data ar_list_headless_source: String = "module v2.test.ar_list_headless\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\nimport v2.test.arp { ArB }\n\nfn ar_list() -> FreeMonoid { [{ n: true, m: false }, { n: false, m: false }] }\n" + fn ar_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceReadWitness { DagSourceReadWitness { source: Medium { carried: source, fidelity: Lossless }, @@ -84,7 +116,17 @@ data ar_ingest: SourceRootIngest = [ ar_read(source: ar_coproduct_source, id: ^ar_coproduct_read, unit: ^ar_coproduct_cu, path: "src/v2/test/fixture/anonymous_record/coproduct.dag"), ar_read(source: ar_binder_source, id: ^ar_binder_read, unit: ^ar_binder_cu, path: "src/v2/test/fixture/anonymous_record/binder.dag"), ar_read(source: ar_value_mismatch_source, id: ^ar_value_mismatch_read, unit: ^ar_value_mismatch_cu, path: "src/v2/test/fixture/anonymous_record/value_mismatch.dag"), - ar_read(source: ar_value_mismatch_headed_source, id: ^ar_value_mismatch_headed_read, unit: ^ar_value_mismatch_headed_cu, path: "src/v2/test/fixture/anonymous_record/value_mismatch_headed.dag") + ar_read(source: ar_value_mismatch_headed_source, id: ^ar_value_mismatch_headed_read, unit: ^ar_value_mismatch_headed_cu, path: "src/v2/test/fixture/anonymous_record/value_mismatch_headed.dag"), + ar_read(source: ar_algebra_source, id: ^ar_algebra_read, unit: ^ar_algebra_cu, path: "src/v2/test/fixture/anonymous_record/algebra.dag"), + ar_read(source: ar_seq_source, id: ^ar_seq_read, unit: ^ar_seq_cu, path: "src/v2/test/fixture/anonymous_record/seq.dag"), + ar_read(source: ar_list_headed_source, id: ^ar_list_headed_read, unit: ^ar_list_headed_cu, path: "src/v2/test/fixture/anonymous_record/list_headed.dag"), + ar_read(source: ar_list_headless_source, id: ^ar_list_headless_read, unit: ^ar_list_headless_cu, path: "src/v2/test/fixture/anonymous_record/list_headless.dag"), + ar_read(source: ar_rename_headed_source, id: ^ar_rename_headed_read, unit: ^ar_rename_headed_cu, path: "src/v2/test/fixture/anonymous_record/rename_headed.dag"), + ar_read(source: ar_rename_headless_source, id: ^ar_rename_headless_read, unit: ^ar_rename_headless_cu, path: "src/v2/test/fixture/anonymous_record/rename_headless.dag"), + ar_read(source: ar_reorder_source, id: ^ar_reorder_read, unit: ^ar_reorder_cu, path: "src/v2/test/fixture/anonymous_record/reorder.dag"), + ar_read(source: ar_constant_source, id: ^ar_constant_read, unit: ^ar_constant_cu, path: "src/v2/test/fixture/anonymous_record/constant.dag"), + ar_read(source: ar_partial_source, id: ^ar_partial_read, unit: ^ar_partial_cu, path: "src/v2/test/fixture/anonymous_record/partial.dag"), + ar_read(source: ar_cycle_source, id: ^ar_cycle_read, unit: ^ar_cycle_cu, path: "src/v2/test/fixture/anonymous_record/cycle.dag") ] fn ar_fixture_normalized() -> Outcome> { @@ -115,17 +157,22 @@ fn ar_resolved_coproduct() -> Outcome { ar_resolve_subject(subject fn ar_resolved_binder() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_binder") } fn ar_resolved_value_mismatch() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_value_mismatch") } fn ar_resolved_value_mismatch_headed() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_value_mismatch_headed") } +fn ar_resolved_list_headed() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_list_headed") } +fn ar_resolved_list_headless() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_list_headless") } +fn ar_resolved_rename_headed() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_rename_headed") } +fn ar_resolved_rename_headless() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_rename_headless") } +fn ar_resolved_reorder() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_reorder") } +fn ar_resolved_constant() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_constant") } +fn ar_resolved_partial() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_partial") } +fn ar_resolved_cycle() -> Outcome { ar_resolve_subject(subject: "v2.test.ar_cycle") } // Every construct under the root, authored or elided, in tree order. fn ar_constructs(root: Node) -> List { fold(node_subtree_nodes(root: root), init: Empty, f: fn(acc, n) { - if construct_tag_is_elided(n: n) { - list_snoc_item(xs: acc, item: n) - } else { - match construct_tag_optional(n: n) { - Present { value: _ } => list_snoc_item(xs: acc, item: n) - Absent => acc - } + match construct_tag_reading(n: n) { + ConstructTagAuthored { reference: _ } => list_snoc_item(xs: acc, item: n) + ConstructTagElided => list_snoc_item(xs: acc, item: n) + NotAConstruct => acc } }) } @@ -229,3 +276,68 @@ test fn ar_infer_verdict_is_the_headed_forms_holds() -> Bool { && (ar_construct_paths(o: headed) == ["v2.test.arp.ArB"]) && (ar_infer_verdict(o: headless) == ar_infer_verdict(o: headed)) } + +// POSITIVE (the direct control, gentle-koi-724): headless elements under an annotated FreeMonoid +// elaborate to EXACTLY their headed twins, by content, and both are tagged ArB. An absence of +// refusals could not tell elaborated from passed-through; this compares the constructs themselves. +test fn ar_list_elements_elaborate_under_the_list_head_holds() -> Bool { + let headed = ar_construct_hashes(o: ar_resolved_list_headed()) + (length(xs: headed) == 2) + && ar_pairwise_equal(xs: headed, ys: ar_construct_hashes(o: ar_resolved_list_headless())) + && (ar_construct_paths(o: ar_resolved_list_headless()) == ["v2.test.arp.ArB", "v2.test.arp.ArB"]) +} + +// PURE RENAMINGS ARE FOLLOWED (ruling, neat-boar-16): under ArSeq, List and ArPairS, each headless literal elaborates to EXACTLY its headed twin, by content. +test fn ar_pure_renaming_aliases_elaborate_holds() -> Bool { + let headed = ar_construct_hashes(o: ar_resolved_rename_headed()) + (length(xs: headed) == 3) + && ar_pairwise_equal(xs: headed, ys: ar_construct_hashes(o: ar_resolved_rename_headless())) + && (ar_construct_paths(o: ar_resolved_rename_headless()) == ["v2.test.arp.ArB", "v2.test.arp.ArB", "v2.test.arseq.ArPair"]) +} + +// A reordering, a constant and a partially applied alias are not renamings, and refuse. +test fn ar_a_non_renaming_alias_refuses_holds() -> Bool { + ar_refuses_with(o: ar_resolved_reorder(), reason: ^resolve_anonymous_record_expected_type_not_record) + && ar_refuses_with(o: ar_resolved_constant(), reason: ^resolve_anonymous_record_expected_type_not_record) + && ar_refuses_with(o: ar_resolved_partial(), reason: ^resolve_anonymous_record_expected_type_not_record) +} + +// A cycle of renamings refuses located, and is not followed. +test fn ar_an_alias_cycle_refuses_holds() -> Bool { + ar_refuses_with(o: ar_resolved_cycle(), reason: ^resolve_anonymous_record_alias_cycle) +} + +// THE KEY'S KIND IS A PRODUCTION, NOT A SPELLING (review 73139, ruled by gentle-koi-724). A quoted key +// and a bare key with the same spelling parse through DIFFERENT productions: only the quoted one +// carries ^dag_surface_field_init_string_key. A grammar that merged the two alternatives back into +// one production -- or classified the key by its text -- makes both counts equal and reds this. +data ar_key_bare_source: String = "module v2.test.ar_key_bare\n\ndata ar_k: Bool = { n: true }\n" + +data ar_key_quoted_source: String = "module v2.test.ar_key_quoted\n\ndata ar_k: Bool = { \"n\": true }\n" + +fn ar_string_key_production_count(source: String, tail: String) -> Int { + match conservation_subject(id_tail: tail, source: source).parsed { + Rejected { diagnostics: _ } => 0 - 1 + Accepted { value: a, diagnostics: _ } => + fold(node_subtree_nodes(root: a.tree), init: 0, f: fn(acc, n) { + match parse_production_emitted_identity_optional(node: n) { + Present { value: id } => if id == ^dag_surface_field_init_string_key { acc + 1 } else { acc } + Absent => acc + } + }) + } +} + +test fn ar_a_quoted_key_takes_the_string_key_production_holds() -> Bool { + (ar_string_key_production_count(source: ar_key_bare_source, tail: "ar_key_bare") == 0) + && (ar_string_key_production_count(source: ar_key_quoted_source, tail: "ar_key_quoted") == 1) +} + +// And the record arm refuses the quoted key at the item, never reading it as field `n`. +test fn ar_a_quoted_key_is_not_a_record_field_holds() -> Bool { + match conservation_subject(id_tail: "ar_key_quoted_n", source: ar_key_quoted_source).normalized { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_field_init_unlowered + } +} diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index 85ddb2690aa..8061d814e7a 100644 --- a/src/v2/workflow/compile_door_cause_ownership.dag +++ b/src/v2/workflow/compile_door_cause_ownership.dag @@ -276,6 +276,12 @@ data known_frontier_causes: List = [ lane: SharedSelfHostCriticalPath, flip_trigger: "a headless record literal whose authored expected type does not resolve, by syntactic peel and the existing lexical lookup, to a single record (v2.std.symbol_index RecordTypePayload): a coproduct with several constructors, a primitive, a type binder, an optional, or a head reached only through alias unfolding resolve does not do. Refused rather than guessed (docs/plans/anonymous-record-literal-design.md, review condition 1). Flips per site when the source writes the constructor head; a Map head is the map arm (gunbc#12734), which takes that population when it lands" }, + CauseOwnership { + cause: ^resolve_anonymous_record_alias_cycle, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a headless record literal whose authored expected type reaches a cycle of pure-renaming aliases (`type A = B`, `type B = A`) while v2.compiler.resolve follows renamings to the record or list head (ruling, neat-boar-16, gunbc#12740). The cycle is refused at the literal rather than followed. Not a frontier: each occurrence is an alias cycle in source, and flips when the source breaks it" + }, CauseOwnership { cause: ^body_lowering_reason_function_value_body_unread, grain: FatalGrain, diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 93e9dd30c4a..3cd87b00d76 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -1038,6 +1038,14 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.body_lowering.anonymous_record.ar_resolved_binder", "v2.test.claim.body_lowering.anonymous_record.ar_resolved_value_mismatch", "v2.test.claim.body_lowering.anonymous_record.ar_resolved_value_mismatch_headed", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_list_headed", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_list_headless", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_rename_headed", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_rename_headless", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_reorder", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_constant", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_partial", + "v2.test.claim.body_lowering.anonymous_record.ar_resolved_cycle", "v2.test.claim.namespace_xl0.cross_module_reference_resolution.xl0r_fixture_normalized", "v2.test.claim.namespace_xl0.cross_module_reference_resolution.xl0r_resolved_local_consumer", "v2.test.claim.namespace_xl0.cross_module_reference_resolution.xl0r_resolved_declared_consumer", From fea1bb83f8e09adee26d69f497850256c246d17c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 10:55:42 +0000 Subject: [PATCH 52/63] PR2: adapt to #12759 (landed first): every string terminal decodes; the spelling-stamped quoted-key arm has no producer and is deleted Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/02_parse.dag | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/v2/compiler/02_parse.dag b/src/v2/compiler/02_parse.dag index 6be20f3f105..1645ffab39a 100644 --- a/src/v2/compiler/02_parse.dag +++ b/src/v2/compiler/02_parse.dag @@ -3328,9 +3328,10 @@ fn parse_expr_terminal_stamped( // A STRING VALUE CARRIES ITS DECODED VALUE (v2.extdeps.languages.dag // dag_string_literal_node_from_lexeme), refusing located on an unframed lexeme or a malformed -// escape, exactly as the int-literal arm of parse_expr_terminal decodes its magnitude. Only a -// class-stamped string terminal is a value: a lexeme-stamped one is a quoted record key, whose -// identity is its spelling, and parse_expr_terminal keeps it on parse_expr_terminal_stamped. +// escape, exactly as the int-literal arm of parse_expr_terminal decodes its magnitude. Every string +// terminal is decoded here, whatever its stamp: a quoted field-init key is the class-stamped terminal +// of its own production (v2.extdeps.languages.dag dag_grammar_field_init_string_key_expr), so there is +// no spelling-stamped string terminal left to keep apart. fn parse_expr_string_literal_value( tok: Token, remaining: List, @@ -3413,13 +3414,7 @@ fn parse_expr_terminal( } } } else if token_class == ^dag_token_string_literal { - match stamp { - StampClass => - parse_expr_string_literal_value(tok: tok, remaining: remaining, table: table, prov: prov) - StampLexeme => parse_expr_terminal_stamped(tok: tok, stamp: stamp, remaining: remaining, table: table, prov: prov) - StampBinding { binding: _ } => - parse_expr_terminal_stamped(tok: tok, stamp: stamp, remaining: remaining, table: table, prov: prov) - } + parse_expr_string_literal_value(tok: tok, remaining: remaining, table: table, prov: prov) } else if token_class == ^dag_token_float_literal { let minted = parse_mint_from_source( prov: prov, From 36280787be30c31b7944e2ed8e158c0ae5b5ed94 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 11:47:27 +0000 Subject: [PATCH 53/63] PR2: the design doc is in the tree (merge PR1 branch) and records the model as built; comments cite its sections (review 73185) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/plans/anonymous-record-literal-design.md | 58 +++++++++++++++++++ src/v2/compiler/03_resolve.dag | 3 +- src/v2/std/type_binder.dag | 4 +- .../anonymous_record_algebra_test.dag | 3 +- .../body_lowering/anonymous_record_test.dag | 12 ++-- .../workflow/compile_door_cause_ownership.dag | 2 +- 6 files changed, 70 insertions(+), 12 deletions(-) diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md index e6b4ddb1220..7cb43d6b562 100644 --- a/docs/plans/anonymous-record-literal-design.md +++ b/docs/plans/anonymous-record-literal-design.md @@ -154,6 +154,64 @@ A tag-elided construct that survives resolve is unwritable downstream. The emit, readers gate on a reference target, and the elision marker is not one. So a missed elaboration refuses at their existing gates, and cannot emit an anonymous struct. +## As built in PR2 (gunbc#12740) + +The model above was refined while the cut was reviewed. Each refinement below is the authority that +the PR2 code cites by section name. + +### One reader of the construct tag + +`v2.std.node_query` `construct_tag_reading` answers +`ConstructTagAuthored { reference } | ConstructTagElided | NotAConstruct`. Every reader (body +lowering's core-substrate test, resolve's construct-tag writer, the claims) matches on that one +answer. `construct_tag_optional` is its projection. The elision is carried as an EDGE LABEL +(`construct_tag_elided_marker`) on the tag's target, the mechanism the tag marker itself uses. A +marker carried as an atom's identity did not survive to resolve. And body lowering must count an +elided construct as core substrate, or its fold re-lowers the literal to its first atom. + +### The string key is a grammar production + +`dag_grammar_field_init_expr` is a choice between a name alternative and a production of its own, +`^dag_production_field_init_string_key` (emitted `^dag_surface_field_init_string_key`), whose key is +the class-stamped string terminal that gunbc#12759 decodes. So the key's kind is a fact of the parse +tree, read by its production identity (`body_lower_field_init_is_string_keyed`), and never recovered +from a spelling. A record literal refuses a string key at the item (`field_init_unlowered`). The +occurrence-role row of the new production reads its names as references, and the field-init reader +answers `NoNameHere` for it. The map arm (gunbc#12758) dispatches on the same identity. Admitting a +quoted key whose DECODED text names a declared field is a declared frontier. It is a follow-up in the +record arm, and it also carries the quoted-key round trip; the first lane that admits quoted keys +(gunbc#12758) carries that round trip for maps. + +### The head resolves the way the annotation resolves + +An annotation written in the literal's own module resolves its head through `resolve_atom` in the +literal's context, the same route the annotation itself takes, so the two cannot bind different +declarations. A field type is written in the record's own module, so it is read at the record's path +through the index's lexical lookup. + +### The list-element rule + +A list literal passes its element type to its elements only when the annotation's head reaches the +declaration infer types list literals with (`v2.std.list_introduction` +`list_introduction_head_path`, `std.algebra.FreeMonoid`). + +### Pure-renaming aliases + +`List` is an alias (`type List = FreeMonoid`), so the list rule needs one kind of +alias to be followed. Exactly one kind is followed: a PURE RENAMING, `type A = H` +(the aliased expression is a head applied to exactly the alias's own binders, in declared order) or +`type A = H`. Following one substitutes nothing, because the annotation's arguments stay in the +positions they were written. + +- Purity is decided ONCE, on the declaration (`v2.std.type_binder` `type_alias_renaming`). It is + recorded when the index records the declaration (`v2.std.symbol_index` `renaming_aliases`, + `symbol_index_renaming_alias_at`), and resolve reads the recorded fact, never re-deriving it at a + use. +- Chains are followed transitively, each renamed head read at the alias's own position. A revisited + path refuses located (`resolve_anonymous_record_alias_cycle`). +- A reordering, constant or partially applied alias is not a renaming. It is the declaration the head + names, so the literal refuses. + ## Consumers (producers, readers, rewriters) Stated as a delta on #12701's census. Every row there still applies; the rows here are the ones diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index ef8d78ee307..5cf4bd31cdf 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1753,7 +1753,8 @@ fn resolve_expected_head_path(ctx: ResolveContext, x: ResolveExpectation) -> Exp } } -// A PURE-RENAMING ALIAS IS FOLLOWED; NO OTHER ALIAS IS (ruling, neat-boar-16). Whether an alias is a +// A PURE-RENAMING ALIAS IS FOLLOWED; NO OTHER ALIAS IS (docs/plans/anonymous-record-literal-design.md, +// section Pure-renaming aliases). Whether an alias is a // pure renaming -- `type A = H`, or `type A = H` -- was decided once, on its // declaration, and recorded in the index (v2.std.type_binder type_alias_renaming, // v2.std.symbol_index symbol_index_renaming_alias_at); this reads that fact and does not re-derive diff --git a/src/v2/std/type_binder.dag b/src/v2/std/type_binder.dag index 8b333aade7f..f24ab88c193 100644 --- a/src/v2/std/type_binder.dag +++ b/src/v2/std/type_binder.dag @@ -426,8 +426,8 @@ fn type_binder_first_mislabelled(root: Node) -> Optional { }) } -// WHETHER AN ALIAS DECLARATION IS A PURE RENAMING, decided once, on the declaration (ruling, -// neat-boar-16, gunbc#12740). `type A = H` -- the aliased expression is a head applied +// WHETHER AN ALIAS DECLARATION IS A PURE RENAMING, decided once, on the declaration +// (docs/plans/anonymous-record-literal-design.md, section Pure-renaming aliases). `type A = H` -- the aliased expression is a head applied // to exactly the alias's own binders, in declared order, or `type A = H` with none -- denotes H with // the same arguments in the same positions, so a reader that follows it substitutes nothing. Every // other declaration is not a renaming: a reordering, constant or partially applied alias, a diff --git a/src/v2/test/claim/body_lowering/anonymous_record_algebra_test.dag b/src/v2/test/claim/body_lowering/anonymous_record_algebra_test.dag index f8dc93c58ec..32e0e55e12e 100644 --- a/src/v2/test/claim/body_lowering/anonymous_record_algebra_test.dag +++ b/src/v2/test/claim/body_lowering/anonymous_record_algebra_test.dag @@ -4,8 +4,7 @@ import std.algebra { ordered_ring_templates, free_monoid_scalar_templates } import v2.std.algebra { length } import v2.std.logic { Bool } -// THE REAL CORPUS'S HEADLESS LIST ROWS ELABORATE ON THE NATIVE ROUTE (ruling, neat-boar-16, for -// gunbc#12740). std.algebra writes its algebra templates as headless records inside list literals +// THE REAL CORPUS'S HEADLESS LIST ROWS ELABORATE ON THE NATIVE ROUTE (docs/plans/anonymous-record-literal-design.md, Pure-renaming aliases). std.algebra writes its algebra templates as headless records inside list literals // under `-> List`, and List is the pure-renaming alias // `type List = FreeMonoid`. On the native route this module's closure resolves // std.algebra, so if those rows did not elaborate -- no expectation through the alias, or a wrong diff --git a/src/v2/test/claim/body_lowering/anonymous_record_test.dag b/src/v2/test/claim/body_lowering/anonymous_record_test.dag index a756f534ee2..4324931c5d1 100644 --- a/src/v2/test/claim/body_lowering/anonymous_record_test.dag +++ b/src/v2/test/claim/body_lowering/anonymous_record_test.dag @@ -74,12 +74,12 @@ data ar_value_mismatch_headed_source: String = "module v2.test.ar_value_mismatch // passed to its elements only when the annotation's head resolves to the one declaration infer types // list literals with (v2.std.list_introduction list_introduction_head_path, std.algebra.FreeMonoid). // The fixture declares that module so the path exists in this ingest. A head reaches it directly, -// or through PURE-RENAMING aliases only (ruling, neat-boar-16; see the renaming claims below). +// or through PURE-RENAMING aliases only (docs/plans/anonymous-record-literal-design.md, Pure-renaming aliases). data ar_algebra_source: String = "module std.algebra\n\ntype FreeMonoid\n = Empty\n | Cons { head: T, tail: FreeMonoid }\n" data ar_seq_source: String = "module v2.test.arseq\n\nimport v2.std.logic { Bool }\nimport std.algebra { FreeMonoid }\n\ntype ArSeq = FreeMonoid\n\ntype List = FreeMonoid\n\ntype ArPair {\n first: x\n second: y\n}\n\ntype ArPairS = ArPair\n\ntype ArPairR = ArPair\n\ntype ArPairC = ArPair\n\ntype ArPairP = ArPair\n\ntype ArCycA = ArCycB\n\ntype ArCycB = ArCycA\n" -// PURE RENAMINGS ELABORATE; ANY OTHER ALIAS REFUSES (ruling, neat-boar-16). ArSeq and List rename +// PURE RENAMINGS ELABORATE; ANY OTHER ALIAS REFUSES (design doc, Pure-renaming aliases). ArSeq and List rename // FreeMonoid; ArPairS renames ArPair with its binders written b, a -- a renaming because its // arguments are its binders IN ITS OWN ORDER, which also reds if binder order were ever sorted away. // ArPairR reorders, ArPairC is constant, ArPairP is partially applied, and ArCycA/ArCycB are a cycle. @@ -250,7 +250,7 @@ test fn ar_a_binder_field_type_is_not_substituted_holds() -> Bool { ar_refuses_with(o: ar_resolved_binder(), reason: ^resolve_anonymous_record_expected_type_not_record) } -// Control 7 (review condition 2, as ruled by gentle-koi-724): resolve's choice is not proof, so an +// Control 7 (design doc, Resolve's choice is not proof): resolve's choice is not proof, so an // elaborated construct gets EXACTLY the infer verdict of its headed spelling -- here on a field // value of the wrong type (`m: 3` where ArB declares Bool). Both constructs must be tagged ArB, so a // headless literal that failed to elaborate cannot pass by reaching the same verdict another way. @@ -277,7 +277,7 @@ test fn ar_infer_verdict_is_the_headed_forms_holds() -> Bool { && (ar_infer_verdict(o: headless) == ar_infer_verdict(o: headed)) } -// POSITIVE (the direct control, gentle-koi-724): headless elements under an annotated FreeMonoid +// POSITIVE (the direct control for the list-element rule): headless elements under an annotated FreeMonoid // elaborate to EXACTLY their headed twins, by content, and both are tagged ArB. An absence of // refusals could not tell elaborated from passed-through; this compares the constructs themselves. test fn ar_list_elements_elaborate_under_the_list_head_holds() -> Bool { @@ -287,7 +287,7 @@ test fn ar_list_elements_elaborate_under_the_list_head_holds() -> Bool { && (ar_construct_paths(o: ar_resolved_list_headless()) == ["v2.test.arp.ArB", "v2.test.arp.ArB"]) } -// PURE RENAMINGS ARE FOLLOWED (ruling, neat-boar-16): under ArSeq, List and ArPairS, List and ArPairS, each headless literal elaborates to EXACTLY its headed twin, by content. test fn ar_pure_renaming_aliases_elaborate_holds() -> Bool { let headed = ar_construct_hashes(o: ar_resolved_rename_headed()) @@ -308,7 +308,7 @@ test fn ar_an_alias_cycle_refuses_holds() -> Bool { ar_refuses_with(o: ar_resolved_cycle(), reason: ^resolve_anonymous_record_alias_cycle) } -// THE KEY'S KIND IS A PRODUCTION, NOT A SPELLING (review 73139, ruled by gentle-koi-724). A quoted key +// THE KEY'S KIND IS A PRODUCTION, NOT A SPELLING (design doc, The string key is a grammar production). A quoted key // and a bare key with the same spelling parse through DIFFERENT productions: only the quoted one // carries ^dag_surface_field_init_string_key. A grammar that merged the two alternatives back into // one production -- or classified the key by its text -- makes both counts equal and reds this. diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index d49970beacd..9dbbab1ef11 100644 --- a/src/v2/workflow/compile_door_cause_ownership.dag +++ b/src/v2/workflow/compile_door_cause_ownership.dag @@ -280,7 +280,7 @@ data known_frontier_causes: List = [ cause: ^resolve_anonymous_record_alias_cycle, grain: FatalGrain, lane: SharedSelfHostCriticalPath, - flip_trigger: "a headless record literal whose authored expected type reaches a cycle of pure-renaming aliases (`type A = B`, `type B = A`) while v2.compiler.resolve follows renamings to the record or list head (ruling, neat-boar-16, gunbc#12740). The cycle is refused at the literal rather than followed. Not a frontier: each occurrence is an alias cycle in source, and flips when the source breaks it" + flip_trigger: "a headless record literal whose authored expected type reaches a cycle of pure-renaming aliases (`type A = B`, `type B = A`) while v2.compiler.resolve follows renamings to the record or list head (docs/plans/anonymous-record-literal-design.md, Pure-renaming aliases). The cycle is refused at the literal rather than followed. Not a frontier: each occurrence is an alias cycle in source, and flips when the source breaks it" }, CauseOwnership { cause: ^body_lowering_reason_function_value_body_unread, From 0520ea8074c52db371d914ea7e100e837e1b8e23 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 11:52:51 +0000 Subject: [PATCH 54/63] Preserve the string-key field_init shell as field_init is preserved (the map arm reads it; was retained as a wrapper and refused at normalize) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/body_lowering_fold.dag | 1 + 1 file changed, 1 insertion(+) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 87b7d66bae5..a209254d091 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -707,6 +707,7 @@ fn body_lower_is_structure_preserved_emitted(emitted: Symbol) -> Bool { || (emitted == ^dag_surface_admit_callers_clause) || (emitted == ^dag_surface_fn_type) || (emitted == ^dag_surface_field_init) + || (emitted == ^dag_surface_field_init_string_key) || (emitted == ^dag_surface_where_refinement_clause) || (emitted == ^dag_surface_where_predicate) || (emitted == ^dag_surface_arrow_lambda) From 7a44f8fd6c1a37e16a33f8a1d25c19d18b924ba4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 30 Sep 2026 11:53:13 +0000 Subject: [PATCH 55/63] PR2: the string-key production is structure-preserved like field_init (found by jolly-boar-246 via #12758) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/body_lowering_fold.dag | 1 + 1 file changed, 1 insertion(+) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 8ab991e9cd5..b1179dc4388 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -704,6 +704,7 @@ fn body_lower_is_structure_preserved_emitted(emitted: Symbol) -> Bool { || (emitted == ^dag_surface_admit_callers_clause) || (emitted == ^dag_surface_fn_type) || (emitted == ^dag_surface_field_init) + || (emitted == ^dag_surface_field_init_string_key) || (emitted == ^dag_surface_where_refinement_clause) || (emitted == ^dag_surface_where_predicate) || (emitted == ^dag_surface_arrow_lambda) From 55a06029dc394b05a74361f5b7dfa454f5eb9777 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 12:02:17 +0000 Subject: [PATCH 56/63] Revert "Preserve the string-key field_init shell as field_init is preserved (the map arm reads it; was retained as a wrapper and refused at normalize)" This reverts commit 0520ea8074c52db371d914ea7e100e837e1b8e23. --- src/v2/compiler/body_lowering_fold.dag | 1 - 1 file changed, 1 deletion(-) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index a209254d091..87b7d66bae5 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -707,7 +707,6 @@ fn body_lower_is_structure_preserved_emitted(emitted: Symbol) -> Bool { || (emitted == ^dag_surface_admit_callers_clause) || (emitted == ^dag_surface_fn_type) || (emitted == ^dag_surface_field_init) - || (emitted == ^dag_surface_field_init_string_key) || (emitted == ^dag_surface_where_refinement_clause) || (emitted == ^dag_surface_where_predicate) || (emitted == ^dag_surface_arrow_lambda) From d6585be0f2dc9a1891b31f13b76bb1c9c07c3198 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 12:22:35 +0000 Subject: [PATCH 57/63] Split control 7: infer refuses an elaborated mismatch (expected-red on #12760) / an elaborated map infers (expected-red on infer typing the introduction call; headed spelling fails too) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/explicit_witness_admission.dag | 12 ++++++++++-- .../test/claim/body_lowering/map_literal_test.dag | 15 +++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/dag/gunbc/explicit_witness_admission.dag b/dag/gunbc/explicit_witness_admission.dag index 4b7968e2663..3cd34c047ad 100644 --- a/dag/gunbc/explicit_witness_admission.dag +++ b/dag/gunbc/explicit_witness_admission.dag @@ -227,12 +227,20 @@ data explicit_witness_admissions: List = [ ), known_red_probe( entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", - f: "ml_infer_still_checks_an_elaborated_map_holds", + f: "ml_infer_refuses_an_elaborated_value_mismatch_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (infer checks an elaborated map's values against V), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + reason: "map arm control under Map (infer refuses an elaborated map whose value does not inhabit V), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), + known_red_probe( + entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", + f: "ml_an_elaborated_map_infers_holds", + kind: CorpusWitnessKind, + budget: FastLaneEvalBudget, + reason: "map arm control: a well-typed elaborated map infers. RED beyond the map arm, measured with gunbc#12760 and gunbc#12759 merged: the HEADED spelling, map_from_entries(entries: [MapIntroductionEntry { key: .., value: .. }, ..]) under Map, does not infer either, so infer cannot yet type the introduction call (a generic std.algebra function over a list of entry constructs, checked against the std.types Map alias). Owner: the v2 infer lane; the elaboration itself is proven by ml_headless_literal_equals_the_headed_introduction_holds.", + dissolution: unbound_dissolution(description: "v2 infer accepts the headed spelling map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }]) as a data initializer of declared type Map (it does not today, measured with #12760 merged); this witness greens with it and the row deletes in that change") + ), known_red_probe( entry: "dag/test/claim/declared_type_inhabitance_direct_call_witness_test.dag", f: "w_a_branded_product_refinement_at_an_unrelated_product_formal_still_refuses", diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 45c0d5cd271..efa83b8b117 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -218,11 +218,18 @@ test fn ml_a_call_argument_has_no_expected_type_holds() -> Bool { ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_no_expected_type) } -// Control 7: resolve's choice is not proof. The good literal elaborates and infers; the Int value -// elaborates too and then infer refuses it against V = Bool. -test fn ml_infer_still_checks_an_elaborated_map_holds() -> Bool { +// Control 7: resolve's choice is not proof. The Int value elaborates (resolve accepts it) and +// infer then refuses it against V = Bool. +test fn ml_infer_refuses_an_elaborated_value_mismatch_holds() -> Bool { let bad = ml_resolve_subject(subject: "v2.test.ml_value_mismatch") - ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_headless")) && ml_accepts(o: bad) && !ml_infers(o: bad) + ml_accepts(o: bad) && (ml_infers(o: bad) == false) +} + +// Control 7's positive half: a well-typed elaborated map infers. Measured with #12760 merged: this +// is red for the HEADED spelling too (map_from_entries(entries: [MapIntroductionEntry { .. }]) under +// Map does not infer), so the gap is infer's, not the arm's; its row says so. +test fn ml_an_elaborated_map_infers_holds() -> Bool { + ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_headless")) } fn ml_infers(o: Outcome) -> Bool { From 2a6ef9dbda69faec99181deeb52e2b8d4a0fdce1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 12:46:18 +0000 Subject: [PATCH 58/63] Infer-gap row: record the executed first refusal (infer_grounding_not_derived, Bool and Int alike) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/explicit_witness_admission.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/explicit_witness_admission.dag b/dag/gunbc/explicit_witness_admission.dag index 3cd34c047ad..e7ce1dda637 100644 --- a/dag/gunbc/explicit_witness_admission.dag +++ b/dag/gunbc/explicit_witness_admission.dag @@ -238,7 +238,7 @@ data explicit_witness_admissions: List = [ f: "ml_an_elaborated_map_infers_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control: a well-typed elaborated map infers. RED beyond the map arm, measured with gunbc#12760 and gunbc#12759 merged: the HEADED spelling, map_from_entries(entries: [MapIntroductionEntry { key: .., value: .. }, ..]) under Map, does not infer either, so infer cannot yet type the introduction call (a generic std.algebra function over a list of entry constructs, checked against the std.types Map alias). Owner: the v2 infer lane; the elaboration itself is proven by ml_headless_literal_equals_the_headed_introduction_holds.", + reason: "map arm control: a well-typed elaborated map infers. RED beyond the map arm, measured with gunbc#12760 and gunbc#12759 merged: the HEADED spelling, map_from_entries(entries: [MapIntroductionEntry { key: .., value: .. }, ..]) under Map, does not infer either -- executed first refusal infer_grounding_not_derived, identically under Map, so the v2 Bool symbol fork is not what fires first -- so infer cannot yet type the introduction call (a generic std.algebra function over a list of entry constructs, checked against the std.types Map alias). Owner: the v2 infer lane; the elaboration itself is proven by ml_headless_literal_equals_the_headed_introduction_holds.", dissolution: unbound_dissolution(description: "v2 infer accepts the headed spelling map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }]) as a data initializer of declared type Map (it does not today, measured with #12760 merged); this witness greens with it and the row deletes in that change") ), known_red_probe( From 3d7125d915c6585b1bc1b022d14890cd8f78f126 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 13:00:52 +0000 Subject: [PATCH 59/63] Infer-gap row: located at the generic-record construct; same-module control rules out cross-module reading Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/explicit_witness_admission.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/explicit_witness_admission.dag b/dag/gunbc/explicit_witness_admission.dag index e7ce1dda637..95d149e4725 100644 --- a/dag/gunbc/explicit_witness_admission.dag +++ b/dag/gunbc/explicit_witness_admission.dag @@ -238,7 +238,7 @@ data explicit_witness_admissions: List = [ f: "ml_an_elaborated_map_infers_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control: a well-typed elaborated map infers. RED beyond the map arm, measured with gunbc#12760 and gunbc#12759 merged: the HEADED spelling, map_from_entries(entries: [MapIntroductionEntry { key: .., value: .. }, ..]) under Map, does not infer either -- executed first refusal infer_grounding_not_derived, identically under Map, so the v2 Bool symbol fork is not what fires first -- so infer cannot yet type the introduction call (a generic std.algebra function over a list of entry constructs, checked against the std.types Map alias). Owner: the v2 infer lane; the elaboration itself is proven by ml_headless_literal_equals_the_headed_introduction_holds.", + reason: "map arm control: a well-typed elaborated map infers. RED beyond the map arm, measured with gunbc#12760 and gunbc#12759 merged: the HEADED spelling, map_from_entries(entries: [MapIntroductionEntry { key: .., value: .. }, ..]) under Map, does not infer either -- executed first refusal infer_grounding_not_derived, identically under Map, so the v2 Bool symbol fork is not what fires first. It is located (NodeLocus) at the authored MapIntroductionEntry { .. } construct, and a same-module control (entry record and introduction declared beside the data decl) refuses identically, so it is not cross-module reading: v2 infer cannot ground a construct of a GENERIC record whose type arguments come only from context (the list element / the callee parameter type). Owner: the v2 infer lane; the elaboration itself is proven by ml_headless_literal_equals_the_headed_introduction_holds.", dissolution: unbound_dissolution(description: "v2 infer accepts the headed spelling map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }]) as a data initializer of declared type Map (it does not today, measured with #12760 merged); this witness greens with it and the row deletes in that change") ), known_red_probe( From c4af14756209800ff1282d2802df9cf533ed3601 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 13:01:29 +0000 Subject: [PATCH 60/63] Infer-gap rows: trigger and owner per quiet-gull-780 (generic record construct grounded from its expected type; smart-newt-725); same-module specimen enrolled Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/explicit_witness_admission.dag | 12 ++++++++++-- src/v2/test/claim/body_lowering/map_literal_test.dag | 11 +++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/dag/gunbc/explicit_witness_admission.dag b/dag/gunbc/explicit_witness_admission.dag index 95d149e4725..3636f69279a 100644 --- a/dag/gunbc/explicit_witness_admission.dag +++ b/dag/gunbc/explicit_witness_admission.dag @@ -238,8 +238,16 @@ data explicit_witness_admissions: List = [ f: "ml_an_elaborated_map_infers_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control: a well-typed elaborated map infers. RED beyond the map arm, measured with gunbc#12760 and gunbc#12759 merged: the HEADED spelling, map_from_entries(entries: [MapIntroductionEntry { key: .., value: .. }, ..]) under Map, does not infer either -- executed first refusal infer_grounding_not_derived, identically under Map, so the v2 Bool symbol fork is not what fires first. It is located (NodeLocus) at the authored MapIntroductionEntry { .. } construct, and a same-module control (entry record and introduction declared beside the data decl) refuses identically, so it is not cross-module reading: v2 infer cannot ground a construct of a GENERIC record whose type arguments come only from context (the list element / the callee parameter type). Owner: the v2 infer lane; the elaboration itself is proven by ml_headless_literal_equals_the_headed_introduction_holds.", - dissolution: unbound_dissolution(description: "v2 infer accepts the headed spelling map_from_entries(entries: [MapIntroductionEntry { key: \"a\", value: true }]) as a data initializer of declared type Map (it does not today, measured with #12760 merged); this witness greens with it and the row deletes in that change") + reason: "map arm control: a well-typed elaborated map infers. RED beyond the map arm: measured with gunbc#12760 merged, infer refuses infer_grounding_not_derived located (NodeLocus) at the MapIntroductionEntry { .. } construct, identically for the HEADED spelling and under Map. The discriminating specimen ml_a_same_module_generic_entry_construct_infers_holds (generic record declared in the same module) refuses the same way, so this is neither cross-module declaration reading (#12726 PR2) nor the v2 Bool fork. Owner: smart-newt-725 (record construct typing, #12763 lineage), per quiet-gull-780.", + dissolution: unbound_dissolution(description: "infer grounds a construct of a generic record whose type arguments come only from its expected type (callee parameter or list element): this witness greens and the row deletes in that change") + ), + known_red_probe( + entry: "src/v2/test/claim/body_lowering/map_literal_test.dag", + f: "ml_a_same_module_generic_entry_construct_infers_holds", + kind: CorpusWitnessKind, + budget: FastLaneEvalBudget, + reason: "the discriminating specimen for ml_an_elaborated_map_infers_holds: LocalEntry and local_from_entries declared beside the data decl, so no cross-module declaration is read; infer refuses infer_grounding_not_derived at the LocalEntry { .. } construct (measured with gunbc#12760 merged; without it, unbound String refuses first). Owner: smart-newt-725.", + dissolution: unbound_dissolution(description: "infer grounds a construct of a generic record whose type arguments come only from its expected type (callee parameter or list element): this witness greens and the row deletes in that change") ), known_red_probe( entry: "dag/test/claim/declared_type_inhabitance_direct_call_witness_test.dag", diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index efa83b8b117..8a8e0a0e9a8 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -74,6 +74,11 @@ data ml_name_key_source: String = "module v2.test.ml_name_key\n\nimport std.type data ml_call_argument_source: String = "module v2.test.ml_call_argument\n\nimport std.types { Map }\n\nfn ml_take(m: Map) -> Bool { true }\n\nfn ml_call() -> Bool { ml_take(m: { \"a\": true }) }\n" +// The discriminating specimen for the infer gap: a generic record and its introduction declared in +// the data declaration's OWN module, so no cross-module declaration is read. It refuses exactly as +// the map introduction does, which places the gap in grounding a generic record construct. +data ml_samemod_source: String = "module v2.test.ml_samemod\n\nimport std.types { Map }\nimport std.algebra { FreeMonoid, FinitelySupportedFunction }\n\ntype LocalEntry {\n key: K\n value: V\n}\n\nfn local_from_entries(entries: FreeMonoid>) -> FinitelySupportedFunction {\n FinitelySupportedFunction { size: 0 }\n}\n\ndata ml_x: Map = local_from_entries(entries: [LocalEntry { key: \"a\", value: 1 }])\n" + data ml_value_mismatch_source: String = "module v2.test.ml_value_mismatch\n\nimport std.types { Map }\n\ndata ml_bad: Map = { \"a\": 3 }\n" // An Int key where K = String: never a map entry, so never admitted. @@ -108,6 +113,7 @@ data ml_ingest: SourceRootIngest = [ ml_read(source: ml_key_kind_source, id: ^ml_key_kind_read, unit: ^ml_key_kind_cu, path: "src/v2/test/fixture/map_literal/key_kind.dag"), ml_read(source: ml_name_key_source, id: ^ml_name_key_read, unit: ^ml_name_key_cu, path: "src/v2/test/fixture/map_literal/name_key.dag"), ml_read(source: ml_call_argument_source, id: ^ml_call_argument_read, unit: ^ml_call_argument_cu, path: "src/v2/test/fixture/map_literal/call_argument.dag"), + ml_read(source: ml_samemod_source, id: ^ml_samemod_read, unit: ^ml_samemod_cu, path: "src/v2/test/fixture/map_literal/samemod.dag"), ml_read(source: ml_value_mismatch_source, id: ^ml_value_mismatch_read, unit: ^ml_value_mismatch_cu, path: "src/v2/test/fixture/map_literal/value_mismatch.dag") ] @@ -232,6 +238,11 @@ test fn ml_an_elaborated_map_infers_holds() -> Bool { ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_headless")) } +// The same-module specimen infers (see ml_samemod_source). +test fn ml_a_same_module_generic_entry_construct_infers_holds() -> Bool { + ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_samemod")) +} + fn ml_infers(o: Outcome) -> Bool { match o { Rejected { diagnostics: _ } => false From d27770432a8c352cb37708bffaa122c3fe5b723d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 13:43:03 +0000 Subject: [PATCH 61/63] Map arm: read pairs and key texts totally; a pair missing key or value refuses located (resolve_anonymous_map_entry_malformed), never substituted (review 73223). Known-red reasons state that only refusal paths execute on this branch. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/explicit_witness_admission.dag | 10 +- src/v2/compiler/03_resolve.dag | 142 ++++++++++-------- .../workflow/compile_door_cause_ownership.dag | 6 + 3 files changed, 87 insertions(+), 71 deletions(-) diff --git a/dag/gunbc/explicit_witness_admission.dag b/dag/gunbc/explicit_witness_admission.dag index 3636f69279a..6ecf83aa352 100644 --- a/dag/gunbc/explicit_witness_admission.dag +++ b/dag/gunbc/explicit_witness_admission.dag @@ -198,7 +198,7 @@ data explicit_witness_admissions: List = [ f: "ml_headless_literal_equals_the_headed_introduction_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (the headless map literal elaborates to exactly the headed introduction), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + reason: "map arm control under Map (the headless map literal elaborates to exactly the headed introduction), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). On this branch only the arm's REFUSAL paths execute (key kind, name key, no expectation, mixed keys); no acceptance path executes here. The acceptance paths were measured GREEN on a throwaway merge of this branch with gunbc#12760's head, which is the evidence this row stands on.", dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( @@ -206,7 +206,7 @@ data explicit_witness_admissions: List = [ f: "ml_a_duplicate_key_refuses_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (a repeated key refuses resolve_anonymous_map_duplicate_key at its second occurrence), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + reason: "map arm control under Map (a repeated key refuses resolve_anonymous_map_duplicate_key at its second occurrence), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). On this branch only the arm's REFUSAL paths execute (key kind, name key, no expectation, mixed keys); no acceptance path executes here. The acceptance paths were measured GREEN on a throwaway merge of this branch with gunbc#12760's head, which is the evidence this row stands on.", dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( @@ -214,7 +214,7 @@ data explicit_witness_admissions: List = [ f: "ml_an_escaped_and_a_raw_key_are_one_key_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (an escaped key and its raw scalar collide under the decoded-text key law), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + reason: "map arm control under Map (an escaped key and its raw scalar collide under the decoded-text key law), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). On this branch only the arm's REFUSAL paths execute (key kind, name key, no expectation, mixed keys); no acceptance path executes here. The acceptance paths were measured GREEN on a throwaway merge of this branch with gunbc#12760's head, which is the evidence this row stands on.", dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( @@ -222,7 +222,7 @@ data explicit_witness_admissions: List = [ f: "ml_distinct_keys_are_accepted_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (distinct keys elaborate), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + reason: "map arm control under Map (distinct keys elaborate), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). On this branch only the arm's REFUSAL paths execute (key kind, name key, no expectation, mixed keys); no acceptance path executes here. The acceptance paths were measured GREEN on a throwaway merge of this branch with gunbc#12760's head, which is the evidence this row stands on.", dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( @@ -230,7 +230,7 @@ data explicit_witness_admissions: List = [ f: "ml_infer_refuses_an_elaborated_value_mismatch_holds", kind: CorpusWitnessKind, budget: FastLaneEvalBudget, - reason: "map arm control under Map (infer refuses an elaborated map whose value does not inhabit V), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). The arm itself is exercised today by the K = Int controls in the same module.", + reason: "map arm control under Map (infer refuses an elaborated map whose value does not inhabit V), RED because an unimported String does not bind on the v2 route (resolve_reason_unbound_symbol before the map arm runs; owner neat-ibex-696, gunbc#12760), and once it does, a string key still carries no text to compare (resolve_anonymous_map_key_text_unavailable; owner sleek-owl-120, gunbc#12759). On this branch only the arm's REFUSAL paths execute (key kind, name key, no expectation, mixed keys); no acceptance path executes here. The acceptance paths were measured GREEN on a throwaway merge of this branch with gunbc#12760's head, which is the evidence this row stands on.", dissolution: unbound_dissolution(description: "BOTH hold: String bound by v2.extdeps.languages.dag dag_kernel_type_binding_optional with an infer text-literal arm (gunbc#12760), AND a string literal lowers to a lexeme-stamped terminal carrying its text rather than the one payload-less dag_token_string_literal atom (gunbc#12759). Measured against #12760 alone, the duplicate and escaped-key rows went green only through that collapse. The row deletes in the change that makes both true, promoting the witness to ordinary DiscoverySelection") ), known_red_probe( diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 4877811b0b4..d1b1b6d44cd 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1918,13 +1918,17 @@ fn resolve_map_literal_walk(ctx: ResolveContext, n: Node, x: ResolveExpectation) match resolve_map_first_name_keyed_item(items: skip(xs: n.children, n: 1)) { Present { value: offending } => resolve_anonymous_record_refused(n: offending, reason: ^resolve_anonymous_map_name_key) Absent => - if resolve_map_key_type_is_string_literal_type(ctx: ctx, position: x.position, t: key_type) { - resolve_map_literal_entries(ctx: ctx, n: n, items: items, value_x: ResolveExpectation { expected: value_type, position: x.position, at_use_site: x.at_use_site }) - } else { - match list_at_optional(xs: items, index: 0) { - Present { value: first } => resolve_anonymous_record_refused(n: resolve_map_entry_key(entry: first.target), reason: ^resolve_anonymous_map_key_kind_mismatch) - Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_key_kind_mismatch) - } + match resolve_map_pairs(pairs: resolve_map_pair_targets(items: items)) { + ResolveMapPairMalformed { at: p } => resolve_anonymous_record_refused(n: p, reason: ^resolve_anonymous_map_entry_malformed) + ResolveMapPairsRead { entries: entries } => + if resolve_map_key_type_is_string_literal_type(ctx: ctx, position: x.position, t: key_type) { + resolve_map_literal_entries(ctx: ctx, n: n, items: items, entries: entries, value_x: ResolveExpectation { expected: value_type, position: x.position, at_use_site: x.at_use_site }) + } else { + match list_at_optional(xs: entries, index: 0) { + Present { value: first } => resolve_anonymous_record_refused(n: first.key, reason: ^resolve_anonymous_map_key_kind_mismatch) + Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_key_kind_mismatch) + } + } } } } @@ -1975,13 +1979,6 @@ fn resolve_named_edge_target_optional(edges: List, field: Symbol) -> Optio }) } -fn resolve_map_entry_key(entry: Node) -> Node { - match resolve_map_entry_field(entry: entry, field: ^key) { - Present { value: k } => k - Absent => entry - } -} - // A KEY'S TEXT, OR NONE, read through the ONE string decoder: a string literal lowers to a terminal // carrying its decoded text (gunbc#12759, v2.extdeps.languages.dag dag_string_literal_value_optional; // a malformed escape already refused at lowering). A key without that payload has no text, and the @@ -1990,70 +1987,83 @@ fn resolve_map_key_text_optional(key: Node) -> Optional { dag_string_literal_value_optional(node: key) } -fn resolve_map_first_textless_key(items: List) -> Optional { - fold(items, init: optional_absent(), f: fn(acc, e) { +// THE PAIRS, READ TOTALLY. A pair is a tag-elided construct carrying `key` and `value` (body lowering +// body_lower_map_literal_entry_edge). Either every pair carries both, and the entries come back typed +// (key, value, source), or the first pair that does not is named -- never substituted: DESIGN 5 forbids +// a fabricated default, so a missing field is a located refusal +// (resolve_anonymous_map_entry_malformed), however unlikely lowering is to produce one. +type ResolveMapPairs + = ResolveMapPairsRead { entries: List } + | ResolveMapPairMalformed { at: Node } + +fn resolve_map_pairs(pairs: List) -> ResolveMapPairs { + fold(pairs, init: ResolveMapPairsRead { entries: [] }, f: fn(acc, p) { match acc { - Present { value: _ } => acc - Absent => - match resolve_map_key_text_optional(key: resolve_map_entry_key(entry: e.target)) { - Present { value: _ } => optional_absent() - Absent => optional_present(value: resolve_map_entry_key(entry: e.target)) + ResolveMapPairMalformed { at: _ } => acc + ResolveMapPairsRead { entries: es } => + match resolve_map_entry_field(entry: p, field: ^key) { + Absent => ResolveMapPairMalformed { at: p } + Present { value: k } => + match resolve_map_entry_field(entry: p, field: ^value) { + Absent => ResolveMapPairMalformed { at: p } + Present { value: v } => + ResolveMapPairsRead { entries: list_snoc_item(xs: es, item: MapIntroductionEntryNodes { key: k, value: v, source: p }) } + } } } }) } -// The pairs are walked under a Transform carrier (positional edges only) that never leaves this -// arm; only its resolved children are read. -fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, items: List, value_x: ResolveExpectation) -> ResolveNodeWalk { - match resolve_map_first_textless_key(items: items) { - Present { value: key } => resolve_anonymous_record_refused(n: key, reason: ^resolve_anonymous_map_key_text_unavailable) - Absent => resolve_map_literal_distinct_entries(ctx: ctx, n: n, items: items, value_x: value_x) - } +fn resolve_map_pair_targets(items: List) -> List { + fold(items, init: [], f: fn(acc, e) { list_snoc_item(xs: acc, item: e.target) }) } -fn resolve_map_literal_distinct_entries(ctx: ResolveContext, n: Node, items: List, value_x: ResolveExpectation) -> ResolveNodeWalk { - let keys = fold(items, init: [], f: fn(acc, e) { - match resolve_map_key_text_optional(key: resolve_map_entry_key(entry: e.target)) { - Present { value: t } => list_snoc_item(xs: acc, item: t) - Absent => acc +// THE KEYS' TEXTS, READ TOTALLY: one text per entry, in order, or the first key with no text, named. +type ResolveMapKeyTexts + = ResolveMapKeyTextsRead { texts: List } + | ResolveMapKeyTextless { at: Node } + +fn resolve_map_key_texts(entries: List) -> ResolveMapKeyTexts { + fold(entries, init: ResolveMapKeyTextsRead { texts: [] }, f: fn(acc, e) { + match acc { + ResolveMapKeyTextless { at: _ } => acc + ResolveMapKeyTextsRead { texts: ts } => + match resolve_map_key_text_optional(key: e.key) { + Present { value: t } => ResolveMapKeyTextsRead { texts: list_snoc_item(xs: ts, item: t) } + Absent => ResolveMapKeyTextless { at: e.key } + } } }) - match map_introduction_duplicate_key(keys: keys) { - MapIntroductionKeyRepeated { first_index: _, second_index: second } => - match list_at_optional(xs: items, index: second) { - Present { value: e } => resolve_anonymous_record_refused(n: resolve_map_entry_key(entry: e.target), reason: ^resolve_anonymous_map_duplicate_key) - Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_duplicate_key) - } - MapIntroductionKeysDistinct => - let walked = fold(items, init: child_walk_init(), f: fn(acc, e) { - child_walk_step(w: acc, e: e, r: resolve_map_entry_walk(ctx: ctx, entry: e.target, value_x: value_x)) - }) - let carrier = node_with_occurrence_id(kind: ComputationNode { behavior: Transform }, children: [], occurrence_id: n.occurrence_id) - match child_walk_node(w: walked, n: carrier) { - ResolveWalkRefused { first: f, rest: r, observation: o } => ResolveWalkRefused { first: f, rest: r, observation: o } - ResolveWalkAccepted { value: resolved, diagnostics: d } => - ResolveWalkAccepted { - value: lower_map_introduction( - entries: fold(resolved.children, init: [], f: fn(acc, e) { - list_snoc_item(xs: acc, item: resolve_map_entry_nodes(entry: e.target)) - }), - source: n - ), - diagnostics: d - } - } - } } -fn resolve_map_entry_nodes(entry: Node) -> MapIntroductionEntryNodes { - MapIntroductionEntryNodes { - key: resolve_map_entry_key(entry: entry), - value: match resolve_map_entry_field(entry: entry, field: ^value) { - Present { value: v } => v - Absent => entry - }, - source: entry +// The key law sees one text per entry, so its indices are entry indices. +// The pairs are walked under a Transform carrier (positional edges only) that never leaves this +// arm; the resolved pairs are read again through resolve_map_pairs, never defaulted. +fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, items: List, entries: List, value_x: ResolveExpectation) -> ResolveNodeWalk { + match resolve_map_key_texts(entries: entries) { + ResolveMapKeyTextless { at: key } => resolve_anonymous_record_refused(n: key, reason: ^resolve_anonymous_map_key_text_unavailable) + ResolveMapKeyTextsRead { texts: texts } => + match map_introduction_duplicate_key(keys: texts) { + MapIntroductionKeyRepeated { first_index: _, second_index: second } => + match list_at_optional(xs: entries, index: second) { + Present { value: e } => resolve_anonymous_record_refused(n: e.key, reason: ^resolve_anonymous_map_duplicate_key) + Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_duplicate_key) + } + MapIntroductionKeysDistinct => + let walked = fold(items, init: child_walk_init(), f: fn(acc, e) { + child_walk_step(w: acc, e: e, r: resolve_map_entry_walk(ctx: ctx, entry: e.target, value_x: value_x)) + }) + let carrier = node_with_occurrence_id(kind: ComputationNode { behavior: Transform }, children: [], occurrence_id: n.occurrence_id) + match child_walk_node(w: walked, n: carrier) { + ResolveWalkRefused { first: f, rest: r, observation: o } => ResolveWalkRefused { first: f, rest: r, observation: o } + ResolveWalkAccepted { value: resolved, diagnostics: d } => + match resolve_map_pairs(pairs: resolve_map_pair_targets(items: resolved.children)) { + ResolveMapPairMalformed { at: p } => resolve_anonymous_record_refused(n: p, reason: ^resolve_anonymous_map_entry_malformed) + ResolveMapPairsRead { entries: resolved_entries } => + ResolveWalkAccepted { value: lower_map_introduction(entries: resolved_entries, source: n), diagnostics: d } + } + } + } } } diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index 50245c8d091..73ee4ca990b 100644 --- a/src/v2/workflow/compile_door_cause_ownership.dag +++ b/src/v2/workflow/compile_door_cause_ownership.dag @@ -312,6 +312,12 @@ data known_frontier_causes: List = [ lane: SharedSelfHostCriticalPath, flip_trigger: "a map literal key that carries no string-literal lexeme, so the key law has no text to compare: in v2 today every string literal lowers to one class-stamped atom with no payload (gunbc.recurring_failure_mode string_literal_lowers_to_one_class_stamped_atom). Refused at the key rather than compared, since comparing payload-less keys would call every pair a duplicate. A frontier: flips to zero when string literals lower to lexeme-stamped terminals (sleek-owl-120, gunbc#12759)" }, + CauseOwnership { + cause: ^resolve_anonymous_map_entry_malformed, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a map literal pair that does not carry both its key and its value field, refused at the pair by v2.compiler.resolve's map arm rather than substituting a node for the missing field (DESIGN 5). Body lowering always writes both, so this names a lowering defect if it ever fires; not a frontier, and expected to stay at zero" + }, CauseOwnership { cause: ^resolve_anonymous_map_duplicate_key, grain: FatalGrain, From 281f9501f9cd1e16effe60858e81c24b6211a135 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 14:27:57 +0000 Subject: [PATCH 62/63] Map literal edges read through the shared label accessors (labeled_named_is, named_child_lookup); delete the two map-label Bool predicates and the hand-rolled first-match searches; an ambiguous entries/field edge refuses as malformed (review 73238) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_resolve.dag | 69 ++++++++++++++------------ src/v2/compiler/body_lowering_fold.dag | 10 ++-- src/v2/std/map_introduction.dag | 41 +++++++-------- 3 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index d1b1b6d44cd..c6f632ae0a1 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -134,10 +134,15 @@ import v2.std.node { Instantiation, symbol_eq, symbol_lexeme, + labeled_named_is, well_formed, } 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 { + NamedChildAmbiguous, + NamedChildFound, + NamedChildMissing, + named_child_lookup, construct_field_edges, construct_tag_reading, ConstructTagAuthored, @@ -155,8 +160,11 @@ import v2.std.map_introduction { MapIntroductionEntryNodes, MapIntroductionKeyRepeated, MapIntroductionKeysDistinct, - edge_is_map_literal_entries, - map_literal_entries_optional, + MapLiteralEntriesMalformed, + MapLiteralEntriesNone, + MapLiteralEntriesPresent, + map_literal_entries, + map_literal_entries_label, lower_map_introduction, map_introduction_duplicate_key, map_introduction_expected_head_is_map @@ -1894,20 +1902,15 @@ fn resolve_elided_construct_walk(ctx: ResolveContext, n: Node, x: ResolveExpecta // value elaborates through this same writer; each key is walked as the ordinary child it is. // Nothing is unified here, and infer still checks every value against V afterwards. fn resolve_elided_construct_is_map_literal(n: Node) -> Bool { - any(xs: skip(xs: n.children, n: 1), predicate: fn(e) { edge_is_map_literal_entries(e: e) }) -} - -// The pairs as positional edges, in authored order; none for `{}`. -fn resolve_map_literal_items(n: Node) -> List { - match map_literal_entries_optional(n: n) { - Absent => [] - Present { value: pairs } => - fold(pairs, init: [], f: fn(acc, p) { list_snoc_item(xs: acc, item: Edge { label: Positional, target: p }) }) + match map_literal_entries(n: n) { + MapLiteralEntriesNone => false + MapLiteralEntriesPresent { pairs: _ } => true + MapLiteralEntriesMalformed => true } } + fn resolve_map_literal_walk(ctx: ResolveContext, n: Node, x: ResolveExpectation) -> ResolveNodeWalk { - let items = resolve_map_literal_items(n: n) let type_args = node_positional_child_targets(node: x.expected) match list_at_optional(xs: type_args, index: 1) { Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_expected_type_not_closed) @@ -1918,11 +1921,11 @@ fn resolve_map_literal_walk(ctx: ResolveContext, n: Node, x: ResolveExpectation) match resolve_map_first_name_keyed_item(items: skip(xs: n.children, n: 1)) { Present { value: offending } => resolve_anonymous_record_refused(n: offending, reason: ^resolve_anonymous_map_name_key) Absent => - match resolve_map_pairs(pairs: resolve_map_pair_targets(items: items)) { + match resolve_map_literal_pairs(n: n) { ResolveMapPairMalformed { at: p } => resolve_anonymous_record_refused(n: p, reason: ^resolve_anonymous_map_entry_malformed) ResolveMapPairsRead { entries: entries } => if resolve_map_key_type_is_string_literal_type(ctx: ctx, position: x.position, t: key_type) { - resolve_map_literal_entries(ctx: ctx, n: n, items: items, entries: entries, value_x: ResolveExpectation { expected: value_type, position: x.position, at_use_site: x.at_use_site }) + resolve_map_literal_entries(ctx: ctx, n: n, entries: entries, value_x: ResolveExpectation { expected: value_type, position: x.position, at_use_site: x.at_use_site }) } else { match list_at_optional(xs: entries, index: 0) { Present { value: first } => resolve_anonymous_record_refused(n: first.key, reason: ^resolve_anonymous_map_key_kind_mismatch) @@ -1939,7 +1942,7 @@ fn resolve_map_first_name_keyed_item(items: List) -> Optional { fold(items, init: optional_absent(), f: fn(acc, e) { match acc { Present { value: _ } => acc - Absent => if edge_is_map_literal_entries(e: e) { optional_absent() } else { optional_present(value: e.target) } + Absent => if labeled_named_is(x: e, label_of: edge_label_of, wanted: map_literal_entries_label()) { optional_absent() } else { optional_present(value: e.target) } } }) } @@ -1962,21 +1965,11 @@ fn resolve_map_key_type_is_string_literal_type(ctx: ResolveContext, position: Qu // A pair is a tag-elided construct carrying `key` and `value` (body lowering // body_lower_map_literal_entry_edge); these read them back. fn resolve_map_entry_field(entry: Node, field: Symbol) -> Optional { - resolve_named_edge_target_optional(edges: skip(xs: entry.children, n: 1), field: field) -} - -// The target of a map entry's first edge labelled `field`. -fn resolve_named_edge_target_optional(edges: List, field: Symbol) -> Optional { - fold(edges, init: optional_absent(), f: fn(acc, e) { - match acc { - Present { value: _ } => acc - Absent => - match e.label { - Named { name: nm } => if symbol_eq(a: nm, b: field) { optional_present(value: e.target) } else { optional_absent() } - Positional => optional_absent() - } - } - }) + match named_child_lookup(root: entry, name: field) { + NamedChildFound { target: t } => optional_present(value: t) + NamedChildMissing => optional_absent() + NamedChildAmbiguous => optional_absent() + } } // A KEY'S TEXT, OR NONE, read through the ONE string decoder: a string literal lowers to a terminal @@ -2018,6 +2011,16 @@ fn resolve_map_pair_targets(items: List) -> List { fold(items, init: [], f: fn(acc, e) { list_snoc_item(xs: acc, item: e.target) }) } +// The literal's pairs, read through v2.std.map_introduction map_literal_entries: `{}` has none, and a +// malformed entries carrier refuses at the literal rather than reading as empty. +fn resolve_map_literal_pairs(n: Node) -> ResolveMapPairs { + match map_literal_entries(n: n) { + MapLiteralEntriesNone => ResolveMapPairsRead { entries: [] } + MapLiteralEntriesMalformed => ResolveMapPairMalformed { at: n } + MapLiteralEntriesPresent { pairs: pairs } => resolve_map_pairs(pairs: pairs) + } +} + // THE KEYS' TEXTS, READ TOTALLY: one text per entry, in order, or the first key with no text, named. type ResolveMapKeyTexts = ResolveMapKeyTextsRead { texts: List } @@ -2039,7 +2042,7 @@ fn resolve_map_key_texts(entries: List) -> ResolveMap // The key law sees one text per entry, so its indices are entry indices. // The pairs are walked under a Transform carrier (positional edges only) that never leaves this // arm; the resolved pairs are read again through resolve_map_pairs, never defaulted. -fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, items: List, entries: List, value_x: ResolveExpectation) -> ResolveNodeWalk { +fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, entries: List, value_x: ResolveExpectation) -> ResolveNodeWalk { match resolve_map_key_texts(entries: entries) { ResolveMapKeyTextless { at: key } => resolve_anonymous_record_refused(n: key, reason: ^resolve_anonymous_map_key_text_unavailable) ResolveMapKeyTextsRead { texts: texts } => @@ -2050,8 +2053,8 @@ fn resolve_map_literal_entries(ctx: ResolveContext, n: Node, items: List, Absent => resolve_anonymous_record_refused(n: n, reason: ^resolve_anonymous_map_duplicate_key) } MapIntroductionKeysDistinct => - let walked = fold(items, init: child_walk_init(), f: fn(acc, e) { - child_walk_step(w: acc, e: e, r: resolve_map_entry_walk(ctx: ctx, entry: e.target, value_x: value_x)) + let walked = fold(entries, init: child_walk_init(), f: fn(acc, e) { + child_walk_step(w: acc, e: Edge { label: Positional, target: e.source }, r: resolve_map_entry_walk(ctx: ctx, entry: e.source, value_x: value_x)) }) let carrier = node_with_occurrence_id(kind: ComputationNode { behavior: Transform }, children: [], occurrence_id: n.occurrence_id) match child_walk_node(w: walked, n: carrier) { diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index a209254d091..1a250c410ae 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -77,7 +77,7 @@ import v2.std.compilers.body_lowering { lower_unary_prefix } import v2.std.list_introduction { lower_list_introduction } -import v2.std.map_introduction { edge_is_map_literal_entry, map_literal_entries_label, map_literal_entry_label } +import v2.std.map_introduction { map_literal_entries_label, map_literal_entry_label } import v2.std.arrow_signature { anonymous_signature_arrow, declared_signature, signature_order_edge } import v2.std.compilers.sugar { SugarSequencePair, sugar_sequence_pair_optional } import v2.std.grammar { GrammarExpr, GrammarExprFold, fold_grammar_expr } @@ -108,6 +108,8 @@ import v2.std.symbol_index { } import v2.std.node { Arrow, + edge_label_of, + labeled_named_is, is_positional, Atom, Branch, @@ -7909,7 +7911,7 @@ fn body_lower_map_literal_entry_edge(key: Node, after_key: Node, item: Node, ref // is returned as it is. fn body_lower_map_literal_entries_collapsed(edges: List, source: Node) -> List { let pairs = fold(edges, init: [], f: fn(acc, e) { - if edge_is_map_literal_entry(e: e) { list_snoc_item(xs: acc, item: e.target) } else { acc } + if labeled_named_is(x: e, label_of: edge_label_of, wanted: map_literal_entry_label()) { list_snoc_item(xs: acc, item: e.target) } else { acc } }) match pairs { Empty => edges @@ -7931,13 +7933,13 @@ fn body_lower_brace_key_kinds_refusal_optional(reference: Node, edges: List acc - Absent => if edge_is_map_literal_entry(e: e) { optional_present(value: e) } else { optional_absent() } + Absent => if labeled_named_is(x: e, label_of: edge_label_of, wanted: map_literal_entry_label()) { optional_present(value: e) } else { optional_absent() } } }) let first_field = fold(edges, init: optional_absent(), f: fn(acc, e) { match acc { Present { value: _ } => acc - Absent => if edge_is_map_literal_entry(e: e) { optional_absent() } else { optional_present(value: e) } + Absent => if labeled_named_is(x: e, label_of: edge_label_of, wanted: map_literal_entry_label()) { optional_absent() } else { optional_present(value: e) } } }) match first_entry { diff --git a/src/v2/std/map_introduction.dag b/src/v2/std/map_introduction.dag index 2458915d5df..c0cc9575d69 100644 --- a/src/v2/std/map_introduction.dag +++ b/src/v2/std/map_introduction.dag @@ -8,7 +8,7 @@ import v2.std.integer { Int } import v2.std.list_introduction { list_introduction_elements_optional, list_introduction_head_path } import v2.std.logic { Bool } import v2.std.node { ComputationNode, Edge, Named, Node, Positional, Symbol, Transform, node_with_occurrence_id } -import v2.std.node_query { construct_node } +import v2.std.node_query { NamedChildAmbiguous, NamedChildFound, NamedChildMissing, construct_node, named_child_lookup } import v2.std.qualified_name { QualifiedName, declaration_reference_node } // THE MAP-LITERAL INTRODUCTION, ONE AUTHORITY FOR ITS PATHS, ITS CONSTRUCTOR AND ITS KEY LAW @@ -34,34 +34,31 @@ fn map_literal_entry_label() -> Symbol { ^map_literal_entry } -fn edge_is_map_literal_entry(e: Edge) -> Bool { - match e.label { - Named { name: n } => n == map_literal_entry_label() - Positional => false - } -} - fn map_literal_entries_label() -> Symbol { ^map_literal_entries } -fn edge_is_map_literal_entries(e: Edge) -> Bool { - match e.label { - Named { name: n } => n == map_literal_entries_label() - Positional => false +// THE AUTHORED PAIRS OF A STRING-KEYED BRACE, read totally through the one named-child lookup +// (v2.std.node_query named_child_lookup): no entries edge, exactly one carrying a list literal of +// pairs, or a malformed carrier (two entries edges, or one that is not a list literal) -- which the +// map arm refuses rather than reading as empty. +type MapLiteralEntries + = MapLiteralEntriesNone + | MapLiteralEntriesPresent { pairs: List } + | MapLiteralEntriesMalformed + +fn map_literal_entries(n: Node) -> MapLiteralEntries { + match named_child_lookup(root: n, name: map_literal_entries_label()) { + NamedChildMissing => MapLiteralEntriesNone + NamedChildAmbiguous => MapLiteralEntriesMalformed + NamedChildFound { target: t } => + match list_introduction_elements_optional(node: t) { + Present { value: pairs } => MapLiteralEntriesPresent { pairs: pairs } + Absent => MapLiteralEntriesMalformed + } } } -// The authored pairs of a string-keyed brace, in order; Absent for any node that carries none. -fn map_literal_entries_optional(n: Node) -> Optional> { - fold(n.children, init: optional_absent(), f: fn(acc, e) { - match acc { - Present { value: _ } => acc - Absent => if edge_is_map_literal_entries(e: e) { list_introduction_elements_optional(node: e.target) } else { optional_absent() } - } - }) -} - fn map_introduction_head_path() -> QualifiedName { [^std, ^algebra, ^map_from_entries] } From b4c00f99412996d19deddcf77fd1ea347afa25c6 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 30 Sep 2026 22:46:10 +0000 Subject: [PATCH 63/63] map_literal controls: the front end runs once -- nullary fixture/resolve/infer producers served from floor_cross_claim_pure_producers_warm, as #12740's anonymous_record controls are (floor enrolment margin) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../claim/body_lowering/map_literal_test.dag | 87 +++++++++++++------ src/v2/workflow/floor_pure_producer_share.dag | 16 ++++ 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/src/v2/test/claim/body_lowering/map_literal_test.dag b/src/v2/test/claim/body_lowering/map_literal_test.dag index 8a8e0a0e9a8..4ba57fe546a 100644 --- a/src/v2/test/claim/body_lowering/map_literal_test.dag +++ b/src/v2/test/claim/body_lowering/map_literal_test.dag @@ -121,8 +121,17 @@ data ml_mixed_ingest: SourceRootIngest = [ ml_read(source: ml_mixed_source, id: ^ml_mixed_read, unit: ^ml_mixed_cu, path: "src/v2/test/fixture/map_literal/mixed.dag") ] +// THE FRONT END RUNS ONCE, NOT ONCE PER CLAIM. The fixture ingest and each subject's resolve are +// nullary pure producers, served across witnesses from v2.workflow.floor_pure_producer_share +// floor_cross_claim_pure_producers_warm (the rows beside #12740's anonymous_record producers); every +// claim below only inspects a produced value (DESIGN section 3: a witness discriminates at one +// interface, and the front end is not its subject). +fn ml_fixture_normalized() -> Outcome> { + module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) +} + fn ml_resolve_subject(subject: String) -> Outcome { - match module_roots_from_source_root_ingest(ingest: ml_ingest, lm: dag_language_model()) { + match ml_fixture_normalized() { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: roots, diagnostics: _ } => resolve_with_admission_context_policy( @@ -138,6 +147,42 @@ fn ml_resolve_subject(subject: String) -> Outcome { } } +fn ml_resolved_headed() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_headed") } +fn ml_resolved_headless() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_headless") } +fn ml_resolved_duplicate() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_duplicate") } +fn ml_resolved_escape_duplicate() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_escape_duplicate") } +fn ml_resolved_distinct() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_distinct") } +fn ml_resolved_key_kind() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_key_kind") } +fn ml_resolved_name_key() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_name_key") } +fn ml_resolved_call_argument() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_call_argument") } +fn ml_resolved_value_mismatch() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_value_mismatch") } +fn ml_resolved_samemod() -> Outcome { ml_resolve_subject(subject: "v2.test.ml_samemod") } + +fn ml_infer_verdict_headless() -> Bool { ml_infers(o: ml_resolved_headless()) } +fn ml_infer_verdict_value_mismatch() -> Bool { ml_infers(o: ml_resolved_value_mismatch()) } +fn ml_infer_verdict_samemod() -> Bool { ml_infers(o: ml_resolved_samemod()) } + +fn ml_int_key_verdict_refused() -> Bool { + match module_roots_from_source_root_ingest(ingest: ml_int_key_ingest, lm: dag_language_model()) { + Rejected { diagnostics: _ } => true + Accepted { value: roots, diagnostics: _ } => + match resolve_with_admission_context_policy( + context: resolution_context(lm: dag_language_model(), roots: roots), + admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.ml_int_key") }, imports: Empty }, + index: source_root_index_empty(), + active_roots: Empty, + policy: default_name_resolution_policy() + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: _ } => true + } + } +} + +fn ml_mixed_normalized() -> Outcome> { + module_roots_from_source_root_ingest(ingest: ml_mixed_ingest, lm: dag_language_model()) +} + // Every node under the root that is a call headed by the introduction, in tree order. fn ml_introductions(root: Node) -> List { fold(node_subtree_nodes(root: root), init: Empty, f: fn(acc, n) { @@ -177,23 +222,23 @@ fn ml_accepts(o: Outcome) -> Bool { // Control 1: the headless literal elaborates to exactly the introduction the headed call spells -- // one of it, equal by content. test fn ml_headless_literal_equals_the_headed_introduction_holds() -> Bool { - let headed = ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headed")) - (length(xs: headed) == 1) && (headed == ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_headless"))) + let headed = ml_introduction_hashes(o: ml_resolved_headed()) + (length(xs: headed) == 1) && (headed == ml_introduction_hashes(o: ml_resolved_headless())) } // Control 2: a repeated key refuses at resolve, never last-wins. test fn ml_a_duplicate_key_refuses_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_duplicate"), reason: ^resolve_anonymous_map_duplicate_key) + ml_refuses_with(o: ml_resolved_duplicate(), reason: ^resolve_anonymous_map_duplicate_key) } // Control 2b: the key law compares DECODED text, so an escape and the scalar it stands for collide. test fn ml_an_escaped_and_a_raw_key_are_one_key_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_escape_duplicate"), reason: ^resolve_anonymous_map_duplicate_key) + ml_refuses_with(o: ml_resolved_escape_duplicate(), reason: ^resolve_anonymous_map_duplicate_key) } // Control 3: distinct keys are accepted and elaborate. test fn ml_distinct_keys_are_accepted_holds() -> Bool { - length(xs: ml_introduction_hashes(o: ml_resolve_subject(subject: "v2.test.ml_distinct"))) == 1 + length(xs: ml_introduction_hashes(o: ml_resolved_distinct())) == 1 } // The key law itself, as a value: the FIRST repeat, with both occurrences. The mutation this @@ -212,35 +257,34 @@ test fn ml_the_key_law_names_both_occurrences_holds() -> Bool { // Control 5: K is not the type a string literal denotes, so the key refuses where it is written. test fn ml_a_key_of_the_wrong_kind_refuses_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_key_kind"), reason: ^resolve_anonymous_map_key_kind_mismatch) + ml_refuses_with(o: ml_resolved_key_kind(), reason: ^resolve_anonymous_map_key_kind_mismatch) } test fn ml_a_name_keyed_brace_under_a_map_refuses_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_name_key"), reason: ^resolve_anonymous_map_name_key) + ml_refuses_with(o: ml_resolved_name_key(), reason: ^resolve_anonymous_map_name_key) } // Control 8: the expectation does not leak into a call argument. test fn ml_a_call_argument_has_no_expected_type_holds() -> Bool { - ml_refuses_with(o: ml_resolve_subject(subject: "v2.test.ml_call_argument"), reason: ^resolve_anonymous_map_no_expected_type) + ml_refuses_with(o: ml_resolved_call_argument(), reason: ^resolve_anonymous_map_no_expected_type) } // Control 7: resolve's choice is not proof. The Int value elaborates (resolve accepts it) and // infer then refuses it against V = Bool. test fn ml_infer_refuses_an_elaborated_value_mismatch_holds() -> Bool { - let bad = ml_resolve_subject(subject: "v2.test.ml_value_mismatch") - ml_accepts(o: bad) && (ml_infers(o: bad) == false) + ml_accepts(o: ml_resolved_value_mismatch()) && (ml_infer_verdict_value_mismatch() == false) } // Control 7's positive half: a well-typed elaborated map infers. Measured with #12760 merged: this // is red for the HEADED spelling too (map_from_entries(entries: [MapIntroductionEntry { .. }]) under // Map does not infer), so the gap is infer's, not the arm's; its row says so. test fn ml_an_elaborated_map_infers_holds() -> Bool { - ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_headless")) + ml_infer_verdict_headless() } // The same-module specimen infers (see ml_samemod_source). test fn ml_a_same_module_generic_entry_construct_infers_holds() -> Bool { - ml_infers(o: ml_resolve_subject(subject: "v2.test.ml_samemod")) + ml_infer_verdict_samemod() } fn ml_infers(o: Outcome) -> Bool { @@ -257,25 +301,12 @@ fn ml_infers(o: Outcome) -> Bool { // An Int key under Map is refused somewhere on the route -- at lowering or at resolve -- // and never elaborates. Were String collapsed onto Int, this is the claim that would go red. test fn ml_an_int_key_under_a_string_map_refuses_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_int_key_ingest, lm: dag_language_model()) { - Rejected { diagnostics: _ } => true - Accepted { value: roots, diagnostics: _ } => - match resolve_with_admission_context_policy( - context: resolution_context(lm: dag_language_model(), roots: roots), - admission: Admission { subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: "v2.test.ml_int_key") }, imports: Empty }, - index: source_root_index_empty(), - active_roots: Empty, - policy: default_name_resolution_policy() - ) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: _ } => true - } - } + ml_int_key_verdict_refused() } // Lowering refuses a brace no key-kind choice could type, at the first item of the minority kind. test fn ml_a_mixed_key_brace_refuses_at_lowering_holds() -> Bool { - match module_roots_from_source_root_ingest(ingest: ml_mixed_ingest, lm: dag_language_model()) { + match ml_mixed_normalized() { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^body_lowering_reason_brace_key_kind_mismatch } diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 59fe3a1f371..fc210094f4d 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -1142,6 +1142,22 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.body_lowering.anonymous_record.ar_infer_verdict_value_mismatch_headed", "v2.test.claim.body_lowering.anonymous_record.ar_key_bare_subject", "v2.test.claim.body_lowering.anonymous_record.ar_key_quoted_subject", + "v2.test.claim.body_lowering.map_literal.ml_fixture_normalized", + "v2.test.claim.body_lowering.map_literal.ml_resolved_headed", + "v2.test.claim.body_lowering.map_literal.ml_resolved_headless", + "v2.test.claim.body_lowering.map_literal.ml_resolved_duplicate", + "v2.test.claim.body_lowering.map_literal.ml_resolved_escape_duplicate", + "v2.test.claim.body_lowering.map_literal.ml_resolved_distinct", + "v2.test.claim.body_lowering.map_literal.ml_resolved_key_kind", + "v2.test.claim.body_lowering.map_literal.ml_resolved_name_key", + "v2.test.claim.body_lowering.map_literal.ml_resolved_call_argument", + "v2.test.claim.body_lowering.map_literal.ml_resolved_value_mismatch", + "v2.test.claim.body_lowering.map_literal.ml_resolved_samemod", + "v2.test.claim.body_lowering.map_literal.ml_infer_verdict_headless", + "v2.test.claim.body_lowering.map_literal.ml_infer_verdict_value_mismatch", + "v2.test.claim.body_lowering.map_literal.ml_infer_verdict_samemod", + "v2.test.claim.body_lowering.map_literal.ml_int_key_verdict_refused", + "v2.test.claim.body_lowering.map_literal.ml_mixed_normalized", "v2.test.claim.compiler.infer_record_construct_field_inhabitance_witness_test.rcf_fixture_normalized", "v2.test.claim.compiler.infer_record_construct_field_inhabitance_witness_test.rcf_resolved_bad", "v2.test.claim.compiler.infer_record_construct_field_inhabitance_witness_test.rcf_resolved_good",