Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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: [],
}
6 changes: 6 additions & 0 deletions src/v2/compiler/03_name_resolve.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
}
Expand Down
14 changes: 13 additions & 1 deletion src/v2/compiler/namespace_graft.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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_marker_node,
namespace_graft_unwrap_module_working_root,
parse_production_emitted_identity_optional,
qualified_name_from_module_node
Expand Down Expand Up @@ -337,10 +338,21 @@ fn namespace_graft_body_dissolved(root: Node, body_edges: List<Edge>) -> 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<Edge>) -> Node {
Node {
kind: TypeNode { connective: Conj },
children: edges,
children: concat(
[
dag_named_edge(
name: ^grammar_production_identity_node_projection,
target: namespace_graft_module_body_marker_node()
)
],
edges
),
occurrence_id: OccurrenceSynthetic
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/v2/compiler/reference_site_collector.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -144,6 +148,8 @@ 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 {
acc
} else {
reference_sites_in_node(
acc: acc,
Expand Down
38 changes: 36 additions & 2 deletions src/v2/extdeps/languages/dag.dag
Original file line number Diff line number Diff line change
Expand Up @@ -2557,7 +2557,11 @@ fn dag_declared_inhabitants_core() -> Node {
Node {
kind: TypeNode { connective: Conj },
children: [
dag_named_edge(name: ^dag_inhabitant_field_int, target: dag_int_inhabitant_node())
dag_named_edge(name: ^dag_inhabitant_field_int, target: dag_int_inhabitant_node()),
dag_named_edge(
name: ^dag_inhabitant_field_module_body,
target: namespace_graft_module_body_marker_node()
)
],
occurrence_id: OccurrenceSynthetic
}
Expand Down Expand Up @@ -5164,10 +5168,40 @@ 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

// The one constructor of the marker node: emitted by namespace_graft_build_body_conj
// and declared in dag_declared_inhabitants_core, so infer grounds it as a node the
// dag language authority declares (infer_node_declared_in_language_inhabitants) and
// a marked body stays a fully evidenced product.
fn namespace_graft_module_body_marker_node() -> Node {
dag_inhabitant_atom(id: namespace_graft_module_body_identity)
}

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<Edge> {
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) {
Expand Down
68 changes: 68 additions & 0 deletions src/v2/test/claim/namespace_graft/graft_shape_test.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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
}
}
}
}