diff --git a/dag/gunbc/instruments/native_emission_controls.dag b/dag/gunbc/instruments/native_emission_controls.dag index 83958efe5cd..c5929ea885d 100644 --- a/dag/gunbc/instruments/native_emission_controls.dag +++ b/dag/gunbc/instruments/native_emission_controls.dag @@ -247,6 +247,38 @@ fn filter_in_branch_condition_cases() -> List { ] } +// RULE: a data row keeps its declared `?`, in its signature and in its body (v1.compiler.emit_rust +// emit_data_def's named-refinement arm; v1.compiler.emit emit_optional_data_value_json). A +// where-refinement such as NonEmptyStr took the one arm of emit_data_def with no optional wrap, so +// gunbc.fleet_tailnet fleet_tailnet_domain (`NonEmptyStr? = Present { .. }`) was emitted +// `-> NonEmptyStr`; and the JSON body spelled Present as a tagged map the native option cannot +// parse. +// Both optional rows pass through a `T?`-typed parameter, so a dropped `?` stops this closure +// compiling, and a wrong body stops it at run time; the cases decide each row answers as declared. +data control_optional_brand_present: NonEmptyStr? = Present { value: "tailnet.example" } + +data control_optional_brand_absent: NonEmptyStr? = none + +fn control_optional_brand_value(d: NonEmptyStr?) -> String { + match d { + Present { value: v } => v + Absent => "" + } +} + +fn optional_data_row_cases() -> List { + [ + NativeEmissionCase { + identity: "optional_data_row_present_answers_its_value", + held: control_optional_brand_value(d: control_optional_brand_present) == "tailnet.example" + }, + NativeEmissionCase { + identity: "optional_data_row_red_absent_answers_absent", + held: control_optional_brand_value(d: control_optional_brand_absent) == "" + } + ] +} + // RULE: a data row takes the JSON serializer only when its value is a literal tree // (v1.compiler.emit_rust data_value_is_json_literal_tree). A `"x" as NonEmptyStr` brand cast, bare or // inside a record literal, and an operator inside a record literal used to reach the serializer and @@ -535,6 +567,8 @@ data native_emission_expected_identities: List = [ "list_index_past_the_end_is_absent", "list_index_negative_is_absent", "list_index_into_empty_is_absent", + "optional_data_row_present_answers_its_value", + "optional_data_row_red_absent_answers_absent", "generic_return_full_arm_answers_its_value", "generic_return_red_empty_arm_answers_empty", "generic_return_effectful_call_answers_empty_on_absent_path", @@ -543,7 +577,7 @@ data native_emission_expected_identities: List = [ ] fn native_emission_cases() -> List { - concat(concat(concat(concat(concat(same_leaf_declaration_cases(), concat(concat(concat(concat(concat(concat(header_bound_cases(), fn_value_bound_cases()), filter_in_branch_condition_cases()), claim_driver_filesystem_cases()), effectful_fold_step_cases()), capability_alias_output_cases()), fold_accumulator_formals_cases())), list_index_cases()), effectful_generic_return_cases()), generic_empty_map_turbofish_cases()), data_literal_tree_cases()) + concat(concat(concat(concat(concat(concat(same_leaf_declaration_cases(), concat(concat(concat(concat(concat(concat(header_bound_cases(), fn_value_bound_cases()), filter_in_branch_condition_cases()), claim_driver_filesystem_cases()), effectful_fold_step_cases()), capability_alias_output_cases()), fold_accumulator_formals_cases())), list_index_cases()), optional_data_row_cases()), effectful_generic_return_cases()), generic_empty_map_turbofish_cases()), data_literal_tree_cases()) } fn case_row(c: NativeEmissionCase) -> String { diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index eb51b348425..5e81a248c8c 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -492,6 +492,49 @@ fn emit_data_fields_json(value: Node, source_indices: Map, ) } +// A DATA ROW DECLARED `T?` IS SPELLED AS THE TARGET'S NATIVE OPTION, selected by the row's DECLARED +// cardinality -- never by the spelling of a variant's parent. Every target realizes `T?` natively +// (Rust Option, Python None, Go nil), and each one's JSON reading of it is transparent: absent is +// `null`, present is the payload itself. The caller (v1.compiler.emit_rust emit_data_def_body) asks +// this only for a row whose declared type carries CardOptional, so an unrelated coproduct that happens +// to be NAMED Optional keeps its own wire policy (review on gunbc#13202: a leaf-name test here was a +// second, unrostered authority that would have serialized such a coproduct as null or a bare payload). +// Within an optional row, a variant literal whose parent IS the declared inner type is a bare T under +// T?: an implicit lift no declared coercion admits. That arm REFUSES, and it is the refusal exhaustive +// matching requires -- NOT coverage. It is unreachable: v1.compiler.infer refuses a bare T data +// initializer under a declared T? (the C5 wall, optional_at_required_obligation_diags at +// PositionDataInitializer), and an accepted row of that shape takes the typed-expression path, never +// this serializer (measured on gunbc#13202). Any other one-field variant literal is the option's +// present arm and is unwrapped to its payload. Control: gunbc.instruments.native_emission_controls optional_data_row_cases +// -- the present and absent rows. A coproduct merely NAMED Optional +// cannot be emitted at all today (type rendering decides host-option by leaf name); that case is +// lane 2K's RED, not covered here. Neither refusal arm (a bare inner-type literal, a present literal +// with more than one field) is coverage of anything; both exist only so the match is exhaustive. +fn emit_declared_optional_row_json(value: Node, inner_type_name: String, source_indices: Map, variant_wire: Map) -> EmitterOutcome { + match value.expr_data { + ExprLiteral { value: LitNull } => Emitted { json: "null" } + ExprRecordLit { parent_enum: pe } => + match pe { + Present { value: parent } => + if qualified_last_segment(name: parent) == qualified_last_segment(name: inner_type_name) { + Refused { reason: concat("a data row declared ", inner_type_name, "? is initialized with a bare ", inner_type_name, " literal: no declared coercion lifts T into T? (DESIGN 4, 2026-09-26), so spell it Present { value: .. } or none") } + } else { + match value.children |> first { + Absent => Emitted { json: "null" } + Present { value: payload_field } => + if (value.children |> count) == 1 { + emit_data_value_json(value: field_init_node_value(n: payload_field), source_indices: source_indices, variant_wire: variant_wire) + } else { + Refused { reason: concat("an optional row's present literal carries ", to_string(value.children |> count), " fields; the option's present arm carries exactly its payload") } + } + } + } + Absent => emit_data_value_json(value: value, source_indices: source_indices, variant_wire: variant_wire) + } + _ => emit_data_value_json(value: value, source_indices: source_indices, variant_wire: variant_wire) + } +} + fn emit_data_value_json(value: Node, source_indices: Map, variant_wire: Map) -> EmitterOutcome { match value.expr_data { ExprLiteral { value: v } => match v { diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index b5477ac289f..f20b2fb9a1c 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -256,7 +256,7 @@ import v1.compiler.emit { emit_lambda_params, emit_null_coalesce, emit_list_lit_expr, emit_shared_expr, emit_typed_cast_shared, emit_string_literal, emit_simple_expr, escape_rust_interp_text, module_emit_scope, scope_after_expr, lookup_item_by_identity, has_nested_records_node, - emit_data_value_json, EmitterOutcome, Emitted, Refused, is_null_coalesce, has_service_items, + emit_data_value_json, emit_declared_optional_row_json, EmitterOutcome, Emitted, Refused, is_null_coalesce, has_service_items, order_typed_call_args_from_semantics, is_tco_eligible, is_self_recursive, emit_shared_tco_expr, tco_reassign_core, shared_tco_reassign, tco_loop_slot_name, tco_loop_iteration_lets, service_fallback_transport, effective_operation_transport, ServiceFieldSet, compute_service_fields, service_field_decls, @@ -17432,6 +17432,13 @@ fn data_def_annotation_is_named_refinement(annotation: Node, source_indices: Map // declared type decided by the carrier and its wrap decided by the spelling, and needs_rc is // threaded into emit_data_def_body, so the value took the same answer as the signature. Routed to // the carrier authority; the spelling key is deleted rather than kept beside it. +// A NAMED REFINEMENT ROW KEEPS ITS DECLARED `?`. A where-refinement such as std.types NonEmptyStr +// resolves to a Conj carrying its base as one child, so it fails preserves_declared_brand's +// childless test and is rendered by the named-refinement arm -- the one arm that rendered through +// coerce_primitive_type without rust_carrier_optional_wrap. gunbc.fleet_tailnet +// fleet_tailnet_domain (`NonEmptyStr? = Present { .. }`) was emitted `-> NonEmptyStr` and its +// consumer refused E0308. The control is gunbc.instruments.native_emission_controls +// optional_data_row_cases. fn emit_data_def(name: String, type_node: Node, value: Node, registry: Map, scope: InferScope, depth: Int, shared_types: Set, emit_info: EmitGraphInfo) -> String { let ambiguous_annotation_head = v1_type_exprs_ambiguous_declared_head(type_exprs: [type_node], type_decl_items: emit_info.type_decl_items, source_indices: scope.type_env.source_indices) if ambiguous_annotation_head != "" { @@ -17453,7 +17460,7 @@ fn emit_data_def(name: String, type_node: Node, value: Node, registry: Map Bool { + type_node.return_cardinality == CardOptional +} + fn emit_data_def_body(type_node: Node, value: Node, registry: Map, scope: InferScope, depth: Int, shared_types: Set, emit_info: EmitGraphInfo, needs_rc: Bool) -> String { let raw_ty_str = render_rust_type(n: type_node, shared_types: shared_types, source_indices: scope.type_env.source_indices, emit_info: emit_info) let type_name = authored_name_at(source_indices: scope.type_env.source_indices, node: type_node) @@ -17579,7 +17591,12 @@ fn emit_data_def_body(type_node: Node, value: Node, registry: Map true _ => false } { concat(" ", emit_typed_expr(texpr: value, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info, fuel: 1024)) } else if has_nested_records_node(n: type_node, source_indices: scope.type_env.source_indices) && data_value_is_json_literal_tree(value: value) && !data_row_type_forbids_deserialize(type_node: type_node, emit_info: emit_info, source_indices: scope.type_env.source_indices) { - match emit_data_value_json(value: value, source_indices: scope.type_env.source_indices, variant_wire: emit_info.data_variant_wire_spellings) { + let row_json = if data_row_declares_optional(type_node: type_node) { + emit_declared_optional_row_json(value: value, inner_type_name: authored_name_at(source_indices: scope.type_env.source_indices, node: type_node), source_indices: scope.type_env.source_indices, variant_wire: emit_info.data_variant_wire_spellings) + } else { + emit_data_value_json(value: value, source_indices: scope.type_env.source_indices, variant_wire: emit_info.data_variant_wire_spellings) + } + match row_json { Refused { reason: r } => concat(" compile_error!(\"", escape_string_literal_body(s: r), "\")") Emitted { json: json_str } => concat(" serde_json::from_str(\"", escape_string_literal_body(s: json_str), "\")\n", " .expect(\"valid data definition\")") diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index 7700517388f..48bf08130f8 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -1026,6 +1026,64 @@ pub fn emit_data_fields_json( ) } +pub fn emit_declared_optional_row_json( + value: Rc, + inner_type_name: String, + source_indices: Rc>>, + variant_wire: Rc>>, +) -> Rc { + match (*value.expr_data.clone()).clone() { + ExprData::ExprLiteral { ref value, .. } + if matches!(value.as_ref(), LiteralValue::LitNull) => + { + let LiteralValue::LitNull = value.as_ref() else { + unreachable!() + }; + Rc::new(EmitterOutcome::Emitted { + json: "null".to_string(), + }) + } + ExprData::ExprRecordLit { + parent_enum: pe, .. + } => match pe.clone() { + Some(parent) => { + if (crate::v1_std_core::qualified_last_segment(parent.clone()) + == crate::v1_std_core::qualified_last_segment(inner_type_name.clone())) + { + Rc::new(EmitterOutcome::Refused { + reason: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("a data row declared ".to_string(), inner_type_name.clone()), "? is initialized with a bare ".to_string()), inner_type_name.clone()), " literal: no declared coercion lifts T into T? (DESIGN 4, 2026-09-26), so spell it Present { value: .. } or none".to_string()), +}) + } else { + match value.children.clone().first().cloned() { + std::option::Option::None => Rc::new(EmitterOutcome::Emitted { + json: "null".to_string(), + }), + Some(payload_field) => { + if ((value.children.clone().len() as i64) == 1) { + emit_data_value_json( + crate::v1_std_core::field_init_node_value( + payload_field.clone(), + ), + source_indices.clone(), + variant_wire.clone(), + ) + } else { + Rc::new(EmitterOutcome::Refused { + reason: v1_rt::concat(v1_rt::concat("an optional row's present literal carries ".to_string(), crate::v1_compiler_emit_core_support::to_string((value.children.clone().len() as i64))), " fields; the option's present arm carries exactly its payload".to_string()), +}) + } + } + } + } + } + std::option::Option::None => { + emit_data_value_json(value.clone(), source_indices.clone(), variant_wire.clone()) + } + }, + _ => emit_data_value_json(value.clone(), source_indices.clone(), variant_wire.clone()), + } +} + pub fn emit_data_value_json( value: Rc, source_indices: Rc>>, diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index e28b0327929..d24afa5a95e 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -186,13 +186,13 @@ use crate::v1_compiler_emit::ShellResultChannel::{ pub use crate::v1_compiler_emit::{ bind_operation_transport, child_from_key, compute_service_fields, effective_operation_transport, emit_bin_op_symbol, emit_container, emit_data_value_json, - emit_error_expr, emit_ident, emit_keyed_container_type, emit_keyword, emit_lambda, - emit_lambda_params, emit_let_binding, emit_let_binding_annotated, emit_list_lit_expr, - emit_literal, emit_node_type, emit_null_coalesce, emit_return, emit_shared_expr, - emit_shared_tco_expr, emit_simple_expr, emit_string_literal, emit_typed_cast_shared, - emit_typed_if_shared, emit_typed_let_shared, emit_unary_op, escape_rust_interp_text, - extract_modifier_names, has_nested_records_node, has_service_items, is_null_coalesce, - is_self_recursive, is_tco_eligible, keyed_container_has_target_inhabitant, + emit_declared_optional_row_json, emit_error_expr, emit_ident, emit_keyed_container_type, + emit_keyword, emit_lambda, emit_lambda_params, emit_let_binding, emit_let_binding_annotated, + emit_list_lit_expr, emit_literal, emit_node_type, emit_null_coalesce, emit_return, + emit_shared_expr, emit_shared_tco_expr, emit_simple_expr, emit_string_literal, + emit_typed_cast_shared, emit_typed_if_shared, emit_typed_let_shared, emit_unary_op, + escape_rust_interp_text, extract_modifier_names, has_nested_records_node, has_service_items, + is_null_coalesce, is_self_recursive, is_tco_eligible, keyed_container_has_target_inhabitant, lookup_item_by_identity, module_emit_scope, order_typed_call_args_from_semantics, render_node_type, render_tuple_parts, rust_literal_for_pattern, scope_after_expr, seed_bindings, service_fallback_transport, service_field_ctors, service_field_decls, @@ -40085,18 +40085,21 @@ pub fn emit_data_def( annotation_type_node.clone(), scope.type_env.clone().source_indices.clone(), ) { - crate::v1_compiler_coercion::coerce_primitive_type( - crate::v1_compiler_coercion::type_reference_realization( - annotation_type_node.clone(), + rust_carrier_optional_wrap( + annotation_type_node.clone(), + crate::v1_compiler_coercion::coerce_primitive_type( + crate::v1_compiler_coercion::type_reference_realization( + annotation_type_node.clone(), + crate::v1_std_core::authored_name_at( + scope.type_env.clone().source_indices.clone(), + annotation_type_node.clone(), + ), + RenderTarget::Rust, + ), crate::v1_std_core::authored_name_at( scope.type_env.clone().source_indices.clone(), annotation_type_node.clone(), ), - RenderTarget::Rust, - ), - crate::v1_std_core::authored_name_at( - scope.type_env.clone().source_indices.clone(), - annotation_type_node.clone(), ), ) } else { @@ -40251,6 +40254,10 @@ pub fn data_row_type_forbids_deserialize( ) } +pub fn data_row_declares_optional(type_node: Rc) -> bool { + (type_node.return_cardinality.clone() == Cardinality::CardOptional) +} + pub fn emit_data_def_body( type_node: Rc, value: Rc, @@ -40437,10 +40444,29 @@ pub fn emit_data_def_body( scope.type_env.clone().source_indices.clone(), )) { - match (*crate::v1_compiler_emit::emit_data_value_json(value.clone(), scope.type_env.clone().source_indices.clone(), emit_info.data_variant_wire_spellings.clone())).clone() { + { + let row_json = if data_row_declares_optional(type_node.clone()) { + crate::v1_compiler_emit::emit_declared_optional_row_json( + value.clone(), + crate::v1_std_core::authored_name_at( + scope.type_env.clone().source_indices.clone(), + type_node.clone(), + ), + scope.type_env.clone().source_indices.clone(), + emit_info.data_variant_wire_spellings.clone(), + ) + } else { + crate::v1_compiler_emit::emit_data_value_json( + value.clone(), + scope.type_env.clone().source_indices.clone(), + emit_info.data_variant_wire_spellings.clone(), + ) + }; + match (*row_json.clone()).clone() { EmitterOutcome::Refused { reason: r, .. } => v1_rt::concat(v1_rt::concat(" compile_error!(\"".to_string(), crate::v1_compiler_emit_core_support::escape_string_literal_body(r.clone())), "\")".to_string()), EmitterOutcome::Emitted { json: json_str, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(" serde_json::from_str(\"".to_string(), crate::v1_compiler_emit_core_support::escape_string_literal_body(json_str.clone())), "\")\n".to_string()), " .expect(\"valid data definition\")".to_string()), } + } } else { { let is_map = crate::v1_compiler_infer_types::node_is_keyed_collection(