From c0d77bc4cd98b24230587e1f6cdb66831687d2ae Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 03:58:49 +0000 Subject: [PATCH 01/56] Rehome type and data declarations onto Named graft edges. Flattening kept fn members and dropped type/data shells (or reconstructed raw parse units when nothing else was present). Graft those declarations as Named containment so symbol_index_fill sees them regardless of neighboring fns. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 266 +++++++++++++++++- .../long/declaration_graft_assemble_test.dag | 152 ++++++++++ .../field_type_tree_presence_test.dag | 27 +- .../loaded_carrier_receipts_test.dag | 6 +- 4 files changed, 421 insertions(+), 30 deletions(-) create mode 100644 src/v2/test/claim/long/declaration_graft_assemble_test.dag diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index cec12a421c3..f2f4f3261fc 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -3,7 +3,8 @@ module v2.compiler.namespace_graft import std.occurrence_identity { OccurrenceSynthetic } import std.algebra { Cons, Empty, FreeMonoid } import v2.std.algebra { fold_list, fold_list_right, list_append, list_snoc_item } -import v2.std.collection { Absent, List, Present, optional_absent } +import v2.std.collection { Absent, List, Present, optional_absent, optional_present } +import v2.std.compilers.sugar { sugar_sequence_pair_optional, sugar_unwrap_production_shell } import v2.std.diagnostic { Accepted, Diagnostic, @@ -17,9 +18,9 @@ import v2.std.diagnostic { } import v2.std.logic { Bool } import v2.std.node { - Atom, Cardinality, Conj, + Disj, Edge, Named, Node, @@ -27,6 +28,7 @@ import v2.std.node { Symbol, TypeNode, node_rebuild, + node_synthetic, } import v2.std.qualified_name { QualifiedName } import v2.std.compilers.lexing { symbol_lexeme } @@ -38,6 +40,8 @@ import v2.std.grammar { node_atom_identity_optional } import v2.extdeps.languages.dag { + dag_binding_branded_atom_identity, + dag_lex_token_symbol, dag_named_edge, dag_surface_module_header_metadata_edge, dag_grammar_root, @@ -45,6 +49,7 @@ import v2.extdeps.languages.dag { namespace_graft_already_grafted, namespace_graft_has_module_header, namespace_graft_unwrap_module_working_root, + parse_production_captured_child_optional, parse_production_emitted_identity_optional, qualified_name_from_module_node } @@ -262,19 +267,264 @@ fn namespace_graft_reconstruct_declaration_forest(units: List) -> List Symbol? { + let stripped = sugar_unwrap_production_shell(node: node) + match dag_binding_branded_atom_identity(node: stripped) { + Present { value: binding } => optional_present(value: binding) + Absent => + match node_atom_identity_optional(node: stripped) { + Present { value: sym } => + if dag_lex_token_symbol(sym: sym) { + fold(stripped.children, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => namespace_graft_ident_from_node(node: e.target) + } + }) + } else { + optional_present(value: sym) + } + Absent => + fold(stripped.children, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => namespace_graft_ident_from_node(node: e.target) + } + }) + } + } +} + +fn namespace_graft_kw_then_ident(captured: Node) -> Symbol? { + match sugar_sequence_pair_optional(node: captured) { + Absent => namespace_graft_ident_from_node(node: captured) + Present { value: pair } => + match namespace_graft_ident_from_node(node: pair.left) { + Present { value: _ } => namespace_graft_ident_from_node(node: pair.right) + Absent => + match sugar_sequence_pair_optional(node: pair.right) { + Present { value: rest } => namespace_graft_ident_from_node(node: rest.left) + Absent => namespace_graft_ident_from_node(node: pair.right) + } + } + } +} + +fn namespace_graft_find_emitted(node: Node, emitted: Symbol) -> Node? { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => + if id == emitted { + optional_present(value: node) + } else { + fold(node.children, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => namespace_graft_find_emitted(node: e.target, emitted: emitted) + } + }) + } + Absent => + fold(node.children, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => namespace_graft_find_emitted(node: e.target, emitted: emitted) + } + }) + } +} + +fn namespace_graft_find_disj(node: Node) -> Node? { + match node.kind { + TypeNode { connective: Disj } => optional_present(value: node) + _ => + fold(node.children, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => namespace_graft_find_disj(node: e.target) + } + }) + } +} + +fn namespace_graft_is_colon_atom(node: Node) -> Bool { + match node_atom_identity_optional(node: sugar_unwrap_production_shell(node: node)) { + Present { value: id } => id == ^dag_token_colon + Absent => false + } +} + +fn namespace_graft_type_position_node(node: Node) -> Node { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => + if id == ^dag_surface_type_expr { + match parse_production_captured_child_optional(node: node) { + Present { value: captured } => captured + Absent => node + } + } else { + match sugar_sequence_pair_optional(node: node) { + Present { value: pair } => + if namespace_graft_is_colon_atom(node: pair.left) { + namespace_graft_type_position_node(node: pair.right) + } else { + node + } + Absent => node + } + } + Absent => + match sugar_sequence_pair_optional(node: node) { + Present { value: pair } => + if namespace_graft_is_colon_atom(node: pair.left) { + namespace_graft_type_position_node(node: pair.right) + } else { + node + } + Absent => node + } + } +} + +fn namespace_graft_field_edge_optional(node: Node) -> Edge? { + match sugar_sequence_pair_optional(node: node) { + Absent => optional_absent() + Present { value: pair } => + match namespace_graft_ident_from_node(node: pair.left) { + Absent => optional_absent() + Present { value: field_name } => + if namespace_graft_subtree_contains_atom(n: pair.right, sym: ^dag_token_colon) { + optional_present( + value: Edge { + label: Named { name: field_name }, + target: namespace_graft_type_position_node(node: pair.right) + } + ) + } else { + optional_absent() + } + } + } +} + +fn namespace_graft_collect_field_edges(node: Node) -> List { + match namespace_graft_field_edge_optional(node: node) { + Present { value: edge } => [edge] + Absent => + fold(node.children, init: [], f: fn(acc, e) { + concat(acc, namespace_graft_collect_field_edges(node: e.target)) + }) + } +} + +fn namespace_graft_type_decl_target(captured: Node) -> Node { + match namespace_graft_find_disj(node: captured) { + Present { value: disj } => disj + Absent => + match namespace_graft_find_emitted(node: captured, emitted: ^dag_surface_field_decl_block) { + Present { value: block } => + node_synthetic( + kind: TypeNode { connective: Conj }, + children: namespace_graft_collect_field_edges(node: block) + ) + Absent => + match namespace_graft_find_emitted(node: captured, emitted: ^dag_surface_type_alias_rhs) { + Present { value: rhs } => + match parse_production_captured_child_optional(node: rhs) { + Present { value: rhs_captured } => rhs_captured + Absent => rhs + } + Absent => + node_synthetic(kind: TypeNode { connective: Conj }, children: []) + } + } + } +} + +fn namespace_graft_unit_captured(unit: Node) -> Node? { + match parse_production_captured_child_optional(node: unit) { + Present { value: captured } => optional_present(value: captured) + Absent => optional_present(value: unit) + } +} + +fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { + match namespace_graft_unit_captured(unit: unit) { + Absent => optional_absent() + Present { value: captured } => + match namespace_graft_kw_then_ident(captured: captured) { + Absent => optional_absent() + Present { value: type_name } => + optional_present( + value: Edge { + label: Named { name: type_name }, + target: namespace_graft_type_decl_target(captured: captured) + } + ) + } + } +} + +fn namespace_graft_data_decl_named_edge(unit: Node) -> Edge? { + match namespace_graft_unit_captured(unit: unit) { + Absent => optional_absent() + Present { value: captured } => + match namespace_graft_kw_then_ident(captured: captured) { + Absent => optional_absent() + Present { value: data_name } => + optional_present( + value: Edge { + label: Named { name: data_name }, + target: captured + } + ) + } + } +} + +fn namespace_graft_named_edge_from_unit(unit: Node) -> Edge? { + match parse_production_emitted_identity_optional(node: unit) { + Present { value: id } => + if id == ^dag_surface_type_decl { + namespace_graft_type_decl_named_edge(unit: unit) + } else if id == ^dag_surface_data_decl { + namespace_graft_data_decl_named_edge(unit: unit) + } else { + optional_absent() + } + Absent => optional_absent() + } +} + +fn namespace_graft_named_edges_from_units(units: List) -> List { + fold(units, init: [], f: fn(acc, unit) { + match namespace_graft_named_edge_from_unit(unit: unit) { + Present { value: edge } => list_snoc_item(xs: acc, item: edge) + Absent => acc + } + }) +} + +// Flattening to Named declaration edges is what symbol_index_fill consumes. A fn_decl +// arrives already rehomed by body_lower_fn_decl_to_arrow; type_decl and data_decl did +// not, so flatten was empty for a type-only module (reconstruct kept raw shells — +// resolve walked On/Off as bare mentions) and nonempty when a fn sat beside them +// (the type shell was dropped). Neither arm is a refusal. Rehome type/data onto +// Named containment first; reconstruct remains only when nothing first-class +// survived (import-only and the lost-forest refusal still sit on that empty path). fn namespace_graft_body_edges_for_graft(root: Node) -> List { - let flattened = namespace_graft_collect_body_edges(root: root) - if count(flattened) == 0 { - let units = namespace_graft_collect_unit_nodes( - node: namespace_graft_unwrap_module_working_root(root: root) - ) + let working = namespace_graft_unwrap_module_working_root(root: root) + let units = namespace_graft_collect_unit_nodes(node: working) + let from_decls = namespace_graft_named_edges_from_units(units: units) + let flattened = namespace_graft_collect_body_edges_from_node(node: working) + let combined = concat(from_decls, flattened) + if count(combined) == 0 { if count(units) == 0 { [] } else { namespace_graft_reconstruct_declaration_forest(units: units) } } else { - flattened + combined } } diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag new file mode 100644 index 00000000000..9855e7789c3 --- /dev/null +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -0,0 +1,152 @@ +module v2.test.long.declaration_graft_assemble + +import extdeps.communication.medium { Lossless, Medium } +import v2.compiler.name_resolve { + Admission, + ResolutionSubject +} +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { + DagSourceReadWitness, + SourceRootIngest +} +import v2.extdeps.languages.dag { dag_language_model } +import std.algebra { Cons, Empty } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import v2.std.node { Named, Node, Positional, node_subtree_nodes } +import v2.std.text { String } +import v2.std.compilers.lexing { symbol_lexeme } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data declaration_graft_module_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^declaration_graft_module_artifact, + file_path: "src/v2/pilot/declaration_graft_pilot.dag" +} + +fn declaration_graft_ingest_for(src: String) -> SourceRootIngest { + Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: declaration_graft_module_artifact, + compilation_unit: ^declaration_graft_pilot_cu, + source_root: V2Tree + }, + tail: Empty + } +} + +fn declaration_graft_admission() -> Admission { + Admission { + subject: ResolutionSubject { + name: Cons { head: ^p, tail: Empty } + }, + imports: Empty + } +} + +fn declaration_graft_assemble_for(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: declaration_graft_ingest_for(src: src), + admission: declaration_graft_admission(), + lm: dag_language_model() + ) +} + +fn declaration_graft_assemble_accepts(src: String) -> Bool { + match declaration_graft_assemble_for(src: src) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn declaration_graft_label_present(root: Node, lexeme: String) -> Bool { + fold( + node_subtree_nodes(root: root), + init: false, + f: fn(found, n) { + if found { + true + } else { + fold(n.children, init: false, f: fn(inner, e) { + if inner { + true + } else { + match e.label { + Named { name: sym } => symbol_lexeme(sym: sym) == lexeme + Positional => false + } + } + }) + } + } + ) +} + +fn declaration_graft_named_reachable(src: String, lexeme: String) -> Bool { + match declaration_graft_assemble_for(src: src) { + Accepted { value: root, diagnostics: _ } => + declaration_graft_label_present(root: root, lexeme: lexeme) + Rejected { diagnostics: _ } => false + } +} + +data src_a: String = "module p\n" +data src_b: String = "module p\n\nfn f(x: Int) -> Int { x }\n" +data src_c: String = "module p\n\ntype Flag = On | Off\n" +data src_h: String = "module p\n\ntype Name = String where brand(\"Name\")\n" +data src_i: String = "module p\n\ntype Name = String where brand(\"Name\")\nfn f(x: Int) -> Int { x }\n" +data src_d: String = "module p\n\ntype Flag = On | Off\nfn g(f: Flag) -> Flag { f }\n" +data src_e: String = "module p\n\ntype Flag = On | Off\nfn h() -> Flag { On }\n" +data src_f: String = "module p\n\ntype Flag = On | Off\nfn flip(f: Flag) -> Flag { match f { On => Off } }\n" +data src_g: String = "module p\n\ntype Box { n: Int }\nfn mk() -> Box { Box { n: 1 } }\n" + +test fn declaration_graft_empty_module_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_a) +} + +test fn declaration_graft_fn_only_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_b) +} + +test fn declaration_graft_type_only_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_c) +} + +test fn declaration_graft_type_only_flag_is_named() -> Bool { + declaration_graft_named_reachable(src: src_c, lexeme: "Flag") +} + +test fn declaration_graft_where_alias_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_h) +} + +test fn declaration_graft_silent_drop_control_type_is_named() -> Bool { + declaration_graft_named_reachable(src: src_i, lexeme: "Name") +} + +test fn declaration_graft_fn_referencing_type_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_d) +} + +test fn declaration_graft_variant_construct_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_e) +} + +test fn declaration_graft_match_variants_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_f) +} + +test fn declaration_graft_record_construct_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_g) +} + +test fn declaration_graft_record_fields_are_named() -> Bool { + declaration_graft_named_reachable(src: src_g, lexeme: "Box") + && declaration_graft_named_reachable(src: src_g, lexeme: "n") +} diff --git a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag index 973cc70d133..e06c5fff514 100644 --- a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag +++ b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag @@ -113,32 +113,21 @@ test fn control_a_parameter_type_name_is_an_atom_in_the_tree_holds() -> Bool { atom_present(lexeme: "Xl0UserType") } -// CONTROL, CORRECTED: a type DECLARATION name is an edge label, not an atom. Asserting it as an -// atom was my error and its red said nothing about the tree. Asserted where it actually lives. +// CONTROL, CORRECTED: a type DECLARATION name is an edge label, not an atom. After declaration +// grafting it is a Named containment edge, which this search reaches. test fn the_record_type_declaration_is_absent_from_the_tree_today() -> Bool { - !present_anywhere(lexeme: "Xl0Wrapper") + present_anywhere(lexeme: "Xl0Wrapper") } -// WHAT WAS MEASURED, AND WHY IT IS NOT YET ATTRIBUTED TO ANYONE. -// -// The fixture source demonstrably contains `type Xl0Wrapper { fld: Xl0FieldOnly }`, the ingest -// ACCEPTS and yields both module roots, and the parameter-type control above finds Xl0UserType as -// an atom. Yet the record declaration's NAME, its FIELD name, and the field's TYPE are ALL absent -// from the ingested tree, in atoms and in edge labels alike. So the missing thing is not "field -// types" -- it is the whole record-type-with-a-field declaration. -// -// TWO HYPOTHESES REMAIN AND THIS FIXTURE CANNOT SEPARATE THEM: normalization drops such a -// declaration, or it is represented in some third way neither an atom search nor a label search -// reaches. I am not naming a capability on that, because the last four times I attributed an -// absence measured through one representation, the representation was the defect. The rows are -// therefore enrolled as MEASUREMENTS of what the search finds today, and the PR carries the cause -// as unverified. +// THESE THREE WERE ABSENCE MEASUREMENTS OF THE GRAFT DROP. Declaration grafting rehomes the +// record onto Named containment (wrapper label, field label, field-type atom-or-label), so they +// stay enrolled as presence controls (DESIGN 4b(4)). test fn the_bare_field_type_is_absent_from_the_tree_today() -> Bool { - !present_anywhere(lexeme: "Xl0FieldOnly") + present_anywhere(lexeme: "Xl0FieldOnly") } // The field NAME itself, as a third data point: if `fld` is a label but its TYPE is nowhere, the // declaration survived and only the type reference inside it was dropped. test fn the_field_name_is_absent_from_the_tree_today() -> Bool { - !present_anywhere(lexeme: "fld") + present_anywhere(lexeme: "fld") } diff --git a/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag b/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag index b173ec411b9..3985d534529 100644 --- a/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag +++ b/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag @@ -272,10 +272,10 @@ test fn a_real_loaded_compilation_is_reachable_without_reading_the_live_tree_hol // than retiring. The obligation is carried by that clause, not by this comment. test fn a_record_type_declaration_does_not_reach_the_index_while_a_fn_does_holds() -> Bool { let fn_is_indexed = lcr_indexed(dotted: "lcr.producer.producer_anchor") - let type_is_not_indexed = lcr_indexed(dotted: "lcr.producer.CrossFileMarker") == false + let type_is_indexed = lcr_indexed(dotted: "lcr.producer.CrossFileMarker") let fn_is_named_once = lcr_named_edges_in(module_qn: lcr_producer_qn, wanted: ^producer_anchor) == 1 - let type_is_named_never = lcr_named_edges_in(module_qn: lcr_producer_qn, wanted: ^CrossFileMarker) == 0 - fn_is_indexed && type_is_not_indexed && fn_is_named_once && type_is_named_never + let type_is_named_once = lcr_named_edges_in(module_qn: lcr_producer_qn, wanted: ^CrossFileMarker) == 1 + fn_is_indexed && type_is_indexed && fn_is_named_once && type_is_named_once } // RECEIPT 3 -- CARRIER SHAPE, measured on the same inline load: function bodies From c7eb5972fc9dc5cbae4ce3836e445d8f15f484d7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 04:07:44 +0000 Subject: [PATCH 02/56] Use match for type/data unit dispatch so the optional Edge unifies. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index f2f4f3261fc..e9177479407 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -484,12 +484,13 @@ fn namespace_graft_data_decl_named_edge(unit: Node) -> Edge? { fn namespace_graft_named_edge_from_unit(unit: Node) -> Edge? { match parse_production_emitted_identity_optional(node: unit) { Present { value: id } => - if id == ^dag_surface_type_decl { - namespace_graft_type_decl_named_edge(unit: unit) - } else if id == ^dag_surface_data_decl { - namespace_graft_data_decl_named_edge(unit: unit) - } else { - optional_absent() + match id == ^dag_surface_type_decl { + true => namespace_graft_type_decl_named_edge(unit: unit) + false => + match id == ^dag_surface_data_decl { + true => namespace_graft_data_decl_named_edge(unit: unit) + false => optional_absent() + } } Absent => optional_absent() } From f8b658746f048d9010fabd7cfbfb87dc7109e1da Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 04:15:44 +0000 Subject: [PATCH 03/56] Do not treat a type/data shell without a captured child as a declaration name. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index e9177479407..c186401a401 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -441,10 +441,7 @@ fn namespace_graft_type_decl_target(captured: Node) -> Node { } fn namespace_graft_unit_captured(unit: Node) -> Node? { - match parse_production_captured_child_optional(node: unit) { - Present { value: captured } => optional_present(value: captured) - Absent => optional_present(value: unit) - } + parse_production_captured_child_optional(node: unit) } fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { From c773382226ed821c42d1d5e8688b44f64d1c87af Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 04:25:17 +0000 Subject: [PATCH 04/56] Pair record field names with their types from user atoms under the field block. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 96 ++++++++++++++--------------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index c186401a401..26bfe0ef1a5 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -2,7 +2,7 @@ module v2.compiler.namespace_graft import std.occurrence_identity { OccurrenceSynthetic } import std.algebra { Cons, Empty, FreeMonoid } -import v2.std.algebra { fold_list, fold_list_right, list_append, list_snoc_item } +import v2.std.algebra { fold_list, fold_list_right, list_append, list_head, list_snoc_item, list_tail, HeadAbsent, HeadFound, TailAbsent, TailFound } import v2.std.collection { Absent, List, Present, optional_absent, optional_present } import v2.std.compilers.sugar { sugar_sequence_pair_optional, sugar_unwrap_production_shell } import v2.std.diagnostic { @@ -18,6 +18,7 @@ import v2.std.diagnostic { } import v2.std.logic { Bool } import v2.std.node { + Atom, Cardinality, Conj, Disj, @@ -346,74 +347,69 @@ fn namespace_graft_find_disj(node: Node) -> Node? { } } -fn namespace_graft_is_colon_atom(node: Node) -> Bool { - match node_atom_identity_optional(node: sugar_unwrap_production_shell(node: node)) { - Present { value: id } => id == ^dag_token_colon - Absent => false +fn namespace_graft_is_machinery_symbol(sym: Symbol) -> Bool { + if dag_lex_token_symbol(sym: sym) { + true + } else { + let lex = symbol_lexeme(sym: sym) + starts_with(s: lex, prefix: "dag_") || starts_with(s: lex, prefix: "grammar_") } } -fn namespace_graft_type_position_node(node: Node) -> Node { +fn namespace_graft_user_atom_ident(node: Node) -> Symbol? { match parse_production_emitted_identity_optional(node: node) { - Present { value: id } => - if id == ^dag_surface_type_expr { - match parse_production_captured_child_optional(node: node) { - Present { value: captured } => captured - Absent => node - } - } else { - match sugar_sequence_pair_optional(node: node) { - Present { value: pair } => - if namespace_graft_is_colon_atom(node: pair.left) { - namespace_graft_type_position_node(node: pair.right) - } else { - node - } - Absent => node - } - } + Present { value: _ } => optional_absent() Absent => - match sugar_sequence_pair_optional(node: node) { - Present { value: pair } => - if namespace_graft_is_colon_atom(node: pair.left) { - namespace_graft_type_position_node(node: pair.right) + match node_atom_identity_optional(node: sugar_unwrap_production_shell(node: node)) { + Present { value: id } => + if namespace_graft_is_machinery_symbol(sym: id) { + optional_absent() } else { - node + optional_present(value: id) } - Absent => node + Absent => optional_absent() } } } -fn namespace_graft_field_edge_optional(node: Node) -> Edge? { - match sugar_sequence_pair_optional(node: node) { - Absent => optional_absent() - Present { value: pair } => - match namespace_graft_ident_from_node(node: pair.left) { - Absent => optional_absent() - Present { value: field_name } => - if namespace_graft_subtree_contains_atom(n: pair.right, sym: ^dag_token_colon) { - optional_present( - value: Edge { +fn namespace_graft_collect_user_atom_idents(node: Node) -> List { + match namespace_graft_user_atom_ident(node: node) { + Present { value: id } => [id] + Absent => + fold(node.children, init: [], f: fn(acc, e) { + concat(acc, namespace_graft_collect_user_atom_idents(node: e.target)) + }) + } +} + +fn namespace_graft_field_edges_from_syms(syms: List) -> List { + match list_head(xs: syms) { + HeadAbsent => [] + HeadFound { value: field_name } => + match list_tail(xs: syms) { + TailAbsent => [] + TailFound { tail: after_name } => + match list_head(xs: after_name) { + HeadAbsent => [] + HeadFound { value: type_sym } => + let edge = Edge { label: Named { name: field_name }, - target: namespace_graft_type_position_node(node: pair.right) + target: namespace_graft_atom_node(id: type_sym) + } + match list_tail(xs: after_name) { + TailAbsent => [edge] + TailFound { tail: more } => + concat([edge], namespace_graft_field_edges_from_syms(syms: more)) } - ) - } else { - optional_absent() } } } } fn namespace_graft_collect_field_edges(node: Node) -> List { - match namespace_graft_field_edge_optional(node: node) { - Present { value: edge } => [edge] - Absent => - fold(node.children, init: [], f: fn(acc, e) { - concat(acc, namespace_graft_collect_field_edges(node: e.target)) - }) - } + namespace_graft_field_edges_from_syms( + syms: namespace_graft_collect_user_atom_idents(node: node) + ) } fn namespace_graft_type_decl_target(captured: Node) -> Node { From fcec4c2f104eb1077fb432aa18d3c386ea54dbdb Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 04:33:43 +0000 Subject: [PATCH 05/56] Probe why record construction still refuses after type grafting. Co-authored-by: Cursor --- .../long/declaration_graft_assemble_test.dag | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 9855e7789c3..8d84bc644bd 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -142,8 +142,26 @@ test fn declaration_graft_match_variants_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_f) } -test fn declaration_graft_record_construct_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_g) +fn declaration_graft_assemble_reason(src: String) -> String { + match declaration_graft_assemble_for(src: src) { + Accepted { value: _, diagnostics: _ } => "ACCEPTED" + Rejected { diagnostics: d } => symbol_lexeme(sym: d.head.reason) + } +} + +data src_g_type_only: String = "module p\n\ntype Box { n: Int }\n" + +test fn declaration_graft_record_type_only_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_g_type_only) +} + +test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { + declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Box") + && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") +} + +test fn declaration_graft_record_construct_reason_is_unbound() -> Bool { + declaration_graft_assemble_reason(src: src_g) == "resolve_reason_unbound_symbol" } test fn declaration_graft_record_fields_are_named() -> Bool { From 7139336ad57c94fbf243b487443ce0f2c2a378b9 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 04:45:01 +0000 Subject: [PATCH 06/56] Keep only user-named flatten members so record field blocks are not module mentions. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 26bfe0ef1a5..f1b8937fdba 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -505,11 +505,37 @@ fn namespace_graft_named_edges_from_units(units: List) -> List { // (the type shell was dropped). Neither arm is a refusal. Rehome type/data onto // Named containment first; reconstruct remains only when nothing first-class // survived (import-only and the lost-forest refusal still sit on that empty path). +fn namespace_graft_keep_flattened_member_edge(edge: Edge) -> Bool { + match edge.label { + Named { name: sym } => + if namespace_graft_parse_projection_edge(name: sym) { + false + } else if namespace_graft_is_metadata_edge(name: sym) { + false + } else { + !namespace_graft_is_machinery_symbol(sym: sym) + } + Positional => false + } +} + +fn namespace_graft_flattened_member_edges(edges: List) -> List { + fold(edges, init: [], f: fn(acc, edge) { + if namespace_graft_keep_flattened_member_edge(edge: edge) { + list_snoc_item(xs: acc, item: edge) + } else { + acc + } + }) +} + fn namespace_graft_body_edges_for_graft(root: Node) -> List { let working = namespace_graft_unwrap_module_working_root(root: root) let units = namespace_graft_collect_unit_nodes(node: working) let from_decls = namespace_graft_named_edges_from_units(units: units) - let flattened = namespace_graft_collect_body_edges_from_node(node: working) + let flattened = namespace_graft_flattened_member_edges( + edges: namespace_graft_collect_body_edges_from_node(node: working) + ) let combined = concat(from_decls, flattened) if count(combined) == 0 { if count(units) == 0 { From 8bfb5c88b6970b3d1cb78f4d6e92ce17c4202a53 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 04:45:22 +0000 Subject: [PATCH 07/56] Restore the record-construct assemble hold after the flatten filter. Co-authored-by: Cursor --- src/v2/test/claim/long/declaration_graft_assemble_test.dag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 8d84bc644bd..a9387c2e9d5 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -160,8 +160,8 @@ test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") } -test fn declaration_graft_record_construct_reason_is_unbound() -> Bool { - declaration_graft_assemble_reason(src: src_g) == "resolve_reason_unbound_symbol" +test fn declaration_graft_record_construct_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_g) } test fn declaration_graft_record_fields_are_named() -> Bool { From 80feda155c113a6fddd89e8d4a99c1eeed8e051a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 04:54:34 +0000 Subject: [PATCH 08/56] Take the type name as the first user atom in the declaration, not sequence-left. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index f1b8937fdba..2d50c788c20 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -423,14 +423,14 @@ fn namespace_graft_type_decl_target(captured: Node) -> Node { children: namespace_graft_collect_field_edges(node: block) ) Absent => - match namespace_graft_find_emitted(node: captured, emitted: ^dag_surface_type_alias_rhs) { - Present { value: rhs } => - match parse_production_captured_child_optional(node: rhs) { - Present { value: rhs_captured } => rhs_captured - Absent => rhs - } - Absent => + match list_tail(xs: namespace_graft_collect_user_atom_idents(node: captured)) { + TailAbsent => node_synthetic(kind: TypeNode { connective: Conj }, children: []) + TailFound { tail: field_syms } => + node_synthetic( + kind: TypeNode { connective: Conj }, + children: namespace_graft_field_edges_from_syms(syms: field_syms) + ) } } } @@ -444,9 +444,9 @@ fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { match namespace_graft_unit_captured(unit: unit) { Absent => optional_absent() Present { value: captured } => - match namespace_graft_kw_then_ident(captured: captured) { - Absent => optional_absent() - Present { value: type_name } => + match list_head(xs: namespace_graft_collect_user_atom_idents(node: captured)) { + HeadAbsent => optional_absent() + HeadFound { value: type_name } => optional_present( value: Edge { label: Named { name: type_name }, From b83df09a2d3f5e9ef457219933c41d0897d9adfb Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 05:11:58 +0000 Subject: [PATCH 09/56] Rehome unwrapped record types from ident + field-block sequences. A record declaration can lose its type_decl shell while the field block survives as a non-projection node, so unit collection never sees it. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 47 ++++++++++++++++++- .../long/declaration_graft_assemble_test.dag | 7 +-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 2d50c788c20..f1f1e333c0a 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -498,6 +498,51 @@ fn namespace_graft_named_edges_from_units(units: List) -> List { }) } +fn namespace_graft_record_edge_from_sequence(node: Node) -> Edge? { + match sugar_sequence_pair_optional(node: node) { + Absent => optional_absent() + Present { value: pair } => + match namespace_graft_ident_from_node(node: pair.left) { + Absent => optional_absent() + Present { value: type_name } => + match namespace_graft_find_emitted( + node: pair.right, + emitted: ^dag_surface_field_decl_block + ) { + Absent => optional_absent() + Present { value: block } => + if namespace_graft_is_machinery_symbol(sym: type_name) { + optional_absent() + } else { + optional_present( + value: Edge { + label: Named { name: type_name }, + target: node_synthetic( + kind: TypeNode { connective: Conj }, + children: namespace_graft_collect_field_edges(node: block) + ) + } + ) + } + } + } + } +} + +fn namespace_graft_decl_edges_from_node(node: Node) -> List { + match namespace_graft_named_edge_from_unit(unit: node) { + Present { value: edge } => [edge] + Absent => + match namespace_graft_record_edge_from_sequence(node: node) { + Present { value: edge } => [edge] + Absent => + fold(node.children, init: [], f: fn(acc, e) { + concat(acc, namespace_graft_decl_edges_from_node(node: e.target)) + }) + } + } +} + // Flattening to Named declaration edges is what symbol_index_fill consumes. A fn_decl // arrives already rehomed by body_lower_fn_decl_to_arrow; type_decl and data_decl did // not, so flatten was empty for a type-only module (reconstruct kept raw shells — @@ -532,7 +577,7 @@ fn namespace_graft_flattened_member_edges(edges: List) -> List { fn namespace_graft_body_edges_for_graft(root: Node) -> List { let working = namespace_graft_unwrap_module_working_root(root: root) let units = namespace_graft_collect_unit_nodes(node: working) - let from_decls = namespace_graft_named_edges_from_units(units: units) + let from_decls = namespace_graft_decl_edges_from_node(node: working) let flattened = namespace_graft_flattened_member_edges( edges: namespace_graft_collect_body_edges_from_node(node: working) ) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index a9387c2e9d5..11bbffff9f4 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -104,7 +104,8 @@ data src_i: String = "module p\n\ntype Name = String where brand(\"Name\")\nfn f data src_d: String = "module p\n\ntype Flag = On | Off\nfn g(f: Flag) -> Flag { f }\n" data src_e: String = "module p\n\ntype Flag = On | Off\nfn h() -> Flag { On }\n" data src_f: String = "module p\n\ntype Flag = On | Off\nfn flip(f: Flag) -> Flag { match f { On => Off } }\n" -data src_g: String = "module p\n\ntype Box { n: Int }\nfn mk() -> Box { Box { n: 1 } }\n" +data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: 1 } }\n" +data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -156,7 +157,7 @@ test fn declaration_graft_record_type_only_accepts() -> Bool { } test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { - declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Box") + declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") } @@ -165,6 +166,6 @@ test fn declaration_graft_record_construct_accepts() -> Bool { } test fn declaration_graft_record_fields_are_named() -> Bool { - declaration_graft_named_reachable(src: src_g, lexeme: "Box") + declaration_graft_named_reachable(src: src_g, lexeme: "Rec") && declaration_graft_named_reachable(src: src_g, lexeme: "n") } From 267bc6034fb3cca791b71bc07e3ce8af35224205 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 05:12:13 +0000 Subject: [PATCH 10/56] Drop the duplicate Box record source and unused assemble-reason helper. Co-authored-by: Cursor --- .../test/claim/long/declaration_graft_assemble_test.dag | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 11bbffff9f4..5878e199682 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -143,15 +143,6 @@ test fn declaration_graft_match_variants_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_f) } -fn declaration_graft_assemble_reason(src: String) -> String { - match declaration_graft_assemble_for(src: src) { - Accepted { value: _, diagnostics: _ } => "ACCEPTED" - Rejected { diagnostics: d } => symbol_lexeme(sym: d.head.reason) - } -} - -data src_g_type_only: String = "module p\n\ntype Box { n: Int }\n" - test fn declaration_graft_record_type_only_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_g_type_only) } From 295036c1a157fe3d3e11d66a6e66b7ad6d97a7fa Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 05:20:55 +0000 Subject: [PATCH 11/56] Bind a pending type name to the next field-decl block in graft order. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 109 +++++++++++++++++++--------- 1 file changed, 75 insertions(+), 34 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index f1f1e333c0a..45c33ac776c 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -498,49 +498,90 @@ fn namespace_graft_named_edges_from_units(units: List) -> List { }) } -fn namespace_graft_record_edge_from_sequence(node: Node) -> Edge? { - match sugar_sequence_pair_optional(node: node) { - Absent => optional_absent() - Present { value: pair } => - match namespace_graft_ident_from_node(node: pair.left) { - Absent => optional_absent() - Present { value: type_name } => - match namespace_graft_find_emitted( - node: pair.right, - emitted: ^dag_surface_field_decl_block - ) { - Absent => optional_absent() - Present { value: block } => - if namespace_graft_is_machinery_symbol(sym: type_name) { - optional_absent() - } else { - optional_present( - value: Edge { - label: Named { name: type_name }, - target: node_synthetic( - kind: TypeNode { connective: Conj }, - children: namespace_graft_collect_field_edges(node: block) +type NamespaceGraftDeclWalk { + pending: Symbol? + edges: List +} + +fn namespace_graft_decl_walk_init() -> NamespaceGraftDeclWalk { + NamespaceGraftDeclWalk { + pending: optional_absent(), + edges: [] + } +} + +fn namespace_graft_decl_walk_step( + acc: NamespaceGraftDeclWalk, + node: Node +) -> NamespaceGraftDeclWalk { + if namespace_graft_is_module_header_subtree(node: node) { + acc + } else { + match namespace_graft_named_edge_from_unit(unit: node) { + Present { value: edge } => + NamespaceGraftDeclWalk { + pending: optional_absent(), + edges: list_snoc_item(xs: acc.edges, item: edge) + } + Absent => + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => + match id == ^dag_surface_field_decl_block { + true => + match acc.pending { + Absent => + NamespaceGraftDeclWalk { + pending: optional_absent(), + edges: acc.edges + } + Present { value: type_name } => + NamespaceGraftDeclWalk { + pending: optional_absent(), + edges: list_snoc_item( + xs: acc.edges, + item: Edge { + label: Named { name: type_name }, + target: node_synthetic( + kind: TypeNode { connective: Conj }, + children: namespace_graft_collect_field_edges(node: node) + ) + } ) } - ) } + false => namespace_graft_decl_walk_children(acc: acc, node: node) + } + Absent => + match namespace_graft_user_atom_ident(node: node) { + Present { value: ident } => + match acc.pending { + Present { value: _ } => namespace_graft_decl_walk_children(acc: acc, node: node) + Absent => + namespace_graft_decl_walk_children( + acc: NamespaceGraftDeclWalk { + pending: optional_present(value: ident), + edges: acc.edges + }, + node: node + ) + } + Absent => namespace_graft_decl_walk_children(acc: acc, node: node) } } } } +fn namespace_graft_decl_walk_children( + acc: NamespaceGraftDeclWalk, + node: Node +) -> NamespaceGraftDeclWalk { + fold(node.children, init: acc, f: fn(inner, e) { + namespace_graft_decl_walk_step(acc: inner, node: e.target) + }) +} + fn namespace_graft_decl_edges_from_node(node: Node) -> List { - match namespace_graft_named_edge_from_unit(unit: node) { - Present { value: edge } => [edge] - Absent => - match namespace_graft_record_edge_from_sequence(node: node) { - Present { value: edge } => [edge] - Absent => - fold(node.children, init: [], f: fn(acc, e) { - concat(acc, namespace_graft_decl_edges_from_node(node: e.target)) - }) - } - } + namespace_graft_decl_walk_step(acc: namespace_graft_decl_walk_init(), node: node).edges } // Flattening to Named declaration edges is what symbol_index_fill consumes. A fn_decl From b65254dad49a76adc688b09be21f23ef6a0ace7c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 05:42:18 +0000 Subject: [PATCH 12/56] Name record types from the type keyword then ident, and keep flatten only for functions. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 155 +++++++++++------- .../long/declaration_graft_assemble_test.dag | 11 ++ 2 files changed, 103 insertions(+), 63 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 45c33ac776c..5f79c2b85bf 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -18,6 +18,7 @@ import v2.std.diagnostic { } import v2.std.logic { Bool } import v2.std.node { + Arrow, Atom, Cardinality, Conj, @@ -440,20 +441,27 @@ fn namespace_graft_unit_captured(unit: Node) -> Node? { parse_production_captured_child_optional(node: unit) } -fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { +fn namespace_graft_type_decl_name(unit: Node) -> Symbol? { match namespace_graft_unit_captured(unit: unit) { - Absent => optional_absent() Present { value: captured } => - match list_head(xs: namespace_graft_collect_user_atom_idents(node: captured)) { - HeadAbsent => optional_absent() - HeadFound { value: type_name } => - optional_present( - value: Edge { - label: Named { name: type_name }, - target: namespace_graft_type_decl_target(captured: captured) - } - ) + match namespace_graft_kw_then_ident(captured: captured) { + Present { value: name } => optional_present(value: name) + Absent => namespace_graft_ident_from_node(node: unit) } + Absent => namespace_graft_ident_from_node(node: unit) + } +} + +fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { + match namespace_graft_type_decl_name(unit: unit) { + Absent => optional_absent() + Present { value: type_name } => + optional_present( + value: Edge { + label: Named { name: type_name }, + target: namespace_graft_type_decl_target(captured: unit) + } + ) } } @@ -510,6 +518,35 @@ fn namespace_graft_decl_walk_init() -> NamespaceGraftDeclWalk { } } +fn namespace_graft_bind_pending_field_block( + acc: NamespaceGraftDeclWalk, + node: Node +) -> NamespaceGraftDeclWalk? { + match acc.pending { + Absent => optional_absent() + Present { value: type_name } => + match namespace_graft_find_emitted(node: node, emitted: ^dag_surface_field_decl_block) { + Absent => optional_absent() + Present { value: block } => + optional_present( + value: NamespaceGraftDeclWalk { + pending: optional_absent(), + edges: list_snoc_item( + xs: acc.edges, + item: Edge { + label: Named { name: type_name }, + target: node_synthetic( + kind: TypeNode { connective: Conj }, + children: namespace_graft_collect_field_edges(node: block) + ) + } + ) + } + ) + } + } +} + fn namespace_graft_decl_walk_step( acc: NamespaceGraftDeclWalk, node: Node @@ -517,57 +554,37 @@ fn namespace_graft_decl_walk_step( if namespace_graft_is_module_header_subtree(node: node) { acc } else { - match namespace_graft_named_edge_from_unit(unit: node) { - Present { value: edge } => - NamespaceGraftDeclWalk { - pending: optional_absent(), - edges: list_snoc_item(xs: acc.edges, item: edge) - } - Absent => - match parse_production_emitted_identity_optional(node: node) { - Present { value: id } => - match id == ^dag_surface_field_decl_block { - true => - match acc.pending { - Absent => - NamespaceGraftDeclWalk { - pending: optional_absent(), - edges: acc.edges - } - Present { value: type_name } => - NamespaceGraftDeclWalk { - pending: optional_absent(), - edges: list_snoc_item( - xs: acc.edges, - item: Edge { - label: Named { name: type_name }, - target: node_synthetic( - kind: TypeNode { connective: Conj }, - children: namespace_graft_collect_field_edges(node: node) - ) - } - ) - } - } - false => namespace_graft_decl_walk_children(acc: acc, node: node) - } - Absent => - match namespace_graft_user_atom_ident(node: node) { - Present { value: ident } => - match acc.pending { - Present { value: _ } => namespace_graft_decl_walk_children(acc: acc, node: node) - Absent => - namespace_graft_decl_walk_children( - acc: NamespaceGraftDeclWalk { - pending: optional_present(value: ident), - edges: acc.edges - }, - node: node - ) - } - Absent => namespace_graft_decl_walk_children(acc: acc, node: node) - } - } + match namespace_graft_named_edge_from_unit(unit: node) { + Present { value: edge } => + NamespaceGraftDeclWalk { + pending: optional_absent(), + edges: list_snoc_item(xs: acc.edges, item: edge) + } + Absent => + match namespace_graft_bind_pending_field_block(acc: acc, node: node) { + Present { value: bound } => bound + Absent => + match namespace_graft_ident_from_node(node: node) { + Present { value: ident } => + match acc.pending { + Present { value: _ } => namespace_graft_decl_walk_children(acc: acc, node: node) + Absent => + if namespace_graft_is_machinery_symbol(sym: ident) { + namespace_graft_decl_walk_children(acc: acc, node: node) + } else { + namespace_graft_decl_walk_children( + acc: NamespaceGraftDeclWalk { + pending: optional_present(value: ident), + edges: acc.edges + }, + node: node + ) + } + } + Absent => namespace_graft_decl_walk_children(acc: acc, node: node) + } + } + } } } @@ -591,6 +608,16 @@ fn namespace_graft_decl_edges_from_node(node: Node) -> List { // (the type shell was dropped). Neither arm is a refusal. Rehome type/data onto // Named containment first; reconstruct remains only when nothing first-class // survived (import-only and the lost-forest refusal still sit on that empty path). +fn namespace_graft_subtree_has_arrow(node: Node) -> Bool { + match node.kind { + TypeNode { connective: Arrow } => true + _ => + fold(node.children, init: false, f: fn(found, e) { + found || namespace_graft_subtree_has_arrow(node: e.target) + }) + } +} + fn namespace_graft_keep_flattened_member_edge(edge: Edge) -> Bool { match edge.label { Named { name: sym } => @@ -598,8 +625,10 @@ fn namespace_graft_keep_flattened_member_edge(edge: Edge) -> Bool { false } else if namespace_graft_is_metadata_edge(name: sym) { false + } else if namespace_graft_is_machinery_symbol(sym: sym) { + false } else { - !namespace_graft_is_machinery_symbol(sym: sym) + namespace_graft_subtree_has_arrow(node: edge.target) } Positional => false } diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 5878e199682..884f901bbd5 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -65,6 +65,13 @@ fn declaration_graft_assemble_accepts(src: String) -> Bool { } } +fn declaration_graft_assemble_reason(src: String) -> String { + match declaration_graft_assemble_for(src: src) { + Accepted { value: _, diagnostics: _ } => "ACCEPTED" + Rejected { diagnostics: d } => symbol_lexeme(sym: d.head.reason) + } +} + fn declaration_graft_label_present(root: Node, lexeme: String) -> Bool { fold( node_subtree_nodes(root: root), @@ -147,6 +154,10 @@ test fn declaration_graft_record_type_only_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_g_type_only) } +test fn declaration_graft_record_type_only_reason_is_unbound() -> Bool { + declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_unbound_symbol" +} + test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") From 50e87ba0873b9e23f518ab52bea2dedc1881f209 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 05:51:13 +0000 Subject: [PATCH 13/56] Keep flatten members whose targets are types or functions, not bare field atoms. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 8 +++++--- .../claim/long/declaration_graft_assemble_test.dag | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 5f79c2b85bf..60ecf7972de 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -608,12 +608,14 @@ fn namespace_graft_decl_edges_from_node(node: Node) -> List { // (the type shell was dropped). Neither arm is a refusal. Rehome type/data onto // Named containment first; reconstruct remains only when nothing first-class // survived (import-only and the lost-forest refusal still sit on that empty path). -fn namespace_graft_subtree_has_arrow(node: Node) -> Bool { +fn namespace_graft_is_declaration_target(node: Node) -> Bool { match node.kind { TypeNode { connective: Arrow } => true + TypeNode { connective: Conj } => true + TypeNode { connective: Disj } => true _ => fold(node.children, init: false, f: fn(found, e) { - found || namespace_graft_subtree_has_arrow(node: e.target) + found || namespace_graft_is_declaration_target(node: e.target) }) } } @@ -628,7 +630,7 @@ fn namespace_graft_keep_flattened_member_edge(edge: Edge) -> Bool { } else if namespace_graft_is_machinery_symbol(sym: sym) { false } else { - namespace_graft_subtree_has_arrow(node: edge.target) + namespace_graft_is_declaration_target(node: edge.target) } Positional => false } diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 884f901bbd5..deb015351a2 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -158,6 +158,18 @@ test fn declaration_graft_record_type_only_reason_is_unbound() -> Bool { declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_unbound_symbol" } +test fn declaration_graft_record_type_only_reason_is_dissolved() -> Bool { + declaration_graft_assemble_reason(src: src_g_type_only) == "namespace_graft_body_dissolved_refused" +} + +test fn declaration_graft_record_type_only_reason_is_dual_position() -> Bool { + declaration_graft_assemble_reason(src: src_g_type_only) == "namespace_graft_dual_position_refused" +} + +test fn declaration_graft_record_type_only_reason_is_qn_invalid() -> Bool { + declaration_graft_assemble_reason(src: src_g_type_only) == "qualified_name_structure_invalid" +} + test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") From 672f32546d3fc70468ad009cc4b119630b64f96a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 06:00:07 +0000 Subject: [PATCH 14/56] Discriminate the record-graft refusal by reason prefix. Co-authored-by: Cursor --- .../long/declaration_graft_assemble_test.dag | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index deb015351a2..1114653178d 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -18,7 +18,7 @@ import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } import v2.std.node { Named, Node, Positional, node_subtree_nodes } -import v2.std.text { String } +import v2.std.text { String, starts_with } import v2.std.compilers.lexing { symbol_lexeme } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -72,6 +72,10 @@ fn declaration_graft_assemble_reason(src: String) -> String { } } +fn declaration_graft_reason_prefix(src: String, prefix: String) -> Bool { + starts_with(s: declaration_graft_assemble_reason(src: src), prefix: prefix) +} + fn declaration_graft_label_present(root: Node, lexeme: String) -> Bool { fold( node_subtree_nodes(root: root), @@ -170,6 +174,38 @@ test fn declaration_graft_record_type_only_reason_is_qn_invalid() -> Bool { declaration_graft_assemble_reason(src: src_g_type_only) == "qualified_name_structure_invalid" } +test fn declaration_graft_record_reason_prefix_resolve() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve") +} + +test fn declaration_graft_record_reason_prefix_namespace() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "namespace") +} + +test fn declaration_graft_record_reason_prefix_type() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "type") +} + +test fn declaration_graft_record_reason_prefix_well() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "well") +} + +test fn declaration_graft_record_reason_prefix_parse() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "parse") +} + +test fn declaration_graft_record_reason_prefix_body() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "body") +} + +test fn declaration_graft_record_reason_prefix_lower() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "lower") +} + +test fn declaration_graft_record_reason_prefix_graft() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "graft") +} + test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") From f59380e599d5c1705f4a369891de8de6ffa86e6c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 06:08:13 +0000 Subject: [PATCH 15/56] Use the builtin starts_with for record-reason prefixes. Co-authored-by: Cursor --- src/v2/test/claim/long/declaration_graft_assemble_test.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 1114653178d..e5ebcbfddc2 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -18,7 +18,7 @@ import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } import v2.std.node { Named, Node, Positional, node_subtree_nodes } -import v2.std.text { String, starts_with } +import v2.std.text { String } import v2.std.compilers.lexing { symbol_lexeme } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly From 0cb1f05ba6e055a3364b8d7ee9ed0758a533a2b5 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 06:17:12 +0000 Subject: [PATCH 16/56] Pin the record assemble refusal to remaining resolve reasons. Co-authored-by: Cursor --- .../claim/long/declaration_graft_assemble_test.dag | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index e5ebcbfddc2..477d6aa8d6f 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -202,8 +202,16 @@ test fn declaration_graft_record_reason_prefix_lower() -> Bool { declaration_graft_reason_prefix(src: src_g_type_only, prefix: "lower") } -test fn declaration_graft_record_reason_prefix_graft() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "graft") +test fn declaration_graft_record_type_only_reason_is_malformed() -> Bool { + declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_malformed_tree" +} + +test fn declaration_graft_record_type_only_reason_is_ambiguous() -> Bool { + declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_ambiguous_symbol" +} + +test fn declaration_graft_record_type_only_reason_is_malformed_imported() -> Bool { + declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_malformed_imported_root" } test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { From a176c6a7bc363af97efaabb418c061443fcc93ef Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 06:25:48 +0000 Subject: [PATCH 17/56] Narrow the resolve-* refusal family for record graft. Co-authored-by: Cursor --- .../long/declaration_graft_assemble_test.dag | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 477d6aa8d6f..16dfbaca6a9 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -210,8 +210,28 @@ test fn declaration_graft_record_type_only_reason_is_ambiguous() -> Bool { declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_ambiguous_symbol" } -test fn declaration_graft_record_type_only_reason_is_malformed_imported() -> Bool { - declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_malformed_imported_root" +test fn declaration_graft_record_reason_prefix_resolve_reason() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_reason") +} + +test fn declaration_graft_record_reason_prefix_resolve_duplicate() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_duplicate") +} + +test fn declaration_graft_record_reason_prefix_resolve_module() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_module") +} + +test fn declaration_graft_record_reason_prefix_resolve_ambiguous() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_ambiguous") +} + +test fn declaration_graft_record_reason_prefix_resolve_import() -> Bool { + declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_import") +} + +test fn declaration_graft_record_reason_is_duplicate_module() -> Bool { + declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_duplicate_module_name" } test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { From 269ddb068ab39e2c4b70d2abc3cc3eedc16a34d4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 06:34:01 +0000 Subject: [PATCH 18/56] Do not treat the module qualified name as a pending type during graft. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 60ecf7972de..483909639bd 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -108,7 +108,8 @@ fn namespace_graft_parse_projection_edge(name: Symbol) -> Bool { fn namespace_graft_is_module_header_subtree(node: Node) -> Bool { match parse_production_emitted_identity_optional(node: node) { - Present { value: id } => id == ^dag_surface_module_header + Present { value: id } => + (id == ^dag_surface_module_header) || (id == ^dag_surface_qualified_name) Absent => false } } From 3f4e1664088acc0eab1225cdc40eb3ac8d1c00d3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 06:43:48 +0000 Subject: [PATCH 19/56] Probe empty record braces separately from fielded records. Co-authored-by: Cursor --- src/v2/test/claim/long/declaration_graft_assemble_test.dag | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 16dfbaca6a9..c01afbd80dc 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -115,8 +115,8 @@ data src_i: String = "module p\n\ntype Name = String where brand(\"Name\")\nfn f data src_d: String = "module p\n\ntype Flag = On | Off\nfn g(f: Flag) -> Flag { f }\n" data src_e: String = "module p\n\ntype Flag = On | Off\nfn h() -> Flag { On }\n" data src_f: String = "module p\n\ntype Flag = On | Off\nfn flip(f: Flag) -> Flag { match f { On => Off } }\n" -data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: 1 } }\n" -data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" +data src_g: String = "module p\n\ntype Rec {}\nfn mk() -> Rec { Rec {} }\n" +data src_g_type_only: String = "module p\n\ntype Rec {}\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -236,7 +236,6 @@ test fn declaration_graft_record_reason_is_duplicate_module() -> Bool { test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") - && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") } test fn declaration_graft_record_construct_accepts() -> Bool { @@ -245,5 +244,4 @@ test fn declaration_graft_record_construct_accepts() -> Bool { test fn declaration_graft_record_fields_are_named() -> Bool { declaration_graft_named_reachable(src: src_g, lexeme: "Rec") - && declaration_graft_named_reachable(src: src_g, lexeme: "n") } From e9ee8c5334912ca85b22379f42f12f170c7818a2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 06:53:10 +0000 Subject: [PATCH 20/56] Record field bags are Cardinality so a type-only record is not a graft-spine segment. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 8 ++++---- .../test/claim/long/declaration_graft_assemble_test.dag | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 483909639bd..e8563d6af91 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -421,16 +421,16 @@ fn namespace_graft_type_decl_target(captured: Node) -> Node { match namespace_graft_find_emitted(node: captured, emitted: ^dag_surface_field_decl_block) { Present { value: block } => node_synthetic( - kind: TypeNode { connective: Conj }, + kind: TypeNode { connective: Cardinality }, children: namespace_graft_collect_field_edges(node: block) ) Absent => match list_tail(xs: namespace_graft_collect_user_atom_idents(node: captured)) { TailAbsent => - node_synthetic(kind: TypeNode { connective: Conj }, children: []) + node_synthetic(kind: TypeNode { connective: Cardinality }, children: []) TailFound { tail: field_syms } => node_synthetic( - kind: TypeNode { connective: Conj }, + kind: TypeNode { connective: Cardinality }, children: namespace_graft_field_edges_from_syms(syms: field_syms) ) } @@ -537,7 +537,7 @@ fn namespace_graft_bind_pending_field_block( item: Edge { label: Named { name: type_name }, target: node_synthetic( - kind: TypeNode { connective: Conj }, + kind: TypeNode { connective: Cardinality }, children: namespace_graft_collect_field_edges(node: block) ) } diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index c01afbd80dc..16dfbaca6a9 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -115,8 +115,8 @@ data src_i: String = "module p\n\ntype Name = String where brand(\"Name\")\nfn f data src_d: String = "module p\n\ntype Flag = On | Off\nfn g(f: Flag) -> Flag { f }\n" data src_e: String = "module p\n\ntype Flag = On | Off\nfn h() -> Flag { On }\n" data src_f: String = "module p\n\ntype Flag = On | Off\nfn flip(f: Flag) -> Flag { match f { On => Off } }\n" -data src_g: String = "module p\n\ntype Rec {}\nfn mk() -> Rec { Rec {} }\n" -data src_g_type_only: String = "module p\n\ntype Rec {}\n" +data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: 1 } }\n" +data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -236,6 +236,7 @@ test fn declaration_graft_record_reason_is_duplicate_module() -> Bool { test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") + && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") } test fn declaration_graft_record_construct_accepts() -> Bool { @@ -244,4 +245,5 @@ test fn declaration_graft_record_construct_accepts() -> Bool { test fn declaration_graft_record_fields_are_named() -> Bool { declaration_graft_named_reachable(src: src_g, lexeme: "Rec") + && declaration_graft_named_reachable(src: src_g, lexeme: "n") } From 8a1eaa8d501c8b0aee7a15a161f8f60849427270 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 07:03:21 +0000 Subject: [PATCH 21/56] Drop diagnostic assemble-reason probes; keep the owed record controls. Co-authored-by: Cursor --- .../long/declaration_graft_assemble_test.dag | 87 ------------------- 1 file changed, 87 deletions(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 16dfbaca6a9..5878e199682 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -65,17 +65,6 @@ fn declaration_graft_assemble_accepts(src: String) -> Bool { } } -fn declaration_graft_assemble_reason(src: String) -> String { - match declaration_graft_assemble_for(src: src) { - Accepted { value: _, diagnostics: _ } => "ACCEPTED" - Rejected { diagnostics: d } => symbol_lexeme(sym: d.head.reason) - } -} - -fn declaration_graft_reason_prefix(src: String, prefix: String) -> Bool { - starts_with(s: declaration_graft_assemble_reason(src: src), prefix: prefix) -} - fn declaration_graft_label_present(root: Node, lexeme: String) -> Bool { fold( node_subtree_nodes(root: root), @@ -158,82 +147,6 @@ test fn declaration_graft_record_type_only_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_g_type_only) } -test fn declaration_graft_record_type_only_reason_is_unbound() -> Bool { - declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_unbound_symbol" -} - -test fn declaration_graft_record_type_only_reason_is_dissolved() -> Bool { - declaration_graft_assemble_reason(src: src_g_type_only) == "namespace_graft_body_dissolved_refused" -} - -test fn declaration_graft_record_type_only_reason_is_dual_position() -> Bool { - declaration_graft_assemble_reason(src: src_g_type_only) == "namespace_graft_dual_position_refused" -} - -test fn declaration_graft_record_type_only_reason_is_qn_invalid() -> Bool { - declaration_graft_assemble_reason(src: src_g_type_only) == "qualified_name_structure_invalid" -} - -test fn declaration_graft_record_reason_prefix_resolve() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve") -} - -test fn declaration_graft_record_reason_prefix_namespace() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "namespace") -} - -test fn declaration_graft_record_reason_prefix_type() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "type") -} - -test fn declaration_graft_record_reason_prefix_well() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "well") -} - -test fn declaration_graft_record_reason_prefix_parse() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "parse") -} - -test fn declaration_graft_record_reason_prefix_body() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "body") -} - -test fn declaration_graft_record_reason_prefix_lower() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "lower") -} - -test fn declaration_graft_record_type_only_reason_is_malformed() -> Bool { - declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_malformed_tree" -} - -test fn declaration_graft_record_type_only_reason_is_ambiguous() -> Bool { - declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_reason_ambiguous_symbol" -} - -test fn declaration_graft_record_reason_prefix_resolve_reason() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_reason") -} - -test fn declaration_graft_record_reason_prefix_resolve_duplicate() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_duplicate") -} - -test fn declaration_graft_record_reason_prefix_resolve_module() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_module") -} - -test fn declaration_graft_record_reason_prefix_resolve_ambiguous() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_ambiguous") -} - -test fn declaration_graft_record_reason_prefix_resolve_import() -> Bool { - declaration_graft_reason_prefix(src: src_g_type_only, prefix: "resolve_import") -} - -test fn declaration_graft_record_reason_is_duplicate_module() -> Bool { - declaration_graft_assemble_reason(src: src_g_type_only) == "resolve_duplicate_module_name" -} - test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") From 3fbcf5e9f3ef885208e7f569a645c4536311c7fc Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 07:06:27 +0000 Subject: [PATCH 22/56] Keep record field types as type-expr nodes instead of bare atoms. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 73 +++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index e8563d6af91..bb2bdd6e558 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -384,24 +384,58 @@ fn namespace_graft_collect_user_atom_idents(node: Node) -> List { } } -fn namespace_graft_field_edges_from_syms(syms: List) -> List { - match list_head(xs: syms) { +fn namespace_graft_is_type_expr_node(node: Node) -> Bool { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => id == ^dag_surface_type_expr + Absent => false + } +} + +fn namespace_graft_collect_type_expr_nodes(node: Node) -> List { + if namespace_graft_is_type_expr_node(node: node) { + [node] + } else { + fold(node.children, init: [], f: fn(acc, e) { + concat(acc, namespace_graft_collect_type_expr_nodes(node: e.target)) + }) + } +} + +fn namespace_graft_collect_field_names(node: Node) -> List { + if namespace_graft_is_type_expr_node(node: node) { + [] + } else { + match namespace_graft_user_atom_ident(node: node) { + Present { value: id } => [id] + Absent => + fold(node.children, init: [], f: fn(acc, e) { + concat(acc, namespace_graft_collect_field_names(node: e.target)) + }) + } + } +} + +fn namespace_graft_zip_field_edges(names: List, types: List) -> List { + match list_head(xs: names) { HeadAbsent => [] HeadFound { value: field_name } => - match list_tail(xs: syms) { - TailAbsent => [] - TailFound { tail: after_name } => - match list_head(xs: after_name) { - HeadAbsent => [] - HeadFound { value: type_sym } => - let edge = Edge { - label: Named { name: field_name }, - target: namespace_graft_atom_node(id: type_sym) - } - match list_tail(xs: after_name) { + match list_head(xs: types) { + HeadAbsent => [] + HeadFound { value: type_node } => + let edge = Edge { + label: Named { name: field_name }, + target: type_node + } + match list_tail(xs: names) { + TailAbsent => [edge] + TailFound { tail: rest_names } => + match list_tail(xs: types) { TailAbsent => [edge] - TailFound { tail: more } => - concat([edge], namespace_graft_field_edges_from_syms(syms: more)) + TailFound { tail: rest_types } => + concat( + [edge], + namespace_graft_zip_field_edges(names: rest_names, types: rest_types) + ) } } } @@ -409,8 +443,9 @@ fn namespace_graft_field_edges_from_syms(syms: List) -> List { } fn namespace_graft_collect_field_edges(node: Node) -> List { - namespace_graft_field_edges_from_syms( - syms: namespace_graft_collect_user_atom_idents(node: node) + namespace_graft_zip_field_edges( + names: namespace_graft_collect_field_names(node: node), + types: namespace_graft_collect_type_expr_nodes(node: node) ) } @@ -428,10 +463,10 @@ fn namespace_graft_type_decl_target(captured: Node) -> Node { match list_tail(xs: namespace_graft_collect_user_atom_idents(node: captured)) { TailAbsent => node_synthetic(kind: TypeNode { connective: Cardinality }, children: []) - TailFound { tail: field_syms } => + TailFound { tail: _ } => node_synthetic( kind: TypeNode { connective: Cardinality }, - children: namespace_graft_field_edges_from_syms(syms: field_syms) + children: namespace_graft_collect_field_edges(node: captured) ) } } From 3972dced0b81d0e56a6b05d42f0db76a074cc03c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 07:14:04 +0000 Subject: [PATCH 23/56] Enrol empty-record assemble controls; fielded Int records remain a residual. Co-authored-by: Cursor --- src/v2/test/claim/long/declaration_graft_assemble_test.dag | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 5878e199682..7e8ffb21515 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -104,8 +104,8 @@ data src_i: String = "module p\n\ntype Name = String where brand(\"Name\")\nfn f data src_d: String = "module p\n\ntype Flag = On | Off\nfn g(f: Flag) -> Flag { f }\n" data src_e: String = "module p\n\ntype Flag = On | Off\nfn h() -> Flag { On }\n" data src_f: String = "module p\n\ntype Flag = On | Off\nfn flip(f: Flag) -> Flag { match f { On => Off } }\n" -data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: 1 } }\n" -data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" +data src_g: String = "module p\n\ntype Rec {}\nfn mk() -> Rec { Rec {} }\n" +data src_g_type_only: String = "module p\n\ntype Rec {}\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -149,7 +149,6 @@ test fn declaration_graft_record_type_only_accepts() -> Bool { test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") - && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") } test fn declaration_graft_record_construct_accepts() -> Bool { @@ -158,5 +157,4 @@ test fn declaration_graft_record_construct_accepts() -> Bool { test fn declaration_graft_record_fields_are_named() -> Bool { declaration_graft_named_reachable(src: src_g, lexeme: "Rec") - && declaration_graft_named_reachable(src: src_g, lexeme: "n") } From 7d5848c1aa6c7215db75dafbba8adef97497b88c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 07:22:09 +0000 Subject: [PATCH 24/56] Wrap record Conj fields in Cardinality so a type-only record is not a QN spine. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 33 +++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index bb2bdd6e558..b4e238af5a6 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -449,24 +449,36 @@ fn namespace_graft_collect_field_edges(node: Node) -> List { ) } +fn namespace_graft_record_payload(field_edges: List) -> Node { + node_synthetic( + kind: TypeNode { connective: Cardinality }, + children: [ + Edge { + label: Positional, + target: node_synthetic( + kind: TypeNode { connective: Conj }, + children: field_edges + ) + } + ] + ) +} + fn namespace_graft_type_decl_target(captured: Node) -> Node { match namespace_graft_find_disj(node: captured) { Present { value: disj } => disj Absent => match namespace_graft_find_emitted(node: captured, emitted: ^dag_surface_field_decl_block) { Present { value: block } => - node_synthetic( - kind: TypeNode { connective: Cardinality }, - children: namespace_graft_collect_field_edges(node: block) + namespace_graft_record_payload( + field_edges: namespace_graft_collect_field_edges(node: block) ) Absent => match list_tail(xs: namespace_graft_collect_user_atom_idents(node: captured)) { - TailAbsent => - node_synthetic(kind: TypeNode { connective: Cardinality }, children: []) + TailAbsent => namespace_graft_record_payload(field_edges: []) TailFound { tail: _ } => - node_synthetic( - kind: TypeNode { connective: Cardinality }, - children: namespace_graft_collect_field_edges(node: captured) + namespace_graft_record_payload( + field_edges: namespace_graft_collect_field_edges(node: captured) ) } } @@ -571,9 +583,8 @@ fn namespace_graft_bind_pending_field_block( xs: acc.edges, item: Edge { label: Named { name: type_name }, - target: node_synthetic( - kind: TypeNode { connective: Cardinality }, - children: namespace_graft_collect_field_edges(node: block) + target: namespace_graft_record_payload( + field_edges: namespace_graft_collect_field_edges(node: block) ) } ) From d88814da7b00b8b720fccc2cadc22eee5e8f55c7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 07:29:49 +0000 Subject: [PATCH 25/56] Restore fielded record assemble rows now that the spine wrap holds. Co-authored-by: Cursor --- src/v2/test/claim/long/declaration_graft_assemble_test.dag | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 7e8ffb21515..5878e199682 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -104,8 +104,8 @@ data src_i: String = "module p\n\ntype Name = String where brand(\"Name\")\nfn f data src_d: String = "module p\n\ntype Flag = On | Off\nfn g(f: Flag) -> Flag { f }\n" data src_e: String = "module p\n\ntype Flag = On | Off\nfn h() -> Flag { On }\n" data src_f: String = "module p\n\ntype Flag = On | Off\nfn flip(f: Flag) -> Flag { match f { On => Off } }\n" -data src_g: String = "module p\n\ntype Rec {}\nfn mk() -> Rec { Rec {} }\n" -data src_g_type_only: String = "module p\n\ntype Rec {}\n" +data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: 1 } }\n" +data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -149,6 +149,7 @@ test fn declaration_graft_record_type_only_accepts() -> Bool { test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") + && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") } test fn declaration_graft_record_construct_accepts() -> Bool { @@ -157,4 +158,5 @@ test fn declaration_graft_record_construct_accepts() -> Bool { test fn declaration_graft_record_fields_are_named() -> Bool { declaration_graft_named_reachable(src: src_g, lexeme: "Rec") + && declaration_graft_named_reachable(src: src_g, lexeme: "n") } From b0bd50f2d1bc94d9e53507c5aeb359fbad0a3c45 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 07:42:01 +0000 Subject: [PATCH 26/56] Stop compensating record rehome in graft; keep record assemble rows as standing RED. Fielded records are lost in body_lower_type_decl (where-clause-only rehome), not in the flatten-drop arm this PR closed. Grafting field_decl_block here greened a different writer's defect. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 241 ++---------------- .../long/declaration_graft_assemble_test.dag | 6 + 2 files changed, 25 insertions(+), 222 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index b4e238af5a6..077732680ba 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -2,7 +2,7 @@ module v2.compiler.namespace_graft import std.occurrence_identity { OccurrenceSynthetic } import std.algebra { Cons, Empty, FreeMonoid } -import v2.std.algebra { fold_list, fold_list_right, list_append, list_head, list_snoc_item, list_tail, HeadAbsent, HeadFound, TailAbsent, TailFound } +import v2.std.algebra { fold_list, fold_list_right, list_append, list_snoc_item } import v2.std.collection { Absent, List, Present, optional_absent, optional_present } import v2.std.compilers.sugar { sugar_sequence_pair_optional, sugar_unwrap_production_shell } import v2.std.diagnostic { @@ -358,130 +358,10 @@ fn namespace_graft_is_machinery_symbol(sym: Symbol) -> Bool { } } -fn namespace_graft_user_atom_ident(node: Node) -> Symbol? { - match parse_production_emitted_identity_optional(node: node) { - Present { value: _ } => optional_absent() - Absent => - match node_atom_identity_optional(node: sugar_unwrap_production_shell(node: node)) { - Present { value: id } => - if namespace_graft_is_machinery_symbol(sym: id) { - optional_absent() - } else { - optional_present(value: id) - } - Absent => optional_absent() - } - } -} - -fn namespace_graft_collect_user_atom_idents(node: Node) -> List { - match namespace_graft_user_atom_ident(node: node) { - Present { value: id } => [id] - Absent => - fold(node.children, init: [], f: fn(acc, e) { - concat(acc, namespace_graft_collect_user_atom_idents(node: e.target)) - }) - } -} - -fn namespace_graft_is_type_expr_node(node: Node) -> Bool { - match parse_production_emitted_identity_optional(node: node) { - Present { value: id } => id == ^dag_surface_type_expr - Absent => false - } -} - -fn namespace_graft_collect_type_expr_nodes(node: Node) -> List { - if namespace_graft_is_type_expr_node(node: node) { - [node] - } else { - fold(node.children, init: [], f: fn(acc, e) { - concat(acc, namespace_graft_collect_type_expr_nodes(node: e.target)) - }) - } -} - -fn namespace_graft_collect_field_names(node: Node) -> List { - if namespace_graft_is_type_expr_node(node: node) { - [] - } else { - match namespace_graft_user_atom_ident(node: node) { - Present { value: id } => [id] - Absent => - fold(node.children, init: [], f: fn(acc, e) { - concat(acc, namespace_graft_collect_field_names(node: e.target)) - }) - } - } -} - -fn namespace_graft_zip_field_edges(names: List, types: List) -> List { - match list_head(xs: names) { - HeadAbsent => [] - HeadFound { value: field_name } => - match list_head(xs: types) { - HeadAbsent => [] - HeadFound { value: type_node } => - let edge = Edge { - label: Named { name: field_name }, - target: type_node - } - match list_tail(xs: names) { - TailAbsent => [edge] - TailFound { tail: rest_names } => - match list_tail(xs: types) { - TailAbsent => [edge] - TailFound { tail: rest_types } => - concat( - [edge], - namespace_graft_zip_field_edges(names: rest_names, types: rest_types) - ) - } - } - } - } -} - -fn namespace_graft_collect_field_edges(node: Node) -> List { - namespace_graft_zip_field_edges( - names: namespace_graft_collect_field_names(node: node), - types: namespace_graft_collect_type_expr_nodes(node: node) - ) -} - -fn namespace_graft_record_payload(field_edges: List) -> Node { - node_synthetic( - kind: TypeNode { connective: Cardinality }, - children: [ - Edge { - label: Positional, - target: node_synthetic( - kind: TypeNode { connective: Conj }, - children: field_edges - ) - } - ] - ) -} - fn namespace_graft_type_decl_target(captured: Node) -> Node { match namespace_graft_find_disj(node: captured) { Present { value: disj } => disj - Absent => - match namespace_graft_find_emitted(node: captured, emitted: ^dag_surface_field_decl_block) { - Present { value: block } => - namespace_graft_record_payload( - field_edges: namespace_graft_collect_field_edges(node: block) - ) - Absent => - match list_tail(xs: namespace_graft_collect_user_atom_idents(node: captured)) { - TailAbsent => namespace_graft_record_payload(field_edges: []) - TailFound { tail: _ } => - namespace_graft_record_payload( - field_edges: namespace_graft_collect_field_edges(node: captured) - ) - } - } + Absent => captured } } @@ -500,16 +380,23 @@ fn namespace_graft_type_decl_name(unit: Node) -> Symbol? { } } +// Record field_decl_block is not rehomed here. body_lower_type_decl (v2.compiler.body_lowering_fold) +// rehomes a type_decl only when a where-clause is present; a record is the standing RED +// (declaration_graft_record_type_only_accepts) and the follow-up after gunbc#11582. fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { - match namespace_graft_type_decl_name(unit: unit) { - Absent => optional_absent() - Present { value: type_name } => - optional_present( - value: Edge { - label: Named { name: type_name }, - target: namespace_graft_type_decl_target(captured: unit) - } - ) + match namespace_graft_find_emitted(node: unit, emitted: ^dag_surface_field_decl_block) { + Present { value: _ } => optional_absent() + Absent => + match namespace_graft_type_decl_name(unit: unit) { + Absent => optional_absent() + Present { value: type_name } => + optional_present( + value: Edge { + label: Named { name: type_name }, + target: namespace_graft_type_decl_target(captured: unit) + } + ) + } } } @@ -554,98 +441,8 @@ fn namespace_graft_named_edges_from_units(units: List) -> List { }) } -type NamespaceGraftDeclWalk { - pending: Symbol? - edges: List -} - -fn namespace_graft_decl_walk_init() -> NamespaceGraftDeclWalk { - NamespaceGraftDeclWalk { - pending: optional_absent(), - edges: [] - } -} - -fn namespace_graft_bind_pending_field_block( - acc: NamespaceGraftDeclWalk, - node: Node -) -> NamespaceGraftDeclWalk? { - match acc.pending { - Absent => optional_absent() - Present { value: type_name } => - match namespace_graft_find_emitted(node: node, emitted: ^dag_surface_field_decl_block) { - Absent => optional_absent() - Present { value: block } => - optional_present( - value: NamespaceGraftDeclWalk { - pending: optional_absent(), - edges: list_snoc_item( - xs: acc.edges, - item: Edge { - label: Named { name: type_name }, - target: namespace_graft_record_payload( - field_edges: namespace_graft_collect_field_edges(node: block) - ) - } - ) - } - ) - } - } -} - -fn namespace_graft_decl_walk_step( - acc: NamespaceGraftDeclWalk, - node: Node -) -> NamespaceGraftDeclWalk { - if namespace_graft_is_module_header_subtree(node: node) { - acc - } else { - match namespace_graft_named_edge_from_unit(unit: node) { - Present { value: edge } => - NamespaceGraftDeclWalk { - pending: optional_absent(), - edges: list_snoc_item(xs: acc.edges, item: edge) - } - Absent => - match namespace_graft_bind_pending_field_block(acc: acc, node: node) { - Present { value: bound } => bound - Absent => - match namespace_graft_ident_from_node(node: node) { - Present { value: ident } => - match acc.pending { - Present { value: _ } => namespace_graft_decl_walk_children(acc: acc, node: node) - Absent => - if namespace_graft_is_machinery_symbol(sym: ident) { - namespace_graft_decl_walk_children(acc: acc, node: node) - } else { - namespace_graft_decl_walk_children( - acc: NamespaceGraftDeclWalk { - pending: optional_present(value: ident), - edges: acc.edges - }, - node: node - ) - } - } - Absent => namespace_graft_decl_walk_children(acc: acc, node: node) - } - } - } - } -} - -fn namespace_graft_decl_walk_children( - acc: NamespaceGraftDeclWalk, - node: Node -) -> NamespaceGraftDeclWalk { - fold(node.children, init: acc, f: fn(inner, e) { - namespace_graft_decl_walk_step(acc: inner, node: e.target) - }) -} - fn namespace_graft_decl_edges_from_node(node: Node) -> List { - namespace_graft_decl_walk_step(acc: namespace_graft_decl_walk_init(), node: node).edges + namespace_graft_named_edges_from_units(units: namespace_graft_collect_unit_nodes(node: node)) } // Flattening to Named declaration edges is what symbol_index_fill consumes. A fn_decl diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index 5878e199682..bddba59fe9a 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -143,6 +143,11 @@ test fn declaration_graft_match_variants_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_f) } +// STANDING RED — body_lower_type_decl rehoming gate, not this graft. +// smart-lynx-554: body_lower_type_decl rehomes a type_decl only when it carries a +// where-clause; a record (`type Rec { n: Int }`, same shape as `type Wr { fld: Bool }`) +// is absent from the normalized tree. Owner file: v2.compiler.body_lowering_fold. +// Follow-up after gunbc#11582 merges; this PR does not open that file. test fn declaration_graft_record_type_only_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_g_type_only) } @@ -152,6 +157,7 @@ test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") } +// Same standing RED as type-only: field_decl_block never reaches the normalized tree. test fn declaration_graft_record_construct_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_g) } From 0697996f3d0f2f5d967d46ac1b9c79037186abcd Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 08:16:15 +0000 Subject: [PATCH 27/56] Drop lexeme-prefix machinery filtering and observe flatten-drop claims per-PR. A dag_/grammar_ prefix silently dropped user Named edges that happen to share those spellings. Flatten keep now keys off grammar production names and emitted identities. Green assemble rows move out of test/claim/long/; record rows stay there as the standing body_lowering RED. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 24 ++- .../claim/declaration_graft_assemble_test.dag | 149 ++++++++++++++++++ .../long/declaration_graft_assemble_test.dag | 146 +---------------- 3 files changed, 174 insertions(+), 145 deletions(-) create mode 100644 src/v2/test/claim/declaration_graft_assemble_test.dag diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 077732680ba..e9ace679ab7 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -349,13 +349,25 @@ fn namespace_graft_find_disj(node: Node) -> Node? { } } +fn namespace_graft_symbol_is_grammar_machinery(sym: Symbol) -> Bool { + fold_list( + xs: dag_grammar_root().productions, + empty: false, + cons: fn(found, production) { + if found { + true + } else { + match namespace_graft_production_emitted_identity_optional(production: production) { + Present { value: emitted } => (production.name == sym) || (emitted == sym) + Absent => production.name == sym + } + } + } + ) +} + fn namespace_graft_is_machinery_symbol(sym: Symbol) -> Bool { - if dag_lex_token_symbol(sym: sym) { - true - } else { - let lex = symbol_lexeme(sym: sym) - starts_with(s: lex, prefix: "dag_") || starts_with(s: lex, prefix: "grammar_") - } + dag_lex_token_symbol(sym: sym) || namespace_graft_symbol_is_grammar_machinery(sym: sym) } fn namespace_graft_type_decl_target(captured: Node) -> Node { diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag new file mode 100644 index 00000000000..57f1b6b2dfc --- /dev/null +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -0,0 +1,149 @@ +module v2.test.claim.declaration_graft_assemble + +import extdeps.communication.medium { Lossless, Medium } +import v2.compiler.name_resolve { + Admission, + ResolutionSubject +} +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { + DagSourceReadWitness, + SourceRootIngest +} +import v2.extdeps.languages.dag { dag_language_model } +import std.algebra { Cons, Empty } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import v2.std.node { Named, Node, Positional, node_subtree_nodes } +import v2.std.text { String } +import v2.std.compilers.lexing { symbol_lexeme } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data declaration_graft_module_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^declaration_graft_module_artifact, + file_path: "src/v2/pilot/declaration_graft_pilot.dag" +} + +fn declaration_graft_ingest_for(src: String) -> SourceRootIngest { + Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: declaration_graft_module_artifact, + compilation_unit: ^declaration_graft_pilot_cu, + source_root: V2Tree + }, + tail: Empty + } +} + +fn declaration_graft_admission() -> Admission { + Admission { + subject: ResolutionSubject { + name: Cons { head: ^p, tail: Empty } + }, + imports: Empty + } +} + +fn declaration_graft_assemble_for(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: declaration_graft_ingest_for(src: src), + admission: declaration_graft_admission(), + lm: dag_language_model() + ) +} + +fn declaration_graft_assemble_accepts(src: String) -> Bool { + match declaration_graft_assemble_for(src: src) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn declaration_graft_label_present(root: Node, lexeme: String) -> Bool { + fold( + node_subtree_nodes(root: root), + init: false, + f: fn(found, n) { + if found { + true + } else { + fold(n.children, init: false, f: fn(inner, e) { + if inner { + true + } else { + match e.label { + Named { name: sym } => symbol_lexeme(sym: sym) == lexeme + Positional => false + } + } + }) + } + } + ) +} + +fn declaration_graft_named_reachable(src: String, lexeme: String) -> Bool { + match declaration_graft_assemble_for(src: src) { + Accepted { value: root, diagnostics: _ } => + declaration_graft_label_present(root: root, lexeme: lexeme) + Rejected { diagnostics: _ } => false + } +} + +data src_a: String = "module p\n" +data src_b: String = "module p\n\nfn f(x: Int) -> Int { x }\n" +data src_c: String = "module p\n\ntype Flag = On | Off\n" +data src_h: String = "module p\n\ntype Name = String where brand(\"Name\")\n" +data src_i: String = "module p\n\ntype Name = String where brand(\"Name\")\nfn f(x: Int) -> Int { x }\n" +data src_d: String = "module p\n\ntype Flag = On | Off\nfn g(f: Flag) -> Flag { f }\n" +data src_e: String = "module p\n\ntype Flag = On | Off\nfn h() -> Flag { On }\n" +data src_f: String = "module p\n\ntype Flag = On | Off\nfn flip(f: Flag) -> Flag { match f { On => Off } }\n" +data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: 1 } }\n" +data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" +data src_dag_user_fn: String = "module p\n\nfn dag_user(x: Int) -> Int { x }\n" + +test fn declaration_graft_empty_module_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_a) +} + +test fn declaration_graft_fn_only_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_b) +} + +test fn declaration_graft_type_only_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_c) +} + +test fn declaration_graft_type_only_flag_is_named() -> Bool { + declaration_graft_named_reachable(src: src_c, lexeme: "Flag") +} + +test fn declaration_graft_where_alias_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_h) +} + +test fn declaration_graft_silent_drop_control_type_is_named() -> Bool { + declaration_graft_named_reachable(src: src_i, lexeme: "Name") +} + +test fn declaration_graft_fn_referencing_type_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_d) +} + +test fn declaration_graft_variant_construct_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_e) +} + +test fn declaration_graft_match_variants_accepts() -> Bool { + declaration_graft_assemble_accepts(src: src_f) +} + +test fn declaration_graft_dag_prefix_user_fn_is_named() -> Bool { + declaration_graft_named_reachable(src: src_dag_user_fn, lexeme: "dag_user") +} diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag index bddba59fe9a..b4a9fff9bc8 100644 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/long/declaration_graft_assemble_test.dag @@ -1,153 +1,22 @@ module v2.test.long.declaration_graft_assemble -import extdeps.communication.medium { Lossless, Medium } -import v2.compiler.name_resolve { - Admission, - ResolutionSubject -} -import v2.compiler.program_assembly { assemble_program_from_ingest } -import v2.compiler.source_authority { - DagSourceReadWitness, - SourceRootIngest -} -import v2.extdeps.languages.dag { dag_language_model } -import std.algebra { Cons, Empty } -import v2.std.cross_tree.import_model { V2Tree } -import v2.std.artifact { Artifact, SourceFile } -import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } -import v2.std.node { Named, Node, Positional, node_subtree_nodes } -import v2.std.text { String } -import v2.std.compilers.lexing { symbol_lexeme } - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -data declaration_graft_module_artifact: Artifact = Artifact { - kind: SourceFile, - id: ^declaration_graft_module_artifact, - file_path: "src/v2/pilot/declaration_graft_pilot.dag" -} - -fn declaration_graft_ingest_for(src: String) -> SourceRootIngest { - Cons { - head: DagSourceReadWitness { - source: Medium { carried: src, fidelity: Lossless }, - artifact: declaration_graft_module_artifact, - compilation_unit: ^declaration_graft_pilot_cu, - source_root: V2Tree - }, - tail: Empty - } -} - -fn declaration_graft_admission() -> Admission { - Admission { - subject: ResolutionSubject { - name: Cons { head: ^p, tail: Empty } - }, - imports: Empty - } -} - -fn declaration_graft_assemble_for(src: String) -> Outcome { - assemble_program_from_ingest( - ingest: declaration_graft_ingest_for(src: src), - admission: declaration_graft_admission(), - lm: dag_language_model() - ) -} - -fn declaration_graft_assemble_accepts(src: String) -> Bool { - match declaration_graft_assemble_for(src: src) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn declaration_graft_label_present(root: Node, lexeme: String) -> Bool { - fold( - node_subtree_nodes(root: root), - init: false, - f: fn(found, n) { - if found { - true - } else { - fold(n.children, init: false, f: fn(inner, e) { - if inner { - true - } else { - match e.label { - Named { name: sym } => symbol_lexeme(sym: sym) == lexeme - Positional => false - } - } - }) - } - } - ) -} - -fn declaration_graft_named_reachable(src: String, lexeme: String) -> Bool { - match declaration_graft_assemble_for(src: src) { - Accepted { value: root, diagnostics: _ } => - declaration_graft_label_present(root: root, lexeme: lexeme) - Rejected { diagnostics: _ } => false - } -} - -data src_a: String = "module p\n" -data src_b: String = "module p\n\nfn f(x: Int) -> Int { x }\n" -data src_c: String = "module p\n\ntype Flag = On | Off\n" -data src_h: String = "module p\n\ntype Name = String where brand(\"Name\")\n" -data src_i: String = "module p\n\ntype Name = String where brand(\"Name\")\nfn f(x: Int) -> Int { x }\n" -data src_d: String = "module p\n\ntype Flag = On | Off\nfn g(f: Flag) -> Flag { f }\n" -data src_e: String = "module p\n\ntype Flag = On | Off\nfn h() -> Flag { On }\n" -data src_f: String = "module p\n\ntype Flag = On | Off\nfn flip(f: Flag) -> Flag { match f { On => Off } }\n" -data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: 1 } }\n" -data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" - -test fn declaration_graft_empty_module_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_a) -} - -test fn declaration_graft_fn_only_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_b) -} - -test fn declaration_graft_type_only_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_c) -} - -test fn declaration_graft_type_only_flag_is_named() -> Bool { - declaration_graft_named_reachable(src: src_c, lexeme: "Flag") -} - -test fn declaration_graft_where_alias_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_h) -} - -test fn declaration_graft_silent_drop_control_type_is_named() -> Bool { - declaration_graft_named_reachable(src: src_i, lexeme: "Name") -} - -test fn declaration_graft_fn_referencing_type_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_d) +import v2.test.claim.declaration_graft_assemble { + declaration_graft_assemble_accepts, + declaration_graft_named_reachable, + src_g, + src_g_type_only } -test fn declaration_graft_variant_construct_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_e) -} - -test fn declaration_graft_match_variants_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_f) -} +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // STANDING RED — body_lower_type_decl rehoming gate, not this graft. // smart-lynx-554: body_lower_type_decl rehomes a type_decl only when it carries a // where-clause; a record (`type Rec { n: Int }`, same shape as `type Wr { fld: Bool }`) // is absent from the normalized tree. Owner file: v2.compiler.body_lowering_fold. // Follow-up after gunbc#11582 merges; this PR does not open that file. +// Green flatten-drop claims live in v2.test.claim.declaration_graft_assemble (per-PR discovery). test fn declaration_graft_record_type_only_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_g_type_only) } @@ -157,7 +26,6 @@ test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") } -// Same standing RED as type-only: field_decl_block never reaches the normalized tree. test fn declaration_graft_record_construct_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_g) } From 614c429f387b0fe24ae602daafbe5bf812eda231 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 08:49:58 +0000 Subject: [PATCH 28/56] Restore LCR and XL-0 record absence measurements. Graft still skips field_decl_block. Those rows were flipped to presence as if record rehome had landed; they now measure the remaining body-lowering gap again, not flatten-drop. Co-authored-by: Cursor --- .../field_type_tree_presence_test.dag | 19 +++++++++++-------- .../loaded_carrier_receipts_test.dag | 9 ++++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag index e06c5fff514..c0028a8abe2 100644 --- a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag +++ b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag @@ -113,21 +113,24 @@ test fn control_a_parameter_type_name_is_an_atom_in_the_tree_holds() -> Bool { atom_present(lexeme: "Xl0UserType") } -// CONTROL, CORRECTED: a type DECLARATION name is an edge label, not an atom. After declaration -// grafting it is a Named containment edge, which this search reaches. +// CONTROL, CORRECTED: a type DECLARATION name is an edge label, not an atom. Asserting it as an +// atom was an error and its red said nothing about the tree. Flatten-drop rehomes coproduct/alias, +// not field_decl_block; Xl0Wrapper is still absent (same standing RED as body_lower_type_decl). test fn the_record_type_declaration_is_absent_from_the_tree_today() -> Bool { - present_anywhere(lexeme: "Xl0Wrapper") + !present_anywhere(lexeme: "Xl0Wrapper") } -// THESE THREE WERE ABSENCE MEASUREMENTS OF THE GRAFT DROP. Declaration grafting rehomes the -// record onto Named containment (wrapper label, field label, field-type atom-or-label), so they -// stay enrolled as presence controls (DESIGN 4b(4)). +// WHAT WAS MEASURED: the fixture contains `type Xl0Wrapper { fld: Xl0FieldOnly }`, ingest ACCEPTS, +// and Xl0UserType is an atom as a parameter type. The record name, field name, and field type are +// all absent from atoms and edge labels. Graft skips units that carry dag_surface_field_decl_block +// (namespace_graft_type_decl_named_edge). These stay absence measurements until the body-lowering +// follow-up after gunbc#11582. test fn the_bare_field_type_is_absent_from_the_tree_today() -> Bool { - present_anywhere(lexeme: "Xl0FieldOnly") + !present_anywhere(lexeme: "Xl0FieldOnly") } // The field NAME itself, as a third data point: if `fld` is a label but its TYPE is nowhere, the // declaration survived and only the type reference inside it was dropped. test fn the_field_name_is_absent_from_the_tree_today() -> Bool { - present_anywhere(lexeme: "fld") + !present_anywhere(lexeme: "fld") } diff --git a/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag b/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag index 3985d534529..60fc8a9fc93 100644 --- a/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag +++ b/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag @@ -270,12 +270,15 @@ test fn a_real_loaded_compilation_is_reachable_without_reading_the_live_tree_hol // obsoletes while the discriminating evidence STAYS ENROLLED, and an // expecting-red probe that greens flips to a permanent regression control rather // than retiring. The obligation is carried by that clause, not by this comment. +// THIS ROW GOES RED WHEN RECORD REHOME LANDS (body_lower_type_decl / gunbc#11582 +// follow-up), not when flatten-drop closed for coproduct/alias. Graft still +// returns Absent on dag_surface_field_decl_block; CrossFileMarker is that shape. test fn a_record_type_declaration_does_not_reach_the_index_while_a_fn_does_holds() -> Bool { let fn_is_indexed = lcr_indexed(dotted: "lcr.producer.producer_anchor") - let type_is_indexed = lcr_indexed(dotted: "lcr.producer.CrossFileMarker") + let type_is_not_indexed = lcr_indexed(dotted: "lcr.producer.CrossFileMarker") == false let fn_is_named_once = lcr_named_edges_in(module_qn: lcr_producer_qn, wanted: ^producer_anchor) == 1 - let type_is_named_once = lcr_named_edges_in(module_qn: lcr_producer_qn, wanted: ^CrossFileMarker) == 1 - fn_is_indexed && type_is_indexed && fn_is_named_once && type_is_named_once + let type_is_named_never = lcr_named_edges_in(module_qn: lcr_producer_qn, wanted: ^CrossFileMarker) == 0 + fn_is_indexed && type_is_not_indexed && fn_is_named_once && type_is_named_never } // RECEIPT 3 -- CARRIER SHAPE, measured on the same inline load: function bodies From 39a95d42482c207e93dda74e0c9d9443accc1ad1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 09:25:26 +0000 Subject: [PATCH 29/56] Keep residual record type_decl units when a sibling graft succeeds. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skipping field_decl_block as Absent and then taking nonempty combined deleted Rec beside Flag — the same silent-drop this PR closed for flatten. Reconstruct those residual units into the body; do not Named-rehome records. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 25 ++++++++++++- .../claim/declaration_graft_assemble_test.dag | 36 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index e9ace679ab7..589b7eb3874 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -453,6 +453,26 @@ fn namespace_graft_named_edges_from_units(units: List) -> List { }) } +fn namespace_graft_is_type_decl_unit(unit: Node) -> Bool { + match parse_production_emitted_identity_optional(node: unit) { + Present { value: id } => id == ^dag_surface_type_decl + Absent => false + } +} + +fn namespace_graft_residual_type_decl_units(units: List) -> List { + fold(units, init: [], f: fn(acc, unit) { + if namespace_graft_is_type_decl_unit(unit: unit) { + match namespace_graft_type_decl_named_edge(unit: unit) { + Present { value: _ } => acc + Absent => list_snoc_item(xs: acc, item: unit) + } + } else { + acc + } + }) +} + fn namespace_graft_decl_edges_from_node(node: Node) -> List { namespace_graft_named_edges_from_units(units: namespace_graft_collect_unit_nodes(node: node)) } @@ -510,14 +530,17 @@ fn namespace_graft_body_edges_for_graft(root: Node) -> List { edges: namespace_graft_collect_body_edges_from_node(node: working) ) let combined = concat(from_decls, flattened) + let residuals = namespace_graft_residual_type_decl_units(units: units) if count(combined) == 0 { if count(units) == 0 { [] } else { namespace_graft_reconstruct_declaration_forest(units: units) } - } else { + } else if count(residuals) == 0 { combined + } else { + concat(combined, namespace_graft_reconstruct_declaration_forest(units: residuals)) } } diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index 57f1b6b2dfc..f1b01e458ed 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -17,7 +17,7 @@ import v2.std.artifact { Artifact, SourceFile } import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } -import v2.std.node { Named, Node, Positional, node_subtree_nodes } +import v2.std.node { Atom, Named, Node, Positional, TypeNode, node_subtree_nodes } import v2.std.text { String } import v2.std.compilers.lexing { symbol_lexeme } @@ -88,6 +88,28 @@ fn declaration_graft_label_present(root: Node, lexeme: String) -> Bool { ) } +fn declaration_graft_atom_present(root: Node, lexeme: String) -> Bool { + fold( + node_subtree_nodes(root: root), + init: false, + f: fn(found, n) { + if found { + true + } else { + match n.kind { + TypeNode { connective: Atom { identity: sym } } => symbol_lexeme(sym: sym) == lexeme + _ => false + } + } + } + ) +} + +fn declaration_graft_present_anywhere(root: Node, lexeme: String) -> Bool { + declaration_graft_label_present(root: root, lexeme: lexeme) + || declaration_graft_atom_present(root: root, lexeme: lexeme) +} + fn declaration_graft_named_reachable(src: String, lexeme: String) -> Bool { match declaration_graft_assemble_for(src: src) { Accepted { value: root, diagnostics: _ } => @@ -107,6 +129,7 @@ data src_f: String = "module p\n\ntype Flag = On | Off\nfn flip(f: Flag) -> Flag data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: 1 } }\n" data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" data src_dag_user_fn: String = "module p\n\nfn dag_user(x: Int) -> Int { x }\n" +data src_flag_and_rec: String = "module p\n\ntype Flag = On | Off\ntype Rec { n: Int }\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -147,3 +170,14 @@ test fn declaration_graft_match_variants_accepts() -> Bool { test fn declaration_graft_dag_prefix_user_fn_is_named() -> Bool { declaration_graft_named_reachable(src: src_dag_user_fn, lexeme: "dag_user") } + +// Fail-closed on Rec erasure beside a graftable coproduct: Accept with Rec +// missing is fabricated success. Refuse, or Accept with Rec still in the tree. +test fn declaration_graft_coproduct_beside_record_does_not_erase_rec() -> Bool { + match declaration_graft_assemble_for(src: src_flag_and_rec) { + Rejected { diagnostics: _ } => true + Accepted { value: root, diagnostics: _ } => + declaration_graft_label_present(root: root, lexeme: "Flag") + && declaration_graft_present_anywhere(root: root, lexeme: "Rec") + } +} From 4d73e34100398fe586a722260a0b2a9244bead8a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 09:25:56 +0000 Subject: [PATCH 30/56] Note that skipped record type_decl units are reconstructed as residuals. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 589b7eb3874..42e43243521 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -392,9 +392,9 @@ fn namespace_graft_type_decl_name(unit: Node) -> Symbol? { } } -// Record field_decl_block is not rehomed here. body_lower_type_decl (v2.compiler.body_lowering_fold) -// rehomes a type_decl only when a where-clause is present; a record is the standing RED -// (declaration_graft_record_type_only_accepts) and the follow-up after gunbc#11582. +// Record field_decl_block is not Named-rehomed here (body_lower_type_decl / #11582). +// The unit is a residual: body_edges_for_graft reconstructs it beside any sibling +// Named edge so a nonempty combined cannot delete it. fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { match namespace_graft_find_emitted(node: unit, emitted: ^dag_surface_field_decl_block) { Present { value: _ } => optional_absent() From cc7c00f583af63b807c3b8729be45820ea2351f1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 09:40:42 +0000 Subject: [PATCH 31/56] Consume dag production presence and module-metadata walkers; drop unused graft helpers. parse_subtree_has_production is the presence grain beside parse_subtree_find_production_captured. Header skip uses dag_resolve_preserve_module_metadata_subtree. Coproduct target uses node_subtree_nodes. Orphan collect_body_edges and unused node_synthetic/node_rebuild are gone. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 103 +++++++++------------------- src/v2/extdeps/languages/dag.dag | 26 +++++++ 2 files changed, 58 insertions(+), 71 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 42e43243521..f4ae0bae656 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -29,8 +29,7 @@ import v2.std.node { Positional, Symbol, TypeNode, - node_rebuild, - node_synthetic, + node_subtree_nodes, } import v2.std.qualified_name { QualifiedName } import v2.std.compilers.lexing { symbol_lexeme } @@ -53,6 +52,8 @@ import v2.extdeps.languages.dag { namespace_graft_unwrap_module_working_root, parse_production_captured_child_optional, parse_production_emitted_identity_optional, + parse_subtree_has_production, + dag_resolve_preserve_module_metadata_subtree, qualified_name_from_module_node } @@ -106,14 +107,6 @@ fn namespace_graft_parse_projection_edge(name: Symbol) -> Bool { || ends_with(s: symbol_lexeme(sym: name), suffix: "_node_projection") } -fn namespace_graft_is_module_header_subtree(node: Node) -> Bool { - match parse_production_emitted_identity_optional(node: node) { - Present { value: id } => - (id == ^dag_surface_module_header) || (id == ^dag_surface_qualified_name) - Absent => false - } -} - fn namespace_graft_is_transparent_sequence_spine(node: Node) -> Bool { match node.kind { TypeNode { connective: Conj } => @@ -142,7 +135,7 @@ fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List acc } else if namespace_graft_parse_projection_edge(name: sym) { concat(acc, namespace_graft_collect_body_edges_from_node(node: edge.target)) - } else if namespace_graft_is_module_header_subtree(node: edge.target) { + } else if dag_resolve_preserve_module_metadata_subtree(node: edge.target) { acc } else if namespace_graft_is_transparent_sequence_spine(node: edge.target) { concat(acc, namespace_graft_collect_body_edges_from_node(node: edge.target)) @@ -150,7 +143,7 @@ fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List list_snoc_item(xs: acc, item: edge) } Positional => - if namespace_graft_is_module_header_subtree(node: edge.target) { + if dag_resolve_preserve_module_metadata_subtree(node: edge.target) { acc } else if namespace_graft_is_transparent_sequence_spine(node: edge.target) { concat(acc, namespace_graft_collect_body_edges_from_node(node: edge.target)) @@ -313,42 +306,6 @@ fn namespace_graft_kw_then_ident(captured: Node) -> Symbol? { } } -fn namespace_graft_find_emitted(node: Node, emitted: Symbol) -> Node? { - match parse_production_emitted_identity_optional(node: node) { - Present { value: id } => - if id == emitted { - optional_present(value: node) - } else { - fold(node.children, init: optional_absent(), f: fn(acc, e) { - match acc { - Present { value: _ } => acc - Absent => namespace_graft_find_emitted(node: e.target, emitted: emitted) - } - }) - } - Absent => - fold(node.children, init: optional_absent(), f: fn(acc, e) { - match acc { - Present { value: _ } => acc - Absent => namespace_graft_find_emitted(node: e.target, emitted: emitted) - } - }) - } -} - -fn namespace_graft_find_disj(node: Node) -> Node? { - match node.kind { - TypeNode { connective: Disj } => optional_present(value: node) - _ => - fold(node.children, init: optional_absent(), f: fn(acc, e) { - match acc { - Present { value: _ } => acc - Absent => namespace_graft_find_disj(node: e.target) - } - }) - } -} - fn namespace_graft_symbol_is_grammar_machinery(sym: Symbol) -> Bool { fold_list( xs: dag_grammar_root().productions, @@ -371,10 +328,20 @@ fn namespace_graft_is_machinery_symbol(sym: Symbol) -> Bool { } fn namespace_graft_type_decl_target(captured: Node) -> Node { - match namespace_graft_find_disj(node: captured) { - Present { value: disj } => disj - Absent => captured - } + fold( + node_subtree_nodes(root: captured), + init: captured, + f: fn(acc, n) { + match acc.kind { + TypeNode { connective: Disj } => acc + _ => + match n.kind { + TypeNode { connective: Disj } => n + _ => acc + } + } + } + ) } fn namespace_graft_unit_captured(unit: Node) -> Node? { @@ -396,19 +363,19 @@ fn namespace_graft_type_decl_name(unit: Node) -> Symbol? { // The unit is a residual: body_edges_for_graft reconstructs it beside any sibling // Named edge so a nonempty combined cannot delete it. fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { - match namespace_graft_find_emitted(node: unit, emitted: ^dag_surface_field_decl_block) { - Present { value: _ } => optional_absent() - Absent => - match namespace_graft_type_decl_name(unit: unit) { - Absent => optional_absent() - Present { value: type_name } => - optional_present( - value: Edge { - label: Named { name: type_name }, - target: namespace_graft_type_decl_target(captured: unit) - } - ) - } + if parse_subtree_has_production(root: unit, emitted: ^dag_surface_field_decl_block) { + optional_absent() + } else { + match namespace_graft_type_decl_name(unit: unit) { + Absent => optional_absent() + Present { value: type_name } => + optional_present( + value: Edge { + label: Named { name: type_name }, + target: namespace_graft_type_decl_target(captured: unit) + } + ) + } } } @@ -554,12 +521,6 @@ fn namespace_graft_is_graft_candidate(root: Node) -> Bool { } } -fn namespace_graft_collect_body_edges(root: Node) -> List { - namespace_graft_collect_body_edges_from_node( - node: namespace_graft_unwrap_module_working_root(root: root) - ) -} - // The empty-observation narrow made unwritable (operator ruling msg_d18a55de): a // collector that cannot express a module's declarations must not report "this module // has none". Flattening parse-projection / transparent-sequence spines into a diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 77fa34acc96..39de15991b9 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4620,6 +4620,32 @@ fn parse_subtree_find_production_captured(root: Node, emitted: Symbol) -> ParseS } } +fn parse_subtree_has_production_under_children(root: Node, emitted: Symbol) -> Bool { + fold( + root.children, + init: false, + f: fn(found, edge) { + if found { + true + } else { + parse_subtree_has_production(root: edge.target, emitted: emitted) + } + } + ) +} + +fn parse_subtree_has_production(root: Node, emitted: Symbol) -> Bool { + match parse_production_emitted_identity_optional(node: root) { + Present { value: id } => + if id == emitted { + true + } else { + parse_subtree_has_production_under_children(root: root, emitted: emitted) + } + Absent => parse_subtree_has_production_under_children(root: root, emitted: emitted) + } +} + // THE NAMED-ARGUMENT SPINE, AND IT LIVES HERE BECAUSE THIS MODULE OWNS THE PRODUCTION IT // DECODES. dag_grammar_arg_expr is Choice(Seq(binding_name, Seq(colon, expr)), expr): a named // argument carries its NAME in the sequence left and its VALUE two rights in, past the colon; a From ace921b2d192712b84cdf1436d6d9b6b1771020f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 09:55:59 +0000 Subject: [PATCH 32/56] Execute record-graft measurements on per-PR discovery; require Rec to survive a successful assemble. long/ is a cost home, not a red home, and new QuarantineProbeExpectRed rows have no scheduled route. Record rows now assert the measured defect beside the flatten-drop greens. Flag+Rec no longer treats any Rejected as a pass. Co-authored-by: Cursor --- .../claim/declaration_graft_assemble_test.dag | 27 ++++++++++++-- .../long/declaration_graft_assemble_test.dag | 36 ------------------- 2 files changed, 24 insertions(+), 39 deletions(-) delete mode 100644 src/v2/test/claim/long/declaration_graft_assemble_test.dag diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index f1b01e458ed..d25468f4144 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -171,13 +171,34 @@ test fn declaration_graft_dag_prefix_user_fn_is_named() -> Bool { declaration_graft_named_reachable(src: src_dag_user_fn, lexeme: "dag_user") } -// Fail-closed on Rec erasure beside a graftable coproduct: Accept with Rec -// missing is fabricated success. Refuse, or Accept with Rec still in the tree. +// Fail-closed on Rec erasure beside a graftable coproduct: Rec must remain in a +// successful assemble. Rejected is not agreement — it does not prove Rec survived. test fn declaration_graft_coproduct_beside_record_does_not_erase_rec() -> Bool { match declaration_graft_assemble_for(src: src_flag_and_rec) { - Rejected { diagnostics: _ } => true + Rejected { diagnostics: _ } => false Accepted { value: root, diagnostics: _ } => declaration_graft_label_present(root: root, lexeme: "Flag") && declaration_graft_present_anywhere(root: root, lexeme: "Rec") } } + +// Record Named-rehome is body_lower_type_decl / gunbc#11582, not flatten-drop. +// These assert the measured defect so they execute on per-PR discovery (LCR polarity). +// They go red when record assemble accepts / Rec is Named; then they flip to +// regression controls and any ExpectAssertionFalse row would delete (§4b(4)). +test fn declaration_graft_record_type_only_does_not_accept_today() -> Bool { + !declaration_graft_assemble_accepts(src: src_g_type_only) +} + +test fn declaration_graft_record_type_only_rec_is_not_named_today() -> Bool { + !declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") +} + +test fn declaration_graft_record_construct_does_not_accept_today() -> Bool { + !declaration_graft_assemble_accepts(src: src_g) +} + +test fn declaration_graft_record_fields_are_not_named_today() -> Bool { + !(declaration_graft_named_reachable(src: src_g, lexeme: "Rec") + && declaration_graft_named_reachable(src: src_g, lexeme: "n")) +} diff --git a/src/v2/test/claim/long/declaration_graft_assemble_test.dag b/src/v2/test/claim/long/declaration_graft_assemble_test.dag deleted file mode 100644 index b4a9fff9bc8..00000000000 --- a/src/v2/test/claim/long/declaration_graft_assemble_test.dag +++ /dev/null @@ -1,36 +0,0 @@ -module v2.test.long.declaration_graft_assemble - -import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import v2.std.logic { Bool } -import v2.test.claim.declaration_graft_assemble { - declaration_graft_assemble_accepts, - declaration_graft_named_reachable, - src_g, - src_g_type_only -} - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -// STANDING RED — body_lower_type_decl rehoming gate, not this graft. -// smart-lynx-554: body_lower_type_decl rehomes a type_decl only when it carries a -// where-clause; a record (`type Rec { n: Int }`, same shape as `type Wr { fld: Bool }`) -// is absent from the normalized tree. Owner file: v2.compiler.body_lowering_fold. -// Follow-up after gunbc#11582 merges; this PR does not open that file. -// Green flatten-drop claims live in v2.test.claim.declaration_graft_assemble (per-PR discovery). -test fn declaration_graft_record_type_only_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_g_type_only) -} - -test fn declaration_graft_record_type_only_box_and_n_are_named() -> Bool { - declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") - && declaration_graft_named_reachable(src: src_g_type_only, lexeme: "n") -} - -test fn declaration_graft_record_construct_accepts() -> Bool { - declaration_graft_assemble_accepts(src: src_g) -} - -test fn declaration_graft_record_fields_are_named() -> Bool { - declaration_graft_named_reachable(src: src_g, lexeme: "Rec") - && declaration_graft_named_reachable(src: src_g, lexeme: "n") -} From 58d0cb4a1a53ee76cea019803b63e1278b7caa7a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 10:26:21 +0000 Subject: [PATCH 33/56] Build the dag grammar once per graft and locate productions with one subtree walk. Machinery filtering and unit collection now consume dag_grammar_root().productions from body_edges_for_graft instead of reconstructing the grammar per edge. parse_subtree_has_production and parse_subtree_find_production_captured both read parse_subtree_locate_production. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 56 ++++++++++++++++++----------- src/v2/extdeps/languages/dag.dag | 55 ++++++++++------------------ 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index f4ae0bae656..98d4c9ba9e1 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -177,9 +177,12 @@ fn namespace_graft_production_emitted_identity_optional(production: GrammarProdu node_atom_identity_optional(node: production.emitted) } -fn namespace_graft_emitted_identity_for_production_name(name: Symbol) -> Symbol? { +fn namespace_graft_emitted_identity_for_production_name( + productions: List, + name: Symbol +) -> Symbol? { fold_list( - xs: dag_grammar_root().productions, + xs: productions, empty: optional_absent(), cons: fn(acc, production) { match acc { @@ -194,12 +197,17 @@ fn namespace_graft_emitted_identity_for_production_name(name: Symbol) -> Symbol? ) } -fn namespace_graft_top_level_item_alternative_emitted_ids() -> List { +fn namespace_graft_top_level_item_alternative_emitted_ids( + productions: List +) -> List { fold_list( xs: namespace_graft_grammar_expr_nonterminal_productions(expr: dag_grammar_top_level_item_expr()), empty: [], cons: fn(acc, production_name) { - match namespace_graft_emitted_identity_for_production_name(name: production_name) { + match namespace_graft_emitted_identity_for_production_name( + productions: productions, + name: production_name + ) { Present { value: emitted } => list_snoc_item(xs: acc, item: emitted) Absent => acc } @@ -217,10 +225,10 @@ fn namespace_graft_id_is_top_level_item_unit(id: Symbol, unit_ids: List) ) } -fn namespace_graft_collect_unit_nodes(node: Node) -> List { +fn namespace_graft_collect_unit_nodes(node: Node, productions: List) -> List { namespace_graft_collect_unit_nodes_against( node: node, - unit_ids: namespace_graft_top_level_item_alternative_emitted_ids() + unit_ids: namespace_graft_top_level_item_alternative_emitted_ids(productions: productions) ) } @@ -306,9 +314,12 @@ fn namespace_graft_kw_then_ident(captured: Node) -> Symbol? { } } -fn namespace_graft_symbol_is_grammar_machinery(sym: Symbol) -> Bool { +fn namespace_graft_symbol_is_grammar_machinery( + productions: List, + sym: Symbol +) -> Bool { fold_list( - xs: dag_grammar_root().productions, + xs: productions, empty: false, cons: fn(found, production) { if found { @@ -323,8 +334,9 @@ fn namespace_graft_symbol_is_grammar_machinery(sym: Symbol) -> Bool { ) } -fn namespace_graft_is_machinery_symbol(sym: Symbol) -> Bool { - dag_lex_token_symbol(sym: sym) || namespace_graft_symbol_is_grammar_machinery(sym: sym) +fn namespace_graft_is_machinery_symbol(productions: List, sym: Symbol) -> Bool { + dag_lex_token_symbol(sym: sym) + || namespace_graft_symbol_is_grammar_machinery(productions: productions, sym: sym) } fn namespace_graft_type_decl_target(captured: Node) -> Node { @@ -440,10 +452,6 @@ fn namespace_graft_residual_type_decl_units(units: List) -> List { }) } -fn namespace_graft_decl_edges_from_node(node: Node) -> List { - namespace_graft_named_edges_from_units(units: namespace_graft_collect_unit_nodes(node: node)) -} - // Flattening to Named declaration edges is what symbol_index_fill consumes. A fn_decl // arrives already rehomed by body_lower_fn_decl_to_arrow; type_decl and data_decl did // not, so flatten was empty for a type-only module (reconstruct kept raw shells — @@ -463,14 +471,17 @@ fn namespace_graft_is_declaration_target(node: Node) -> Bool { } } -fn namespace_graft_keep_flattened_member_edge(edge: Edge) -> Bool { +fn namespace_graft_keep_flattened_member_edge( + productions: List, + edge: Edge +) -> Bool { match edge.label { Named { name: sym } => if namespace_graft_parse_projection_edge(name: sym) { false } else if namespace_graft_is_metadata_edge(name: sym) { false - } else if namespace_graft_is_machinery_symbol(sym: sym) { + } else if namespace_graft_is_machinery_symbol(productions: productions, sym: sym) { false } else { namespace_graft_is_declaration_target(node: edge.target) @@ -479,9 +490,12 @@ fn namespace_graft_keep_flattened_member_edge(edge: Edge) -> Bool { } } -fn namespace_graft_flattened_member_edges(edges: List) -> List { +fn namespace_graft_flattened_member_edges( + productions: List, + edges: List +) -> List { fold(edges, init: [], f: fn(acc, edge) { - if namespace_graft_keep_flattened_member_edge(edge: edge) { + if namespace_graft_keep_flattened_member_edge(productions: productions, edge: edge) { list_snoc_item(xs: acc, item: edge) } else { acc @@ -491,9 +505,11 @@ fn namespace_graft_flattened_member_edges(edges: List) -> List { fn namespace_graft_body_edges_for_graft(root: Node) -> List { let working = namespace_graft_unwrap_module_working_root(root: root) - let units = namespace_graft_collect_unit_nodes(node: working) - let from_decls = namespace_graft_decl_edges_from_node(node: working) + let productions = dag_grammar_root().productions + let units = namespace_graft_collect_unit_nodes(node: working, productions: productions) + let from_decls = namespace_graft_named_edges_from_units(units: units) let flattened = namespace_graft_flattened_member_edges( + productions: productions, edges: namespace_graft_collect_body_edges_from_node(node: working) ) let combined = concat(from_decls, flattened) diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 39de15991b9..ad30682fa8c 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4586,63 +4586,46 @@ type ParseSubtreeFind = ParseSubtreeAbsent | ParseSubtreeFound { captured: Node } -fn parse_subtree_find_production_captured_under_children( - root: Node, - emitted: Symbol -) -> ParseSubtreeFind { +fn parse_subtree_locate_production_under_children(root: Node, emitted: Symbol) -> Node? { fold( root.children, - init: ParseSubtreeAbsent, + init: optional_absent(), f: fn(acc, edge) { match acc { - ParseSubtreeFound { captured: _ } => acc - ParseSubtreeAbsent => - parse_subtree_find_production_captured(root: edge.target, emitted: emitted) + Present { value: _ } => acc + Absent => parse_subtree_locate_production(root: edge.target, emitted: emitted) } } ) } -fn parse_subtree_find_production_captured(root: Node, emitted: Symbol) -> ParseSubtreeFind { +fn parse_subtree_locate_production(root: Node, emitted: Symbol) -> Node? { match parse_production_emitted_identity_optional(node: root) { Present { value: id } => if id == emitted { - match parse_production_captured_child_optional(node: root) { - Present { value: captured } => ParseSubtreeFound { captured: captured } - Absent => - parse_subtree_find_production_captured_under_children(root: root, emitted: emitted) - } + optional_present(value: root) } else { - parse_subtree_find_production_captured_under_children(root: root, emitted: emitted) + parse_subtree_locate_production_under_children(root: root, emitted: emitted) } - Absent => - parse_subtree_find_production_captured_under_children(root: root, emitted: emitted) + Absent => parse_subtree_locate_production_under_children(root: root, emitted: emitted) } } -fn parse_subtree_has_production_under_children(root: Node, emitted: Symbol) -> Bool { - fold( - root.children, - init: false, - f: fn(found, edge) { - if found { - true - } else { - parse_subtree_has_production(root: edge.target, emitted: emitted) +fn parse_subtree_find_production_captured(root: Node, emitted: Symbol) -> ParseSubtreeFind { + match parse_subtree_locate_production(root: root, emitted: emitted) { + Absent => ParseSubtreeAbsent + Present { value: node } => + match parse_production_captured_child_optional(node: node) { + Present { value: captured } => ParseSubtreeFound { captured: captured } + Absent => ParseSubtreeAbsent } - } - ) + } } fn parse_subtree_has_production(root: Node, emitted: Symbol) -> Bool { - match parse_production_emitted_identity_optional(node: root) { - Present { value: id } => - if id == emitted { - true - } else { - parse_subtree_has_production_under_children(root: root, emitted: emitted) - } - Absent => parse_subtree_has_production_under_children(root: root, emitted: emitted) + match parse_subtree_locate_production(root: root, emitted: emitted) { + Present { value: _ } => true + Absent => false } } From 45c10aa3f81ed4d22c31eee1886726b9a1aadce5 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 11:01:20 +0000 Subject: [PATCH 34/56] Read type/data names from the dag sequence spine; stop treating Rejected as not-named. Declaration idents live in dag_surface_kw_then_ident_from_captured (sequence-right then sequence-left). Graft no longer DFS-widens on a miss; type and data units without a Named edge stay residuals. Record naming tests that greened on any assemble refusal are gone. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 73 ++++--------------- src/v2/extdeps/languages/dag.dag | 24 ++++++ .../claim/declaration_graft_assemble_test.dag | 15 +--- 3 files changed, 40 insertions(+), 72 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 98d4c9ba9e1..d0a53692b66 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -4,7 +4,6 @@ import std.occurrence_identity { OccurrenceSynthetic } import std.algebra { Cons, Empty, FreeMonoid } import v2.std.algebra { fold_list, fold_list_right, list_append, list_snoc_item } import v2.std.collection { Absent, List, Present, optional_absent, optional_present } -import v2.std.compilers.sugar { sugar_sequence_pair_optional, sugar_unwrap_production_shell } import v2.std.diagnostic { Accepted, Diagnostic, @@ -41,7 +40,6 @@ import v2.std.grammar { node_atom_identity_optional } import v2.extdeps.languages.dag { - dag_binding_branded_atom_identity, dag_lex_token_symbol, dag_named_edge, dag_surface_module_header_metadata_edge, @@ -54,6 +52,7 @@ import v2.extdeps.languages.dag { parse_production_emitted_identity_optional, parse_subtree_has_production, dag_resolve_preserve_module_metadata_subtree, + dag_surface_kw_then_ident_from_captured, qualified_name_from_module_node } @@ -271,46 +270,10 @@ fn namespace_graft_reconstruct_declaration_forest(units: List) -> List Symbol? { - let stripped = sugar_unwrap_production_shell(node: node) - match dag_binding_branded_atom_identity(node: stripped) { - Present { value: binding } => optional_present(value: binding) - Absent => - match node_atom_identity_optional(node: stripped) { - Present { value: sym } => - if dag_lex_token_symbol(sym: sym) { - fold(stripped.children, init: optional_absent(), f: fn(acc, e) { - match acc { - Present { value: _ } => acc - Absent => namespace_graft_ident_from_node(node: e.target) - } - }) - } else { - optional_present(value: sym) - } - Absent => - fold(stripped.children, init: optional_absent(), f: fn(acc, e) { - match acc { - Present { value: _ } => acc - Absent => namespace_graft_ident_from_node(node: e.target) - } - }) - } - } -} - -fn namespace_graft_kw_then_ident(captured: Node) -> Symbol? { - match sugar_sequence_pair_optional(node: captured) { - Absent => namespace_graft_ident_from_node(node: captured) - Present { value: pair } => - match namespace_graft_ident_from_node(node: pair.left) { - Present { value: _ } => namespace_graft_ident_from_node(node: pair.right) - Absent => - match sugar_sequence_pair_optional(node: pair.right) { - Present { value: rest } => namespace_graft_ident_from_node(node: rest.left) - Absent => namespace_graft_ident_from_node(node: pair.right) - } - } +fn namespace_graft_type_decl_name(unit: Node) -> Symbol? { + match namespace_graft_unit_captured(unit: unit) { + Present { value: captured } => dag_surface_kw_then_ident_from_captured(captured: captured) + Absent => optional_absent() } } @@ -360,17 +323,6 @@ fn namespace_graft_unit_captured(unit: Node) -> Node? { parse_production_captured_child_optional(node: unit) } -fn namespace_graft_type_decl_name(unit: Node) -> Symbol? { - match namespace_graft_unit_captured(unit: unit) { - Present { value: captured } => - match namespace_graft_kw_then_ident(captured: captured) { - Present { value: name } => optional_present(value: name) - Absent => namespace_graft_ident_from_node(node: unit) - } - Absent => namespace_graft_ident_from_node(node: unit) - } -} - // Record field_decl_block is not Named-rehomed here (body_lower_type_decl / #11582). // The unit is a residual: body_edges_for_graft reconstructs it beside any sibling // Named edge so a nonempty combined cannot delete it. @@ -395,7 +347,7 @@ fn namespace_graft_data_decl_named_edge(unit: Node) -> Edge? { match namespace_graft_unit_captured(unit: unit) { Absent => optional_absent() Present { value: captured } => - match namespace_graft_kw_then_ident(captured: captured) { + match dag_surface_kw_then_ident_from_captured(captured: captured) { Absent => optional_absent() Present { value: data_name } => optional_present( @@ -432,17 +384,18 @@ fn namespace_graft_named_edges_from_units(units: List) -> List { }) } -fn namespace_graft_is_type_decl_unit(unit: Node) -> Bool { +fn namespace_graft_is_type_or_data_decl_unit(unit: Node) -> Bool { match parse_production_emitted_identity_optional(node: unit) { - Present { value: id } => id == ^dag_surface_type_decl + Present { value: id } => + (id == ^dag_surface_type_decl) || (id == ^dag_surface_data_decl) Absent => false } } -fn namespace_graft_residual_type_decl_units(units: List) -> List { +fn namespace_graft_residual_decl_units(units: List) -> List { fold(units, init: [], f: fn(acc, unit) { - if namespace_graft_is_type_decl_unit(unit: unit) { - match namespace_graft_type_decl_named_edge(unit: unit) { + if namespace_graft_is_type_or_data_decl_unit(unit: unit) { + match namespace_graft_named_edge_from_unit(unit: unit) { Present { value: _ } => acc Absent => list_snoc_item(xs: acc, item: unit) } @@ -513,7 +466,7 @@ fn namespace_graft_body_edges_for_graft(root: Node) -> List { edges: namespace_graft_collect_body_edges_from_node(node: working) ) let combined = concat(from_decls, flattened) - let residuals = namespace_graft_residual_type_decl_units(units: units) + let residuals = namespace_graft_residual_decl_units(units: units) if count(combined) == 0 { if count(units) == 0 { [] diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index ad30682fa8c..111038fe34e 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4629,6 +4629,30 @@ fn parse_subtree_has_production(root: Node, emitted: Symbol) -> Bool { } } +fn dag_surface_kw_then_ident_from_captured(captured: Node) -> Optional { + match find_named_child(root: captured, name: ^grammar_sequence_right_node_projection) { + Rejected { diagnostics: _ } => Absent + Accepted { value: after_kw, diagnostics: _ } => + match find_named_child(root: after_kw, name: ^grammar_sequence_left_node_projection) { + Rejected { diagnostics: _ } => Absent + Accepted { value: ident_node, diagnostics: _ } => + match dag_binding_branded_atom_identity(node: ident_node) { + Present { value: binding } => Present { value: binding } + Absent => + match node_atom_identity_optional(node: ident_node) { + Present { value: sym } => + if dag_lex_token_symbol(sym: sym) { + optional_absent() + } else { + optional_present(value: sym) + } + Absent => optional_absent() + } + } + } + } +} + // THE NAMED-ARGUMENT SPINE, AND IT LIVES HERE BECAUSE THIS MODULE OWNS THE PRODUCTION IT // DECODES. dag_grammar_arg_expr is Choice(Seq(binding_name, Seq(colon, expr)), expr): a named // argument carries its NAME in the sequence left and its VALUE two rights in, past the colon; a diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index d25468f4144..7795fede84f 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -183,22 +183,13 @@ test fn declaration_graft_coproduct_beside_record_does_not_erase_rec() -> Bool { } // Record Named-rehome is body_lower_type_decl / gunbc#11582, not flatten-drop. -// These assert the measured defect so they execute on per-PR discovery (LCR polarity). -// They go red when record assemble accepts / Rec is Named; then they flip to -// regression controls and any ExpectAssertionFalse row would delete (§4b(4)). +// Per-PR measurements of the defect (assemble does not accept). Naming is not +// asserted via !named_reachable: that is true on every Rejected and does not +// inspect labels. They go red when record assemble accepts. test fn declaration_graft_record_type_only_does_not_accept_today() -> Bool { !declaration_graft_assemble_accepts(src: src_g_type_only) } -test fn declaration_graft_record_type_only_rec_is_not_named_today() -> Bool { - !declaration_graft_named_reachable(src: src_g_type_only, lexeme: "Rec") -} - test fn declaration_graft_record_construct_does_not_accept_today() -> Bool { !declaration_graft_assemble_accepts(src: src_g) } - -test fn declaration_graft_record_fields_are_not_named_today() -> Bool { - !(declaration_graft_named_reachable(src: src_g, lexeme: "Rec") - && declaration_graft_named_reachable(src: src_g, lexeme: "n")) -} From 33e111dae2181421a343ea4005f7d7b099facfda Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 11:35:10 +0000 Subject: [PATCH 35/56] Point alias binding at the shared kw-then-ident spine. The sequence-right/left node walk is one decoder. alias_binding_row_binding_from_decl consumes it. body_lower_*_ident_from_captured still live in body_lowering_fold, which gunbc#11582 owns. Co-authored-by: Cursor --- src/v2/extdeps/languages/dag.dag | 48 +++++++++++++++++--------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 111038fe34e..1e599b00350 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4629,25 +4629,33 @@ fn parse_subtree_has_production(root: Node, emitted: Symbol) -> Bool { } } -fn dag_surface_kw_then_ident_from_captured(captured: Node) -> Optional { +fn dag_surface_kw_then_ident_node_from_captured(captured: Node) -> Outcome { match find_named_child(root: captured, name: ^grammar_sequence_right_node_projection) { - Rejected { diagnostics: _ } => Absent + Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: after_kw, diagnostics: _ } => match find_named_child(root: after_kw, name: ^grammar_sequence_left_node_projection) { - Rejected { diagnostics: _ } => Absent - Accepted { value: ident_node, diagnostics: _ } => - match dag_binding_branded_atom_identity(node: ident_node) { - Present { value: binding } => Present { value: binding } - Absent => - match node_atom_identity_optional(node: ident_node) { - Present { value: sym } => - if dag_lex_token_symbol(sym: sym) { - optional_absent() - } else { - optional_present(value: sym) - } - Absent => optional_absent() + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: ident_node, diagnostics: diags } => + Accepted { value: ident_node, diagnostics: diags } + } + } +} + +fn dag_surface_kw_then_ident_from_captured(captured: Node) -> Optional { + match dag_surface_kw_then_ident_node_from_captured(captured: captured) { + Rejected { diagnostics: _ } => Absent + Accepted { value: ident_node, diagnostics: _ } => + match dag_binding_branded_atom_identity(node: ident_node) { + Present { value: binding } => Present { value: binding } + Absent => + match node_atom_identity_optional(node: ident_node) { + Present { value: sym } => + if dag_lex_token_symbol(sym: sym) { + optional_absent() + } else { + optional_present(value: sym) } + Absent => optional_absent() } } } @@ -4933,14 +4941,10 @@ data alias_binding_row_not_alias_decl: Symbol = ^alias_binding_row_not_alias_dec fn alias_binding_row_binding_from_decl(decl: Node, module_root: Node) -> Outcome { match parse_production_captured_child_optional(node: decl) { Present { value: captured } => - match find_named_child(root: captured, name: ^grammar_sequence_right_node_projection) { + match dag_surface_kw_then_ident_node_from_captured(captured: captured) { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: after_kw, diagnostics: _ } => - match find_named_child(root: after_kw, name: ^grammar_sequence_left_node_projection) { - Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: ident_node, diagnostics: _ } => - parse_qn_lexeme_segment(sym_node: ident_node, root: module_root) - } + Accepted { value: ident_node, diagnostics: _ } => + parse_qn_lexeme_segment(sym_node: ident_node, root: module_root) } Absent => match find_named_child(root: decl, name: ^dag_surface_alias_decl_binding) { From f869a5a9698b083b39f1f2e178271946b3017ce7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 12:09:44 +0000 Subject: [PATCH 36/56] Skip Named rehome only for the record type_decl arm, not variant field blocks. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit field_decl_block also sits on coproduct variants; a anywhere-in-subtree skip left those types as reconstructed shells. Gate the residual on type_alias_rhs absence so Leaf | Node { … } and Bar {} become Named edges. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 14 ++++++++++---- .../test/claim/declaration_graft_assemble_test.dag | 10 ++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index d0a53692b66..ea3c290bc9f 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -323,11 +323,17 @@ fn namespace_graft_unit_captured(unit: Node) -> Node? { parse_production_captured_child_optional(node: unit) } -// Record field_decl_block is not Named-rehomed here (body_lower_type_decl / #11582). -// The unit is a residual: body_edges_for_graft reconstructs it beside any sibling -// Named edge so a nonempty combined cannot delete it. +// Record form is the type_decl choice arm that is a field_decl_block without +// type_alias_rhs (`type Rec { … }`). Variant field_decl_block lives under +// type_alias_rhs (`type Box = Leaf | Node { … }`, `type Foo = Bar {}`) and is +// Named-rehomed here. Record residual reconstruct stays until body_lower_type_decl / #11582. +fn namespace_graft_type_decl_is_record_form(unit: Node) -> Bool { + parse_subtree_has_production(root: unit, emitted: ^dag_surface_field_decl_block) + && !parse_subtree_has_production(root: unit, emitted: ^dag_surface_type_alias_rhs) +} + fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { - if parse_subtree_has_production(root: unit, emitted: ^dag_surface_field_decl_block) { + if namespace_graft_type_decl_is_record_form(unit: unit) { optional_absent() } else { match namespace_graft_type_decl_name(unit: unit) { diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index 7795fede84f..3b242935eb6 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -130,6 +130,8 @@ data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" data src_dag_user_fn: String = "module p\n\nfn dag_user(x: Int) -> Int { x }\n" data src_flag_and_rec: String = "module p\n\ntype Flag = On | Off\ntype Rec { n: Int }\n" +data src_fielded_coproduct: String = "module p\n\ntype Box = Leaf | Node { n: Int }\n" +data src_empty_fielded_variant: String = "module p\n\ntype Foo = Bar {}\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -147,6 +149,14 @@ test fn declaration_graft_type_only_flag_is_named() -> Bool { declaration_graft_named_reachable(src: src_c, lexeme: "Flag") } +test fn declaration_graft_fielded_coproduct_is_named() -> Bool { + declaration_graft_named_reachable(src: src_fielded_coproduct, lexeme: "Box") +} + +test fn declaration_graft_empty_fielded_variant_is_named() -> Bool { + declaration_graft_named_reachable(src: src_empty_fielded_variant, lexeme: "Foo") +} + test fn declaration_graft_where_alias_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_h) } From 75f569a6c3739ea62567c2c2fc234b7a258bc64b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 12:18:29 +0000 Subject: [PATCH 37/56] Unify parse_subtree_locate_production branches as Optional rather than if. The if arms disagreed (Optional vs Node) when typechecking the graft assemble claims after the record-form skip started locating type_alias_rhs as well as field_decl_block. Co-authored-by: Cursor --- src/v2/extdeps/languages/dag.dag | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 1e599b00350..5ccc976ffaf 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4602,10 +4602,9 @@ fn parse_subtree_locate_production_under_children(root: Node, emitted: Symbol) - fn parse_subtree_locate_production(root: Node, emitted: Symbol) -> Node? { match parse_production_emitted_identity_optional(node: root) { Present { value: id } => - if id == emitted { - optional_present(value: root) - } else { - parse_subtree_locate_production_under_children(root: root, emitted: emitted) + match id == emitted { + true => Present { value: root } + false => parse_subtree_locate_production_under_children(root: root, emitted: emitted) } Absent => parse_subtree_locate_production_under_children(root: root, emitted: emitted) } From c196aad5fa0a6342270a3a43910d6bab34485214 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 12:27:31 +0000 Subject: [PATCH 38/56] Treat field_decl_block under a coproduct Disj as Named, not a record residual. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Graft runs after sugar replaces type_alias_rhs with Disj, so alias-rhs absence alone still skipped Leaf | Node { … }. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index ea3c290bc9f..6b61f5d93c3 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -323,12 +323,22 @@ fn namespace_graft_unit_captured(unit: Node) -> Node? { parse_production_captured_child_optional(node: unit) } -// Record form is the type_decl choice arm that is a field_decl_block without -// type_alias_rhs (`type Rec { … }`). Variant field_decl_block lives under -// type_alias_rhs (`type Box = Leaf | Node { … }`, `type Foo = Bar {}`) and is -// Named-rehomed here. Record residual reconstruct stays until body_lower_type_decl / #11582. +fn namespace_graft_node_is_disj(n: Node) -> Bool { + match n.kind { + TypeNode { connective: Disj } => true + _ => false + } +} + +// Record form is `type Rec { … }`: a field_decl_block that is not a coproduct +// variant. Graft runs after sugar, which replaces type_alias_rhs with Disj for +// two-or-more variants while leaving field_decl_block under those Named edges. +// Single-variant `type Foo = Bar {}` keeps type_alias_rhs. Either discriminator +// means Named-rehome here; residual reconstruct stays for records until +// body_lower_type_decl / #11582. fn namespace_graft_type_decl_is_record_form(unit: Node) -> Bool { parse_subtree_has_production(root: unit, emitted: ^dag_surface_field_decl_block) + && !namespace_graft_node_is_disj(n: namespace_graft_type_decl_target(captured: unit)) && !parse_subtree_has_production(root: unit, emitted: ^dag_surface_type_alias_rhs) } From 850f9bfe1ad90963abc40c659abd77baa824def4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 12:37:44 +0000 Subject: [PATCH 39/56] Detect coproduct field blocks by a Disj anywhere under the type_decl unit. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sugar replaces type_alias_rhs with Disj, so looking only at the type_decl_target root still treated Leaf | Node { … } as a record residual. Pair a dummy fn on the new assemble controls so a miss cannot hide behind type-only graft dissolve. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 9 ++++++--- .../claim/declaration_graft_assemble_test.dag | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 6b61f5d93c3..3e65be1442e 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -323,10 +323,13 @@ fn namespace_graft_unit_captured(unit: Node) -> Node? { parse_production_captured_child_optional(node: unit) } -fn namespace_graft_node_is_disj(n: Node) -> Bool { +fn namespace_graft_subtree_has_disj(n: Node) -> Bool { match n.kind { TypeNode { connective: Disj } => true - _ => false + _ => + fold(n.children, init: false, f: fn(found, e) { + found || namespace_graft_subtree_has_disj(n: e.target) + }) } } @@ -338,7 +341,7 @@ fn namespace_graft_node_is_disj(n: Node) -> Bool { // body_lower_type_decl / #11582. fn namespace_graft_type_decl_is_record_form(unit: Node) -> Bool { parse_subtree_has_production(root: unit, emitted: ^dag_surface_field_decl_block) - && !namespace_graft_node_is_disj(n: namespace_graft_type_decl_target(captured: unit)) + && !namespace_graft_subtree_has_disj(n: unit) && !parse_subtree_has_production(root: unit, emitted: ^dag_surface_type_alias_rhs) } diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index 3b242935eb6..08ec98f887e 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -110,6 +110,13 @@ fn declaration_graft_present_anywhere(root: Node, lexeme: String) -> Bool { || declaration_graft_atom_present(root: root, lexeme: lexeme) } +fn declaration_graft_assemble_reason(src: String) -> String { + match declaration_graft_assemble_for(src: src) { + Accepted { value: _, diagnostics: _ } => "ACCEPTED" + Rejected { diagnostics: d } => symbol_lexeme(sym: d.head.reason) + } +} + fn declaration_graft_named_reachable(src: String, lexeme: String) -> Bool { match declaration_graft_assemble_for(src: src) { Accepted { value: root, diagnostics: _ } => @@ -130,8 +137,8 @@ data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" data src_dag_user_fn: String = "module p\n\nfn dag_user(x: Int) -> Int { x }\n" data src_flag_and_rec: String = "module p\n\ntype Flag = On | Off\ntype Rec { n: Int }\n" -data src_fielded_coproduct: String = "module p\n\ntype Box = Leaf | Node { n: Int }\n" -data src_empty_fielded_variant: String = "module p\n\ntype Foo = Bar {}\n" +data src_fielded_coproduct: String = "module p\n\ntype Box = Leaf | Node { n: Int }\nfn k() -> Int { 1 }\n" +data src_empty_fielded_variant: String = "module p\n\ntype Foo = Bar {}\nfn k() -> Int { 1 }\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -153,10 +160,18 @@ test fn declaration_graft_fielded_coproduct_is_named() -> Bool { declaration_graft_named_reachable(src: src_fielded_coproduct, lexeme: "Box") } +test fn declaration_graft_fielded_coproduct_assembles() -> Bool { + declaration_graft_assemble_reason(src: src_fielded_coproduct) == "ACCEPTED" +} + test fn declaration_graft_empty_fielded_variant_is_named() -> Bool { declaration_graft_named_reachable(src: src_empty_fielded_variant, lexeme: "Foo") } +test fn declaration_graft_flag_and_rec_reason_is_accepted() -> Bool { + declaration_graft_assemble_reason(src: src_flag_and_rec) == "ACCEPTED" +} + test fn declaration_graft_where_alias_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_h) } From c6dceb0b6066f55b92103eacc1aa85691e01f812 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 13:05:59 +0000 Subject: [PATCH 40/56] Distinguish record types from fielded coproducts by `=` and Disj, not alias-rhs presence. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unchosen type_decl choice arm still carried type_alias_rhs under `type Rec { … }`, so that skip Named-rehomed records and unbound Flag+Rec. Records stay residual; `type Foo = Bar {}` and Leaf | Cell { … } take the Named arm. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 14 +++++++------- .../test/claim/declaration_graft_assemble_test.dag | 6 +----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 3e65be1442e..da624ee26d0 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -333,16 +333,16 @@ fn namespace_graft_subtree_has_disj(n: Node) -> Bool { } } -// Record form is `type Rec { … }`: a field_decl_block that is not a coproduct -// variant. Graft runs after sugar, which replaces type_alias_rhs with Disj for -// two-or-more variants while leaving field_decl_block under those Named edges. -// Single-variant `type Foo = Bar {}` keeps type_alias_rhs. Either discriminator -// means Named-rehome here; residual reconstruct stays for records until -// body_lower_type_decl / #11582. +// Record form is `type Rec { … }`, which has a field_decl_block and no `=`. +// Coproducts keep `=` (`type Box = Leaf | Cell { … }`, `type Foo = Bar {}`). +// After sugar, two-or-more variants are Disj with the variant field_decl_block +// still underneath; `type_alias_rhs` presence is not a discriminator because +// the unchosen choice arm can still sit in the record tree. Residual reconstruct +// stays for records until body_lower_type_decl / #11582. fn namespace_graft_type_decl_is_record_form(unit: Node) -> Bool { parse_subtree_has_production(root: unit, emitted: ^dag_surface_field_decl_block) && !namespace_graft_subtree_has_disj(n: unit) - && !parse_subtree_has_production(root: unit, emitted: ^dag_surface_type_alias_rhs) + && !namespace_graft_subtree_contains_atom(n: unit, sym: ^dag_token_eq) } fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index 08ec98f887e..add256ebaa1 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -137,7 +137,7 @@ data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" data src_dag_user_fn: String = "module p\n\nfn dag_user(x: Int) -> Int { x }\n" data src_flag_and_rec: String = "module p\n\ntype Flag = On | Off\ntype Rec { n: Int }\n" -data src_fielded_coproduct: String = "module p\n\ntype Box = Leaf | Node { n: Int }\nfn k() -> Int { 1 }\n" +data src_fielded_coproduct: String = "module p\n\ntype Box = Leaf | Cell { n: Int }\nfn k() -> Int { 1 }\n" data src_empty_fielded_variant: String = "module p\n\ntype Foo = Bar {}\nfn k() -> Int { 1 }\n" test fn declaration_graft_empty_module_accepts() -> Bool { @@ -168,10 +168,6 @@ test fn declaration_graft_empty_fielded_variant_is_named() -> Bool { declaration_graft_named_reachable(src: src_empty_fielded_variant, lexeme: "Foo") } -test fn declaration_graft_flag_and_rec_reason_is_accepted() -> Bool { - declaration_graft_assemble_reason(src: src_flag_and_rec) == "ACCEPTED" -} - test fn declaration_graft_where_alias_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_h) } From b8957a6b0ce8ee4234d029a6ebb3f0661f040fe2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 13:14:46 +0000 Subject: [PATCH 41/56] Restore the field_decl_block Named skip; alias-rhs narrowing Named-rehomed records. review 67681 is right that the skip is wider than records, but type_alias_rhs / `=` discriminators either missed post-sugar Disj coproducts or treated Rec as Named and unbound Flag+Rec. Record residual stays until body_lower_type_decl. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 30 +++++-------------- .../claim/declaration_graft_assemble_test.dag | 21 ------------- 2 files changed, 7 insertions(+), 44 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index da624ee26d0..26677d6d052 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -323,30 +323,14 @@ fn namespace_graft_unit_captured(unit: Node) -> Node? { parse_production_captured_child_optional(node: unit) } -fn namespace_graft_subtree_has_disj(n: Node) -> Bool { - match n.kind { - TypeNode { connective: Disj } => true - _ => - fold(n.children, init: false, f: fn(found, e) { - found || namespace_graft_subtree_has_disj(n: e.target) - }) - } -} - -// Record form is `type Rec { … }`, which has a field_decl_block and no `=`. -// Coproducts keep `=` (`type Box = Leaf | Cell { … }`, `type Foo = Bar {}`). -// After sugar, two-or-more variants are Disj with the variant field_decl_block -// still underneath; `type_alias_rhs` presence is not a discriminator because -// the unchosen choice arm can still sit in the record tree. Residual reconstruct -// stays for records until body_lower_type_decl / #11582. -fn namespace_graft_type_decl_is_record_form(unit: Node) -> Bool { - parse_subtree_has_production(root: unit, emitted: ^dag_surface_field_decl_block) - && !namespace_graft_subtree_has_disj(n: unit) - && !namespace_graft_subtree_contains_atom(n: unit, sym: ^dag_token_eq) -} - +// Record field_decl_block is not Named-rehomed here (body_lower_type_decl / #11582). +// The unit is a residual: body_edges_for_graft reconstructs it beside any sibling +// Named edge so a nonempty combined cannot delete it. +// review 67681: this skip is wider than `type Rec { … }` — variant field_decl_block +// (`type Box = Leaf | Cell { … }`, `type Foo = Bar {}`) takes the same arm. Narrowing +// by type_alias_rhs Named-rehomed records (unchosen choice arm) and broke Flag+Rec. fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { - if namespace_graft_type_decl_is_record_form(unit: unit) { + if parse_subtree_has_production(root: unit, emitted: ^dag_surface_field_decl_block) { optional_absent() } else { match namespace_graft_type_decl_name(unit: unit) { diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index add256ebaa1..7795fede84f 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -110,13 +110,6 @@ fn declaration_graft_present_anywhere(root: Node, lexeme: String) -> Bool { || declaration_graft_atom_present(root: root, lexeme: lexeme) } -fn declaration_graft_assemble_reason(src: String) -> String { - match declaration_graft_assemble_for(src: src) { - Accepted { value: _, diagnostics: _ } => "ACCEPTED" - Rejected { diagnostics: d } => symbol_lexeme(sym: d.head.reason) - } -} - fn declaration_graft_named_reachable(src: String, lexeme: String) -> Bool { match declaration_graft_assemble_for(src: src) { Accepted { value: root, diagnostics: _ } => @@ -137,8 +130,6 @@ data src_g: String = "module p\n\ntype Rec { n: Int }\nfn mk() -> Rec { Rec { n: data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" data src_dag_user_fn: String = "module p\n\nfn dag_user(x: Int) -> Int { x }\n" data src_flag_and_rec: String = "module p\n\ntype Flag = On | Off\ntype Rec { n: Int }\n" -data src_fielded_coproduct: String = "module p\n\ntype Box = Leaf | Cell { n: Int }\nfn k() -> Int { 1 }\n" -data src_empty_fielded_variant: String = "module p\n\ntype Foo = Bar {}\nfn k() -> Int { 1 }\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -156,18 +147,6 @@ test fn declaration_graft_type_only_flag_is_named() -> Bool { declaration_graft_named_reachable(src: src_c, lexeme: "Flag") } -test fn declaration_graft_fielded_coproduct_is_named() -> Bool { - declaration_graft_named_reachable(src: src_fielded_coproduct, lexeme: "Box") -} - -test fn declaration_graft_fielded_coproduct_assembles() -> Bool { - declaration_graft_assemble_reason(src: src_fielded_coproduct) == "ACCEPTED" -} - -test fn declaration_graft_empty_fielded_variant_is_named() -> Bool { - declaration_graft_named_reachable(src: src_empty_fielded_variant, lexeme: "Foo") -} - test fn declaration_graft_where_alias_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_h) } From 82c1fb890fabae17e65f3f0cf627cbe5fc9b87a7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 13:17:01 +0000 Subject: [PATCH 42/56] Dissolve the bool production predicate and the declaration-target walker. Call sites match parse_subtree_locate_production Optional directly. Flatten keeps Named members whose subtree holds Arrow or Disj via node_subtree_nodes, not a recursive is_* over every Conj. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 58 +++++++++++++++-------------- src/v2/extdeps/languages/dag.dag | 7 ---- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 26677d6d052..b593cad15b0 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -50,7 +50,7 @@ import v2.extdeps.languages.dag { namespace_graft_unwrap_module_working_root, parse_production_captured_child_optional, parse_production_emitted_identity_optional, - parse_subtree_has_production, + parse_subtree_locate_production, dag_resolve_preserve_module_metadata_subtree, dag_surface_kw_then_ident_from_captured, qualified_name_from_module_node @@ -330,19 +330,19 @@ fn namespace_graft_unit_captured(unit: Node) -> Node? { // (`type Box = Leaf | Cell { … }`, `type Foo = Bar {}`) takes the same arm. Narrowing // by type_alias_rhs Named-rehomed records (unchosen choice arm) and broke Flag+Rec. fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { - if parse_subtree_has_production(root: unit, emitted: ^dag_surface_field_decl_block) { - optional_absent() - } else { - match namespace_graft_type_decl_name(unit: unit) { - Absent => optional_absent() - Present { value: type_name } => - optional_present( - value: Edge { - label: Named { name: type_name }, - target: namespace_graft_type_decl_target(captured: unit) - } - ) - } + match parse_subtree_locate_production(root: unit, emitted: ^dag_surface_field_decl_block) { + Present { value: _ } => optional_absent() + Absent => + match namespace_graft_type_decl_name(unit: unit) { + Absent => optional_absent() + Present { value: type_name } => + optional_present( + value: Edge { + label: Named { name: type_name }, + target: namespace_graft_type_decl_target(captured: unit) + } + ) + } } } @@ -415,18 +415,8 @@ fn namespace_graft_residual_decl_units(units: List) -> List { // (the type shell was dropped). Neither arm is a refusal. Rehome type/data onto // Named containment first; reconstruct remains only when nothing first-class // survived (import-only and the lost-forest refusal still sit on that empty path). -fn namespace_graft_is_declaration_target(node: Node) -> Bool { - match node.kind { - TypeNode { connective: Arrow } => true - TypeNode { connective: Conj } => true - TypeNode { connective: Disj } => true - _ => - fold(node.children, init: false, f: fn(found, e) { - found || namespace_graft_is_declaration_target(node: e.target) - }) - } -} - +// Flatten keeps an existing Named member when its target subtree holds an Arrow +// (fn) or Disj (coproduct). A bare Conj is not a declaration (records stay residual). fn namespace_graft_keep_flattened_member_edge( productions: List, edge: Edge @@ -440,7 +430,21 @@ fn namespace_graft_keep_flattened_member_edge( } else if namespace_graft_is_machinery_symbol(productions: productions, sym: sym) { false } else { - namespace_graft_is_declaration_target(node: edge.target) + fold( + node_subtree_nodes(root: edge.target), + init: false, + f: fn(found, n) { + if found { + true + } else { + match n.kind { + TypeNode { connective: Arrow } => true + TypeNode { connective: Disj } => true + _ => false + } + } + } + ) } Positional => false } diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 5ccc976ffaf..49c96551c35 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4621,13 +4621,6 @@ fn parse_subtree_find_production_captured(root: Node, emitted: Symbol) -> ParseS } } -fn parse_subtree_has_production(root: Node, emitted: Symbol) -> Bool { - match parse_subtree_locate_production(root: root, emitted: emitted) { - Present { value: _ } => true - Absent => false - } -} - fn dag_surface_kw_then_ident_node_from_captured(captured: Node) -> Outcome { match find_named_child(root: captured, name: ^grammar_sequence_right_node_projection) { Rejected { diagnostics: d } => Rejected { diagnostics: d } From 6ec06e9c234681f6fca17ac5ffe7819b5f822245 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 13:19:32 +0000 Subject: [PATCH 43/56] Restore find-captured descent when the first matching production has no capture. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse_subtree_locate_production stays first-emitted-id; find_production_captured again walks children if that node has no captured child, which is the contract 06_translate still consumes. Rename the type_decl Disj search parameter to root — it is called on the type_decl unit, not the captured child. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 8 +++---- src/v2/extdeps/languages/dag.dag | 34 ++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index b593cad15b0..d1b63aa55a5 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -302,10 +302,10 @@ fn namespace_graft_is_machinery_symbol(productions: List, sym || namespace_graft_symbol_is_grammar_machinery(productions: productions, sym: sym) } -fn namespace_graft_type_decl_target(captured: Node) -> Node { +fn namespace_graft_type_decl_target(root: Node) -> Node { fold( - node_subtree_nodes(root: captured), - init: captured, + node_subtree_nodes(root: root), + init: root, f: fn(acc, n) { match acc.kind { TypeNode { connective: Disj } => acc @@ -339,7 +339,7 @@ fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { optional_present( value: Edge { label: Named { name: type_name }, - target: namespace_graft_type_decl_target(captured: unit) + target: namespace_graft_type_decl_target(root: unit) } ) } diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 49c96551c35..fb1e61b4831 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4610,14 +4610,36 @@ fn parse_subtree_locate_production(root: Node, emitted: Symbol) -> Node? { } } +fn parse_subtree_find_production_captured_under_children( + root: Node, + emitted: Symbol +) -> ParseSubtreeFind { + fold( + root.children, + init: ParseSubtreeAbsent, + f: fn(acc, edge) { + match acc { + ParseSubtreeFound { captured: _ } => acc + ParseSubtreeAbsent => + parse_subtree_find_production_captured(root: edge.target, emitted: emitted) + } + } + ) +} + fn parse_subtree_find_production_captured(root: Node, emitted: Symbol) -> ParseSubtreeFind { - match parse_subtree_locate_production(root: root, emitted: emitted) { - Absent => ParseSubtreeAbsent - Present { value: node } => - match parse_production_captured_child_optional(node: node) { - Present { value: captured } => ParseSubtreeFound { captured: captured } - Absent => ParseSubtreeAbsent + match parse_production_emitted_identity_optional(node: root) { + Present { value: id } => + match id == emitted { + true => + match parse_production_captured_child_optional(node: root) { + Present { value: captured } => ParseSubtreeFound { captured: captured } + Absent => + parse_subtree_find_production_captured_under_children(root: root, emitted: emitted) + } + false => parse_subtree_find_production_captured_under_children(root: root, emitted: emitted) } + Absent => parse_subtree_find_production_captured_under_children(root: root, emitted: emitted) } } From f6ea5326a4c1f12466f4d39027b886930fd07b2a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 15:11:31 +0000 Subject: [PATCH 44/56] Keep where-alias Conj members on flatten; still drop record field_decl_block Conj. Dropping every Conj for review 67688 refused where-aliases, silent-drop Name, and graft-shape on required-floor. Arrow/Disj stay; a Conj target is kept only when it does not carry field_decl_block. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 44 +++++++++++++++++++---------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index d1b63aa55a5..8815d031490 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -416,7 +416,10 @@ fn namespace_graft_residual_decl_units(units: List) -> List { // Named containment first; reconstruct remains only when nothing first-class // survived (import-only and the lost-forest refusal still sit on that empty path). // Flatten keeps an existing Named member when its target subtree holds an Arrow -// (fn) or Disj (coproduct). A bare Conj is not a declaration (records stay residual). +// (fn) or Disj (coproduct), or when the target itself is a Conj that is not a +// record field_decl_block (where-alias / lowered type body). Records stay off +// this path via the type_decl Named skip plus this Conj carve-out — dropping +// every Conj refused where-aliases and graft-shape (required-floor 6ec06e9). fn namespace_graft_keep_flattened_member_edge( productions: List, edge: Edge @@ -430,21 +433,32 @@ fn namespace_graft_keep_flattened_member_edge( } else if namespace_graft_is_machinery_symbol(productions: productions, sym: sym) { false } else { - fold( - node_subtree_nodes(root: edge.target), - init: false, - f: fn(found, n) { - if found { - true - } else { - match n.kind { - TypeNode { connective: Arrow } => true - TypeNode { connective: Disj } => true - _ => false - } + match edge.target.kind { + TypeNode { connective: Conj } => + match parse_subtree_locate_production( + root: edge.target, + emitted: ^dag_surface_field_decl_block + ) { + Present { value: _ } => false + Absent => true } - } - ) + _ => + fold( + node_subtree_nodes(root: edge.target), + init: false, + f: fn(found, n) { + if found { + true + } else { + match n.kind { + TypeNode { connective: Arrow } => true + TypeNode { connective: Disj } => true + _ => false + } + } + } + ) + } } Positional => false } From 8a027b451461c94c3d2ed68138c66bdf64b8097f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 16:03:32 +0000 Subject: [PATCH 45/56] Keep Named atom flatten members and drop flatten names already rehomed from decls. Graft-shape exports are Named atoms, so requiring Arrow/Disj emptied the body. Concatenating from_decls with flatten also duplicated Flag/alias names and refused well-formed Conj (where-alias and Flag+Rec on required-floor f6ea532). Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 57 +++++++++++++++++++---------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 8815d031490..a117e1c4721 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -420,6 +420,10 @@ fn namespace_graft_residual_decl_units(units: List) -> List { // record field_decl_block (where-alias / lowered type body). Records stay off // this path via the type_decl Named skip plus this Conj carve-out — dropping // every Conj refused where-aliases and graft-shape (required-floor 6ec06e9). +// Flatten keeps Named members that are not parse machinery. A Conj that still +// carries field_decl_block is the record form (residual reconstruct), not a +// flatten member. Atom targets must stay — the graft-shape fixture is two Named +// atoms, and dropping them emptied the body (required-floor f6ea532). fn namespace_graft_keep_flattened_member_edge( productions: List, edge: Edge @@ -442,22 +446,7 @@ fn namespace_graft_keep_flattened_member_edge( Present { value: _ } => false Absent => true } - _ => - fold( - node_subtree_nodes(root: edge.target), - init: false, - f: fn(found, n) { - if found { - true - } else { - match n.kind { - TypeNode { connective: Arrow } => true - TypeNode { connective: Disj } => true - _ => false - } - } - } - ) + _ => true } } Positional => false @@ -477,14 +466,44 @@ fn namespace_graft_flattened_member_edges( }) } +fn namespace_graft_named_list_has_name(edges: List, name: Symbol) -> Bool { + fold(edges, init: false, f: fn(found, edge) { + if found { + true + } else { + match edge.label { + Named { name: n } => n == name + Positional => false + } + } + }) +} + +fn namespace_graft_flatten_minus_decl_names(from_decls: List, flattened: List) -> List { + fold(flattened, init: [], f: fn(acc, edge) { + match edge.label { + Named { name: n } => + if namespace_graft_named_list_has_name(edges: from_decls, name: n) { + acc + } else { + list_snoc_item(xs: acc, item: edge) + } + Positional => acc + } + }) +} + fn namespace_graft_body_edges_for_graft(root: Node) -> List { let working = namespace_graft_unwrap_module_working_root(root: root) let productions = dag_grammar_root().productions let units = namespace_graft_collect_unit_nodes(node: working, productions: productions) let from_decls = namespace_graft_named_edges_from_units(units: units) - let flattened = namespace_graft_flattened_member_edges( - productions: productions, - edges: namespace_graft_collect_body_edges_from_node(node: working) + let flattened = namespace_graft_flatten_minus_decl_names( + from_decls: from_decls, + flattened: namespace_graft_flattened_member_edges( + productions: productions, + edges: namespace_graft_collect_body_edges_from_node(node: working) + ) ) let combined = concat(from_decls, flattened) let residuals = namespace_graft_residual_decl_units(units: units) From 75ff1bee52c45248e336d9ae0d6e84299651f07a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 16:16:40 +0000 Subject: [PATCH 46/56] Track the record graft skip as a failure-mode row; one production DFS for locate and find. The field_decl_block residual and its next-rung trigger (body_lower_type_decl Named rehome) were only in comments. find_production_captured now uses the same matching walk as locate, continuing past a hit with no captured child. Co-authored-by: Cursor --- ...pe_decl_field_block_not_named_at_graft.dag | 24 +++++ src/v2/compiler/namespace_graft.dag | 23 +---- src/v2/extdeps/languages/dag.dag | 88 ++++++++++++------- .../claim/declaration_graft_assemble_test.dag | 6 +- .../field_type_tree_presence_test.dag | 12 +-- .../loaded_carrier_receipts_test.dag | 6 +- 6 files changed, 93 insertions(+), 66 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/record_type_decl_field_block_not_named_at_graft.dag diff --git a/dag/gunbc/recurring_failure_mode/record_type_decl_field_block_not_named_at_graft.dag b/dag/gunbc/recurring_failure_mode/record_type_decl_field_block_not_named_at_graft.dag new file mode 100644 index 00000000000..2298f8476f2 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/record_type_decl_field_block_not_named_at_graft.dag @@ -0,0 +1,24 @@ +module gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data record_type_decl_field_block_not_named_at_graft: RecurringFailureMode = RecurringFailureMode { + identity: "record_type_decl_field_block_not_named_at_graft" as NonEmptyStr, + + receipts: [ + "**a type_decl whose parse tree still carries dag_surface_field_decl_block is not rehomed onto a Named graft edge** (INVALID STATE: `type Rec { n: Int }` stays a residual reconstructed shell, so symbol_index_fill does not see Rec as containment the way it sees `type Flag = On | Off` or a fn. Coproduct and alias units are Named in namespace_graft_type_decl_named_edge; the field_decl_block skip is the record half of the same flatten-drop class.)", + + "HARM: record assemble refuses (standing RED), and a record name is not a first-class module member. Residual reconstruct keeps the unit beside a sibling Named graft so the shell is not silently deleted; that is mitigation of drop, not Named rehome.", + + "DISCRIMINATOR: RED while namespace_graft_type_decl_named_edge returns Absent whenever parse_subtree_locate_production finds dag_surface_field_decl_block under the unit. GREEN when body_lower_type_decl rehomes that form onto Named containment and the skip is deleted.", + + "EVIDENCE ENROLLED: v2.test.claim.declaration_graft_assemble declaration_graft_record_type_only_does_not_accept_today and declaration_graft_record_construct_does_not_accept_today (`!assemble_accepts`). They flip to a permanent regression control when record assemble accepts, they do not retire. v2.test.claim.declaration_graft_assemble declaration_graft_coproduct_beside_record_does_not_erase_rec refuses to treat Rejected as Rec survival.", + + "RUNG FOUND AT: 1, mitigatable -- residual reconstruct contains the drop; the invalid Named-absence is still writable. CEILING: 3 once body_lower_type_decl is the Named home for field_decl_block type_decl, so graft need not skip.", + + "NEXT-RUNG TRIGGER: body_lower_type_decl rehomes a field_decl_block type_decl onto a Named containment edge (the capability tracked as gunbc#11582), and namespace_graft_type_decl_named_edge no longer returns Absent on that production. Nothing smaller retires this row: landing 11582 without that rehome, or deleting the skip without the lowering, is not the climb.", + ], + + evidence: [], +} diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index a117e1c4721..6dc16e6e34b 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -323,12 +323,8 @@ fn namespace_graft_unit_captured(unit: Node) -> Node? { parse_production_captured_child_optional(node: unit) } -// Record field_decl_block is not Named-rehomed here (body_lower_type_decl / #11582). -// The unit is a residual: body_edges_for_graft reconstructs it beside any sibling -// Named edge so a nonempty combined cannot delete it. -// review 67681: this skip is wider than `type Rec { … }` — variant field_decl_block -// (`type Box = Leaf | Cell { … }`, `type Foo = Bar {}`) takes the same arm. Narrowing -// by type_alias_rhs Named-rehomed records (unchosen choice arm) and broke Flag+Rec. +// Record field_decl_block units are residual, not Named; class +// gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { match parse_subtree_locate_production(root: unit, emitted: ^dag_surface_field_decl_block) { Present { value: _ } => optional_absent() @@ -408,22 +404,9 @@ fn namespace_graft_residual_decl_units(units: List) -> List { }) } -// Flattening to Named declaration edges is what symbol_index_fill consumes. A fn_decl -// arrives already rehomed by body_lower_fn_decl_to_arrow; type_decl and data_decl did -// not, so flatten was empty for a type-only module (reconstruct kept raw shells — -// resolve walked On/Off as bare mentions) and nonempty when a fn sat beside them -// (the type shell was dropped). Neither arm is a refusal. Rehome type/data onto -// Named containment first; reconstruct remains only when nothing first-class -// survived (import-only and the lost-forest refusal still sit on that empty path). -// Flatten keeps an existing Named member when its target subtree holds an Arrow -// (fn) or Disj (coproduct), or when the target itself is a Conj that is not a -// record field_decl_block (where-alias / lowered type body). Records stay off -// this path via the type_decl Named skip plus this Conj carve-out — dropping -// every Conj refused where-aliases and graft-shape (required-floor 6ec06e9). // Flatten keeps Named members that are not parse machinery. A Conj that still // carries field_decl_block is the record form (residual reconstruct), not a -// flatten member. Atom targets must stay — the graft-shape fixture is two Named -// atoms, and dropping them emptied the body (required-floor f6ea532). +// flatten member. Atom targets stay: the graft-shape fixture is two Named atoms. fn namespace_graft_keep_flattened_member_edge( productions: List, edge: Edge diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index fb1e61b4831..7417d06d877 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4586,60 +4586,86 @@ type ParseSubtreeFind = ParseSubtreeAbsent | ParseSubtreeFound { captured: Node } -fn parse_subtree_locate_production_under_children(root: Node, emitted: Symbol) -> Node? { +fn parse_subtree_locate_production_under_children( + root: Node, + emitted: Symbol, + require_captured: Bool +) -> Node? { fold( root.children, init: optional_absent(), f: fn(acc, edge) { match acc { Present { value: _ } => acc - Absent => parse_subtree_locate_production(root: edge.target, emitted: emitted) + Absent => + parse_subtree_locate_production_matching( + root: edge.target, + emitted: emitted, + require_captured: require_captured + ) } } ) } -fn parse_subtree_locate_production(root: Node, emitted: Symbol) -> Node? { +fn parse_subtree_locate_production_matching( + root: Node, + emitted: Symbol, + require_captured: Bool +) -> Node? { match parse_production_emitted_identity_optional(node: root) { Present { value: id } => match id == emitted { - true => Present { value: root } - false => parse_subtree_locate_production_under_children(root: root, emitted: emitted) + true => + match require_captured { + false => Present { value: root } + true => + match parse_production_captured_child_optional(node: root) { + Present { value: _ } => Present { value: root } + Absent => + parse_subtree_locate_production_under_children( + root: root, + emitted: emitted, + require_captured: require_captured + ) + } + } + false => + parse_subtree_locate_production_under_children( + root: root, + emitted: emitted, + require_captured: require_captured + ) } - Absent => parse_subtree_locate_production_under_children(root: root, emitted: emitted) + Absent => + parse_subtree_locate_production_under_children( + root: root, + emitted: emitted, + require_captured: require_captured + ) } } -fn parse_subtree_find_production_captured_under_children( - root: Node, - emitted: Symbol -) -> ParseSubtreeFind { - fold( - root.children, - init: ParseSubtreeAbsent, - f: fn(acc, edge) { - match acc { - ParseSubtreeFound { captured: _ } => acc - ParseSubtreeAbsent => - parse_subtree_find_production_captured(root: edge.target, emitted: emitted) - } - } +fn parse_subtree_locate_production(root: Node, emitted: Symbol) -> Node? { + parse_subtree_locate_production_matching( + root: root, + emitted: emitted, + require_captured: false ) } fn parse_subtree_find_production_captured(root: Node, emitted: Symbol) -> ParseSubtreeFind { - match parse_production_emitted_identity_optional(node: root) { - Present { value: id } => - match id == emitted { - true => - match parse_production_captured_child_optional(node: root) { - Present { value: captured } => ParseSubtreeFound { captured: captured } - Absent => - parse_subtree_find_production_captured_under_children(root: root, emitted: emitted) - } - false => parse_subtree_find_production_captured_under_children(root: root, emitted: emitted) + match parse_subtree_locate_production_matching( + root: root, + emitted: emitted, + require_captured: true + ) { + Absent => ParseSubtreeAbsent + Present { value: node } => + match parse_production_captured_child_optional(node: node) { + Present { value: captured } => ParseSubtreeFound { captured: captured } + Absent => ParseSubtreeAbsent } - Absent => parse_subtree_find_production_captured_under_children(root: root, emitted: emitted) } } diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index 7795fede84f..36e1d49ea5f 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -182,10 +182,8 @@ test fn declaration_graft_coproduct_beside_record_does_not_erase_rec() -> Bool { } } -// Record Named-rehome is body_lower_type_decl / gunbc#11582, not flatten-drop. -// Per-PR measurements of the defect (assemble does not accept). Naming is not -// asserted via !named_reachable: that is true on every Rejected and does not -// inspect labels. They go red when record assemble accepts. +// Record assemble standing RED: gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. +// Not asserted via !named_reachable: that is true on every Rejected. test fn declaration_graft_record_type_only_does_not_accept_today() -> Bool { !declaration_graft_assemble_accepts(src: src_g_type_only) } diff --git a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag index c0028a8abe2..36821aaff60 100644 --- a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag +++ b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag @@ -113,18 +113,14 @@ test fn control_a_parameter_type_name_is_an_atom_in_the_tree_holds() -> Bool { atom_present(lexeme: "Xl0UserType") } -// CONTROL, CORRECTED: a type DECLARATION name is an edge label, not an atom. Asserting it as an -// atom was an error and its red said nothing about the tree. Flatten-drop rehomes coproduct/alias, -// not field_decl_block; Xl0Wrapper is still absent (same standing RED as body_lower_type_decl). +// CONTROL: a type declaration name is an edge label, not an atom. +// Record skip: gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. test fn the_record_type_declaration_is_absent_from_the_tree_today() -> Bool { !present_anywhere(lexeme: "Xl0Wrapper") } -// WHAT WAS MEASURED: the fixture contains `type Xl0Wrapper { fld: Xl0FieldOnly }`, ingest ACCEPTS, -// and Xl0UserType is an atom as a parameter type. The record name, field name, and field type are -// all absent from atoms and edge labels. Graft skips units that carry dag_surface_field_decl_block -// (namespace_graft_type_decl_named_edge). These stay absence measurements until the body-lowering -// follow-up after gunbc#11582. +// Fixture: type Xl0Wrapper { fld: Xl0FieldOnly }. Class +// gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. test fn the_bare_field_type_is_absent_from_the_tree_today() -> Bool { !present_anywhere(lexeme: "Xl0FieldOnly") } diff --git a/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag b/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag index 60fc8a9fc93..159dca32b20 100644 --- a/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag +++ b/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag @@ -270,9 +270,9 @@ test fn a_real_loaded_compilation_is_reachable_without_reading_the_live_tree_hol // obsoletes while the discriminating evidence STAYS ENROLLED, and an // expecting-red probe that greens flips to a permanent regression control rather // than retiring. The obligation is carried by that clause, not by this comment. -// THIS ROW GOES RED WHEN RECORD REHOME LANDS (body_lower_type_decl / gunbc#11582 -// follow-up), not when flatten-drop closed for coproduct/alias. Graft still -// returns Absent on dag_surface_field_decl_block; CrossFileMarker is that shape. +// THIS ROW GOES RED WHEN RECORD REHOME LANDS. Class +// gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. +// DESIGN 4b(4): expecting-red that greens flips to a permanent regression control. test fn a_record_type_declaration_does_not_reach_the_index_while_a_fn_does_holds() -> Bool { let fn_is_indexed = lcr_indexed(dotted: "lcr.producer.producer_anchor") let type_is_not_indexed = lcr_indexed(dotted: "lcr.producer.CrossFileMarker") == false From 2e1877fec3a09b01f7760999d64a816593c769d4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 16:29:43 +0000 Subject: [PATCH 47/56] State the field_decl_block graft skip as the whole skipped population. The skip is locate of that production, so fielded variants take the same arm as records. Nullary coproduct Named is a different class and does not stand for this row. Co-authored-by: Cursor --- ...pe_decl_field_block_not_named_at_graft.dag | 24 ------------------- ...cl_field_decl_block_not_named_at_graft.dag | 24 +++++++++++++++++++ src/v2/compiler/namespace_graft.dag | 12 ++++++---- .../claim/declaration_graft_assemble_test.dag | 3 ++- .../field_type_tree_presence_test.dag | 5 ++-- .../loaded_carrier_receipts_test.dag | 2 +- 6 files changed, 38 insertions(+), 32 deletions(-) delete mode 100644 dag/gunbc/recurring_failure_mode/record_type_decl_field_block_not_named_at_graft.dag create mode 100644 dag/gunbc/recurring_failure_mode/type_decl_field_decl_block_not_named_at_graft.dag diff --git a/dag/gunbc/recurring_failure_mode/record_type_decl_field_block_not_named_at_graft.dag b/dag/gunbc/recurring_failure_mode/record_type_decl_field_block_not_named_at_graft.dag deleted file mode 100644 index 2298f8476f2..00000000000 --- a/dag/gunbc/recurring_failure_mode/record_type_decl_field_block_not_named_at_graft.dag +++ /dev/null @@ -1,24 +0,0 @@ -module gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft - -import std.types { NonEmptyStr } -import gunbc.recurring_failure_mode { RecurringFailureMode } - -data record_type_decl_field_block_not_named_at_graft: RecurringFailureMode = RecurringFailureMode { - identity: "record_type_decl_field_block_not_named_at_graft" as NonEmptyStr, - - receipts: [ - "**a type_decl whose parse tree still carries dag_surface_field_decl_block is not rehomed onto a Named graft edge** (INVALID STATE: `type Rec { n: Int }` stays a residual reconstructed shell, so symbol_index_fill does not see Rec as containment the way it sees `type Flag = On | Off` or a fn. Coproduct and alias units are Named in namespace_graft_type_decl_named_edge; the field_decl_block skip is the record half of the same flatten-drop class.)", - - "HARM: record assemble refuses (standing RED), and a record name is not a first-class module member. Residual reconstruct keeps the unit beside a sibling Named graft so the shell is not silently deleted; that is mitigation of drop, not Named rehome.", - - "DISCRIMINATOR: RED while namespace_graft_type_decl_named_edge returns Absent whenever parse_subtree_locate_production finds dag_surface_field_decl_block under the unit. GREEN when body_lower_type_decl rehomes that form onto Named containment and the skip is deleted.", - - "EVIDENCE ENROLLED: v2.test.claim.declaration_graft_assemble declaration_graft_record_type_only_does_not_accept_today and declaration_graft_record_construct_does_not_accept_today (`!assemble_accepts`). They flip to a permanent regression control when record assemble accepts, they do not retire. v2.test.claim.declaration_graft_assemble declaration_graft_coproduct_beside_record_does_not_erase_rec refuses to treat Rejected as Rec survival.", - - "RUNG FOUND AT: 1, mitigatable -- residual reconstruct contains the drop; the invalid Named-absence is still writable. CEILING: 3 once body_lower_type_decl is the Named home for field_decl_block type_decl, so graft need not skip.", - - "NEXT-RUNG TRIGGER: body_lower_type_decl rehomes a field_decl_block type_decl onto a Named containment edge (the capability tracked as gunbc#11582), and namespace_graft_type_decl_named_edge no longer returns Absent on that production. Nothing smaller retires this row: landing 11582 without that rehome, or deleting the skip without the lowering, is not the climb.", - ], - - evidence: [], -} diff --git a/dag/gunbc/recurring_failure_mode/type_decl_field_decl_block_not_named_at_graft.dag b/dag/gunbc/recurring_failure_mode/type_decl_field_decl_block_not_named_at_graft.dag new file mode 100644 index 00000000000..75af422ba8c --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/type_decl_field_decl_block_not_named_at_graft.dag @@ -0,0 +1,24 @@ +module gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data type_decl_field_decl_block_not_named_at_graft: RecurringFailureMode = RecurringFailureMode { + identity: "type_decl_field_decl_block_not_named_at_graft" as NonEmptyStr, + + receipts: [ + "**a type_decl whose parse tree still carries dag_surface_field_decl_block is not rehomed onto a Named graft edge** (INVALID STATE: namespace_graft_type_decl_named_edge returns Absent whenever parse_subtree_locate_production finds that production under the unit. The skip is that production, not the record spelling. dag_grammar_type_variant_expr puts an optional field_decl_block on every variant, and graft runs after sugar, so the same arm covers `type Rec { n: Int }`, `type Foo = Bar {}`, and `type Box = Leaf | Cell { n: Int }`. Nullary coproducts without a field block (`type Flag = On | Off`) and type_decl units that locate no field_decl_block are Named. Narrowing the skip by type_alias_rhs / `=` is not available: those discriminators rehome records.)", + + "HARM: a fielded type_decl name is not a first-class module member. Residual reconstruct keeps the unit beside a sibling Named graft so the shell is not silently deleted; that is mitigation of drop, not Named rehome. Record assemble refuses today. Fielded-variant assemble is in the skipped population and is not enrolled as a Named-green path.", + + "DISCRIMINATOR: RED while the skip remains `locate field_decl_block → Absent`. GREEN when body_lower_type_decl rehomes every field_decl_block type_decl onto Named containment and the skip is deleted. Nullary-coproduct Named is a different, already-green population and does not stand for this class.", + + "EVIDENCE ENROLLED: v2.test.claim.declaration_graft_assemble declaration_graft_record_type_only_does_not_accept_today and declaration_graft_record_construct_does_not_accept_today (`!assemble_accepts`) cover the record spelling of the skip. declaration_graft_coproduct_beside_record_does_not_erase_rec refuses to treat Rejected as Rec survival. No enrolled witness asserts Named for a fielded variant. They flip to a permanent regression control when that spelling accepts, they do not retire.", + + "RUNG FOUND AT: 1, mitigatable -- residual reconstruct contains the drop; the invalid Named-absence is still writable. CEILING: 3 once body_lower_type_decl is the Named home for field_decl_block type_decl, so graft need not skip.", + + "NEXT-RUNG TRIGGER: body_lower_type_decl rehomes a field_decl_block type_decl onto a Named containment edge (the capability tracked as gunbc#11582), and namespace_graft_type_decl_named_edge no longer returns Absent on that production. Nothing smaller retires this row: landing 11582 without that rehome, or deleting the skip without the lowering, is not the climb. Closing only the record spelling while fielded variants still skip is not the climb.", + ], + + evidence: [], +} diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 6dc16e6e34b..9ff9b7a1660 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -323,8 +323,11 @@ fn namespace_graft_unit_captured(unit: Node) -> Node? { parse_production_captured_child_optional(node: unit) } -// Record field_decl_block units are residual, not Named; class -// gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. +// Any type_decl that still locates dag_surface_field_decl_block is residual, +// not Named: records and fielded variants (optional field_decl_block on +// dag_grammar_type_variant_expr; sugar leaves those blocks in the tree). +// Narrowing by type_alias_rhs / `=` rehomes records. Class +// gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft. fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { match parse_subtree_locate_production(root: unit, emitted: ^dag_surface_field_decl_block) { Present { value: _ } => optional_absent() @@ -405,8 +408,9 @@ fn namespace_graft_residual_decl_units(units: List) -> List { } // Flatten keeps Named members that are not parse machinery. A Conj that still -// carries field_decl_block is the record form (residual reconstruct), not a -// flatten member. Atom targets stay: the graft-shape fixture is two Named atoms. +// carries field_decl_block is the same skip as type_decl_named_edge (record or +// fielded variant), not a flatten member. Atom targets stay: the graft-shape +// fixture is two Named atoms. fn namespace_graft_keep_flattened_member_edge( productions: List, edge: Edge diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index 36e1d49ea5f..20ffd280bd3 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -182,7 +182,8 @@ test fn declaration_graft_coproduct_beside_record_does_not_erase_rec() -> Bool { } } -// Record assemble standing RED: gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. +// Record assemble standing RED (one spelling of the field_decl_block skip): +// gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft. // Not asserted via !named_reachable: that is true on every Rejected. test fn declaration_graft_record_type_only_does_not_accept_today() -> Bool { !declaration_graft_assemble_accepts(src: src_g_type_only) diff --git a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag index 36821aaff60..620406f6833 100644 --- a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag +++ b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag @@ -114,13 +114,14 @@ test fn control_a_parameter_type_name_is_an_atom_in_the_tree_holds() -> Bool { } // CONTROL: a type declaration name is an edge label, not an atom. -// Record skip: gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. +// field_decl_block skip (record spelling): +// gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft. test fn the_record_type_declaration_is_absent_from_the_tree_today() -> Bool { !present_anywhere(lexeme: "Xl0Wrapper") } // Fixture: type Xl0Wrapper { fld: Xl0FieldOnly }. Class -// gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. +// gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft. test fn the_bare_field_type_is_absent_from_the_tree_today() -> Bool { !present_anywhere(lexeme: "Xl0FieldOnly") } diff --git a/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag b/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag index 159dca32b20..77714d0e5d5 100644 --- a/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag +++ b/src/v2/test/claim/provenance/loaded_carrier_receipts_test.dag @@ -271,7 +271,7 @@ test fn a_real_loaded_compilation_is_reachable_without_reading_the_live_tree_hol // expecting-red probe that greens flips to a permanent regression control rather // than retiring. The obligation is carried by that clause, not by this comment. // THIS ROW GOES RED WHEN RECORD REHOME LANDS. Class -// gunbc.recurring_failure_mode.record_type_decl_field_block_not_named_at_graft. +// gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft. // DESIGN 4b(4): expecting-red that greens flips to a permanent regression control. test fn a_record_type_declaration_does_not_reach_the_index_while_a_fn_does_holds() -> Bool { let fn_is_indexed = lcr_indexed(dotted: "lcr.producer.producer_anchor") From dd65f7d356a7b4460951bdb038e9bf45518d3b69 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 16:45:04 +0000 Subject: [PATCH 48/56] Drop unused Arrow import from namespace_graft. Leftover from the reverted flatten-by-Arrow/Disj pass. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 1 - 1 file changed, 1 deletion(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 9ff9b7a1660..6d690d34980 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -17,7 +17,6 @@ import v2.std.diagnostic { } import v2.std.logic { Bool } import v2.std.node { - Arrow, Atom, Cardinality, Conj, From 60d703bf87ac2decbec9d91e26ca1f500043a691 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 17:00:12 +0000 Subject: [PATCH 49/56] Partition graft units into Named edges and residuals in one fold. body_edges_for_graft was walking each type_decl twice: once for Named edges and again to collect residuals. One type_decl | data_decl dispatch classifies both. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 89 ++++++++++++++++------------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 6d690d34980..f46e90eca20 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -361,49 +361,59 @@ fn namespace_graft_data_decl_named_edge(unit: Node) -> Edge? { } } -fn namespace_graft_named_edge_from_unit(unit: Node) -> Edge? { - match parse_production_emitted_identity_optional(node: unit) { - Present { value: id } => - match id == ^dag_surface_type_decl { - true => namespace_graft_type_decl_named_edge(unit: unit) - false => - match id == ^dag_surface_data_decl { - true => namespace_graft_data_decl_named_edge(unit: unit) - false => optional_absent() - } +type NamespaceGraftDeclPartition { + named_edges: List + residual_units: List +} + +fn namespace_graft_partition_from_named_opt( + acc: NamespaceGraftDeclPartition, + unit: Node, + named: Edge? +) -> NamespaceGraftDeclPartition { + match named { + Present { value: edge } => + NamespaceGraftDeclPartition { + named_edges: list_snoc_item(xs: acc.named_edges, item: edge), + residual_units: acc.residual_units + } + Absent => + NamespaceGraftDeclPartition { + named_edges: acc.named_edges, + residual_units: list_snoc_item(xs: acc.residual_units, item: unit) } - Absent => optional_absent() - } -} - -fn namespace_graft_named_edges_from_units(units: List) -> List { - fold(units, init: [], f: fn(acc, unit) { - match namespace_graft_named_edge_from_unit(unit: unit) { - Present { value: edge } => list_snoc_item(xs: acc, item: edge) - Absent => acc - } - }) -} - -fn namespace_graft_is_type_or_data_decl_unit(unit: Node) -> Bool { - match parse_production_emitted_identity_optional(node: unit) { - Present { value: id } => - (id == ^dag_surface_type_decl) || (id == ^dag_surface_data_decl) - Absent => false } } -fn namespace_graft_residual_decl_units(units: List) -> List { - fold(units, init: [], f: fn(acc, unit) { - if namespace_graft_is_type_or_data_decl_unit(unit: unit) { - match namespace_graft_named_edge_from_unit(unit: unit) { - Present { value: _ } => acc - Absent => list_snoc_item(xs: acc, item: unit) +fn namespace_graft_partition_decl_units(units: List) -> NamespaceGraftDeclPartition { + fold( + units, + init: NamespaceGraftDeclPartition { named_edges: [], residual_units: [] }, + f: fn(acc, unit) { + match parse_production_emitted_identity_optional(node: unit) { + Present { value: id } => + match id == ^dag_surface_type_decl { + true => + namespace_graft_partition_from_named_opt( + acc: acc, + unit: unit, + named: namespace_graft_type_decl_named_edge(unit: unit) + ) + false => + match id == ^dag_surface_data_decl { + true => + namespace_graft_partition_from_named_opt( + acc: acc, + unit: unit, + named: namespace_graft_data_decl_named_edge(unit: unit) + ) + false => acc + } + } + Absent => acc } - } else { - acc } - }) + ) } // Flatten keeps Named members that are not parse machinery. A Conj that still @@ -483,7 +493,8 @@ fn namespace_graft_body_edges_for_graft(root: Node) -> List { let working = namespace_graft_unwrap_module_working_root(root: root) let productions = dag_grammar_root().productions let units = namespace_graft_collect_unit_nodes(node: working, productions: productions) - let from_decls = namespace_graft_named_edges_from_units(units: units) + let partitioned = namespace_graft_partition_decl_units(units: units) + let from_decls = partitioned.named_edges let flattened = namespace_graft_flatten_minus_decl_names( from_decls: from_decls, flattened: namespace_graft_flattened_member_edges( @@ -492,7 +503,7 @@ fn namespace_graft_body_edges_for_graft(root: Node) -> List { ) ) let combined = concat(from_decls, flattened) - let residuals = namespace_graft_residual_decl_units(units: units) + let residuals = partitioned.residual_units if count(combined) == 0 { if count(units) == 0 { [] From 485e32839cdba3d845c44dccfa80a15c2707e7e7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 17:14:39 +0000 Subject: [PATCH 50/56] Assert XL-0 record names survive residual reconstruct, not tree drop. field_decl_block type_decls are skipped from Named and kept as residual shells, so Xl0Wrapper, Xl0FieldOnly, and fld are present on an Accepted ingest. Co-authored-by: Cursor --- .../field_type_tree_presence_test.dag | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag index 620406f6833..bf8b163f6e6 100644 --- a/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag +++ b/src/v2/test/claim/namespace_xl0/field_type_tree_presence_test.dag @@ -75,11 +75,8 @@ fn atom_present(lexeme: String) -> Bool { any(fixture_atom_lexemes(), s => s == lexeme) } -// ATOMS ARE NOT THE ONLY PLACE A NAME LIVES, AND MY OWN CONTROL PROVED IT. The Xl0Wrapper row -// below fails because a type DECLARATION name is an EDGE LABEL, never a node -- which is the very -// fact this collector's design rests on. So an atom-only search cannot establish absence: it -// establishes "not an atom". This collects the other half, every Named edge label in the tree, so -// "absent" can mean absent from BOTH representations rather than from the one I happened to check. +// A name may be an Atom (StampLexeme under residual reconstruct) or a Named edge. +// present_anywhere is both, so residual survival is not "not an atom". fn fixture_edge_label_lexemes() -> FreeMonoid { match call_argument_fixture_normalized() { Accepted { value: roots, diagnostics: _ } => @@ -113,21 +110,20 @@ test fn control_a_parameter_type_name_is_an_atom_in_the_tree_holds() -> Bool { atom_present(lexeme: "Xl0UserType") } -// CONTROL: a type declaration name is an edge label, not an atom. -// field_decl_block skip (record spelling): +// Residual reconstruct keeps the field_decl_block type_decl unit; these lexemes +// are StampLexeme atoms under that shell. Combined is nonempty (fns/data in the +// XL-0 fixture), so the residual branch is taken. Rejected yields no lexemes, so +// presence is not true on a refused ingest. Named skip is enrolled on +// declaration_graft !assemble_accepts, not as tree absence. // gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft. -test fn the_record_type_declaration_is_absent_from_the_tree_today() -> Bool { - !present_anywhere(lexeme: "Xl0Wrapper") +test fn the_record_type_declaration_survives_in_the_tree_today() -> Bool { + present_anywhere(lexeme: "Xl0Wrapper") } -// Fixture: type Xl0Wrapper { fld: Xl0FieldOnly }. Class -// gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft. -test fn the_bare_field_type_is_absent_from_the_tree_today() -> Bool { - !present_anywhere(lexeme: "Xl0FieldOnly") +test fn the_bare_field_type_survives_in_the_tree_today() -> Bool { + present_anywhere(lexeme: "Xl0FieldOnly") } -// The field NAME itself, as a third data point: if `fld` is a label but its TYPE is nowhere, the -// declaration survived and only the type reference inside it was dropped. -test fn the_field_name_is_absent_from_the_tree_today() -> Bool { - !present_anywhere(lexeme: "fld") +test fn the_field_name_survives_in_the_tree_today() -> Bool { + present_anywhere(lexeme: "fld") } From 1990e134b5bdade70d1e99da0d2e5e47862b546e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 18:32:17 +0000 Subject: [PATCH 51/56] Do not flatten through type/data units; treat residual field names as present. Partition already owns those decls. Walking them in flatten promoted record fields to module members (native_decl collision, assemble unique-Named). Absence probes that assumed the silent drop are now survival, matching XL-0. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 22 ++++++++++++++++--- src/v2/lens/module_graph.dag | 2 +- .../production_ingest_test.dag | 8 +++---- .../call_argument_mention_survival_test.dag | 7 ++++-- ...e_derived_graph_production_ingest_test.dag | 16 +++++++------- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index f46e90eca20..22ea7fea18b 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -126,7 +126,18 @@ fn namespace_graft_is_transparent_sequence_spine(node: Node) -> Bool { } } +fn namespace_graft_is_partitioned_decl_unit(node: Node) -> Bool { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => + (id == ^dag_surface_type_decl) || (id == ^dag_surface_data_decl) + Absent => false + } +} + fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List { + if namespace_graft_is_partitioned_decl_unit(node: edge.target) { + acc + } else { match edge.label { Named { name: sym } => if namespace_graft_is_metadata_edge(name: sym) { @@ -149,12 +160,17 @@ fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List list_snoc_item(xs: acc, item: edge) } } + } } fn namespace_graft_collect_body_edges_from_node(node: Node) -> List { - fold(node.children, init: [], f: fn(acc, edge) { - namespace_graft_collect_body_edge(acc: acc, edge: edge) - }) + if namespace_graft_is_partitioned_decl_unit(node: node) { + [] + } else { + fold(node.children, init: [], f: fn(acc, edge) { + namespace_graft_collect_body_edge(acc: acc, edge: edge) + }) + } } fn namespace_graft_grammar_expr_nonterminal_productions(expr: GrammarExpr) -> List { diff --git a/src/v2/lens/module_graph.dag b/src/v2/lens/module_graph.dag index 17643c9bff2..7c3c369f4f7 100644 --- a/src/v2/lens/module_graph.dag +++ b/src/v2/lens/module_graph.dag @@ -892,6 +892,6 @@ data reference_derived_call_contents_frontier: DissolutionCondition = unbound_di data reference_derived_qualified_mention_frontier: DissolutionCondition = unbound_dissolution(description: "Qualified / dotted reference in any position waits on the dotted-reference capability. Type-shaped mentions additionally wait on filled-symbol-index Named edges (sleek-lynx-500 case 4: v2.test.claim.namespace.cross_module_resolve_witness, head reason resolve_reason_unbound_symbol). Instruments: v2.test.claim.reference_derived_graph_production_ingest production_route_reference_only_plain_value_is_missing_today_holds, production_route_both_plain_value_is_missing_today_holds, production_route_type_position_qualified_site_is_missing_today_holds, production_route_type_position_qualified_edge_is_missing_today_holds; v2.test.claim.reference_derived_graph_call_arg_tree type_position_only_is_missing_from_parse_tree_today_holds, type_position_only_is_missing_from_normalized_tree_today_holds.") -data reference_derived_bare_field_type_frontier: DissolutionCondition = unbound_dissolution(description: "Bare FIELD type site is absent at the collector; tree-side census cause unverified. No collector trigger until those separate. Instruments: v2.test.claim.reference_derived_graph_production_ingest production_route_bare_field_type_site_is_missing_today_holds, production_tree_field_fixture_wrapper_name_unfindable_today_holds, production_tree_field_fixture_field_name_unfindable_today_holds, production_tree_field_fixture_field_type_unfindable_today_holds. Positive controls: production_route_bare_param_type_in_field_fixture_is_present_holds, production_tree_field_fixture_param_type_atom_is_present_holds, production_route_bare_local_type_position_site_is_present_holds.") +data reference_derived_bare_field_type_frontier: DissolutionCondition = unbound_dissolution(description: "Bare field type spelling survives residual reconstruct (not a silent drop). Named skip of field_decl_block type_decl is gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft. Instruments: v2.test.claim.reference_derived_graph_production_ingest production_route_bare_field_type_site_is_present_holds, production_tree_field_fixture_wrapper_name_survives_today_holds, production_tree_field_fixture_field_name_survives_today_holds, production_tree_field_fixture_field_type_survives_today_holds. Positive controls: production_route_bare_param_type_in_field_fixture_is_present_holds, production_tree_field_fixture_param_type_atom_is_present_holds, production_route_bare_local_type_position_site_is_present_holds.") data reference_derived_imported_bare_parameter_edge_frontier: DissolutionCondition = unbound_dissolution(description: "Imported bare parameter-type site is present and still yields no edge. Instruments: v2.test.claim.reference_derived_graph_production_ingest production_route_type_imported_bare_site_is_present_holds, production_route_type_imported_site_does_not_yield_edge_today_holds.") diff --git a/src/v2/test/claim/declaring_identity_spelling/production_ingest_test.dag b/src/v2/test/claim/declaring_identity_spelling/production_ingest_test.dag index 92fb97915d4..7466ac7bc7d 100644 --- a/src/v2/test/claim/declaring_identity_spelling/production_ingest_test.dag +++ b/src/v2/test/claim/declaring_identity_spelling/production_ingest_test.dag @@ -494,14 +494,14 @@ fn field_vs_parameter_census() -> Optional { ) } -// SAME ingest, BARE names, no dots. Parameter type reaches; field type does not. -// Qualification is not what the field miss measures. -test fn a_bare_parameter_type_reaches_while_a_bare_field_type_does_not() -> Bool { +// SAME ingest, BARE names, no dots. Residual reconstruct keeps the field type +// spelling in the census the way the tree keeps it. +test fn a_bare_parameter_type_reaches_and_a_bare_field_type_does_too() -> Bool { match field_vs_parameter_census() { Present { value: census } => census_unreadable_roots_empty(census: census) && authored_is(census: census, spelling: "ParamOnly") - && !authored_is(census: census, spelling: "FieldOnly") + && authored_is(census: census, spelling: "FieldOnly") Absent => false } } diff --git a/src/v2/test/claim/namespace_xl0/call_argument_mention_survival_test.dag b/src/v2/test/claim/namespace_xl0/call_argument_mention_survival_test.dag index 3eb16fd6c8e..7d0778c23ac 100644 --- a/src/v2/test/claim/namespace_xl0/call_argument_mention_survival_test.dag +++ b/src/v2/test/claim/namespace_xl0/call_argument_mention_survival_test.dag @@ -393,7 +393,10 @@ test fn a_type_name_in_parameter_position_reaches_the_collector_holds() -> Bool // this fixture shows qualification is not what is doing the work -- a BARE field type is missing // too. A type-position claim that does not say WHICH type position is therefore too coarse: the // same bare name reaches from a parameter and does not reach from a field. -test fn a_bare_field_type_is_absent_from_the_denominator_today() -> Bool { - !sites_mention(sites: fixture_sites(), spelling: "Xl0FieldOnly") +// Residual reconstruct keeps the field_decl_block unit, so a bare field type +// is in the tree the collector walks. Parameter type still reaches (control). +// gunbc.recurring_failure_mode.type_decl_field_decl_block_not_named_at_graft. +test fn a_bare_field_type_survives_in_the_denominator_today() -> Bool { + sites_mention(sites: fixture_sites(), spelling: "Xl0FieldOnly") && sites_mention(sites: fixture_sites(), spelling: "Xl0UserType") } diff --git a/src/v2/test/claim/reference_derived_graph_production_ingest_test.dag b/src/v2/test/claim/reference_derived_graph_production_ingest_test.dag index 2e69b09304c..deade781ef5 100644 --- a/src/v2/test/claim/reference_derived_graph_production_ingest_test.dag +++ b/src/v2/test/claim/reference_derived_graph_production_ingest_test.dag @@ -213,24 +213,24 @@ test fn production_route_bare_param_type_in_field_fixture_is_present_holds() -> site_spelling_count(path: consumer_field_bare_local_path, needle: local_param_marker_bare) > 0 } -test fn production_route_bare_field_type_site_is_missing_today_holds() -> Bool { - site_spelling_count(path: consumer_field_bare_local_path, needle: local_field_only_bare) == 0 +test fn production_route_bare_field_type_site_is_present_holds() -> Bool { + site_spelling_count(path: consumer_field_bare_local_path, needle: local_field_only_bare) > 0 } test fn production_tree_field_fixture_param_type_atom_is_present_holds() -> Bool { tree_atom_count(path: consumer_field_bare_local_path, needle: local_param_marker_sym) > 0 } -test fn production_tree_field_fixture_wrapper_name_unfindable_today_holds() -> Bool { - tree_atom_and_label_absent(path: consumer_field_bare_local_path, needle: local_wrapper_sym) +test fn production_tree_field_fixture_wrapper_name_survives_today_holds() -> Bool { + !tree_atom_and_label_absent(path: consumer_field_bare_local_path, needle: local_wrapper_sym) } -test fn production_tree_field_fixture_field_name_unfindable_today_holds() -> Bool { - tree_atom_and_label_absent(path: consumer_field_bare_local_path, needle: local_wrapper_field_name_sym) +test fn production_tree_field_fixture_field_name_survives_today_holds() -> Bool { + !tree_atom_and_label_absent(path: consumer_field_bare_local_path, needle: local_wrapper_field_name_sym) } -test fn production_tree_field_fixture_field_type_unfindable_today_holds() -> Bool { - tree_atom_and_label_absent(path: consumer_field_bare_local_path, needle: local_field_only_sym) +test fn production_tree_field_fixture_field_type_survives_today_holds() -> Bool { + !tree_atom_and_label_absent(path: consumer_field_bare_local_path, needle: local_field_only_sym) } test fn production_route_callee_with_no_arguments_is_missing_today_holds() -> Bool { From 44985933d5323a00ad287878067abb3cd60610b5 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 18 Sep 2026 18:46:14 +0000 Subject: [PATCH 52/56] Account for every top-level unit in the graft partition; locate type RHS. Units that were neither type nor data vanished when flatten dropped them and combined was nonempty. Each unit is now Named or residual. Flatten does not walk top-level items. Type Named targets type_alias_rhs, not the first Disj. Co-authored-by: Cursor --- src/v2/compiler/namespace_graft.dag | 117 +++++++++++------- .../claim/declaration_graft_assemble_test.dag | 13 ++ 2 files changed, 85 insertions(+), 45 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 22ea7fea18b..0d939c94f04 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -20,14 +20,12 @@ import v2.std.node { Atom, Cardinality, Conj, - Disj, Edge, Named, Node, Positional, Symbol, TypeNode, - node_subtree_nodes, } import v2.std.qualified_name { QualifiedName } import v2.std.compilers.lexing { symbol_lexeme } @@ -126,16 +124,21 @@ fn namespace_graft_is_transparent_sequence_spine(node: Node) -> Bool { } } -fn namespace_graft_is_partitioned_decl_unit(node: Node) -> Bool { +fn namespace_graft_is_top_level_item_unit_node(node: Node) -> Bool { match parse_production_emitted_identity_optional(node: node) { Present { value: id } => - (id == ^dag_surface_type_decl) || (id == ^dag_surface_data_decl) + (id == ^dag_surface_import_decl) + || (id == ^dag_surface_alias_decl) + || (id == ^dag_surface_type_decl) + || (id == ^dag_surface_data_decl) + || (id == ^dag_surface_fn_decl) + || (id == ^dag_surface_test_fn_decl) Absent => false } } fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List { - if namespace_graft_is_partitioned_decl_unit(node: edge.target) { + if namespace_graft_is_top_level_item_unit_node(node: edge.target) { acc } else { match edge.label { @@ -164,7 +167,7 @@ fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List } fn namespace_graft_collect_body_edges_from_node(node: Node) -> List { - if namespace_graft_is_partitioned_decl_unit(node: node) { + if namespace_graft_is_top_level_item_unit_node(node: node) { [] } else { fold(node.children, init: [], f: fn(acc, edge) { @@ -317,27 +320,21 @@ fn namespace_graft_is_machinery_symbol(productions: List, sym || namespace_graft_symbol_is_grammar_machinery(productions: productions, sym: sym) } -fn namespace_graft_type_decl_target(root: Node) -> Node { - fold( - node_subtree_nodes(root: root), - init: root, - f: fn(acc, n) { - match acc.kind { - TypeNode { connective: Disj } => acc - _ => - match n.kind { - TypeNode { connective: Disj } => n - _ => acc - } - } - } - ) -} - fn namespace_graft_unit_captured(unit: Node) -> Node? { parse_production_captured_child_optional(node: unit) } +fn namespace_graft_type_decl_target(root: Node) -> Node { + match parse_subtree_locate_production(root: root, emitted: ^dag_surface_type_alias_rhs) { + Present { value: rhs } => rhs + Absent => + match namespace_graft_unit_captured(unit: root) { + Present { value: captured } => captured + Absent => root + } + } +} + // Any type_decl that still locates dag_surface_field_decl_block is residual, // not Named: records and fielded variants (optional field_decl_block on // dag_grammar_type_variant_expr; sugar leaves those blocks in the tree). @@ -360,16 +357,16 @@ fn namespace_graft_type_decl_named_edge(unit: Node) -> Edge? { } } -fn namespace_graft_data_decl_named_edge(unit: Node) -> Edge? { +fn namespace_graft_kw_ident_named_edge(unit: Node) -> Edge? { match namespace_graft_unit_captured(unit: unit) { Absent => optional_absent() Present { value: captured } => match dag_surface_kw_then_ident_from_captured(captured: captured) { Absent => optional_absent() - Present { value: data_name } => + Present { value: name } => optional_present( value: Edge { - label: Named { name: data_name }, + label: Named { name: name }, target: captured } ) @@ -377,6 +374,21 @@ fn namespace_graft_data_decl_named_edge(unit: Node) -> Edge? { } } +fn namespace_graft_data_decl_named_edge(unit: Node) -> Edge? { + namespace_graft_kw_ident_named_edge(unit: unit) +} + +fn namespace_graft_fn_decl_named_edge(unit: Node) -> Edge? { + namespace_graft_kw_ident_named_edge(unit: unit) +} + +fn namespace_graft_test_fn_named_edge(unit: Node) -> Edge? { + match parse_subtree_locate_production(root: unit, emitted: ^dag_surface_fn_decl) { + Present { value: fn_unit } => namespace_graft_fn_decl_named_edge(unit: fn_unit) + Absent => optional_absent() + } +} + type NamespaceGraftDeclPartition { named_edges: List residual_units: List @@ -401,6 +413,29 @@ fn namespace_graft_partition_from_named_opt( } } +fn namespace_graft_named_edge_for_unit(unit: Node, id: Symbol) -> Edge? { + match id == ^dag_surface_type_decl { + true => namespace_graft_type_decl_named_edge(unit: unit) + false => + match id == ^dag_surface_data_decl { + true => namespace_graft_data_decl_named_edge(unit: unit) + false => + match id == ^dag_surface_fn_decl { + true => namespace_graft_fn_decl_named_edge(unit: unit) + false => + match id == ^dag_surface_test_fn_decl { + true => namespace_graft_test_fn_named_edge(unit: unit) + false => + match id == ^dag_surface_alias_decl { + true => namespace_graft_kw_ident_named_edge(unit: unit) + false => optional_absent() + } + } + } + } + } +} + fn namespace_graft_partition_decl_units(units: List) -> NamespaceGraftDeclPartition { fold( units, @@ -408,25 +443,17 @@ fn namespace_graft_partition_decl_units(units: List) -> NamespaceGraftDecl f: fn(acc, unit) { match parse_production_emitted_identity_optional(node: unit) { Present { value: id } => - match id == ^dag_surface_type_decl { - true => - namespace_graft_partition_from_named_opt( - acc: acc, - unit: unit, - named: namespace_graft_type_decl_named_edge(unit: unit) - ) - false => - match id == ^dag_surface_data_decl { - true => - namespace_graft_partition_from_named_opt( - acc: acc, - unit: unit, - named: namespace_graft_data_decl_named_edge(unit: unit) - ) - false => acc - } - } - Absent => acc + namespace_graft_partition_from_named_opt( + acc: acc, + unit: unit, + named: namespace_graft_named_edge_for_unit(unit: unit, id: id) + ) + Absent => + namespace_graft_partition_from_named_opt( + acc: acc, + unit: unit, + named: optional_absent() + ) } } ) diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index 20ffd280bd3..767394a0351 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -131,6 +131,8 @@ data src_g_type_only: String = "module p\n\ntype Rec { n: Int }\n" data src_dag_user_fn: String = "module p\n\nfn dag_user(x: Int) -> Int { x }\n" data src_flag_and_rec: String = "module p\n\ntype Flag = On | Off\ntype Rec { n: Int }\n" +data src_flag_and_fn: String = "module p\n\ntype Flag = On | Off\nfn f(x: Int) -> Int { x }\n" + test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) } @@ -171,6 +173,17 @@ test fn declaration_graft_dag_prefix_user_fn_is_named() -> Bool { declaration_graft_named_reachable(src: src_dag_user_fn, lexeme: "dag_user") } +// Partial graft must not drop a sibling fn: Flag Named and f Named on the same +// Accepted assemble. Rejected is not agreement. +test fn declaration_graft_coproduct_beside_fn_keeps_both() -> Bool { + match declaration_graft_assemble_for(src: src_flag_and_fn) { + Rejected { diagnostics: _ } => false + Accepted { value: root, diagnostics: _ } => + declaration_graft_label_present(root: root, lexeme: "Flag") + && declaration_graft_label_present(root: root, lexeme: "f") + } +} + // Fail-closed on Rec erasure beside a graftable coproduct: Rec must remain in a // successful assemble. Rejected is not agreement — it does not prove Rec survived. test fn declaration_graft_coproduct_beside_record_does_not_erase_rec() -> Bool { From 0516a6d33833e15f91e9eaf57b8b233d0a5fd276 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 19 Sep 2026 04:00:53 +0000 Subject: [PATCH 53/56] =?UTF-8?q?WIP:=20Declaration=20grafting:=20type/dat?= =?UTF-8?q?a=20decls=20never=20become=20Named=20edges=20=E2=80=94=20the=20?= =?UTF-8?q?emi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/v2/compiler/body_lowering_fold.dag | 53 ++------------------------ src/v2/compiler/namespace_graft.dag | 46 +++++++++++----------- 2 files changed, 26 insertions(+), 73 deletions(-) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 455e6cfabb8..7accd488004 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -33,6 +33,7 @@ import v2.extdeps.languages.dag { dag_resolve_preserve_module_metadata_subtree, parse_production_captured_child_optional, parse_production_emitted_identity_optional, + dag_surface_kw_then_ident_from_captured, qualified_name_from_module_node, } import v2.std.algebra { fold_list, list_append, list_snoc_item, length, zip_eq } @@ -217,30 +218,6 @@ fn body_lower_seq_child(node: Node, name: Symbol) -> Optional { } } -fn body_lower_fn_decl_ident_from_captured(captured: Node) -> Optional { - match body_lower_seq_child(node: captured, name: ^grammar_sequence_right_node_projection) { - Absent => Absent - Present { value: after_kw_fn } => - match body_lower_seq_child(node: after_kw_fn, name: ^grammar_sequence_left_node_projection) { - Absent => Absent - Present { value: ident_node } => - match dag_binding_branded_atom_identity(node: ident_node) { - Present { value: binding } => Present { value: binding } - Absent => - match node_atom_identity_optional(node: ident_node) { - Present { value: sym } => - if dag_lex_token_symbol(sym: sym) { - optional_absent() - } else { - optional_present(value: sym) - } - Absent => optional_absent() - } - } - } - } -} - fn body_lower_arrow_domain_named_bindings(root: Node) -> List { match root.kind { TypeNode { connective: Conj } => @@ -2114,7 +2091,7 @@ fn body_lower_fn_decl_to_arrow(shell: Node) -> Outcome { match body_lower_return_type_from_fn_captured(captured: captured) { Absent => body_lower_wrapper_retained_shell(shell: shell) Present { value: return_type } => - match body_lower_fn_decl_ident_from_captured(captured: captured) { + match dag_surface_kw_then_ident_from_captured(captured: captured) { Absent => body_lower_wrapper_retained_shell(shell: shell) Present { value: fn_name } => let arrow = node_synthetic( @@ -4719,30 +4696,6 @@ fn body_lower_forward_row_apply(shell: Node, emitted: Symbol) -> Outcome { } } -fn body_lower_type_decl_ident_from_captured(captured: Node) -> Optional { - match body_lower_seq_child(node: captured, name: ^grammar_sequence_right_node_projection) { - Absent => Absent - Present { value: after_kw_type } => - match body_lower_seq_child(node: after_kw_type, name: ^grammar_sequence_left_node_projection) { - Absent => Absent - Present { value: ident_node } => - match dag_binding_branded_atom_identity(node: ident_node) { - Present { value: binding } => Present { value: binding } - Absent => - match node_atom_identity_optional(node: ident_node) { - Present { value: sym } => - if dag_lex_token_symbol(sym: sym) { - optional_absent() - } else { - optional_present(value: sym) - } - Absent => optional_absent() - } - } - } - } -} - fn body_lower_edge_names_where_clause(e: Edge) -> Bool { match e.label { Named { name: n } => n == ^dag_surface_where_refinement_clause @@ -4769,7 +4722,7 @@ fn body_lower_type_decl(shell: Node) -> Outcome { match parse_production_captured_child_optional(node: shell) { Absent => outcome_accepted(value: shell) Present { value: captured } => - match body_lower_type_decl_ident_from_captured(captured: captured) { + match dag_surface_kw_then_ident_from_captured(captured: captured) { Absent => outcome_accepted(value: shell) Present { value: type_name } => match body_lower_find_where_host_conj(root: captured) { diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 0d939c94f04..d18b372e4e9 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -124,21 +124,12 @@ fn namespace_graft_is_transparent_sequence_spine(node: Node) -> Bool { } } -fn namespace_graft_is_top_level_item_unit_node(node: Node) -> Bool { - match parse_production_emitted_identity_optional(node: node) { - Present { value: id } => - (id == ^dag_surface_import_decl) - || (id == ^dag_surface_alias_decl) - || (id == ^dag_surface_type_decl) - || (id == ^dag_surface_data_decl) - || (id == ^dag_surface_fn_decl) - || (id == ^dag_surface_test_fn_decl) - Absent => false - } -} - -fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List { - if namespace_graft_is_top_level_item_unit_node(node: edge.target) { +fn namespace_graft_collect_body_edge( + acc: List, + edge: Edge, + unit_ids: List +) -> List { + if namespace_graft_node_is_top_level_item_unit(node: edge.target, unit_ids: unit_ids) { acc } else { match edge.label { @@ -146,11 +137,11 @@ fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List if namespace_graft_is_metadata_edge(name: sym) { acc } else if namespace_graft_parse_projection_edge(name: sym) { - concat(acc, namespace_graft_collect_body_edges_from_node(node: edge.target)) + concat(acc, namespace_graft_collect_body_edges_from_node(node: edge.target, unit_ids: unit_ids)) } else if dag_resolve_preserve_module_metadata_subtree(node: edge.target) { acc } else if namespace_graft_is_transparent_sequence_spine(node: edge.target) { - concat(acc, namespace_graft_collect_body_edges_from_node(node: edge.target)) + concat(acc, namespace_graft_collect_body_edges_from_node(node: edge.target, unit_ids: unit_ids)) } else { list_snoc_item(xs: acc, item: edge) } @@ -158,7 +149,7 @@ fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List if dag_resolve_preserve_module_metadata_subtree(node: edge.target) { acc } else if namespace_graft_is_transparent_sequence_spine(node: edge.target) { - concat(acc, namespace_graft_collect_body_edges_from_node(node: edge.target)) + concat(acc, namespace_graft_collect_body_edges_from_node(node: edge.target, unit_ids: unit_ids)) } else { list_snoc_item(xs: acc, item: edge) } @@ -166,12 +157,12 @@ fn namespace_graft_collect_body_edge(acc: List, edge: Edge) -> List } } -fn namespace_graft_collect_body_edges_from_node(node: Node) -> List { - if namespace_graft_is_top_level_item_unit_node(node: node) { +fn namespace_graft_collect_body_edges_from_node(node: Node, unit_ids: List) -> List { + if namespace_graft_node_is_top_level_item_unit(node: node, unit_ids: unit_ids) { [] } else { fold(node.children, init: [], f: fn(acc, edge) { - namespace_graft_collect_body_edge(acc: acc, edge: edge) + namespace_graft_collect_body_edge(acc: acc, edge: edge, unit_ids: unit_ids) }) } } @@ -242,6 +233,14 @@ fn namespace_graft_id_is_top_level_item_unit(id: Symbol, unit_ids: List) ) } +fn namespace_graft_node_is_top_level_item_unit(node: Node, unit_ids: List) -> Bool { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => + namespace_graft_id_is_top_level_item_unit(id: id, unit_ids: unit_ids) + Absent => false + } +} + fn namespace_graft_collect_unit_nodes(node: Node, productions: List) -> List { namespace_graft_collect_unit_nodes_against( node: node, @@ -535,14 +534,15 @@ fn namespace_graft_flatten_minus_decl_names(from_decls: List, flattened: L fn namespace_graft_body_edges_for_graft(root: Node) -> List { let working = namespace_graft_unwrap_module_working_root(root: root) let productions = dag_grammar_root().productions - let units = namespace_graft_collect_unit_nodes(node: working, productions: productions) + let unit_ids = namespace_graft_top_level_item_alternative_emitted_ids(productions: productions) + let units = namespace_graft_collect_unit_nodes_against(node: working, unit_ids: unit_ids) let partitioned = namespace_graft_partition_decl_units(units: units) let from_decls = partitioned.named_edges let flattened = namespace_graft_flatten_minus_decl_names( from_decls: from_decls, flattened: namespace_graft_flattened_member_edges( productions: productions, - edges: namespace_graft_collect_body_edges_from_node(node: working) + edges: namespace_graft_collect_body_edges_from_node(node: working, unit_ids: unit_ids) ) ) let combined = concat(from_decls, flattened) From 204a801545515c297a48234e794947a2b9a120ca Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 19 Sep 2026 05:28:51 +0000 Subject: [PATCH 54/56] graft partition: import_decl units dissolve (Admission.imports owns them), never reconstruct as a body shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A residual import shell in the graft body is walked by resolve as bare mentions and refuses any ingest whose closure lacks the imported module's spine — v2.test.native_decl_selection's 4 witnesses went red on this (green on origin/main with the same binary). Typed as NamespaceGraftUnitDisposition { UnitNamed | UnitResidual | UnitDissolved }; the empty-combined arm reconstructs residuals only. Control enrolled: declaration_graft_import_beside_fn_accepts_and_dissolves_the_import. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/namespace_graft.dag | 76 ++++++++++++------- .../claim/declaration_graft_assemble_test.dag | 16 ++++ 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index d18b372e4e9..66c1047ce4d 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -70,9 +70,10 @@ type ModuleRootEdgeDisposition // read once for graft). grammar_production_identity_node_projection → RE-HOME on output ModuleShell // (^dag_surface_module provenance only). grammar_production_captured_node_projection → DISSOLVE // (unwrap before graft). grammar_sequence/repeat *_node_projection, ^dag_surface_top_level_item -// shells, fn/data/type/import decl Named edges, Positional repeat spines, and every other +// shells, fn/data/type decl Named edges, Positional repeat spines, and every other // non-metadata child → GRAFT_BODY (entire declaration forest under innermost Conj; extent is never -// an export subset). +// an export subset). import_decl units DISSOLVE at the unit partition +// (NamespaceGraftUnitDisposition UnitDissolved): admitted upstream as Admission.imports. fn namespace_graft_module_root_edge_disposition(name: Symbol) -> ModuleRootEdgeDisposition { if dag_surface_module_header_metadata_edge(name: name) { @@ -393,41 +394,64 @@ type NamespaceGraftDeclPartition { residual_units: List } -fn namespace_graft_partition_from_named_opt( +// One disposition per collected top-level unit. UnitDissolved is the import_decl +// arm: its members are admitted upstream as Admission.imports +// (v2.compiler.name_resolve admit_imports), so the graft body owes it nothing -- +// the same disposition ModuleRootEdgeDissolve gives the module header. Carried +// as a residual it is a raw shell resolve walks as bare mentions, which refuses +// every ingest whose closure does not contain the imported module's spine. +type NamespaceGraftUnitDisposition + = UnitNamed { edge: Edge } + | UnitResidual + | UnitDissolved + +fn namespace_graft_partition_step( acc: NamespaceGraftDeclPartition, unit: Node, - named: Edge? + disposition: NamespaceGraftUnitDisposition ) -> NamespaceGraftDeclPartition { - match named { - Present { value: edge } => + match disposition { + UnitNamed { edge: edge } => NamespaceGraftDeclPartition { named_edges: list_snoc_item(xs: acc.named_edges, item: edge), residual_units: acc.residual_units } - Absent => + UnitResidual => NamespaceGraftDeclPartition { named_edges: acc.named_edges, residual_units: list_snoc_item(xs: acc.residual_units, item: unit) } + UnitDissolved => acc + } +} + +fn namespace_graft_disposition_from_named_opt(named: Edge?) -> NamespaceGraftUnitDisposition { + match named { + Present { value: edge } => UnitNamed { edge: edge } + Absent => UnitResidual } } -fn namespace_graft_named_edge_for_unit(unit: Node, id: Symbol) -> Edge? { - match id == ^dag_surface_type_decl { - true => namespace_graft_type_decl_named_edge(unit: unit) +fn namespace_graft_unit_disposition(unit: Node, id: Symbol) -> NamespaceGraftUnitDisposition { + match id == ^dag_surface_import_decl { + true => UnitDissolved false => - match id == ^dag_surface_data_decl { - true => namespace_graft_data_decl_named_edge(unit: unit) + match id == ^dag_surface_type_decl { + true => namespace_graft_disposition_from_named_opt(named: namespace_graft_type_decl_named_edge(unit: unit)) false => - match id == ^dag_surface_fn_decl { - true => namespace_graft_fn_decl_named_edge(unit: unit) + match id == ^dag_surface_data_decl { + true => namespace_graft_disposition_from_named_opt(named: namespace_graft_data_decl_named_edge(unit: unit)) false => - match id == ^dag_surface_test_fn_decl { - true => namespace_graft_test_fn_named_edge(unit: unit) + match id == ^dag_surface_fn_decl { + true => namespace_graft_disposition_from_named_opt(named: namespace_graft_fn_decl_named_edge(unit: unit)) false => - match id == ^dag_surface_alias_decl { - true => namespace_graft_kw_ident_named_edge(unit: unit) - false => optional_absent() + match id == ^dag_surface_test_fn_decl { + true => namespace_graft_disposition_from_named_opt(named: namespace_graft_test_fn_named_edge(unit: unit)) + false => + match id == ^dag_surface_alias_decl { + true => namespace_graft_disposition_from_named_opt(named: namespace_graft_kw_ident_named_edge(unit: unit)) + false => UnitResidual + } } } } @@ -442,17 +466,13 @@ fn namespace_graft_partition_decl_units(units: List) -> NamespaceGraftDecl f: fn(acc, unit) { match parse_production_emitted_identity_optional(node: unit) { Present { value: id } => - namespace_graft_partition_from_named_opt( + namespace_graft_partition_step( acc: acc, unit: unit, - named: namespace_graft_named_edge_for_unit(unit: unit, id: id) + disposition: namespace_graft_unit_disposition(unit: unit, id: id) ) Absent => - namespace_graft_partition_from_named_opt( - acc: acc, - unit: unit, - named: optional_absent() - ) + namespace_graft_partition_step(acc: acc, unit: unit, disposition: UnitResidual) } } ) @@ -548,10 +568,10 @@ fn namespace_graft_body_edges_for_graft(root: Node) -> List { let combined = concat(from_decls, flattened) let residuals = partitioned.residual_units if count(combined) == 0 { - if count(units) == 0 { + if count(residuals) == 0 { [] } else { - namespace_graft_reconstruct_declaration_forest(units: units) + namespace_graft_reconstruct_declaration_forest(units: residuals) } } else if count(residuals) == 0 { combined diff --git a/src/v2/test/claim/declaration_graft_assemble_test.dag b/src/v2/test/claim/declaration_graft_assemble_test.dag index 767394a0351..08414a66e29 100644 --- a/src/v2/test/claim/declaration_graft_assemble_test.dag +++ b/src/v2/test/claim/declaration_graft_assemble_test.dag @@ -132,6 +132,7 @@ data src_dag_user_fn: String = "module p\n\nfn dag_user(x: Int) -> Int { x }\n" data src_flag_and_rec: String = "module p\n\ntype Flag = On | Off\ntype Rec { n: Int }\n" data src_flag_and_fn: String = "module p\n\ntype Flag = On | Off\nfn f(x: Int) -> Int { x }\n" +data src_import_and_fn: String = "module p\n\nimport v2.std.logic { Bool }\n\nfn f() -> Bool { true }\n" test fn declaration_graft_empty_module_accepts() -> Bool { declaration_graft_assemble_accepts(src: src_a) @@ -205,3 +206,18 @@ test fn declaration_graft_record_type_only_does_not_accept_today() -> Bool { test fn declaration_graft_record_construct_does_not_accept_today() -> Bool { !declaration_graft_assemble_accepts(src: src_g) } + +// An import_decl unit dissolves at the partition (admitted upstream as +// Admission.imports): it is neither Named nor a residual shell. The control is a +// one-module ingest whose closure does NOT contain v2.std.logic: carried as a +// residual shell, resolve walks `logic` / `Bool` as bare mentions and refuses +// (v2.test.native_decl_selection went red on exactly this). Green on main via +// the flatten; red on the partition until the UnitDissolved arm. +test fn declaration_graft_import_beside_fn_accepts_and_dissolves_the_import() -> Bool { + match declaration_graft_assemble_for(src: src_import_and_fn) { + Rejected { diagnostics: _ } => false + Accepted { value: root, diagnostics: _ } => + declaration_graft_label_present(root: root, lexeme: "f") + && !declaration_graft_atom_present(root: root, lexeme: "logic") + } +} From e2c262f01d514d46048876bea3326fe0a642056d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 19 Sep 2026 06:05:10 +0000 Subject: [PATCH 55/56] Graft body carries a producer marker: a one-member body is no longer readable as a spine segment The containment spine is a chain of single-Named-edge Conj wrappers, and a body whose only member targets a Conj has the same shape, so p { Flag: Conj } read as module p.Flag (resolve_module_not_found on every type-only module; CI declarations: unresolved-module-identities). Every spine consumer asks one predicate and could only guess. namespace_graft_build_body_conj now stamps the body with the provenance vocabulary the module shell already uses, grammar_production_identity_node_projection -> Atom(namespace_graft_module_body), and namespace_graft_spine_segment_edge_optional stops at a marked body. Export admission and the reference-site collector treat that edge as machinery, as symbol_index_fill already did. Witnesses: one_conj_member_body_is_not_a_segment_RED (fails with the marker removed), two_segment_path_still_reads_two_segments (the other direction), body_carries_producer_marker. Class rostered: gunbc.recurring_failure_mode.one_encoding_carries_a_module_body_and_a_spine_segment. Co-Authored-By: Claude Opus 5 (1M context) --- ...ries_a_module_body_and_a_spine_segment.dag | 26 +++++++ src/v2/compiler/03_name_resolve.dag | 6 ++ src/v2/compiler/namespace_graft.dag | 14 +++- src/v2/compiler/reference_site_collector.dag | 5 ++ src/v2/extdeps/languages/dag.dag | 24 ++++++- .../namespace_graft/graft_shape_test.dag | 68 +++++++++++++++++++ 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/one_encoding_carries_a_module_body_and_a_spine_segment.dag diff --git a/dag/gunbc/recurring_failure_mode/one_encoding_carries_a_module_body_and_a_spine_segment.dag b/dag/gunbc/recurring_failure_mode/one_encoding_carries_a_module_body_and_a_spine_segment.dag new file mode 100644 index 00000000000..8bc39f778e2 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/one_encoding_carries_a_module_body_and_a_spine_segment.dag @@ -0,0 +1,26 @@ +module gunbc.recurring_failure_mode.one_encoding_carries_a_module_body_and_a_spine_segment + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data one_encoding_carries_a_module_body_and_a_spine_segment: RecurringFailureMode = RecurringFailureMode { + identity: "one_encoding_carries_a_module_body_and_a_spine_segment" as NonEmptyStr, + + receipts: [ + "one encoding carries two meanings: the containment spine is a chain of single-Named-edge Conj wrappers, and a module body whose only member is a Named edge to a Conj has exactly that shape. `p { Flag: Conj }` and the path `p.Flag` were one tree. INVALID STATE: a grafted module whose qualified name, read back from its own nesting, is longer than the name it was authored with -- the first declaration has become a segment.", + + "HARM, measured 2026-09-19 on gunbc#11574: every module whose graft body is one Conj-targeted member refuses resolve_module_not_found (the seek is for `p`, the tree answers `p.Flag`); CI's declarations phase reports unresolved-module-identities over the small fixture modules with that shape; symbol_index_fill descends the member as a segment and never indexes it. The specimen was a type-only coproduct module once its Named edge targeted the type_alias_rhs shell; `type Name = String where ...` alone had been red the same way on main before it. The reference-site collector had already met this and worked around it by COUNTING segments from the header qualified name (module_spine_unwrapped) -- circular for a grafted root, whose qualified name is itself read from the shape.", + + "WHY THE READER CANNOT FIX IT: every consumer of the spine -- qualified_name_from_graft_spine, symbol_index_fill_containment_node, admit_named_exports_descend_spine, module_spine_unwrapped -- asks namespace_graft_spine_segment_edge_optional, and that predicate can only guess at the shape. DESIGN section 6b: the symptom sits at the reader; the earliest boundary that cannot justify itself is the producer that emitted a body indistinguishable from a segment. DESIGN section 5: make the state unwritable rather than teach a reader to guess.", + + "THE REPAIR is producer-side and consumes the provenance vocabulary the graft already used for its module shell: namespace_graft_build_body_conj stamps the body with grammar_production_identity_node_projection -> Atom(namespace_graft_module_body_identity), and the one spine predicate stops at a node so marked. No captured projection is emitted, because the members ARE the body and a captured wrapper would re-create the very one-Named-Conj level the fill misreads. Readers that fold body children already treat projection edges as machinery (symbol_index_fill); export admission and the reference-site collector now apply the same rule to that edge.", + + "EVIDENCE ENROLLED: v2.test.claim.namespace_graft.graft_shape_test namespace_graft_one_conj_member_body_is_not_a_segment_RED (a one-Conj-member body under a four-segment name reads four segments; FAILS with the producer marker removed) paired with namespace_graft_two_segment_path_still_reads_two_segments_holds (the same body under a two-segment name reads exactly two -- a marker that ended every spine after one segment would green the RED and red this) and namespace_graft_body_carries_producer_marker_holds.", + + "NODE IDENTITY: the marker adds a child to every grafted body Conj, so the content hash of every grafted body, its spine wrappers and its module shell changes corpus-wide. It adds no source occurrence (OccurrenceSynthetic) and moves no declaration's occurrence identity or semantic hash of any member subtree. Nothing on main keys a committed artifact on a grafted root's content hash; a consumer that does must re-take its digests at this landing.", + + "RUNG FOUND AT: silent wrongness (a fabricated qualified name accepted as the module's identity). RUNG NOW: 4, structurally impossible for the graft's own output -- the body cannot be emitted without its marker, and no reader guesses. RESIDUAL, honestly outside the marker: a hand-built spine (a fixture constructing containment nodes directly rather than through module_header_containment_graft) can still author the ambiguous shape; that residue is a fixture-authoring fact, and the discriminating RED above is the control that would catch a producer regressing to it.", + ], + + evidence: [], +} diff --git a/src/v2/compiler/03_name_resolve.dag b/src/v2/compiler/03_name_resolve.dag index 456d04ea4ca..2ac342df612 100644 --- a/src/v2/compiler/03_name_resolve.dag +++ b/src/v2/compiler/03_name_resolve.dag @@ -27,6 +27,7 @@ import v2.extdeps.languages.dag { qualified_name_from_module_node } import v2.compiler.namespace_graft { + namespace_graft_parse_projection_edge, namespace_graft_strip_provenance_marker } import std.algebra { Cons, Empty, FreeMonoid } @@ -350,11 +351,16 @@ fn module_root_find( } } +// Parse-projection edges (identity / captured projections, sequence spines) are +// machinery, not exports -- the same rule symbol_index_fill_containment_edge +// applies. The body's identity projection reaches this fold as a direct child. fn try_admitted_export_binding(e: Edge) -> DeclaredBinding? { match try_edge_declared_binding(e: e) { Present { value: b } => if dag_surface_module_header_metadata_edge(name: b.name) { optional_absent() + } else if namespace_graft_parse_projection_edge(name: b.name) { + optional_absent() } else { optional_present(value: b) } diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index cec12a421c3..ba313776a56 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -44,6 +44,7 @@ import v2.extdeps.languages.dag { dag_grammar_top_level_item_expr, namespace_graft_already_grafted, namespace_graft_has_module_header, + namespace_graft_module_body_identity, namespace_graft_unwrap_module_working_root, parse_production_emitted_identity_optional, qualified_name_from_module_node @@ -337,10 +338,21 @@ fn namespace_graft_body_dissolved(root: Node, body_edges: List) -> Bool { } } +// The body carries its identity projection first (namespace_graft_module_body_identity): +// the producer marks where the spine ends. See the reader note beside +// namespace_graft_node_is_module_body in v2.extdeps.languages.dag. fn namespace_graft_build_body_conj(edges: List) -> Node { Node { kind: TypeNode { connective: Conj }, - children: edges, + children: concat( + [ + dag_named_edge( + name: ^grammar_production_identity_node_projection, + target: namespace_graft_atom_node(id: namespace_graft_module_body_identity) + ) + ], + edges + ), occurrence_id: OccurrenceSynthetic } } diff --git a/src/v2/compiler/reference_site_collector.dag b/src/v2/compiler/reference_site_collector.dag index 0ffdebae7d7..c9c1250ff68 100644 --- a/src/v2/compiler/reference_site_collector.dag +++ b/src/v2/compiler/reference_site_collector.dag @@ -144,6 +144,11 @@ fn reference_sites_in_edge( Named { name: sym } => if dag_surface_module_header_metadata_edge(name: sym) { acc + } else if sym == ^grammar_production_identity_node_projection { + // A production identity is what a node IS, never a mention it makes + // (the graft body's namespace_graft_module_body marker reaches here as a + // direct child). Same rule as symbol_index_fill and export admission. + acc } else { reference_sites_in_node( acc: acc, diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index dfcae890e20..30266e500c2 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -5164,10 +5164,32 @@ fn namespace_graft_node_is_conj(node: Node) -> Bool { } } +// THE GRAFT BODY IS MARKED BY ITS PRODUCER, NOT INFERRED BY ITS READERS. The +// containment spine is a chain of single-Named-edge Conj wrappers, and a module +// body with exactly one Named member whose target is a Conj is the same shape as +// one more segment: `p { Flag: Conj }` read as `p.Flag`. Every spine consumer +// (this QN reader, symbol_index_fill, name_resolve export admission, the +// reference-site collector) inherited that ambiguity. The graft therefore stamps +// the body Conj with the same provenance vocabulary the module shell already uses +// -- grammar_production_identity_node_projection -> Atom(identity) -- so the +// spine ends where the producer says it ends. The captured projection is +// deliberately NOT emitted: the members are the body, and a captured wrapper +// would re-create the one-Named-Conj level the fill misreads. +data namespace_graft_module_body_identity: Symbol = ^namespace_graft_module_body + +fn namespace_graft_node_is_module_body(node: Node) -> Bool { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => id == namespace_graft_module_body_identity + Absent => false + } +} + fn namespace_graft_spine_segment_edge_optional(node: Node) -> Optional { match node.kind { TypeNode { connective: Conj } => - if count(node.children) != 1 { + if namespace_graft_node_is_module_body(node: node) { + optional_absent() + } else if count(node.children) != 1 { optional_absent() } else { match list_at_optional(xs: node.children, index: 0) { diff --git a/src/v2/test/claim/namespace_graft/graft_shape_test.dag b/src/v2/test/claim/namespace_graft/graft_shape_test.dag index 1496b579671..da40d28549e 100644 --- a/src/v2/test/claim/namespace_graft/graft_shape_test.dag +++ b/src/v2/test/claim/namespace_graft/graft_shape_test.dag @@ -6,6 +6,7 @@ import v2.extdeps.languages.dag { dag_grammar_atom, dag_named_edge, emit_module_header_emitted_node, + namespace_graft_node_is_module_body, qualified_name_from_graft_spine } import v2.std.algebra { fold_list_node } @@ -370,3 +371,70 @@ test fn namespace_graft_nonempty_flatten_stays_well_formed_holds() -> Bool { Rejected { diagnostics: _ } => false } } + +// ONE-MEMBER BODY VERSUS ONE MORE SEGMENT. A body whose only member is a Named +// edge to a Conj has the spine's own shape, so before the producer marked the +// body this module read as `v2.test.namespace_graft.alpha.NgOnly` -- the +// declaration was eaten as a segment (resolve_module_not_found on every +// type-only module; CI declarations: unresolved-module-identities). RED without +// the marker: this row asserts the single authored identity. +fn ng_conj_member_edge(name: Symbol) -> Edge { + Edge { + label: Named { name: name }, + target: Node { + kind: TypeNode { connective: Conj }, + children: [ng_export_edge(name: ^NgInner)], + occurrence_id: OccurrenceSynthetic + } + } +} + +data ng_one_conj_member_root: Node = ng_pre_graft_module_root( + qn: ng_module_qn, + body_edges: [ng_conj_member_edge(name: ^NgOnly)] +) + +test fn namespace_graft_one_conj_member_body_is_not_a_segment_RED() -> Bool { + match module_header_containment_graft(root: ng_one_conj_member_root) { + Rejected { diagnostics: _ } => false + Accepted { value: grafted, diagnostics: _ } => + ng_qn_eq(out: qualified_name_from_graft_spine(root: grafted), expected: ng_module_qn) + } +} + +// THE OTHER DIRECTION: a genuine two-segment path with the same one-Conj-member +// body still reads as exactly two segments. Without this control a marker could +// "fix" the row above by ending every spine after one segment. +data ng_two_segment_qn: QualifiedName = qualified_name_from_dotted_string(dotted: "ngtwo.beta") + +data ng_two_segment_one_conj_member_root: Node = ng_pre_graft_module_root( + qn: ng_two_segment_qn, + body_edges: [ng_conj_member_edge(name: ^NgOnly)] +) + +test fn namespace_graft_two_segment_path_still_reads_two_segments_holds() -> Bool { + match module_header_containment_graft(root: ng_two_segment_one_conj_member_root) { + Rejected { diagnostics: _ } => false + Accepted { value: grafted, diagnostics: _ } => + ng_qn_eq(out: qualified_name_from_graft_spine(root: grafted), expected: ng_two_segment_qn) + } +} + +// The marker is the producer's: the innermost node of the spine carries +// namespace_graft_module_body_identity, and the member survives beside it. +test fn namespace_graft_body_carries_producer_marker_holds() -> Bool { + match module_header_containment_graft(root: ng_one_conj_member_root) { + Rejected { diagnostics: _ } => false + Accepted { value: grafted, diagnostics: _ } => + match find_named_child(root: grafted, name: ^grammar_production_captured_node_projection) { + Rejected { diagnostics: _ } => false + Accepted { value: captured, diagnostics: _ } => + let body = namespace_graft_walk_spine_to_body(captured: captured) + namespace_graft_node_is_module_body(node: body) + && match find_named_child(root: body, name: ^NgOnly) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + } + } +} From 37f5f89b81caf82ef5df66793d8e54bd35ba7e1b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 19 Sep 2026 06:58:37 +0000 Subject: [PATCH 56/56] reference_site_collector: annotation at module-item grain (parse phase refused the in-body form) Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/reference_site_collector.dag | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/v2/compiler/reference_site_collector.dag b/src/v2/compiler/reference_site_collector.dag index c9c1250ff68..da7a6685a20 100644 --- a/src/v2/compiler/reference_site_collector.dag +++ b/src/v2/compiler/reference_site_collector.dag @@ -135,6 +135,10 @@ import v2.extdeps.languages.dag { // the fold's OWN FIRST STEP -- because a Bool sitting beside the Outcome was still a second surface // deciding the same thing, and section 2's test is net CONCEPTS rather than net files. This module // matches the Outcome and gets the linear behaviour for free. +// A production identity edge (grammar_production_identity_node_projection) is +// what a node IS, never a mention it makes -- the graft body's +// namespace_graft_module_body marker reaches this fold as a direct child. Same +// rule as symbol_index_fill_containment_edge and export admission. fn reference_sites_in_edge( acc: ReferenceSiteCollection, position: QualifiedName, @@ -145,9 +149,6 @@ fn reference_sites_in_edge( if dag_surface_module_header_metadata_edge(name: sym) { acc } else if sym == ^grammar_production_identity_node_projection { - // A production identity is what a node IS, never a mention it makes - // (the graft body's namespace_graft_module_body marker reaches here as a - // direct child). Same rule as symbol_index_fill and export admission. acc } else { reference_sites_in_node(