From f4e90ace1b23d7b4c4047e00ef92e2b1d8f51271 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 8 Oct 2026 01:28:01 +0000 Subject: [PATCH 1/4] Skip module-surface members in reference-derived residency reading. A module or namespace is not a record payload: judge binders only on stamped module surfaces (declaration identity, graft-body stamp, or header production edge), and keep CarrierMemberNotABinder for a genuine hand-built field. Co-authored-by: Cursor --- src/v2/extdeps/languages/dag.dag | 32 +++++++++++ .../reference_derived_residency_reading.dag | 52 +++++++++++------- .../structural_label_readers_test.dag | 25 +++++++++ ...ference_derived_residency_reading_test.dag | 53 ++++++++++++++++++- 4 files changed, 142 insertions(+), 20 deletions(-) diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index e4b4bcb6d73..795228c2a16 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4266,6 +4266,38 @@ fn dag_node_is_module_root_conj(n: Node) -> Bool { } } +// A MODULE SURFACE is identified by a stamp the producer actually wrote, never by the shape of its +// Authored children. Those children are declarations or nested modules, not record fields, and a +// lens that asks member_edge_type_node of them reports member_not_a_binder for a well-formed module. +fn dag_production_is_module_member_declaration(id: Symbol) -> Bool { + (id == ^dag_surface_fn_decl) + || (id == ^dag_surface_test_fn_decl) + || (id == ^dag_surface_data_decl) + || (id == ^dag_surface_type_decl) + || (id == ^dag_surface_alias_decl) + || (id == ^dag_surface_service_decl) + || (id == ^dag_surface_resource_decl) + || (id == ^dag_surface_import_decl) +} + +fn dag_node_is_module_surface(n: Node) -> Bool { + match parse_production_emitted_identity_optional(node: n) { + Present { value: id } => + (id == ^dag_surface_module) + || (id == ^dag_surface_module_header) + || (id == ^namespace_graft_module_body) + || dag_production_is_module_member_declaration(id: id) + Absent => dag_node_is_module_root_conj(n: n) + } +} + +// The complementary question for a binder-field fold: a payload that is not a module surface. A +// hand-built record Conj has neither a declaration identity nor a header production edge, so it +// remains a record-field carrier. +fn dag_node_is_record_field_carrier(n: Node) -> Bool { + !dag_node_is_module_surface(n: n) +} + fn dag_inhabitant_atom(id: Symbol) -> Node { Node { kind: TypeNode { connective: Atom { identity: id } }, diff --git a/src/v2/lens/reference_derived_residency_reading.dag b/src/v2/lens/reference_derived_residency_reading.dag index 8f6fb06ca19..163774cf3c8 100644 --- a/src/v2/lens/reference_derived_residency_reading.dag +++ b/src/v2/lens/reference_derived_residency_reading.dag @@ -20,6 +20,10 @@ import std.decl_ref { DeclarationRef } import v2.std.algebra { length } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.extdeps.languages.dag { + dag_node_is_module_surface, + dag_node_is_record_field_carrier, +} import v2.std.node_query { member_edge_type_node } import v2.std.node { Arrow, @@ -168,26 +172,36 @@ fn decl_first_unrecognised_atom(decl: Node) -> Optional { } // A Named member that is not a binder is read first: the carrier declares no type for it, and that -// refuses the whole reading rather than letting the remaining fields decide it. +// refuses the whole reading rather than letting the remaining fields decide it. A module surface +// (declaration identity, graft-body stamp, or dag_surface_module_header production edge) is not a +// record payload, so its Authored members are skipped rather than judged as fields. fn decl_first_member_not_a_binder(decl: Node) -> Optional { - match decl.kind { - TypeNode { connective: _ } => - fold(decl.children, init: optional_absent(), f: fn(acc, edge) { - match acc { - Present { value: _ } => acc - Absent => - match edge.label { - Positional => acc - StructuralLabel { label: _ } => acc - Authored { name: nm } => - match member_edge_type_node(e: edge) { - Present { value: _ } => acc - Absent => optional_present(value: nm) - } - } - } - }) - ComputationNode { behavior: _ } => optional_absent() + if !dag_node_is_record_field_carrier(n: decl) { + optional_absent() + } else { + match decl.kind { + TypeNode { connective: _ } => + fold(decl.children, init: optional_absent(), f: fn(acc, edge) { + match acc { + Present { value: _ } => acc + Absent => + match edge.label { + Positional => acc + StructuralLabel { label: _ } => acc + Authored { name: nm } => + if dag_node_is_module_surface(n: edge.target) { + acc + } else { + match member_edge_type_node(e: edge) { + Present { value: _ } => acc + Absent => optional_present(value: nm) + } + } + } + } + }) + ComputationNode { behavior: _ } => optional_absent() + } } } diff --git a/src/v2/test/claim/edge_label/structural_label_readers_test.dag b/src/v2/test/claim/edge_label/structural_label_readers_test.dag index 20a2562395f..8960cf29b93 100644 --- a/src/v2/test/claim/edge_label/structural_label_readers_test.dag +++ b/src/v2/test/claim/edge_label/structural_label_readers_test.dag @@ -26,6 +26,9 @@ import v2.extdeps.languages.dag { dag_surface_edge_label, dag_surface_module_header_metadata_edge, dag_node_is_module_root_conj, + dag_node_is_module_surface, + dag_node_is_record_field_carrier, + dag_named_edge, } import v2.compiler.body_lowering_fold { body_lower_edge_names_where_clause } import v2.compiler.resolution_provenance { ReferenceSite } @@ -74,6 +77,28 @@ test fn a_member_spelled_like_the_header_does_not_make_a_module_root() -> Bool { && !dag_node_is_module_root_conj(n: slr_conj(children: [slr_authored(name: ^dag_surface_module_header)])) } +fn slr_identity(id: Symbol) -> Node { + slr_conj(children: [dag_named_edge(name: ^grammar_production_identity_node_projection, target: slr_atom(id: id))]) +} + +// THE THREE STAMPS, AND NOT THE CHILDREN SHAPE. A module surface is a declaration identity, a +// graft-body stamp, or a dag_surface_module_header production edge. A Conj of Authored members that +// merely looks like a container is a record-field carrier. +test fn a_module_surface_is_stamped_not_guessed_from_children() -> Bool { + dag_node_is_module_surface(n: slr_identity(id: ^dag_surface_module)) + && dag_node_is_module_surface(n: slr_identity(id: ^dag_surface_fn_decl)) + && dag_node_is_module_surface(n: slr_identity(id: ^namespace_graft_module_body)) + && dag_node_is_module_surface(n: slr_conj(children: [dag_surface_edge(edge: ^dag_surface_module_header, target: slr_atom(id: ^h))])) + && !dag_node_is_module_surface(n: slr_conj(children: [slr_authored(name: ^dag_surface_module_header)])) + && !dag_node_is_module_surface(n: slr_conj(children: [slr_authored(name: ^x)])) +} + +test fn a_hand_built_record_is_a_field_carrier_and_a_module_surface_is_not() -> Bool { + dag_node_is_record_field_carrier(n: slr_conj(children: [slr_authored(name: ^x)])) + && !dag_node_is_record_field_carrier(n: slr_identity(id: ^dag_surface_module)) + && !dag_node_is_record_field_carrier(n: slr_identity(id: ^dag_surface_type_decl)) +} + // Reader 3: v2.compiler.body_lowering_fold body_lower_edge_names_where_clause. test fn a_field_spelled_like_the_where_clause_is_not_one() -> Bool { body_lower_edge_names_where_clause(e: dag_surface_edge(edge: ^dag_surface_where_refinement_clause, target: slr_atom(id: ^w))) diff --git a/src/v2/test/claim/reference_derived_residency_reading_test.dag b/src/v2/test/claim/reference_derived_residency_reading_test.dag index 206bdda0a43..f8b5ae091d2 100644 --- a/src/v2/test/claim/reference_derived_residency_reading_test.dag +++ b/src/v2/test/claim/reference_derived_residency_reading_test.dag @@ -34,7 +34,11 @@ import v2.lens.reference_derived_residency_reading { carrier_reading_from_type_d import v2.std.diagnostic { Accepted, Rejected } import std.optional { Absent, Present } import v2.std.qualified_name { qualified_name_snoc } -import v2.extdeps.languages.dag { qualified_name_from_module_node } +import v2.extdeps.languages.dag { + qualified_name_from_module_node, + dag_named_edge, + dag_surface_edge, +} import v2.std.symbol_index { empty_symbol_index, symbol_index_lookup } import v2.test.claim.reference_derived_graph_fixture { xl4_fixture_trees, xl4_item_at, consumer_field_bare_local_path } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -238,6 +242,53 @@ test fn an_alias_typed_field_is_not_readable() -> Bool { !readability_is_recognised(r: reading_of(decl_name: "ReferenceDerivedPool", decl: alias_typed_declaration())) } +fn identity_stamped(id: Symbol) -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [dag_named_edge(name: ^grammar_production_identity_node_projection, target: atom(identity: id))], + occurrence_id: OccurrenceSynthetic, + } +} + +fn module_container_with_surface_members() -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + dag_surface_edge(edge: ^dag_surface_module_header, target: atom(identity: ^dag_surface_module_header)), + Edge { label: Authored { name: ^bit }, target: identity_stamped(id: ^dag_surface_fn_decl) }, + Edge { label: Authored { name: ^nested }, target: identity_stamped(id: ^dag_surface_module) }, + ], + occurrence_id: OccurrenceSynthetic, + } +} + +fn hand_built_record_field_declaration() -> Node { + record_of(fields: [Edge { label: Authored { name: ^trees }, target: atom(identity: ^Node) }]) +} + +fn readability_is_member_not_a_binder(r: ProducerCarrierReading, expected: Symbol) -> Bool { + match r.readability { + CarrierMemberNotABinder { member: member } => member == expected + CarrierTypeRecognized => false + CarrierTypeUnrecognized { spelling: _ } => false + } +} + +// A MODULE CONTAINER IS NOT A RECORD PAYLOAD. Its Authored members are dag_surface_module nodes and +// declarations; asking member_edge_type_node of them used to mint CarrierMemberNotABinder. +test fn a_module_container_is_not_read_as_a_non_binder_record() -> Bool { + readability_is_recognised(r: reading_of(decl_name: "ModuleContainer", decl: module_container_with_surface_members())) +} + +// THE RED THE SKIP MUST NOT ERASE: a hand-built name-to-type field on a genuine record still reports +// CarrierMemberNotABinder, named. +test fn a_hand_built_record_field_still_reports_member_not_a_binder() -> Bool { + readability_is_member_not_a_binder( + r: reading_of(decl_name: "HandBuiltCarrier", decl: hand_built_record_field_declaration()), + expected: ^trees, + ) +} + // THE BLOCKING ARM. Under the earlier derivation this qualified -- both axes fell through to their // admitting values -- so a producer could buy the admission by renaming a type while the same corpus // stayed resident (review 68724). It must refuse, and it must refuse NAMING the carrier and the From 6fd8c6039d07d118b4cc27ac78e0363541a03dd4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 8 Oct 2026 04:48:53 +0000 Subject: [PATCH 2/4] Keep the residency skip identity-only; do not share a Conj-wide pair. #13560 owns the languages.dag predicates until that skip is constructional on a landed head. Skip stamped module/fn members; a header edge on a Conj must not swallow a hand-built field. Co-authored-by: Cursor --- src/v2/extdeps/languages/dag.dag | 32 --------- .../reference_derived_residency_reading.dag | 72 ++++++++++--------- .../structural_label_readers_test.dag | 25 ------- ...ference_derived_residency_reading_test.dag | 21 ++++++ 4 files changed, 61 insertions(+), 89 deletions(-) diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 795228c2a16..e4b4bcb6d73 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -4266,38 +4266,6 @@ fn dag_node_is_module_root_conj(n: Node) -> Bool { } } -// A MODULE SURFACE is identified by a stamp the producer actually wrote, never by the shape of its -// Authored children. Those children are declarations or nested modules, not record fields, and a -// lens that asks member_edge_type_node of them reports member_not_a_binder for a well-formed module. -fn dag_production_is_module_member_declaration(id: Symbol) -> Bool { - (id == ^dag_surface_fn_decl) - || (id == ^dag_surface_test_fn_decl) - || (id == ^dag_surface_data_decl) - || (id == ^dag_surface_type_decl) - || (id == ^dag_surface_alias_decl) - || (id == ^dag_surface_service_decl) - || (id == ^dag_surface_resource_decl) - || (id == ^dag_surface_import_decl) -} - -fn dag_node_is_module_surface(n: Node) -> Bool { - match parse_production_emitted_identity_optional(node: n) { - Present { value: id } => - (id == ^dag_surface_module) - || (id == ^dag_surface_module_header) - || (id == ^namespace_graft_module_body) - || dag_production_is_module_member_declaration(id: id) - Absent => dag_node_is_module_root_conj(n: n) - } -} - -// The complementary question for a binder-field fold: a payload that is not a module surface. A -// hand-built record Conj has neither a declaration identity nor a header production edge, so it -// remains a record-field carrier. -fn dag_node_is_record_field_carrier(n: Node) -> Bool { - !dag_node_is_module_surface(n: n) -} - fn dag_inhabitant_atom(id: Symbol) -> Node { Node { kind: TypeNode { connective: Atom { identity: id } }, diff --git a/src/v2/lens/reference_derived_residency_reading.dag b/src/v2/lens/reference_derived_residency_reading.dag index 163774cf3c8..0bf096af227 100644 --- a/src/v2/lens/reference_derived_residency_reading.dag +++ b/src/v2/lens/reference_derived_residency_reading.dag @@ -20,10 +20,7 @@ import std.decl_ref { DeclarationRef } import v2.std.algebra { length } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import std.optional { Absent, Optional, Present, optional_absent, optional_present } -import v2.extdeps.languages.dag { - dag_node_is_module_surface, - dag_node_is_record_field_carrier, -} +import v2.extdeps.languages.dag { parse_production_emitted_identity_optional } import v2.std.node_query { member_edge_type_node } import v2.std.node { Arrow, @@ -171,37 +168,48 @@ fn decl_first_unrecognised_atom(decl: Node) -> Optional { } } +// Parse-identity skip for a MEMBER TARGET, not for the enclosing Conj (DESIGN §5: refuse the +// member, never widen to the container). Shared languages.dag predicates wait on #13560 landing +// constructionally; this list is the same identities that skip names: module surface, graft-body +// stamp, dag_surface_fn_decl, dag_surface_test_fn_decl. A missing identity is not guessed from +// children shape. +fn residency_target_is_module_or_fn_identity(target: Node) -> Bool { + match parse_production_emitted_identity_optional(node: target) { + Present { value: id } => + (id == ^dag_surface_module) + || (id == ^namespace_graft_module_body) + || (id == ^dag_surface_fn_decl) + || (id == ^dag_surface_test_fn_decl) + Absent => false + } +} + // A Named member that is not a binder is read first: the carrier declares no type for it, and that -// refuses the whole reading rather than letting the remaining fields decide it. A module surface -// (declaration identity, graft-body stamp, or dag_surface_module_header production edge) is not a -// record payload, so its Authored members are skipped rather than judged as fields. +// refuses the whole reading rather than letting the remaining fields decide it. Identity-stamped +// module or fn members are skipped; a hand-built field on the same Conj is still refused. fn decl_first_member_not_a_binder(decl: Node) -> Optional { - if !dag_node_is_record_field_carrier(n: decl) { - optional_absent() - } else { - match decl.kind { - TypeNode { connective: _ } => - fold(decl.children, init: optional_absent(), f: fn(acc, edge) { - match acc { - Present { value: _ } => acc - Absent => - match edge.label { - Positional => acc - StructuralLabel { label: _ } => acc - Authored { name: nm } => - if dag_node_is_module_surface(n: edge.target) { - acc - } else { - match member_edge_type_node(e: edge) { - Present { value: _ } => acc - Absent => optional_present(value: nm) - } + match decl.kind { + TypeNode { connective: _ } => + fold(decl.children, init: optional_absent(), f: fn(acc, edge) { + match acc { + Present { value: _ } => acc + Absent => + match edge.label { + Positional => acc + StructuralLabel { label: _ } => acc + Authored { name: nm } => + if residency_target_is_module_or_fn_identity(target: edge.target) { + acc + } else { + match member_edge_type_node(e: edge) { + Present { value: _ } => acc + Absent => optional_present(value: nm) } - } - } - }) - ComputationNode { behavior: _ } => optional_absent() - } + } + } + } + }) + ComputationNode { behavior: _ } => optional_absent() } } diff --git a/src/v2/test/claim/edge_label/structural_label_readers_test.dag b/src/v2/test/claim/edge_label/structural_label_readers_test.dag index 8960cf29b93..20a2562395f 100644 --- a/src/v2/test/claim/edge_label/structural_label_readers_test.dag +++ b/src/v2/test/claim/edge_label/structural_label_readers_test.dag @@ -26,9 +26,6 @@ import v2.extdeps.languages.dag { dag_surface_edge_label, dag_surface_module_header_metadata_edge, dag_node_is_module_root_conj, - dag_node_is_module_surface, - dag_node_is_record_field_carrier, - dag_named_edge, } import v2.compiler.body_lowering_fold { body_lower_edge_names_where_clause } import v2.compiler.resolution_provenance { ReferenceSite } @@ -77,28 +74,6 @@ test fn a_member_spelled_like_the_header_does_not_make_a_module_root() -> Bool { && !dag_node_is_module_root_conj(n: slr_conj(children: [slr_authored(name: ^dag_surface_module_header)])) } -fn slr_identity(id: Symbol) -> Node { - slr_conj(children: [dag_named_edge(name: ^grammar_production_identity_node_projection, target: slr_atom(id: id))]) -} - -// THE THREE STAMPS, AND NOT THE CHILDREN SHAPE. A module surface is a declaration identity, a -// graft-body stamp, or a dag_surface_module_header production edge. A Conj of Authored members that -// merely looks like a container is a record-field carrier. -test fn a_module_surface_is_stamped_not_guessed_from_children() -> Bool { - dag_node_is_module_surface(n: slr_identity(id: ^dag_surface_module)) - && dag_node_is_module_surface(n: slr_identity(id: ^dag_surface_fn_decl)) - && dag_node_is_module_surface(n: slr_identity(id: ^namespace_graft_module_body)) - && dag_node_is_module_surface(n: slr_conj(children: [dag_surface_edge(edge: ^dag_surface_module_header, target: slr_atom(id: ^h))])) - && !dag_node_is_module_surface(n: slr_conj(children: [slr_authored(name: ^dag_surface_module_header)])) - && !dag_node_is_module_surface(n: slr_conj(children: [slr_authored(name: ^x)])) -} - -test fn a_hand_built_record_is_a_field_carrier_and_a_module_surface_is_not() -> Bool { - dag_node_is_record_field_carrier(n: slr_conj(children: [slr_authored(name: ^x)])) - && !dag_node_is_record_field_carrier(n: slr_identity(id: ^dag_surface_module)) - && !dag_node_is_record_field_carrier(n: slr_identity(id: ^dag_surface_type_decl)) -} - // Reader 3: v2.compiler.body_lowering_fold body_lower_edge_names_where_clause. test fn a_field_spelled_like_the_where_clause_is_not_one() -> Bool { body_lower_edge_names_where_clause(e: dag_surface_edge(edge: ^dag_surface_where_refinement_clause, target: slr_atom(id: ^w))) diff --git a/src/v2/test/claim/reference_derived_residency_reading_test.dag b/src/v2/test/claim/reference_derived_residency_reading_test.dag index f8b5ae091d2..92c26275d4a 100644 --- a/src/v2/test/claim/reference_derived_residency_reading_test.dag +++ b/src/v2/test/claim/reference_derived_residency_reading_test.dag @@ -262,6 +262,17 @@ fn module_container_with_surface_members() -> Node { } } +fn conj_with_header_edge_and_hand_built_field() -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + dag_surface_edge(edge: ^dag_surface_module_header, target: atom(identity: ^dag_surface_module_header)), + Edge { label: Authored { name: ^trees }, target: atom(identity: ^Node) }, + ], + occurrence_id: OccurrenceSynthetic, + } +} + fn hand_built_record_field_declaration() -> Node { record_of(fields: [Edge { label: Authored { name: ^trees }, target: atom(identity: ^Node) }]) } @@ -289,6 +300,16 @@ test fn a_hand_built_record_field_still_reports_member_not_a_binder() -> Bool { ) } +// A HEADER EDGE ON THE CONJ IS NOT A CONTAINER-WIDE SKIP. The member that is not identity-stamped +// is still CarrierMemberNotABinder; skipping the whole Conj because it carries a header is the +// widen the door-lens ruling forbids. +test fn a_header_edge_on_a_conj_does_not_skip_a_hand_built_field() -> Bool { + readability_is_member_not_a_binder( + r: reading_of(decl_name: "HeaderAndField", decl: conj_with_header_edge_and_hand_built_field()), + expected: ^trees, + ) +} + // THE BLOCKING ARM. Under the earlier derivation this qualified -- both axes fell through to their // admitting values -- so a producer could buy the admission by renaming a type while the same corpus // stayed resident (review 68724). It must refuse, and it must refuse NAMING the carrier and the From 0aa89c7d133c8e0dff8bb187cfda7d3d40257551 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 8 Oct 2026 05:03:45 +0000 Subject: [PATCH 3/4] Home the module-member identity skip in languages.dag. The residency lens was carrying a four-identity copy and omitting data/type/alias declarations; the skip is now one parse-identity predicate, still not a Conj-wide container skip. Co-authored-by: Cursor --- src/v2/extdeps/languages/dag.dag | 24 +++++++++++++++++++ .../reference_derived_residency_reading.dag | 23 ++++-------------- ...ference_derived_residency_reading_test.dag | 24 +++++++++++++++++++ 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index e4b4bcb6d73..26db397553a 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -6774,6 +6774,30 @@ fn parse_production_emitted_identity_optional(node: Node) -> Optional { } } +// Parse identities that inhabit a module as Authored members and are not record binders: +// the module surface, the graft-body stamp, and every declaration production the grammar +// emits as a top-level item. Identity only — a missing stamp is false, never a Conj-shape +// guess (DESIGN §5: refuse the member, never widen to the container). +fn dag_surface_identity_is_module_member(id: Symbol) -> Bool { + (id == ^dag_surface_module) + || (id == ^namespace_graft_module_body) + || (id == ^dag_surface_fn_decl) + || (id == ^dag_surface_test_fn_decl) + || (id == ^dag_surface_data_decl) + || (id == ^dag_surface_type_decl) + || (id == ^dag_surface_alias_decl) + || (id == ^dag_surface_service_decl) + || (id == ^dag_surface_resource_decl) + || (id == ^dag_surface_import_decl) +} + +fn dag_node_is_module_member_surface(node: Node) -> Bool { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => dag_surface_identity_is_module_member(id: id) + Absent => false + } +} + fn dag_node_is_fn_decl(node: Node) -> Bool { match parse_production_emitted_identity_optional(node: node) { Present { value: id } => id == ^dag_surface_fn_decl diff --git a/src/v2/lens/reference_derived_residency_reading.dag b/src/v2/lens/reference_derived_residency_reading.dag index 0bf096af227..57a30b9abe7 100644 --- a/src/v2/lens/reference_derived_residency_reading.dag +++ b/src/v2/lens/reference_derived_residency_reading.dag @@ -20,7 +20,7 @@ import std.decl_ref { DeclarationRef } import v2.std.algebra { length } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import std.optional { Absent, Optional, Present, optional_absent, optional_present } -import v2.extdeps.languages.dag { parse_production_emitted_identity_optional } +import v2.extdeps.languages.dag { dag_node_is_module_member_surface } import v2.std.node_query { member_edge_type_node } import v2.std.node { Arrow, @@ -168,25 +168,10 @@ fn decl_first_unrecognised_atom(decl: Node) -> Optional { } } -// Parse-identity skip for a MEMBER TARGET, not for the enclosing Conj (DESIGN §5: refuse the -// member, never widen to the container). Shared languages.dag predicates wait on #13560 landing -// constructionally; this list is the same identities that skip names: module surface, graft-body -// stamp, dag_surface_fn_decl, dag_surface_test_fn_decl. A missing identity is not guessed from -// children shape. -fn residency_target_is_module_or_fn_identity(target: Node) -> Bool { - match parse_production_emitted_identity_optional(node: target) { - Present { value: id } => - (id == ^dag_surface_module) - || (id == ^namespace_graft_module_body) - || (id == ^dag_surface_fn_decl) - || (id == ^dag_surface_test_fn_decl) - Absent => false - } -} - // A Named member that is not a binder is read first: the carrier declares no type for it, and that // refuses the whole reading rather than letting the remaining fields decide it. Identity-stamped -// module or fn members are skipped; a hand-built field on the same Conj is still refused. +// module or declaration members (v2.extdeps.languages.dag dag_node_is_module_member_surface) are +// skipped per target; a hand-built field on the same Conj is still refused. fn decl_first_member_not_a_binder(decl: Node) -> Optional { match decl.kind { TypeNode { connective: _ } => @@ -198,7 +183,7 @@ fn decl_first_member_not_a_binder(decl: Node) -> Optional { Positional => acc StructuralLabel { label: _ } => acc Authored { name: nm } => - if residency_target_is_module_or_fn_identity(target: edge.target) { + if dag_node_is_module_member_surface(node: edge.target) { acc } else { match member_edge_type_node(e: edge) { diff --git a/src/v2/test/claim/reference_derived_residency_reading_test.dag b/src/v2/test/claim/reference_derived_residency_reading_test.dag index 92c26275d4a..e0f8dae588c 100644 --- a/src/v2/test/claim/reference_derived_residency_reading_test.dag +++ b/src/v2/test/claim/reference_derived_residency_reading_test.dag @@ -257,6 +257,21 @@ fn module_container_with_surface_members() -> Node { dag_surface_edge(edge: ^dag_surface_module_header, target: atom(identity: ^dag_surface_module_header)), Edge { label: Authored { name: ^bit }, target: identity_stamped(id: ^dag_surface_fn_decl) }, Edge { label: Authored { name: ^nested }, target: identity_stamped(id: ^dag_surface_module) }, + Edge { label: Authored { name: ^probe }, target: identity_stamped(id: ^dag_surface_test_fn_decl) }, + Edge { label: Authored { name: ^Flag }, target: identity_stamped(id: ^dag_surface_data_decl) }, + Edge { label: Authored { name: ^Wrapper }, target: identity_stamped(id: ^dag_surface_type_decl) }, + Edge { label: Authored { name: ^Alias }, target: identity_stamped(id: ^dag_surface_alias_decl) }, + ], + occurrence_id: OccurrenceSynthetic, + } +} + +fn mixed_declaration_and_hand_built_field() -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Authored { name: ^Flag }, target: identity_stamped(id: ^dag_surface_data_decl) }, + Edge { label: Authored { name: ^trees }, target: atom(identity: ^Node) }, ], occurrence_id: OccurrenceSynthetic, } @@ -310,6 +325,15 @@ test fn a_header_edge_on_a_conj_does_not_skip_a_hand_built_field() -> Bool { ) } +// A DATA DECLARATION SIBLING DOES NOT WIDEN THE SKIP. The identity-stamped member is skipped; +// the hand-built field on the same Conj is still CarrierMemberNotABinder. +test fn a_data_decl_member_does_not_skip_a_hand_built_sibling() -> Bool { + readability_is_member_not_a_binder( + r: reading_of(decl_name: "Mixed", decl: mixed_declaration_and_hand_built_field()), + expected: ^trees, + ) +} + // THE BLOCKING ARM. Under the earlier derivation this qualified -- both axes fell through to their // admitting values -- so a producer could buy the admission by renaming a type while the same corpus // stayed resident (review 68724). It must refuse, and it must refuse NAMING the carrier and the From 48b707822ef08788fd104ad3112f143018e2c81d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 8 Oct 2026 05:16:23 +0000 Subject: [PATCH 4/4] Derive top-level declaration identities from the grammar rows. One production list now feeds the module-item choice, the residency skip, and iard_is_declaration. Module surface and the graft-body stamp stay the only extras. Co-authored-by: Cursor --- src/v2/cli/if_arm_reader_differential.dag | 9 +- src/v2/extdeps/languages/dag.dag | 170 +++++++++--------- ...ference_derived_residency_reading_test.dag | 9 + 3 files changed, 96 insertions(+), 92 deletions(-) diff --git a/src/v2/cli/if_arm_reader_differential.dag b/src/v2/cli/if_arm_reader_differential.dag index d9347aaea27..8b6d497f9f2 100644 --- a/src/v2/cli/if_arm_reader_differential.dag +++ b/src/v2/cli/if_arm_reader_differential.dag @@ -23,6 +23,7 @@ import v2.extdeps.languages.dag { ParseSubtreeAbsent, ParseSubtreeFound, dag_lex, + dag_surface_identity_is_top_level_item, dag_surface_kw_then_ident_from_captured, parse_production_captured_child_optional, parse_production_emitted_identity_optional @@ -280,13 +281,7 @@ fn iard_declaration_name(shell: Node, emitted: Symbol) -> String { } fn iard_is_declaration(id: Symbol) -> Bool { - (id == ^dag_surface_fn_decl) - || (id == ^dag_surface_test_fn_decl) - || (id == ^dag_surface_data_decl) - || (id == ^dag_surface_type_decl) - || (id == ^dag_surface_alias_decl) - || (id == ^dag_surface_service_decl) - || (id == ^dag_surface_resource_decl) + dag_surface_identity_is_top_level_item(id: id) } // EVERY if_expr IN THE TREE, IN PRE-ORDER, WITH ITS ENCLOSING DECLARATION. Nested ifs are visited: diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 26db397553a..115232f3d18 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -3113,28 +3113,67 @@ fn dag_grammar_resource_decl_expr() -> GrammarExpr { ) } -fn dag_grammar_top_level_item_expr() -> GrammarExpr { - dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_import_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_alias_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_type_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_data_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_fn_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_test_fn_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_service_decl), - right: dag_grammar_nonterminal(production: ^dag_production_resource_decl) - ) - ) - ) - ) - ) +// THE TOP-LEVEL DECLARATION ROWS: one list is the authority for which productions inhabit a +// module as items, for the choice the module grammar repeats, and for which emitted identities +// are declaration members rather than record binders. +fn dag_grammar_top_level_item_productions() -> List { + [ + dag_grammar_production( + name: ^dag_production_import_decl, + expression: dag_grammar_import_decl_expr(), + emitted: ^dag_surface_import_decl + ), + dag_grammar_production( + name: ^dag_production_alias_decl, + expression: dag_grammar_alias_decl_expr(), + emitted: ^dag_surface_alias_decl + ), + dag_grammar_production( + name: ^dag_production_type_decl, + expression: dag_grammar_type_decl_expr(), + emitted: ^dag_surface_type_decl + ), + dag_grammar_production( + name: ^dag_production_data_decl, + expression: dag_grammar_data_decl_expr(), + emitted: ^dag_surface_data_decl + ), + dag_grammar_production( + name: ^dag_production_fn_decl, + expression: dag_grammar_fn_decl_expr(), + emitted: ^dag_surface_fn_decl + ), + dag_grammar_production( + name: ^dag_production_test_fn_decl, + expression: dag_grammar_test_fn_decl_expr(), + emitted: ^dag_surface_test_fn_decl + ), + dag_grammar_production( + name: ^dag_production_service_decl, + expression: dag_grammar_service_decl_expr(), + emitted: ^dag_surface_service_decl + ), + dag_grammar_production( + name: ^dag_production_resource_decl, + expression: dag_grammar_resource_decl_expr(), + emitted: ^dag_surface_resource_decl ) + ] +} + +fn dag_grammar_choice_of_production_names(names: List) -> GrammarExpr { + match names { + Cons { head: first, tail: rest } => + fold(rest, init: dag_grammar_nonterminal(production: first), f: fn(acc, name) { + dag_grammar_choice(left: acc, right: dag_grammar_nonterminal(production: name)) + }) + Empty => dag_grammar_nonterminal(production: ^dag_production_top_level_item) + } +} + +fn dag_grammar_top_level_item_expr() -> GrammarExpr { + dag_grammar_choice_of_production_names( + names: list_map(xs: dag_grammar_top_level_item_productions(), f: fn(p) { p.name }) ) } @@ -3368,46 +3407,6 @@ fn dag_grammar_root() -> GrammarRoot { expression: dag_grammar_top_level_item_expr(), emitted: ^dag_surface_top_level_item ) - let import_decl = dag_grammar_production( - name: ^dag_production_import_decl, - expression: dag_grammar_import_decl_expr(), - emitted: ^dag_surface_import_decl - ) - let alias_decl = dag_grammar_production( - name: ^dag_production_alias_decl, - expression: dag_grammar_alias_decl_expr(), - emitted: ^dag_surface_alias_decl - ) - let type_decl = dag_grammar_production( - name: ^dag_production_type_decl, - expression: dag_grammar_type_decl_expr(), - emitted: ^dag_surface_type_decl - ) - let data_decl = dag_grammar_production( - name: ^dag_production_data_decl, - expression: dag_grammar_data_decl_expr(), - emitted: ^dag_surface_data_decl - ) - let fn_decl = dag_grammar_production( - name: ^dag_production_fn_decl, - expression: dag_grammar_fn_decl_expr(), - emitted: ^dag_surface_fn_decl - ) - let test_fn_decl = dag_grammar_production( - name: ^dag_production_test_fn_decl, - expression: dag_grammar_test_fn_decl_expr(), - emitted: ^dag_surface_test_fn_decl - ) - let service_decl = dag_grammar_production( - name: ^dag_production_service_decl, - expression: dag_grammar_service_decl_expr(), - emitted: ^dag_surface_service_decl - ) - let resource_decl = dag_grammar_production( - name: ^dag_production_resource_decl, - expression: dag_grammar_resource_decl_expr(), - emitted: ^dag_surface_resource_decl - ) let resource_body_entry = dag_grammar_production( name: ^dag_production_resource_body_entry, expression: dag_grammar_resource_body_entry_expr(), @@ -3823,19 +3822,12 @@ fn dag_grammar_root() -> GrammarRoot { ) GrammarRoot { start: ^dag_production_module, - productions: [ - module_prod, - module_header, - top_level_item, - import_decl, - alias_decl, - type_decl, - data_decl, - fn_decl, - test_fn_decl, - service_decl, + productions: list_append( + left: [module_prod, module_header, top_level_item], + right: list_append( + left: dag_grammar_top_level_item_productions(), + right: [ service_body_entry, - resource_decl, resource_body_entry, resource_kind, resource_mode, @@ -3917,7 +3909,9 @@ fn dag_grammar_root() -> GrammarRoot { if_expr, fn_literal, arrow_lambda - ], + ] + ) + ), sync_tokens: dag_sync_tokens() } } @@ -6774,21 +6768,27 @@ fn parse_production_emitted_identity_optional(node: Node) -> Optional { } } +fn dag_surface_identity_is_top_level_item(id: Symbol) -> Bool { + fold_list( + xs: dag_grammar_top_level_item_productions(), + empty: false, + cons: fn(acc, production) { + acc || match node_atom_identity_optional(node: production.emitted) { + Present { value: emitted } => emitted == id + Absent => false + } + } + ) +} + // Parse identities that inhabit a module as Authored members and are not record binders: -// the module surface, the graft-body stamp, and every declaration production the grammar -// emits as a top-level item. Identity only — a missing stamp is false, never a Conj-shape -// guess (DESIGN §5: refuse the member, never widen to the container). +// the module surface and graft-body stamp as the only explicit extras, plus every emitted +// identity of dag_grammar_top_level_item_productions. Identity only — a missing stamp is +// false, never a Conj-shape guess (DESIGN §5: refuse the member, never widen to the container). fn dag_surface_identity_is_module_member(id: Symbol) -> Bool { (id == ^dag_surface_module) || (id == ^namespace_graft_module_body) - || (id == ^dag_surface_fn_decl) - || (id == ^dag_surface_test_fn_decl) - || (id == ^dag_surface_data_decl) - || (id == ^dag_surface_type_decl) - || (id == ^dag_surface_alias_decl) - || (id == ^dag_surface_service_decl) - || (id == ^dag_surface_resource_decl) - || (id == ^dag_surface_import_decl) + || dag_surface_identity_is_top_level_item(id: id) } fn dag_node_is_module_member_surface(node: Node) -> Bool { diff --git a/src/v2/test/claim/reference_derived_residency_reading_test.dag b/src/v2/test/claim/reference_derived_residency_reading_test.dag index e0f8dae588c..2a10574d00f 100644 --- a/src/v2/test/claim/reference_derived_residency_reading_test.dag +++ b/src/v2/test/claim/reference_derived_residency_reading_test.dag @@ -38,6 +38,8 @@ import v2.extdeps.languages.dag { qualified_name_from_module_node, dag_named_edge, dag_surface_edge, + dag_surface_identity_is_module_member, + dag_surface_identity_is_top_level_item, } import v2.std.symbol_index { empty_symbol_index, symbol_index_lookup } import v2.test.claim.reference_derived_graph_fixture { xl4_fixture_trees, xl4_item_at, consumer_field_bare_local_path } @@ -325,6 +327,13 @@ test fn a_header_edge_on_a_conj_does_not_skip_a_hand_built_field() -> Bool { ) } +test fn top_level_item_identities_come_from_the_grammar_rows() -> Bool { + dag_surface_identity_is_top_level_item(id: ^dag_surface_data_decl) + && dag_surface_identity_is_top_level_item(id: ^dag_surface_import_decl) + && !dag_surface_identity_is_top_level_item(id: ^dag_surface_module) + && dag_surface_identity_is_module_member(id: ^dag_surface_module) +} + // A DATA DECLARATION SIBLING DOES NOT WIDEN THE SKIP. The identity-stamped member is skipped; // the hand-built field on the same Conj is still CarrierMemberNotABinder. test fn a_data_decl_member_does_not_skip_a_hand_built_sibling() -> Bool {