diff --git a/dag/gunbc/ci/ci_layer_roots.dag b/dag/gunbc/ci/ci_layer_roots.dag index 4ea322fb00a..87b967ae634 100644 --- a/dag/gunbc/ci/ci_layer_roots.dag +++ b/dag/gunbc/ci/ci_layer_roots.dag @@ -1720,27 +1720,27 @@ data falsifier_substrate_long_lane_rows: List = [ }, SubstrateLongLaneRow { entry: "dag/test/claim/long/namespace_graft_body_dissolution_witness_test.dag", - function: "witness_type_fn_module_dissolution_refuses_RED", - reason: "~16s measured eval (valiant-boar-65, 2026-08-10): interpreted production lex+parse+normalize per control — the graft empty-observation-narrow guard matrix (operator ruling msg_d18a55de: the collector reporting a declaration-carrying module as declaration-free now REFUSES)", + function: "witness_type_fn_module_grafts", + reason: "~16s measured eval: interpreted tokenize/parse/normalize — type+fn source must graft (not namespace_graft_body_dissolved_refused)", dissolution: unbound_dissolution(description: "witness realization lands the lex/parse natively or eval drops under gunbc_ci_fast_lane_witness_eval_budget on a measured run — then re-enroll per-PR discovery and delete this row") }, SubstrateLongLaneRow { entry: "dag/test/claim/long/namespace_graft_body_dissolution_witness_test.dag", - function: "witness_type_only_module_dissolution_refuses_RED", - reason: "same interpreted lex/parse/normalize wall as sibling witness_type_fn_module_dissolution_refuses_RED (~16s measured)", - dissolution: unbound_dissolution(description: "same as witness_type_fn_module_dissolution_refuses_RED row") + function: "witness_type_only_module_grafts", + reason: "same interpreted tokenize/parse/normalize wall as sibling witness_type_fn_module_grafts (~16s measured)", + dissolution: unbound_dissolution(description: "same as witness_type_fn_module_grafts row") }, SubstrateLongLaneRow { entry: "dag/test/claim/long/namespace_graft_body_dissolution_witness_test.dag", function: "witness_lowered_body_module_still_grafts", reason: "two full lex/parse/normalize passes over the lowered-body control (~35s measured; acceptance plus the non-vacuous declaration-presence read)", - dissolution: unbound_dissolution(description: "same as witness_type_fn_module_dissolution_refuses_RED row") + dissolution: unbound_dissolution(description: "same as witness_type_fn_module_grafts row") }, SubstrateLongLaneRow { entry: "dag/test/claim/long/namespace_graft_body_dissolution_witness_test.dag", function: "witness_header_only_module_still_accepts", reason: "same interpreted lex/parse/normalize wall as sibling witness_type_fn_module_dissolution_refuses_RED (~18s measured)", - dissolution: unbound_dissolution(description: "same as witness_type_fn_module_dissolution_refuses_RED row") + dissolution: unbound_dissolution(description: "same as witness_type_fn_module_grafts row") }, SubstrateLongLaneRow { entry: "dag/test/claim/witness_execution_class_live_census_test.dag", diff --git a/dag/test/claim/long/namespace_graft_body_dissolution_witness_test.dag b/dag/test/claim/long/namespace_graft_body_dissolution_witness_test.dag index 7abd53444fa..24ad6ba59f1 100644 --- a/dag/test/claim/long/namespace_graft_body_dissolution_witness_test.dag +++ b/dag/test/claim/long/namespace_graft_body_dissolution_witness_test.dag @@ -13,18 +13,11 @@ import v2.compiler.parse { parse_module, ParseArtifact } import v2.compiler.normalize { normalize } import v2.extdeps.languages.dag { dag_lex_rules, dag_grammar } -// The empty-observation narrow refused at the graft (operator ruling msg_d18a55de, 2026-08-10): -// namespace_graft's body collector dissolved every declaration of a type-decl-carrying module into -// childless atoms and reported the module as declaration-free — normalize then ACCEPTED a hollow -// module shell, so a loud sugar refusal two commits earlier had become a silent drop (the §4b(3) -// rung demotion declared in gunbc#8080's body). module_header_containment_graft now REFUSES -// (namespace_graft_body_dissolved_refused) when the collected body is empty while the pre-graft -// tree carries top-level declarations. The discriminant is declaration PRESENCE, so a genuinely -// declaration-free module keeps its legitimate empty body. This guard covers total dissolution -// only; partial-drop fidelity belongs to the deeper graft repair. Controls: the formerly-hollow -// shapes now refuse WITH THE EXACT GUARD REASON asserted (discriminating them from the well_formed -// rejection class), a lowered-body module still grafts and its normalized tree still carries its -// declarations (asserted non-vacuously), and a header-only module still accepts. +// Empty flatten with surviving top-level-item alternatives reconstructs a declaration forest +// (operator ruling msg_d18a55de). Type-only and type+fn source must ACCEPT through tokenize/parse/ +// normalize and must not carry namespace_graft_body_dissolved_refused. Header-only still accepts. +// Lost forest (top_level_item wrapper, no unit) still refuses — that RED is the synthetic in +// graft_shape_test, because source cannot author a unit-less wrapper. fn artifact_tree(artifact: ParseArtifact) -> ParseTree { artifact.tree @@ -35,7 +28,7 @@ fn artifact_tree(artifact: ParseArtifact) -> ParseTree { // the sweep's first_located_cause already skips. The assertion is therefore containment: the // refusal CARRIES the guard's typed reason, wherever the pending carriers put it. fn rejection_reasons(source: String) -> List { - match lex_walk_artifact(source: source, file: ^graft_guard_probe, rules: dag_lex_rules()) { + match lex_walk_artifact(source: chars(s: source), file: ^graft_guard_probe, rules: dag_lex_rules()) { v2.std.diagnostic.Rejected { diagnostics: _ } => no_reasons() v2.std.diagnostic.Accepted { value: a, diagnostics: _ } => match parse_module(tokens: lex_artifact_semantic_tokens(artifact: a), grammar: dag_grammar()) { @@ -70,7 +63,7 @@ fn reasons_contain(reasons: List, sym: Symbol) -> Bool { } fn normalize_accepts(source: String) -> Bool { - match lex_walk_artifact(source: source, file: ^graft_guard_probe, rules: dag_lex_rules()) { + match lex_walk_artifact(source: chars(s: source), file: ^graft_guard_probe, rules: dag_lex_rules()) { v2.std.diagnostic.Rejected { diagnostics: _ } => false v2.std.diagnostic.Accepted { value: a, diagnostics: _ } => match parse_module(tokens: lex_artifact_semantic_tokens(artifact: a), grammar: dag_grammar()) { @@ -85,7 +78,7 @@ fn normalize_accepts(source: String) -> Bool { } fn normalized_tree_optional(source: String) -> Node? { - match lex_walk_artifact(source: source, file: ^graft_guard_probe, rules: dag_lex_rules()) { + match lex_walk_artifact(source: chars(s: source), file: ^graft_guard_probe, rules: dag_lex_rules()) { v2.std.diagnostic.Rejected { diagnostics: _ } => optional_absent() v2.std.diagnostic.Accepted { value: a, diagnostics: _ } => match parse_module(tokens: lex_artifact_semantic_tokens(artifact: a), grammar: dag_grammar()) { @@ -124,20 +117,28 @@ fn children_contain_atom(n: Node, sym: Symbol) -> Bool { }) } -data mixed_type_fn_source: String = "module graft_guard_probe\n\ntype G1 = A1 | B1\n\nfn g1(x: Int) -> Int {\n let w = x\n w\n}\n" +data mixed_type_fn_source: String = "module graft_guard_probe\n\ntype G1 = A1 | B1\n\nfn g1(x: Int) -> Int { x }\n" -data type_only_source: String = "module graft_guard_probe\n\ntype G2 = A2 | B2\n" +data type_only_source: String = "module m\n\ntype A { x: Int }\n" data lowered_body_source: String = "module graft_guard_probe\n\ndata d3: LiveTreeDisposition = SubstrateInputsOnly\n\nfn g3() -> List {\n fold_list(xs: [1, 2], empty: Empty, cons: fn(acc, item) {\n list_append(left: [item], right: acc)\n })\n}\n" data header_only_source: String = "module graft_guard_probe\n" -test fn witness_type_fn_module_dissolution_refuses_RED() -> Bool { - reasons_contain(reasons: rejection_reasons(source: mixed_type_fn_source), sym: ^namespace_graft_body_dissolved_refused) +test fn witness_type_fn_module_grafts() -> Bool { + normalize_accepts(source: mixed_type_fn_source) + && !reasons_contain( + reasons: rejection_reasons(source: mixed_type_fn_source), + sym: ^namespace_graft_body_dissolved_refused + ) } -test fn witness_type_only_module_dissolution_refuses_RED() -> Bool { - reasons_contain(reasons: rejection_reasons(source: type_only_source), sym: ^namespace_graft_body_dissolved_refused) +test fn witness_type_only_module_grafts() -> Bool { + normalize_accepts(source: type_only_source) + && !reasons_contain( + reasons: rejection_reasons(source: type_only_source), + sym: ^namespace_graft_body_dissolved_refused + ) } test fn witness_lowered_body_module_still_grafts() -> Bool { diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 962dcaf4aac..cec12a421c3 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -2,8 +2,8 @@ module v2.compiler.namespace_graft import std.occurrence_identity { OccurrenceSynthetic } import std.algebra { Cons, Empty, FreeMonoid } -import v2.std.algebra { fold_list_right, list_snoc_item } -import v2.std.collection { Absent, List, Present } +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.diagnostic { Accepted, Diagnostic, @@ -18,6 +18,7 @@ import v2.std.diagnostic { import v2.std.logic { Bool } import v2.std.node { Atom, + Cardinality, Conj, Edge, Named, @@ -29,9 +30,18 @@ import v2.std.node { } import v2.std.qualified_name { QualifiedName } import v2.std.compilers.lexing { symbol_lexeme } +import v2.std.grammar { + GrammarExpr, + GrammarExprFold, + GrammarProduction, + fold_grammar_expr, + node_atom_identity_optional +} import v2.extdeps.languages.dag { dag_named_edge, dag_surface_module_header_metadata_edge, + dag_grammar_root, + dag_grammar_top_level_item_expr, namespace_graft_already_grafted, namespace_graft_has_module_header, namespace_graft_unwrap_module_working_root, @@ -148,6 +158,126 @@ fn namespace_graft_collect_body_edges_from_node(node: Node) -> List { }) } +fn namespace_graft_grammar_expr_nonterminal_productions(expr: GrammarExpr) -> List { + fold_grammar_expr( + expr: expr, + algebra: GrammarExprFold { + terminal: fn(_, _) { [] }, + nonterminal: fn(production) { [production] }, + sequence: fn(left, right) { list_append(left: left, right: right) }, + choice: fn(left, right) { list_append(left: left, right: right) }, + optional: fn(xs) { xs }, + repeat: fn(xs) { xs } + } + ) +} + +fn namespace_graft_production_emitted_identity_optional(production: GrammarProduction) -> Symbol? { + node_atom_identity_optional(node: production.emitted) +} + +fn namespace_graft_emitted_identity_for_production_name(name: Symbol) -> Symbol? { + fold_list( + xs: dag_grammar_root().productions, + empty: optional_absent(), + cons: fn(acc, production) { + match acc { + Present { value: _ } => acc + Absent => + match production.name == name { + true => namespace_graft_production_emitted_identity_optional(production: production) + false => optional_absent() + } + } + } + ) +} + +fn namespace_graft_top_level_item_alternative_emitted_ids() -> 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) { + Present { value: emitted } => list_snoc_item(xs: acc, item: emitted) + Absent => acc + } + } + ) +} + +fn namespace_graft_id_is_top_level_item_unit(id: Symbol, unit_ids: List) -> Bool { + fold_list( + xs: unit_ids, + empty: false, + cons: fn(found, emitted) { + found || (emitted == id) + } + ) +} + +fn namespace_graft_collect_unit_nodes(node: Node) -> List { + namespace_graft_collect_unit_nodes_against( + node: node, + unit_ids: namespace_graft_top_level_item_alternative_emitted_ids() + ) +} + +fn namespace_graft_collect_unit_nodes_against(node: Node, unit_ids: List) -> List { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => + if namespace_graft_id_is_top_level_item_unit(id: id, unit_ids: unit_ids) { + [node] + } else { + fold(node.children, init: [], f: fn(acc, edge) { + concat(acc, namespace_graft_collect_unit_nodes_against(node: edge.target, unit_ids: unit_ids)) + }) + } + Absent => + fold(node.children, init: [], f: fn(acc, edge) { + concat(acc, namespace_graft_collect_unit_nodes_against(node: edge.target, unit_ids: unit_ids)) + }) + } +} + +fn namespace_graft_reconstruct_unit_edge(node: Node) -> Edge { + Edge { + label: Positional, + target: node + } +} + +fn namespace_graft_reconstruct_declaration_forest(units: List) -> List { + [ + Edge { + label: Named { name: ^dag_surface_top_level_item }, + target: Node { + kind: TypeNode { connective: Cardinality }, + children: fold(units, init: [], f: fn(acc, unit) { + list_snoc_item(xs: acc, item: namespace_graft_reconstruct_unit_edge(node: unit)) + }), + occurrence_id: OccurrenceSynthetic + } + } + ] +} + +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) + ) + if count(units) == 0 { + [] + } else { + namespace_graft_reconstruct_declaration_forest(units: units) + } + } else { + flattened + } +} + fn namespace_graft_is_graft_candidate(root: Node) -> Bool { if namespace_graft_already_grafted(root: root) { false @@ -166,13 +296,14 @@ fn namespace_graft_collect_body_edges(root: Node) -> List { // 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". A pre-graft tree that carries top-level declarations (the -// dag_surface_top_level_item identity atoms survive the normalize fold on every -// measured shape) while the collected body is EMPTY is the collector failing to -// carry the forest — a decidable comparison of two trees in hand, refused loudly. -// A module with no declarations at all (header only) keeps its legitimate empty -// body: the discriminant is declaration presence, not body emptiness alone. This -// guard covers total dissolution; partial-drop fidelity is the deeper graft repair. +// has none". Flattening parse-projection / transparent-sequence spines into a +// declaration's CHILDREN dropped type/data/import/fn shells (every child is itself +// a projection), so a module whose declarations contribute no function body looked +// identical to a LOST forest. Conj body edges must be uniquely Named (well_formed). Flattening +// already produces that shape for a nonempty forest, so a nonempty flatten is kept as-is. +// An empty flatten with surviving top-level-item alternatives reconstructs one Named forest +// (Cardinality of those units as Positional children, interiors kept). Empty flatten, no units, +// and a leftover dag_surface_top_level_item is a genuine loss and still refuses. fn namespace_graft_subtree_contains_atom(n: Node, sym: Symbol) -> Bool { match n.kind { TypeNode { connective: Atom { identity: id } } => @@ -329,7 +460,7 @@ fn module_header_containment_graft(root: Node) -> Outcome { correction: Unavailable { reason: ExternalContractUnknown } }) Cons { head: _, tail: _ } => { - let body_edges = namespace_graft_collect_body_edges(root: root) + let body_edges = namespace_graft_body_edges_for_graft(root: root) if namespace_graft_body_dissolved(root: root, body_edges: body_edges) { outcome_rejected(Diagnostic { reason: ^namespace_graft_body_dissolved_refused, 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 8887b19973e..1496b579671 100644 --- a/src/v2/test/claim/namespace_graft/graft_shape_test.dag +++ b/src/v2/test/claim/namespace_graft/graft_shape_test.dag @@ -19,7 +19,7 @@ import v2.compiler.namespace_graft { namespace_graft_strip_provenance_marker } import v2.std.node_query { find_named_child } -import v2.std.node { Atom, Conj, Edge, Named, Node, Symbol, TypeNode, ComputationNode } +import v2.std.node { Atom, Conj, Edge, Named, Node, Symbol, TypeNode, ComputationNode, well_formed } import v2.std.qualified_name { QualifiedName, qualified_name_from_dotted_string, @@ -261,3 +261,112 @@ data claim_namespace_graft_dual_position_refused: TestClaim = EqualsClaim { test fn namespace_graft_dual_position_refused_holds() -> Bool { !ng_outcome_accepted(out: module_header_containment_graft(root: ng_dual_position_root)) } + +fn ng_production_identity_shell(id: Symbol) -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + dag_named_edge( + name: ^grammar_production_identity_node_projection, + target: dag_grammar_atom(id: id) + ) + ], + occurrence_id: OccurrenceSynthetic + } +} + +fn ng_projection_edge(target: Node) -> Edge { + Edge { + label: Named { name: ^grammar_production_captured_node_projection }, + target: target + } +} + +data ng_type_decl_unit_root: Node = ng_pre_graft_module_root( + qn: ng_module_qn, + body_edges: [ + ng_projection_edge( + target: ng_production_identity_shell(id: ^dag_surface_type_decl) + ) + ] +) + +data ng_lost_body_root: Node = ng_pre_graft_module_root( + qn: ng_module_qn, + body_edges: [ + ng_projection_edge( + target: ng_production_identity_shell(id: ^dag_surface_top_level_item) + ) + ] +) + +fn ng_graft_reason_is(root: Node, reason: Symbol) -> Bool { + match module_header_containment_graft(root: root) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => d.head.reason == reason + } +} + +fn ng_is_type_decl_atom(n: Node) -> Bool { + match n.kind { + TypeNode { connective: Atom { identity: id } } => id == ^dag_surface_type_decl + _ => false + } +} + +fn ng_has_type_decl_identity_projection(n: Node) -> Bool { + fold( + n.children, + init: false, + f: fn(acc, e) { + acc || ng_edge_carries_type_decl_identity(e: e) + } + ) +} + +fn ng_edge_carries_type_decl_identity(e: Edge) -> Bool { + match e.label { + Named { name: sym } => + if (sym == ^grammar_production_identity_node_projection) && ng_is_type_decl_atom(n: e.target) { + true + } else { + ng_has_type_decl_identity_projection(n: e.target) + } + Positional => ng_has_type_decl_identity_projection(n: e.target) + } +} + +test fn namespace_graft_type_decl_unit_is_not_dissolved_holds() -> Bool { + match module_header_containment_graft(root: ng_type_decl_unit_root) { + Accepted { value: grafted, diagnostics: _ } => + well_formed(n: grafted) && ng_has_type_decl_identity_projection(n: grafted) + Rejected { diagnostics: _ } => false + } +} + +test fn namespace_graft_lost_top_level_item_without_unit_refuses_RED() -> Bool { + ng_graft_reason_is( + root: ng_lost_body_root, + reason: ^namespace_graft_body_dissolved_refused + ) +} + +data ng_types_and_fn_root: Node = ng_pre_graft_module_root( + qn: ng_module_qn, + body_edges: [ + ng_projection_edge( + target: ng_production_identity_shell(id: ^dag_surface_type_decl) + ), + ng_projection_edge( + target: ng_production_identity_shell(id: ^dag_surface_data_decl) + ), + ng_export_edge(name: ^NgPilotFn) + ] +) + +test fn namespace_graft_nonempty_flatten_stays_well_formed_holds() -> Bool { + match module_header_containment_graft(root: ng_types_and_fn_root) { + Accepted { value: grafted, diagnostics: _ } => well_formed(n: grafted) + Rejected { diagnostics: _ } => false + } +}