From e571a382e4878d887dbf3bc9540879cb175dff14 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 2 Oct 2026 16:30:35 +0000 Subject: [PATCH 1/2] Seal reaches through nested coproducts: one member-type reader for the deserialize walk type_expr_reaches_sealed_carrier read a looked-up coproduct's variants as fields; child_type_node resolves a variant to the coproduct itself, already in the seen-set, so a seal two coproducts deep was never reached and its containers kept a derived Deserialize (a second, unsealed mint). The top-level enum entry read variants correctly, so the two readings disagreed. member_type_nodes now defines a declaration's member types once (product: fields; coproduct: variant payload fields) and the walk and both entries use it. Controls in emitter_sole_constructor_seal_witness_test: a seal two coproducts deep drops Deserialize from both containers; the same shape over an Int keeps it. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ter_sole_constructor_seal_witness_test.dag | 38 +++++++++++++++++ src/v1/05_emit_rust.dag | 42 ++++++++++++------- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/dag/test/claim/emitter_sole_constructor_seal_witness_test.dag b/dag/test/claim/emitter_sole_constructor_seal_witness_test.dag index 683e4d5f16d..8ac62f90519 100644 --- a/dag/test/claim/emitter_sole_constructor_seal_witness_test.dag +++ b/dag/test/claim/emitter_sole_constructor_seal_witness_test.dag @@ -92,6 +92,36 @@ fn w_plain_record_keeps_deserialize() -> Bool { ) } +// THE SEAL REACHES THROUGH NESTED COPRODUCTS. A seal held two coproducts deep (Outer's variant +// carries Inner, whose variant carries Sealed) must drop Deserialize from both containers: deriving +// it on Outer mints a Sealed through serde_json with no mint call, the one-level forge one level +// further out. The walk once read a looked-up coproduct's variants as fields and stopped at the +// coproduct's own name, so Outer kept the derive (v2.std.node EdgeLabel -> StructuralEdgeLabel -> +// CoreEdgeLabel -> BodyTermLoweredSeal refused E0277 in the emitted crate). The plain pair beside it +// is the control: the same shape over an Int must keep Deserialize, or the pattern below matches +// nothing and the red is vacuous. +fn nested_seal_fixture_source() -> String { + "module solector_nested_seal\n\ntype Sealed sole_constructor { admitted: Int }\n\ntype Inner\n = Wrap { s: Sealed }\n | Bare\n\ntype Outer\n = Hold { i: Inner }\n | Vacant\n\ntype PlainInner\n = PlainWrap { n: Int }\n | PlainBare\n\ntype PlainOuter\n = PlainHold { i: PlainInner }\n | PlainVacant\n\nfn mint(v: Int) -> Sealed {\n Sealed { admitted: v }\n}\n" +} + +fn w_nested_coproduct_over_a_seal_drops_deserialize() -> Bool { + compile_dag_rust_emit_check( + nested_seal_fixture_source(), + "src/solector_nested_seal.rs", + ["pub enum Outer {", "pub enum Inner {"], + ["serde::Deserialize)]\n#[serde(tag = \"_variant\")]\npub enum Outer {", "serde::Deserialize)]\n#[serde(tag = \"_variant\")]\npub enum Inner {"] + ) +} + +fn w_nested_plain_coproduct_keeps_deserialize() -> Bool { + compile_dag_rust_emit_check( + nested_seal_fixture_source(), + "src/solector_nested_seal.rs", + ["serde::Deserialize)]\n#[serde(tag = \"_variant\")]\npub enum PlainOuter {", "serde::Deserialize)]\n#[serde(tag = \"_variant\")]\npub enum PlainInner {"], + [] + ) +} + test fn sealed_record_field_is_private() -> Bool { w_sealed_record_field_is_private() } @@ -119,3 +149,11 @@ test fn plain_record_keeps_public_field() -> Bool { test fn plain_record_keeps_deserialize() -> Bool { w_plain_record_keeps_deserialize() } + +test fn nested_coproduct_over_a_seal_drops_deserialize() -> Bool { + w_nested_coproduct_over_a_seal_drops_deserialize() +} + +test fn nested_plain_coproduct_keeps_deserialize() -> Bool { + w_nested_plain_coproduct_keeps_deserialize() +} diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 196ebd4a099..baecaef5347 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -8209,7 +8209,7 @@ fn enum_derives(name: String, children: List, has_fn_fields: Bool, deseria // same has_fn_fields here rather than deriving it twice. fn emit_enum_from_children(name: String, type_params: String, generic_param_names: List, children: List, recursive_types: Set, shared_types: Set, env: TypeEnv, serde_policy: RustEnumWireSerde, emit_info: EmitGraphInfo) -> String { let has_fn_fields = type_has_fn_fields(name: name, emit_info: emit_info) - let deserialize_forbidden = enum_variant_payloads_forbid_deserialize(name: name, children: children, emit_info: emit_info, source_indices: env.source_indices) + let deserialize_forbidden = members_forbid_deserialize(name: name, members: member_type_nodes(is_product: false, children: children), emit_info: emit_info, source_indices: env.source_indices) let derives = enum_derives(name: name, children: children, has_fn_fields: has_fn_fields, deserialize_forbidden: deserialize_forbidden, generic_param_names: generic_param_names, emit_info: emit_info, source_indices: env.source_indices) let effective_serde_policy = if has_fn_fields { rust_serde_policy(enum_attr: "", rename_prefix: none, rename_suffix: none, rename_style: none) @@ -10881,8 +10881,8 @@ fn type_expr_reaches_sealed_carrier(n: Node, emit_info: EmitGraphInfo, source_in true } else { let next_seen = map_insert(seen, name, true) - decl.children |> any(child => - type_expr_reaches_sealed_carrier(n: child_type_node(ch: child), emit_info: emit_info, source_indices: source_indices, seen: next_seen) + decl_member_type_nodes(decl: decl) |> any(member => + type_expr_reaches_sealed_carrier(n: member, emit_info: emit_info, source_indices: source_indices, seen: next_seen) ) } Absent => false @@ -10890,10 +10890,29 @@ fn type_expr_reaches_sealed_carrier(n: Node, emit_info: EmitGraphInfo, source_in } } -fn decl_children_forbid_deserialize(name: String, children: List, emit_info: EmitGraphInfo, source_indices: Map) -> Bool { +// THE MEMBER TYPES OF A DECLARATION, read the same way at every depth. A product's members are its +// fields; a coproduct's are its variants' payload fields. The walk once read a LOOKED-UP coproduct's +// children as if they were fields, and child_type_node resolves a variant to the coproduct itself -- +// already in the seen-set -- so a seal two coproducts deep was never reached: v2.std.node's +// EdgeLabel -> StructuralEdgeLabel -> CoreEdgeLabel -> BodyTermLoweredSeal kept a derived Deserialize +// on every container and the emitted crate refused E0277. The top-level enum entry read variants +// correctly, so the defect was the two readings disagreeing; one reader closes it. +fn member_type_nodes(is_product: Bool, children: List) -> List { + if is_product { + children |> map(field => child_type_node(ch: field)) + } else { + children |> flat_map(variant => variant.children |> map(field => child_type_node(ch: field))) + } +} + +fn decl_member_type_nodes(decl: Node) -> List { + member_type_nodes(is_product: is_product_type(n: decl), children: decl.children) +} + +fn members_forbid_deserialize(name: String, members: List, emit_info: EmitGraphInfo, source_indices: Map) -> Bool { let seen = map_insert(empty_map(), qualified_last_segment(name: name), true) - children |> any(child => - type_expr_reaches_sealed_carrier(n: child_type_node(ch: child), emit_info: emit_info, source_indices: source_indices, seen: seen) + members |> any(member => + type_expr_reaches_sealed_carrier(n: member, emit_info: emit_info, source_indices: source_indices, seen: seen) ) } @@ -10901,19 +10920,10 @@ fn item_forbids_deserialize(item: Node, emit_info: EmitGraphInfo, source_indices if item_seals_construction(item: item) { true } else { - decl_children_forbid_deserialize(name: authored_name_at(source_indices: source_indices, node: item), children: item.children, emit_info: emit_info, source_indices: source_indices) + members_forbid_deserialize(name: authored_name_at(source_indices: source_indices, node: item), members: decl_member_type_nodes(decl: item), emit_info: emit_info, source_indices: source_indices) } } -fn enum_variant_payloads_forbid_deserialize(name: String, children: List, emit_info: EmitGraphInfo, source_indices: Map) -> Bool { - let seen = map_insert(empty_map(), qualified_last_segment(name: name), true) - children |> any(variant => - variant.children |> any(field => - type_expr_reaches_sealed_carrier(n: child_type_node(ch: field), emit_info: emit_info, source_indices: source_indices, seen: seen) - ) - ) -} - // A sealed record's fields are private in the emitted crate, so a stored-field read // must go through the accessor emit_sealed_struct_accessors wrote. THE CHOKE POINT WAS // ALREADY HERE AND WAS SIMPLY UNROUTED FOR PRODUCTS: the FieldSummary access_style From 45da73c780e023047fdb340949d9f12f8a3cea7a Mon Sep 17 00:00:00 2001 From: neat-boar-16 Date: Fri, 2 Oct 2026 18:26:18 +0000 Subject: [PATCH 2/2] Regenerate stage0 v1_compiler_emit_rust.rs mirror (required-regen candidate, srv1) --- src/v1/stage0/src/v1_compiler_emit_rust.rs | 104 +++++++++++---------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index a5c5a21c1a4..329de35f3f0 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -17473,9 +17473,9 @@ pub fn emit_enum_from_children( ) -> String { { let has_fn_fields = type_has_fn_fields(name.clone(), emit_info.clone()); - let deserialize_forbidden = enum_variant_payloads_forbid_deserialize( + let deserialize_forbidden = members_forbid_deserialize( name.clone(), - children.clone(), + member_type_nodes(false, children.clone()), emit_info.clone(), env.source_indices.clone(), ); @@ -23206,11 +23206,11 @@ pub fn type_expr_reaches_sealed_carrier( v1_rt::rc_map_insert(seen.clone(), name.clone(), true); { let mut __found = false; - for child in decl.children.clone().iter().cloned() { + for member in + decl_member_type_nodes(decl.clone()).iter().cloned() + { if type_expr_reaches_sealed_carrier( - crate::v1_compiler_infer_types::child_type_node( - child.clone(), - ), + member.clone(), emit_info.clone(), source_indices.clone(), next_seen.clone(), @@ -23231,9 +23231,50 @@ pub fn type_expr_reaches_sealed_carrier( }) } -pub fn decl_children_forbid_deserialize( +pub fn member_type_nodes(is_product: bool, children: Rc>>) -> Rc>> { + if is_product.clone() { + Rc::new({ + let mut __result = Vec::new(); + for field in children.iter().cloned() { + __result.push(crate::v1_compiler_infer_types::child_type_node( + field.clone(), + )); + } + __result + }) + } else { + Rc::new({ + let mut __result = Vec::new(); + for variant in children.iter().cloned() { + __result.extend( + (*Rc::new({ + let mut __result = Vec::new(); + for field in variant.children.clone().iter().cloned() { + __result.push(crate::v1_compiler_infer_types::child_type_node( + field.clone(), + )); + } + __result + })) + .iter() + .cloned(), + ); + } + __result + }) + } +} + +pub fn decl_member_type_nodes(decl: Rc) -> Rc>> { + member_type_nodes( + crate::v1_compiler_infer_types::is_product_type(decl.clone()), + decl.children.clone(), + ) +} + +pub fn members_forbid_deserialize( name: String, - children: Rc>>, + members: Rc>>, emit_info: Rc, source_indices: Rc>>, ) -> bool { @@ -23245,9 +23286,9 @@ pub fn decl_children_forbid_deserialize( ); { let mut __found = false; - for child in children.iter().cloned() { + for member in members.iter().cloned() { if type_expr_reaches_sealed_carrier( - crate::v1_compiler_infer_types::child_type_node(child.clone()), + member.clone(), emit_info.clone(), source_indices.clone(), seen.clone(), @@ -23269,54 +23310,15 @@ pub fn item_forbids_deserialize( if item_seals_construction(item.clone()) { true } else { - decl_children_forbid_deserialize( + members_forbid_deserialize( crate::v1_std_core::authored_name_at(source_indices.clone(), item.clone()), - item.children.clone(), + decl_member_type_nodes(item.clone()), emit_info.clone(), source_indices.clone(), ) } } -pub fn enum_variant_payloads_forbid_deserialize( - name: String, - children: Rc>>, - emit_info: Rc, - source_indices: Rc>>, -) -> bool { - { - let seen = v1_rt::rc_map_insert( - v1_rt::rc_empty_map::(), - crate::v1_std_core::qualified_last_segment(name.clone()), - true, - ); - { - let mut __found = false; - for variant in children.iter().cloned() { - if { - let mut __found = false; - for field in variant.children.clone().iter().cloned() { - if type_expr_reaches_sealed_carrier( - crate::v1_compiler_infer_types::child_type_node(field.clone()), - emit_info.clone(), - source_indices.clone(), - seen.clone(), - ) { - __found = true; - break; - } - } - __found - } { - __found = true; - break; - } - } - __found - } - } -} - pub fn emit_typed_field_access( base: Rc, field: String,