diff --git a/src/v3/compiler/src/dag.rs b/src/v3/compiler/src/dag.rs index 4b28d2d3a72..1d21abfa213 100644 --- a/src/v3/compiler/src/dag.rs +++ b/src/v3/compiler/src/dag.rs @@ -1363,6 +1363,27 @@ pub(crate) struct PatternBindingRuleVariants { pub not_applicable: Option, } +/// Cached `VariantPayloadFieldAccessRule` variant DeclarationIds +/// resolved from `src/v3/std/clean_emission.dag`. Populated at +/// bootstrap end and consumed by emitters when parsing +/// `CleanEmissionContract.variant_payload_field_access`. +/// +/// Like `PatternBindingRuleVariants`, this central cache is the sole +/// bridge from `clean_emission.dag`'s variant labels to typed +/// `DeclarationId`s. That keeps the "how do target specs classify +/// variant-payload field access?" fact in one place instead of letting +/// three emitters re-resolve the same names independently (Q5 +/// construction authority). +#[derive(Debug, Default, Clone)] +pub(crate) struct VariantPayloadFieldAccessRuleVariants { + /// `AccessFromPayloadBinding` — the bound payload expression is a + /// whole carrier whose fields can be projected directly. + pub access_from_payload_binding: Option, + /// `OverrideNamedFieldsAtBindingSite` — named payload fields must + /// be broken into per-field bindings at the match site. + pub override_named_fields_at_binding_site: Option, +} + /// Cached `VerifierOutputPolicy` variant DeclarationIds resolved /// from `src/v3/std/clean_emission.dag`. Populated at bootstrap /// end alongside `PatternBindingRuleVariants`. The @@ -1430,6 +1451,11 @@ pub struct Dag { /// central cache is the right shape instead of per-emitter /// name lookups. pattern_binding_rule_variants: PatternBindingRuleVariants, + /// Cached `VariantPayloadFieldAccessRule` variant DeclarationIds + /// resolved from `src/v3/std/clean_emission.dag`. Every emitter + /// reads these typed ids when parsing + /// `CleanEmissionContract.variant_payload_field_access`. + variant_payload_field_access_rule_variants: VariantPayloadFieldAccessRuleVariants, /// Cached `VerifierOutputPolicy` variant DeclarationIds /// resolved from `src/v3/std/clean_emission.dag`. The /// `post_emit_verifier` harness dispatches on the cached typed @@ -1467,6 +1493,8 @@ impl Dag { target_syntax: TargetSyntaxCache::default(), stdlib_types: StdlibTypeCache::default(), pattern_binding_rule_variants: PatternBindingRuleVariants::default(), + variant_payload_field_access_rule_variants: + VariantPayloadFieldAccessRuleVariants::default(), verifier_output_policy_variants: VerifierOutputPolicyVariants::default(), clusters: Vec::new(), optional_match_disjs: HashMap::new(), @@ -1664,6 +1692,17 @@ impl Dag { &self.pattern_binding_rule_variants } + /// Typed accessor for the cached `VariantPayloadFieldAccessRule` + /// variant handles resolved from + /// `src/v3/std/clean_emission.dag`. Consumed by per-target + /// emitters when parsing + /// `CleanEmissionContract.variant_payload_field_access`. + pub(crate) fn variant_payload_field_access_rule_variants( + &self, + ) -> &VariantPayloadFieldAccessRuleVariants { + &self.variant_payload_field_access_rule_variants + } + /// Typed accessor for the cached `VerifierOutputPolicy` variant /// handles resolved from `src/v3/std/clean_emission.dag` at /// bootstrap end. Consumed by the shared `post_emit_verifier` @@ -2073,6 +2112,27 @@ impl Dag { } self.pattern_binding_rule_variants = pattern_binding_variants; + let mut variant_payload_field_access_variants = + VariantPayloadFieldAccessRuleVariants::default(); + if let Some(parent) = self.declaration_by_name("VariantPayloadFieldAccessRule") { + if let TypeConnective::Disj { variants } = &parent.connective { + for variant in variants { + match variant.label.as_str() { + "AccessFromPayloadBinding" => { + variant_payload_field_access_variants.access_from_payload_binding = + Some(variant.ty); + } + "OverrideNamedFieldsAtBindingSite" => { + variant_payload_field_access_variants + .override_named_fields_at_binding_site = Some(variant.ty); + } + _ => {} + } + } + } + } + self.variant_payload_field_access_rule_variants = variant_payload_field_access_variants; + // `VerifierOutputPolicy` variant resolution. Same shape as // the pattern-binding cache above: one walk of the Disj's // variants at bootstrap end, then every downstream consumer diff --git a/src/v3/compiler/src/emit_go.rs b/src/v3/compiler/src/emit_go.rs index 676d58f5136..c06b4eea6c1 100644 --- a/src/v3/compiler/src/emit_go.rs +++ b/src/v3/compiler/src/emit_go.rs @@ -7,6 +7,10 @@ use crate::dag::{ }; use crate::emit_rust::{EmitError, RealizationCategory}; use crate::operators::OperatorKind; +use crate::variant_payload::{ + variant_payload_shape, VariantPayloadBinding, VariantPayloadFieldAccessRuleBinding, + VariantPayloadShape, +}; use crate::Dag; #[derive(Debug, Clone, PartialEq, Eq)] @@ -156,6 +160,7 @@ struct TargetExecutionModelBinding { #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct CleanEmissionContractBinding { pattern_bindings: PatternBindingRuleBinding, + variant_payload_field_access: VariantPayloadFieldAccessRuleBinding, } /// Go-valid slice of `std.clean_emission.PatternBindingRule`. Parsed @@ -405,8 +410,8 @@ impl RealizationIndexes { impl CleanEmissionContractBinding { /// Parse the portion of `data go_clean_emission: /// CleanEmissionContract` this emitter consumes. Currently only - /// `pattern_bindings` dispatches (Lane 1 Stage 1c PR 2 pilot); - /// other rules parse when their consumers land. + /// `pattern_bindings` and `variant_payload_field_access` + /// dispatch; other rules parse when their consumers land. fn build(dag: &Dag) -> Result { let declaration = dag .go_clean_emission_spec() @@ -422,7 +427,24 @@ impl CleanEmissionContractBinding { })?; let pattern_bindings = parse_pattern_binding_rule(dag, pattern_bindings_value, declaration)?; - Ok(Self { pattern_bindings }) + let variant_payload_field_access_value = fields + .iter() + .find(|(label, _)| label == "variant_payload_field_access") + .map(|(_, value)| value) + .ok_or(EmitError::MalformedTargetSyntax { + declaration, + detail: + "go_clean_emission is missing required `variant_payload_field_access` field", + })?; + let variant_payload_field_access = parse_variant_payload_field_access_rule( + dag, + variant_payload_field_access_value, + declaration, + )?; + Ok(Self { + pattern_bindings, + variant_payload_field_access, + }) } } @@ -494,6 +516,58 @@ fn parse_pattern_binding_rule( } } +fn parse_variant_payload_field_access_rule( + dag: &Dag, + value: &FieldValue, + declaration: DeclarationId, +) -> Result { + let FieldValue::Variant { + constructor, + payload, + } = value + else { + return Err(EmitError::MalformedTargetSyntax { + declaration, + detail: + "go_clean_emission.variant_payload_field_access must be a VariantPayloadFieldAccessRule variant", + }); + }; + if !payload.is_empty() { + return Err(EmitError::MalformedTargetSyntax { + declaration, + detail: "VariantPayloadFieldAccessRule variants must not carry payload fields", + }); + } + let variants = dag.variant_payload_field_access_rule_variants(); + let access_from_payload_binding = + variants + .access_from_payload_binding + .ok_or(EmitError::MalformedTargetSyntax { + declaration, + detail: + "VariantPayloadFieldAccessRule.AccessFromPayloadBinding declaration was not found", + })?; + let override_named_fields_at_binding_site = variants + .override_named_fields_at_binding_site + .ok_or(EmitError::MalformedTargetSyntax { + declaration, + detail: "VariantPayloadFieldAccessRule.OverrideNamedFieldsAtBindingSite declaration was not found", + })?; + if *constructor == access_from_payload_binding { + Ok(VariantPayloadFieldAccessRuleBinding::AccessFromPayloadBinding) + } else if *constructor == override_named_fields_at_binding_site { + Err(EmitError::MalformedTargetSyntax { + declaration, + detail: "go_clean_emission.variant_payload_field_access cannot use VariantPayloadFieldAccessRule.OverrideNamedFieldsAtBindingSite; Go requires AccessFromPayloadBinding for native sum payloads", + }) + } else { + Err(EmitError::MalformedTargetSyntax { + declaration, + detail: "go_clean_emission.variant_payload_field_access constructor is not a known VariantPayloadFieldAccessRule variant", + }) + } +} + impl GoLanguageSyntax { fn build(dag: &Dag) -> Result { let language_decl = dag @@ -694,7 +768,7 @@ struct Ctx<'a> { #[derive(Debug, Clone, Default)] struct RenderLocals { names: HashMap, - field_overrides: HashMap>, + payload_bindings: HashMap>, } impl<'a> Ctx<'a> { @@ -702,6 +776,13 @@ impl<'a> Ctx<'a> { if let Some(name) = locals.names.get(&port) { return Ok(name.clone()); } + if let Some(name) = locals + .payload_bindings + .get(&port) + .and_then(VariantPayloadBinding::direct) + { + return Ok(name.clone()); + } if let Some(name) = self.bound_names.get(&port) { return Ok(name.clone()); } @@ -772,10 +853,12 @@ impl<'a> Ctx<'a> { "field projection .{field_label} expected one input" ))); } - if let Some(fields) = locals.field_overrides.get(&t.inputs[0]) { - if let Some(binding) = fields.get(field_label) { - return Ok(binding.clone()); - } + if let Some(binding) = locals + .payload_bindings + .get(&t.inputs[0]) + .and_then(|binding| binding.field(field_label)) + { + return Ok(binding.clone()); } let parent = self.render_port(t.inputs[0], locals)?; let parent_type = primitive_type_id_for_port(self.dag, t.inputs[0])?; @@ -936,12 +1019,12 @@ impl<'a> Ctx<'a> { let scrutinee = self.render_port(branch.input, locals)?; let ret = self.go_type_name_for_port(branch.output)?; let mut arms = Vec::new(); - // E-5 / Lane 1 Stage 1c PR 2: track whether any arm emitted a - // `name := v.field;` prefix. If zero arms do, the type-switch + // E-5 / Lane 1 Stage 1c PR 2: track whether any arm actually + // consumes the payload binding. If zero arms do, the type-switch // header drops `v :=` — otherwise Go would flag `v` as // `declared and not used`. Structural: driven by port liveness, // not by scanning the rendered arm text. - let mut any_arm_binds_v = false; + let mut any_arm_uses_v = false; for path in &branch.paths { let variant_id = match &path.pattern { BranchPattern::ResolvedVariant(id) => *id, @@ -961,27 +1044,26 @@ impl<'a> Ctx<'a> { .unwrap_or_else(|| "UnknownVariant".to_string()) }); let mut arm_locals = locals.clone(); - let mut binding_prefix = String::new(); if let Some(binding) = &path.binding { let elide = matches!( self.indexes.clean_emission.pattern_bindings, PatternBindingRuleBinding::EmitUnderscoreWhenUnused ) && !self.port_is_consumed_from(path.output, binding.payload_port); if !elide { - let binding_expr = self.variant_binding_expr(variant_id)?; - binding_prefix = format!("{} := {binding_expr}; ", binding.binding_name); - arm_locals - .names - .insert(binding.payload_port, binding.binding_name.clone()); - any_arm_binds_v = true; + if let Some(payload_binding) = + self.variant_payload_binding_for_variant(variant_id, "v")? + { + arm_locals + .payload_bindings + .insert(binding.payload_port, payload_binding); + any_arm_uses_v = true; + } } } let body = self.render_port(path.output, &arm_locals)?; - arms.push(format!( - "case {variant_name}: {binding_prefix}return {body}" - )); + arms.push(format!("case {variant_name}: return {body}")); } - let switch_header = if any_arm_binds_v { + let switch_header = if any_arm_uses_v { format!("switch v := any({scrutinee}).(type)") } else { format!("switch any({scrutinee}).(type)") @@ -1122,8 +1204,8 @@ impl<'a> Ctx<'a> { fields.insert("head".to_string(), head_expr); fields.insert("tail".to_string(), tail_expr); cons_locals - .field_overrides - .insert(payload.payload_port, fields); + .payload_bindings + .insert(payload.payload_port, VariantPayloadBinding::Fields(fields)); } let cons_body = self.render_port(cons_path.output, &cons_locals)?; Ok(format!( @@ -1132,13 +1214,7 @@ impl<'a> Ctx<'a> { } fn render_path_body(&self, path: &Path, locals: &RenderLocals) -> Result { - let mut arm_locals = locals.clone(); - if let Some(binding) = &path.binding { - arm_locals - .names - .insert(binding.payload_port, binding.binding_name.clone()); - } - self.render_port(path.output, &arm_locals) + self.render_port(path.output, locals) } fn render_realized_callable( @@ -1741,17 +1817,38 @@ impl<'a> Ctx<'a> { self.go_type_name_for_decl(element.value) } - fn variant_binding_expr(&self, variant_id: DeclarationId) -> Result { - let TypeConnective::Conj { children } = &self.dag.declaration(variant_id).connective else { + fn variant_payload_binding_for_variant( + &self, + variant_id: DeclarationId, + binding_expr: &str, + ) -> Result>, EmitError> { + let Some(shape) = variant_payload_shape(self.dag, variant_id) else { return Err(EmitError::UnsupportedBehavior( "variant payload expected a product declaration".to_string(), )); }; - match children.as_slice() { - [] => Ok("v".to_string()), - [field] => Ok(format!("v.{}", field.label)), - _ => Ok("v".to_string()), - } + Ok(match shape { + VariantPayloadShape::Empty => None, + VariantPayloadShape::PositionalSingle => { + Some(VariantPayloadBinding::Direct(format!("{binding_expr}._0"))) + } + VariantPayloadShape::NamedFields(field_labels) => { + match self.indexes.clean_emission.variant_payload_field_access { + VariantPayloadFieldAccessRuleBinding::AccessFromPayloadBinding => { + Some(VariantPayloadBinding::Direct(binding_expr.to_string())) + } + VariantPayloadFieldAccessRuleBinding::OverrideNamedFieldsAtBindingSite => { + let fields = field_labels + .into_iter() + .map(|field_label| { + (field_label.clone(), format!("{binding_expr}.{field_label}")) + }) + .collect(); + Some(VariantPayloadBinding::Fields(fields)) + } + } + } + }) } } @@ -2708,7 +2805,25 @@ mod tests { ) .expect("compiles"); let rendered = emit_go_module(&dag).expect("go emitter should render match"); - assert!(rendered.contains("payload := v; return"), "got: {rendered}"); - assert!(!rendered.contains("payload := v.value;"), "got: {rendered}"); + assert!( + rendered.contains("case Cons: return (v).head"), + "got: {rendered}" + ); + assert!(!rendered.contains("payload := v;"), "got: {rendered}"); + } + + #[test] + fn go_named_single_field_variant_payload_binding_uses_the_variant_value() { + let dag = compile_to_dag( + "type Point { x: Int y: Int }\ntype Wrapped = Wrap { inner: Point } | Empty\nfn unwrap_or_zero(w: Wrapped) -> Int = match w { Wrap(payload) => payload.inner.x, Empty => 0 }\n", + "variant_payload_named_single.v3", + ) + .expect("compiles"); + let rendered = emit_go_module(&dag).expect("go emitter should render match"); + assert!( + rendered.contains("case Wrap: return ((v).inner).x"), + "got: {rendered}" + ); + assert!(!rendered.contains("payload := v.inner;"), "got: {rendered}"); } } diff --git a/src/v3/compiler/src/emit_python.rs b/src/v3/compiler/src/emit_python.rs index 6d58eb4acea..565b97c4e0c 100644 --- a/src/v3/compiler/src/emit_python.rs +++ b/src/v3/compiler/src/emit_python.rs @@ -6,6 +6,10 @@ use crate::dag::{ TypeConnective, }; use crate::operators::OperatorKind; +use crate::variant_payload::{ + variant_payload_shape, VariantPayloadBinding, VariantPayloadFieldAccessRuleBinding, + VariantPayloadShape, +}; use crate::Dag; #[derive(Debug, Clone)] @@ -104,6 +108,7 @@ struct PythonTarget { #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct CleanEmissionContractBinding { pattern_bindings: PatternBindingRuleBinding, + variant_payload_field_access: VariantPayloadFieldAccessRuleBinding, } /// Python-valid slice of `std.clean_emission.PatternBindingRule`. @@ -141,6 +146,7 @@ struct PythonIndexes { #[derive(Debug, Clone, Default)] struct RenderLocals { names: HashMap, + payload_bindings: HashMap>, } struct Ctx<'a> { @@ -351,7 +357,24 @@ impl CleanEmissionContractBinding { })?; let pattern_bindings = parse_pattern_binding_rule(dag, pattern_bindings_value, declaration)?; - Ok(Self { pattern_bindings }) + let variant_payload_field_access_value = fields + .iter() + .find(|(label, _)| label == "variant_payload_field_access") + .map(|(_, value)| value) + .ok_or(EmitPythonError::MalformedSpec { + declaration, + detail: + "python_clean_emission is missing required `variant_payload_field_access` field", + })?; + let variant_payload_field_access = parse_variant_payload_field_access_rule( + dag, + variant_payload_field_access_value, + declaration, + )?; + Ok(Self { + pattern_bindings, + variant_payload_field_access, + }) } } @@ -438,6 +461,58 @@ fn parse_pattern_binding_rule( } } +fn parse_variant_payload_field_access_rule( + dag: &Dag, + value: &FieldValue, + declaration: DeclarationId, +) -> Result { + let FieldValue::Variant { + constructor, + payload, + } = value + else { + return Err(EmitPythonError::MalformedSpec { + declaration, + detail: + "python_clean_emission.variant_payload_field_access must be a VariantPayloadFieldAccessRule variant", + }); + }; + if !payload.is_empty() { + return Err(EmitPythonError::MalformedSpec { + declaration, + detail: "VariantPayloadFieldAccessRule variants must not carry payload fields", + }); + } + let variants = dag.variant_payload_field_access_rule_variants(); + let access_from_payload_binding = + variants + .access_from_payload_binding + .ok_or(EmitPythonError::MalformedSpec { + declaration, + detail: + "VariantPayloadFieldAccessRule.AccessFromPayloadBinding declaration was not found", + })?; + let override_named_fields_at_binding_site = variants + .override_named_fields_at_binding_site + .ok_or(EmitPythonError::MalformedSpec { + declaration, + detail: "VariantPayloadFieldAccessRule.OverrideNamedFieldsAtBindingSite declaration was not found", + })?; + if *constructor == access_from_payload_binding { + Ok(VariantPayloadFieldAccessRuleBinding::AccessFromPayloadBinding) + } else if *constructor == override_named_fields_at_binding_site { + Err(EmitPythonError::MalformedSpec { + declaration, + detail: "python_clean_emission.variant_payload_field_access cannot use VariantPayloadFieldAccessRule.OverrideNamedFieldsAtBindingSite; Python requires AccessFromPayloadBinding for native match carriers", + }) + } else { + Err(EmitPythonError::MalformedSpec { + declaration, + detail: "python_clean_emission.variant_payload_field_access constructor is not a known VariantPayloadFieldAccessRule variant", + }) + } +} + pub fn emit_python(dag: &Dag) -> Result { emit_python_with_mode(dag, EmitPythonMode::Program) } @@ -572,6 +647,13 @@ impl<'a> Ctx<'a> { if let Some(name) = locals.names.get(&port) { return Ok(name.clone()); } + if let Some(name) = locals + .payload_bindings + .get(&port) + .and_then(VariantPayloadBinding::direct) + { + return Ok(name.clone()); + } if let Some(name) = self.bound_names.get(&port) { return Ok(name.clone()); } @@ -605,6 +687,13 @@ impl<'a> Ctx<'a> { match &t.target { TransformTarget::Operator(op) => self.render_operator(t, *op, locals), TransformTarget::FieldProject { field_label, .. } => { + if let Some(binding) = locals + .payload_bindings + .get(&t.inputs[0]) + .and_then(|binding| binding.field(field_label)) + { + return Ok(binding.clone()); + } let object = self.render_port(t.inputs[0], locals)?; Ok(render_named_template( &self.indexes.syntax.field_access, @@ -706,9 +795,11 @@ impl<'a> Ctx<'a> { let empty_body = self.render_path_body(empty_path, locals)?; let mut cons_locals = locals.clone(); if let Some(binding) = &cons_path.binding { - cons_locals.names.insert( + cons_locals.payload_bindings.insert( binding.payload_port, - "types.SimpleNamespace(head=__match[0], tail=__match[1:])".to_string(), + VariantPayloadBinding::Direct( + "types.SimpleNamespace(head=__match[0], tail=__match[1:])".to_string(), + ), ); } let cons_body = self.render_port(cons_path.output, &cons_locals)?; @@ -734,7 +825,7 @@ impl<'a> Ctx<'a> { let mut rendered = "__v3_unreachable(\"non-exhaustive match\")".to_string(); for path in branch.paths.iter().rev() { let cond = self.render_branch_condition(disj_id, is_optional, path)?; - let body = self.render_branch_body_expr(disj_id, is_optional, path, locals)?; + let body = self.render_branch_body_expr(is_optional, path, locals)?; rendered = format!("({body} if {cond} else {rendered})"); } Ok(format!("(lambda __match: {rendered})({scrutinee})")) @@ -773,7 +864,6 @@ impl<'a> Ctx<'a> { /// the renderer only sees Python-valid states. fn render_branch_body_expr( &self, - disj_id: DeclarationId, is_optional: bool, path: &Path, locals: &RenderLocals, @@ -782,42 +872,53 @@ impl<'a> Ctx<'a> { if let Some(binding) = &path.binding { match self.indexes.clean_emission.pattern_bindings { PatternBindingRuleBinding::NotApplicable => { - let extracted = self.render_match_binding(disj_id, is_optional, path)?; - arm_locals.names.insert(binding.payload_port, extracted); + if let Some(payload_binding) = + self.render_variant_payload_binding(is_optional, path)? + { + arm_locals + .payload_bindings + .insert(binding.payload_port, payload_binding); + } } } } self.render_port(path.output, &arm_locals) } - fn render_match_binding( + fn render_variant_payload_binding( &self, - disj_id: DeclarationId, is_optional: bool, path: &Path, - ) -> Result { + ) -> Result>, EmitPythonError> { if is_optional { - return Ok("__match".to_string()); + return Ok(Some(VariantPayloadBinding::Direct("__match".to_string()))); } let variant_id = resolved_pattern_id(path)?; - let TypeConnective::Disj { variants } = &self.dag.declaration(disj_id).connective else { - unreachable!("match parent must be a disjunction") - }; - let variant = variants - .iter() - .find(|variant| variant.ty == variant_id) - .ok_or_else(|| { - EmitPythonError::Unsupported( - "match variant missing from parent disjunction".to_string(), - ) - })?; - let TypeConnective::Conj { children } = &self.dag.declaration(variant.ty).connective else { - return Ok("__match".to_string()); + let Some(shape) = variant_payload_shape(self.dag, variant_id) else { + return Ok(Some(VariantPayloadBinding::Direct("__match".to_string()))); }; - if children.len() == 1 && children[0].label == "_0" { - return Ok("__match._0".to_string()); - } - Ok("__match".to_string()) + Ok(match shape { + VariantPayloadShape::Empty => None, + VariantPayloadShape::PositionalSingle => { + Some(VariantPayloadBinding::Direct("__match._0".to_string())) + } + VariantPayloadShape::NamedFields(field_labels) => { + match self.indexes.clean_emission.variant_payload_field_access { + VariantPayloadFieldAccessRuleBinding::AccessFromPayloadBinding => { + Some(VariantPayloadBinding::Direct("__match".to_string())) + } + VariantPayloadFieldAccessRuleBinding::OverrideNamedFieldsAtBindingSite => { + let fields = field_labels + .into_iter() + .map(|field_label| { + (field_label.clone(), format!("__match.{field_label}")) + }) + .collect(); + Some(VariantPayloadBinding::Fields(fields)) + } + } + } + }) } fn render_path_body( @@ -825,13 +926,7 @@ impl<'a> Ctx<'a> { path: &Path, locals: &RenderLocals, ) -> Result { - let mut arm_locals = locals.clone(); - if let Some(binding) = &path.binding { - arm_locals - .names - .insert(binding.payload_port, binding.binding_name.clone()); - } - self.render_port(path.output, &arm_locals) + self.render_port(path.output, locals) } fn render_loop( diff --git a/src/v3/compiler/src/emit_rust.rs b/src/v3/compiler/src/emit_rust.rs index 3fcf3a245ba..575ed292e06 100644 --- a/src/v3/compiler/src/emit_rust.rs +++ b/src/v3/compiler/src/emit_rust.rs @@ -51,6 +51,10 @@ use crate::dag::{ TypeConnective, ValueBody, ValueNode, }; use crate::operators::OperatorKind; +use crate::variant_payload::{ + variant_payload_shape, VariantPayloadBinding, VariantPayloadFieldAccessRuleBinding, + VariantPayloadShape, +}; /// Errors the Rust emitter surfaces when the DAG reaches a shape it /// cannot render under the PR-B scope. Each variant names a specific @@ -496,6 +500,7 @@ struct RustLanguageSyntax { #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct CleanEmissionContractBinding { pattern_bindings: PatternBindingRuleBinding, + variant_payload_field_access: VariantPayloadFieldAccessRuleBinding, } /// Rust-valid slice of `std.clean_emission.PatternBindingRule`. @@ -881,8 +886,8 @@ impl RealizationIndexes { impl CleanEmissionContractBinding { /// Parse the portion of `data rust_clean_emission: /// CleanEmissionContract` this emitter consumes. Currently only - /// `pattern_bindings` dispatches (Lane 1 Stage 1c PR 1 pilot); - /// other rules parse when their consumers land. + /// `pattern_bindings` and `variant_payload_field_access` + /// dispatch; other rules parse when their consumers land. fn build(dag: &Dag) -> Result { let declaration = dag .rust_clean_emission_spec() @@ -898,7 +903,24 @@ impl CleanEmissionContractBinding { })?; let pattern_bindings = parse_pattern_binding_rule(dag, pattern_bindings_value, declaration)?; - Ok(Self { pattern_bindings }) + let variant_payload_field_access_value = fields + .iter() + .find(|(label, _)| label == "variant_payload_field_access") + .map(|(_, value)| value) + .ok_or(EmitError::MalformedTargetSyntax { + declaration, + detail: + "rust_clean_emission is missing required `variant_payload_field_access` field", + })?; + let variant_payload_field_access = parse_variant_payload_field_access_rule( + dag, + variant_payload_field_access_value, + declaration, + )?; + Ok(Self { + pattern_bindings, + variant_payload_field_access, + }) } } @@ -970,6 +992,58 @@ fn parse_pattern_binding_rule( } } +fn parse_variant_payload_field_access_rule( + dag: &Dag, + value: &FieldValue, + declaration: DeclarationId, +) -> Result { + let FieldValue::Variant { + constructor, + payload, + } = value + else { + return Err(EmitError::MalformedTargetSyntax { + declaration, + detail: + "rust_clean_emission.variant_payload_field_access must be a VariantPayloadFieldAccessRule variant", + }); + }; + if !payload.is_empty() { + return Err(EmitError::MalformedTargetSyntax { + declaration, + detail: "VariantPayloadFieldAccessRule variants must not carry payload fields", + }); + } + let variants = dag.variant_payload_field_access_rule_variants(); + let access_from_payload_binding = + variants + .access_from_payload_binding + .ok_or(EmitError::MalformedTargetSyntax { + declaration, + detail: + "VariantPayloadFieldAccessRule.AccessFromPayloadBinding declaration was not found", + })?; + let override_named_fields_at_binding_site = variants + .override_named_fields_at_binding_site + .ok_or(EmitError::MalformedTargetSyntax { + declaration, + detail: "VariantPayloadFieldAccessRule.OverrideNamedFieldsAtBindingSite declaration was not found", + })?; + if *constructor == override_named_fields_at_binding_site { + Ok(VariantPayloadFieldAccessRuleBinding::OverrideNamedFieldsAtBindingSite) + } else if *constructor == access_from_payload_binding { + Err(EmitError::MalformedTargetSyntax { + declaration, + detail: "rust_clean_emission.variant_payload_field_access cannot use VariantPayloadFieldAccessRule.AccessFromPayloadBinding; Rust requires OverrideNamedFieldsAtBindingSite for named payloads", + }) + } else { + Err(EmitError::MalformedTargetSyntax { + declaration, + detail: "rust_clean_emission.variant_payload_field_access constructor is not a known VariantPayloadFieldAccessRule variant", + }) + } +} + /// DB-14: build `(accessor_decl → realization_decl, universe)` for /// the active target language. /// @@ -1433,10 +1507,10 @@ fn render_named_template(template: &str, bindings: &[(&str, &str)]) -> String { /// Local name for a destructured field of a multi-field struct-variant /// payload. Used by both the pattern emitter and the arm-body renderer -/// so the aliased destructure and the `field_overrides` lookup stay in -/// lockstep. The leading `__` avoids colliding with user identifiers -/// and silences unused-binding warnings on fields the arm body never -/// references. +/// so the aliased destructure and the payload-binding field routing +/// stay in lockstep. The leading `__` avoids colliding with user +/// identifiers and silences unused-binding warnings on fields the arm +/// body never references. fn destructured_field_alias(binding_name: &str, field_label: &str) -> String { format!("__{binding_name}_{field_label}") } @@ -2697,7 +2771,7 @@ enum RenderMode { #[derive(Debug, Clone, Default)] struct RenderLocals { names: HashMap, - field_overrides: HashMap>, + payload_bindings: HashMap>, } #[allow(dead_code)] @@ -2855,6 +2929,13 @@ impl<'a> Ctx<'a> { if let Some(binding) = locals.names.get(&port) { return self.render_binding(port, binding, mode); } + if let Some(binding) = locals + .payload_bindings + .get(&port) + .and_then(VariantPayloadBinding::direct) + { + return self.render_binding(port, binding, mode); + } if let Some(binding) = self.bound_names.get(&port) { return self.render_binding(port, binding, mode); } @@ -3097,10 +3178,12 @@ impl<'a> Ctx<'a> { t.inputs.len() ))); } - if let Some(fields) = locals.field_overrides.get(&t.inputs[0]) { - if let Some(binding) = fields.get(field_label) { - return self.render_binding(t.output, binding, mode); - } + if let Some(binding) = locals + .payload_bindings + .get(&t.inputs[0]) + .and_then(|binding| binding.field(field_label)) + { + return self.render_binding(t.output, binding, mode); } let parent_expr = self.render_input_use( InputConsumer::Transform(t), @@ -3399,8 +3482,8 @@ impl<'a> Ctx<'a> { )), ); cons_locals - .field_overrides - .insert(payload.payload_port, fields); + .payload_bindings + .insert(payload.payload_port, VariantPayloadBinding::Fields(fields)); } let cons_body = self.render_port( cons_path.output, @@ -3430,37 +3513,10 @@ impl<'a> Ctx<'a> { fn render_path_body(&self, path: &Path, locals: &RenderLocals) -> Result { let mut arm_locals = locals.clone(); if let Some(binding) = &path.binding { - arm_locals.names.insert( - binding.payload_port, - LocalBinding::Borrowed(binding.binding_name.clone()), - ); - // Named struct-variant payloads are observed at the .dag - // layer as a record value, even when the variant has just - // one field. Rust pattern matching binds named fields - // directly, so route downstream `.dag`-level - // `binding.field` projections through `field_overrides` - // instead of trying to read fields off the bound leaf. - if let BranchPattern::ResolvedVariant(resolved_id) = &path.pattern { - if let TypeConnective::Conj { children } = - &self.dag.declaration(*resolved_id).connective - { - if children.iter().any(|child| child.label != "_0") { - let overrides = children - .iter() - .map(|child| { - let local_name = if children.len() == 1 { - binding.binding_name.clone() - } else { - destructured_field_alias(&binding.binding_name, &child.label) - }; - (child.label.clone(), LocalBinding::Borrowed(local_name)) - }) - .collect(); - arm_locals - .field_overrides - .insert(binding.payload_port, overrides); - } - } + if let Some(payload_binding) = self.render_variant_payload_binding(path, binding)? { + arm_locals + .payload_bindings + .insert(binding.payload_port, payload_binding); } } self.render_port(path.output, &arm_locals, RenderMode::OwnedConstructLastUse) @@ -3505,18 +3561,23 @@ impl<'a> Ctx<'a> { &[("name", &qualified_name)], )); }; - let TypeConnective::Conj { children } = &self.dag.declaration(resolved_id).connective - else { + let Some(payload_shape) = variant_payload_shape(self.dag, resolved_id) else { return Err(EmitError::UnsupportedBehavior(format!( "matched variant `{variant_name}` does not lower to a payload product" ))); }; let rendered_binding = self.render_payload_binding_name(path, binding); - if children.len() != 1 { + if matches!( + payload_shape, + VariantPayloadShape::NamedFields(ref fields) if fields.len() > 1 + ) { + let VariantPayloadShape::NamedFields(field_labels) = payload_shape else { + unreachable!("guarded above") + }; // Multi-field struct-variant: Rust won't let us access // `binding.field` on the enum because the payload has // no nominal type. The body can ONLY consume this payload - // via destructured fields routed through `field_overrides` + // via destructured fields routed through `payload_bindings` // — there is no Rust-level value the outer `binding @` // alias could refer to (Rust rejects taking the anonymous // struct payload as a value). So we always omit the @@ -3527,22 +3588,22 @@ impl<'a> Ctx<'a> { // unused, every field renders as wildcard too. When it's // used, every field renders as its aliased local // (`___`), and `render_path_body`'s - // `field_overrides` population at the same payload port + // `payload_bindings` population at the same payload port // routes downstream `binding.field` reads to those locals // via `render_field_project`'s override lookup. let wildcard = self.indexes.syntax.patterns.wildcard.clone(); let payload_unused = rendered_binding == wildcard; - let field_bindings = children + let field_bindings = field_labels .iter() .map(|child| { let binding_text = if payload_unused { wildcard.clone() } else { - destructured_field_alias(&binding.binding_name, &child.label) + destructured_field_alias(&binding.binding_name, child) }; Ok(render_named_template( &self.indexes.syntax.patterns.field_binding, - &[("field", &child.label), ("binding", &binding_text)], + &[("field", child), ("binding", &binding_text)], )) }) .collect::, EmitError>>()?; @@ -3560,7 +3621,7 @@ impl<'a> Ctx<'a> { ], )); } - if children[0].label == "_0" + if payload_shape == VariantPayloadShape::PositionalSingle && (self.indexes.types.contains_key(&disj_id) || is_optional_match) { return Ok(render_named_template( @@ -3568,17 +3629,32 @@ impl<'a> Ctx<'a> { &[("name", &qualified_name), ("binding", &rendered_binding)], )); } - let bindings = render_named_template( - &self.indexes.syntax.patterns.field_binding, - &[ - ("field", &children[0].label), - ("binding", &rendered_binding), - ], - ); - Ok(render_named_template( - &self.indexes.syntax.patterns.variant_pattern, - &[("name", &qualified_name), ("bindings", &bindings)], - )) + match payload_shape { + VariantPayloadShape::Empty => Ok(render_named_template( + &self.indexes.syntax.patterns.variant_pattern_empty, + &[("name", &qualified_name)], + )), + VariantPayloadShape::PositionalSingle => { + let bindings = render_named_template( + &self.indexes.syntax.patterns.field_binding, + &[("field", "_0"), ("binding", &rendered_binding)], + ); + Ok(render_named_template( + &self.indexes.syntax.patterns.variant_pattern, + &[("name", &qualified_name), ("bindings", &bindings)], + )) + } + VariantPayloadShape::NamedFields(field_labels) => { + let bindings = render_named_template( + &self.indexes.syntax.patterns.field_binding, + &[("field", &field_labels[0]), ("binding", &rendered_binding)], + ); + Ok(render_named_template( + &self.indexes.syntax.patterns.variant_pattern, + &[("name", &qualified_name), ("bindings", &bindings)], + )) + } + } } /// E-5 / Lane 1 Stage 1c: render the arm's payload binding name @@ -3604,6 +3680,51 @@ impl<'a> Ctx<'a> { } } + fn render_variant_payload_binding( + &self, + path: &Path, + binding: &crate::dag::PayloadBinding, + ) -> Result>, EmitError> { + let BranchPattern::ResolvedVariant(variant_id) = &path.pattern else { + return Ok(None); + }; + let Some(shape) = variant_payload_shape(self.dag, *variant_id) else { + return Ok(None); + }; + let payload_binding_name = self.render_payload_binding_name(path, binding); + let wildcard = self.indexes.syntax.patterns.wildcard.clone(); + Ok(match shape { + VariantPayloadShape::Empty => None, + VariantPayloadShape::PositionalSingle => Some(VariantPayloadBinding::Direct( + LocalBinding::Borrowed(payload_binding_name), + )), + VariantPayloadShape::NamedFields(field_labels) => { + match self.indexes.clean_emission.variant_payload_field_access { + VariantPayloadFieldAccessRuleBinding::AccessFromPayloadBinding => Some( + VariantPayloadBinding::Direct(LocalBinding::Borrowed(payload_binding_name)), + ), + VariantPayloadFieldAccessRuleBinding::OverrideNamedFieldsAtBindingSite => { + let multiple_fields = field_labels.len() > 1; + let fields = field_labels + .into_iter() + .map(|field_label| { + let local_name = if payload_binding_name == wildcard { + wildcard.clone() + } else if multiple_fields { + destructured_field_alias(&binding.binding_name, &field_label) + } else { + payload_binding_name.clone() + }; + (field_label, LocalBinding::Borrowed(local_name)) + }) + .collect(); + Some(VariantPayloadBinding::Fields(fields)) + } + } + } + }) + } + /// Structural port-liveness walk. Returns true if `target` /// appears as any port reachable from `root` via /// producer→input edges. Cost is bounded by the size of the diff --git a/src/v3/compiler/src/lib.rs b/src/v3/compiler/src/lib.rs index 7a3caa5094a..f8f7e57c9e6 100644 --- a/src/v3/compiler/src/lib.rs +++ b/src/v3/compiler/src/lib.rs @@ -128,6 +128,7 @@ mod lower; mod parse; mod pipeline_authority; mod tokenize; +mod variant_payload; pub use dag::Dag; pub use diagnostics::{Diagnostic, SourceSpan}; diff --git a/src/v3/compiler/src/variant_payload.rs b/src/v3/compiler/src/variant_payload.rs new file mode 100644 index 00000000000..52412e09fae --- /dev/null +++ b/src/v3/compiler/src/variant_payload.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; + +use crate::dag::{Dag, DeclarationId, TypeConnective}; + +/// Shared emitter-side classification for a resolved variant payload. +/// Distinguishes the two payload forms that affect field projection +/// lowering: +/// +/// - positional single-field payloads (`Variant(T)`) bind directly to +/// the carried value +/// - named payload fields (`Variant { x: T, ... }`) require either a +/// whole-payload carrier expression or per-field overrides, +/// depending on the target's spec rule +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum VariantPayloadShape { + Empty, + PositionalSingle, + NamedFields(Vec), +} + +/// Shared emitter-side mirror of +/// `std.clean_emission.VariantPayloadFieldAccessRule`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum VariantPayloadFieldAccessRuleBinding { + AccessFromPayloadBinding, + OverrideNamedFieldsAtBindingSite, +} + +/// Per-payload-port rendering authority used by the emitters. +/// `Direct` means the payload port itself renders to one expression; +/// `Fields` means the whole payload value is not renderable directly, +/// so downstream field projections must be answered by the provided +/// per-field bindings. +#[derive(Debug, Clone)] +pub(crate) enum VariantPayloadBinding { + Direct(T), + Fields(HashMap), +} + +impl VariantPayloadBinding { + pub(crate) fn direct(&self) -> Option<&T> { + match self { + Self::Direct(value) => Some(value), + Self::Fields(_) => None, + } + } + + pub(crate) fn field(&self, label: &str) -> Option<&T> { + match self { + Self::Direct(_) => None, + Self::Fields(fields) => fields.get(label), + } + } +} + +pub(crate) fn variant_payload_shape( + dag: &Dag, + variant_id: DeclarationId, +) -> Option { + let TypeConnective::Conj { children } = &dag.declaration(variant_id).connective else { + return None; + }; + match children.as_slice() { + [] => Some(VariantPayloadShape::Empty), + [field] if field.label == "_0" => Some(VariantPayloadShape::PositionalSingle), + fields => Some(VariantPayloadShape::NamedFields( + fields.iter().map(|field| field.label.clone()).collect(), + )), + } +} diff --git a/src/v3/compiler/tests/m1_3_emit_go_test.rs b/src/v3/compiler/tests/m1_3_emit_go_test.rs index 7adefea6f96..0f6bce76fab 100644 --- a/src/v3/compiler/tests/m1_3_emit_go_test.rs +++ b/src/v3/compiler/tests/m1_3_emit_go_test.rs @@ -174,8 +174,8 @@ let zero: Int = 0 } /// E-5 / Lane 1 Stage 1c PR 2 — when at least one arm consumes its -/// payload, the emitter must keep emitting `v :=` and the consuming -/// arm's `name := v.field;` prefix. Companion regression test to +/// payload, the emitter must keep emitting `v :=` and route reads +/// through the type-switch witness. Companion regression test to /// `emit_go_unused_payload_binding_is_elided`; proves elision is /// keyed on port liveness per arm, not blanket suppression. #[test] @@ -188,8 +188,8 @@ let zero: Int = 0 let dag = compile_to_dag(source, "clean_emission.v3").expect("compiles"); let rendered = emit_go(&dag).expect("emits go"); assert!( - rendered.contains("value := v._0"), - "used binding must still be declared, got: {rendered}" + rendered.contains("case Boxed: return v._0"), + "used positional payload must still read from the type-switch witness, got: {rendered}" ); assert!( rendered.contains("switch v := any("), @@ -197,6 +197,26 @@ let zero: Int = 0 ); } +#[test] +fn emit_go_named_single_field_payload_uses_the_variant_value() { + let source = "\ +type Point { x: Int y: Int } +type Wrapped = Wrap { inner: Point } | Empty +fn unwrap_or_zero(w: Wrapped) -> Int = match w { Wrap(payload) => payload.inner.x, Empty => 0 } +let zero: Int = 0 +"; + let dag = compile_to_dag(source, "variant_payload_named_single.v3").expect("compiles"); + let rendered = emit_go(&dag).expect("emits go"); + assert!( + rendered.contains("case Wrap: return ((v).inner).x"), + "named single-field payload access must project from the variant value, got: {rendered}" + ); + assert!( + !rendered.contains("payload := v.inner"), + "named single-field payload must not rebind to the bare field value, got: {rendered}" + ); +} + /// E-5 / Lane 1 Stage 1c PR 2 roundtrip — emitted Go with an unused /// payload binding compiles and runs under `go run`. Proves the /// pilot fires end-to-end against Go's unused-local compile error, diff --git a/src/v3/compiler/tests/m1_3_emit_rust_test.rs b/src/v3/compiler/tests/m1_3_emit_rust_test.rs index bb3950a9b1b..bdcfef8639b 100644 --- a/src/v3/compiler/tests/m1_3_emit_rust_test.rs +++ b/src/v3/compiler/tests/m1_3_emit_rust_test.rs @@ -352,6 +352,24 @@ let zero: Int = 0", assert!(out.contains("BoxedInt::Empty => 0,"), "got: {out}"); } +#[test] +fn emit_rust_named_single_field_payload_routes_field_access_through_binding() { + let out = emit( + "type Point { x: Int y: Int } +type Wrapped = Wrap { inner: Point } | Empty +fn unwrap_or_zero(w: Wrapped) -> Int = match w { Wrap(payload) => payload.inner.x, Empty => 0 } +let zero: Int = 0", + ); + assert!( + out.contains("Wrapped::Wrap { inner: payload } => (payload).x,"), + "expected named single-field payload access to route through the bound field, got: {out}" + ); + assert!( + !out.contains("(payload).inner"), + "named single-field payload access must not project through a synthetic whole-payload binding, got: {out}" + ); +} + /// E-5 / Lane 1 Stage 1c pilot — unused match-arm payload bindings /// render as `_` under `rust_clean_emission.pattern_bindings = /// EmitUnderscoreWhenUnused`. Before the pilot the emitter rendered diff --git a/src/v3/compiler/tests/m1_4_emit_python_test.rs b/src/v3/compiler/tests/m1_4_emit_python_test.rs index adeec5d308e..47bbe6a57ac 100644 --- a/src/v3/compiler/tests/m1_4_emit_python_test.rs +++ b/src/v3/compiler/tests/m1_4_emit_python_test.rs @@ -588,6 +588,43 @@ fn use_payload(b: BoxedInt) -> Int = match b { Boxed(unique_value) => unique_val run_python_module(&module); } +#[test] +fn emit_python_named_single_field_payload_projects_from_match_value() { + let module = emit_python_module_from_source( + "type Point { x: Int y: Int } +type Wrapped = Wrap { inner: Point } | Empty +fn unwrap_or_zero(w: Wrapped) -> Int = match w { Wrap(payload) => payload.inner.x, Empty => 0 }", + "python_variant_payload_named_single.v3", + ); + assert!( + module.contains("__match.inner.x"), + "expected named single-field payload access to project from __match, got:\n{module}" + ); + assert!( + !module.contains("__match.inner.inner"), + "named single-field payload access must not double-project through the inner field, got:\n{module}" + ); + run_python_module(&module); +} + +#[test] +fn emit_python_named_multi_field_payload_projects_from_match_value() { + let module = emit_python_module_from_source( + "type Pair = Both { left: Int right: Int } | Empty +fn right_or_zero(p: Pair) -> Int = match p { Both(payload) => payload.right, Empty => 0 }", + "python_variant_payload_named_multi.v3", + ); + assert!( + module.contains("__match.right"), + "expected named multi-field payload access to project from __match, got:\n{module}" + ); + assert!( + !module.contains("payload.right"), + "named multi-field payload access must not leak the source binding name, got:\n{module}" + ); + run_python_module(&module); +} + /// E-5 / Lane 1 Stage 1c PR 4 — emitted Python passes /// `python_clean_emission.post_emit_verifier` as invoked through /// the shared harness (`python3 -m py_compile` + diff --git a/src/v3/spec/go.dag b/src/v3/spec/go.dag index 99e2d6e9fac..99336d52f3c 100644 --- a/src/v3/spec/go.dag +++ b/src/v3/spec/go.dag @@ -920,6 +920,10 @@ data go_language: LanguageSpec = { // header when no arm binds). Go's compiler treats unused locals // as errors, so elision is required for correctness — not // merely a warnings-posture choice. +// - `variant_payload_field_access` is LIVE — `emit_go` treats the +// type-switch witness `v` as the whole payload carrier for named +// payloads, while positional single-field payloads bind directly +// to `v._0`. // - Every other field is authored-but-unread. Each field has a // named rollout: the emitter change that consumes it removes // the corresponding lint from the test-wrapper allow list in @@ -928,6 +932,7 @@ data go_language: LanguageSpec = { data go_clean_emission: CleanEmissionContract = { expression_wrapping: WrapOnlyInOperandPosition pattern_bindings: EmitUnderscoreWhenUnused + variant_payload_field_access: AccessFromPayloadBinding imports: IncludeOnlyReferenced block_return: ExplicitReturnKeyword variable_bindings: EmitLetUnderscoreWhenUnused diff --git a/src/v3/spec/python.dag b/src/v3/spec/python.dag index fa3eade7b8f..09f1b025bcf 100644 --- a/src/v3/spec/python.dag +++ b/src/v3/spec/python.dag @@ -423,6 +423,9 @@ data python_list_instantiation: PythonTypeInstantiationRealization = { // dispatches on it. `NotApplicablePatternBinding` selects the // substitute-at-render-time path (the emitter's existing behavior); // any other rule is rejected at contract-parse time. +// - `variant_payload_field_access` is LIVE — `emit_python` treats +// `__match` as the whole payload carrier for named payloads, while +// positional single-field payloads substitute `__match._0`. // - Every other field is authored-but-unread. Each field has a // named rollout: the emitter change that consumes it removes // the corresponding lint from the test-wrapper allow list in @@ -435,6 +438,7 @@ data python_list_instantiation: PythonTypeInstantiationRealization = { data python_clean_emission: CleanEmissionContract = { expression_wrapping: WrapOnlyInOperandPosition pattern_bindings: NotApplicablePatternBinding + variant_payload_field_access: AccessFromPayloadBinding imports: IncludeOnlyReferenced block_return: ExplicitReturnKeyword variable_bindings: EmitLetBindingAlways diff --git a/src/v3/spec/rust.dag b/src/v3/spec/rust.dag index a5103660e08..7e8084b47f6 100644 --- a/src/v3/spec/rust.dag +++ b/src/v3/spec/rust.dag @@ -988,6 +988,10 @@ data rust_language: LanguageSpec = { // **Dispatch state at this PR (Lane 1 Stage 1c PR 1):** // - `pattern_bindings` is LIVE — `emit_rust::render_branch_pattern` // dispatches on it for the unused-payload-binding pilot. +// - `variant_payload_field_access` is LIVE — `emit_rust` routes +// named variant-payload field reads through match-site field +// bindings because Rust cannot hold anonymous struct-variant +// payloads as first-class values. // - Every other field is authored-but-unread. Each field has a // named rollout: the emitter change that consumes it removes // the corresponding lint from the test-wrapper allow list in @@ -996,6 +1000,7 @@ data rust_language: LanguageSpec = { data rust_clean_emission: CleanEmissionContract = { expression_wrapping: WrapOnlyInOperandPosition pattern_bindings: EmitUnderscoreWhenUnused + variant_payload_field_access: OverrideNamedFieldsAtBindingSite imports: IncludeOnlyReferenced block_return: NoWrappingOnTerminalExpression variable_bindings: EmitLetUnderscoreWhenUnused diff --git a/src/v3/std/clean_emission.dag b/src/v3/std/clean_emission.dag index 3d9be093f02..bacc3de1d65 100644 --- a/src/v3/std/clean_emission.dag +++ b/src/v3/std/clean_emission.dag @@ -12,6 +12,7 @@ import std.list { List } type CleanEmissionContract { expression_wrapping: ExpressionWrappingRule pattern_bindings: PatternBindingRule + variant_payload_field_access: VariantPayloadFieldAccessRule imports: ImportRule block_return: BlockReturnRule variable_bindings: VariableBindingRule @@ -41,6 +42,22 @@ type PatternBindingRule | EmitPrefixedUnderscoreWhenUnused | NotApplicablePatternBinding +// 🟢 TERMINAL. Governs how a match-arm payload binding answers +// downstream field projections (`payload.head`, `payload.count`) +// after the arm has introduced the payload-binding port. +// +// `AccessFromPayloadBinding` means the binding expression itself is a +// whole payload carrier whose fields can be projected directly by the +// target emitter (`v.head`, `__match.count`). +// +// `OverrideNamedFieldsAtBindingSite` means named payload fields cannot +// be projected off the bound payload value in the target surface, so +// the emitter must install per-field bindings at the match site and +// route later `payload.field` reads through those bindings instead. +type VariantPayloadFieldAccessRule + = AccessFromPayloadBinding + | OverrideNamedFieldsAtBindingSite + // 🟢 TERMINAL. Governs which declared imports make it into the // emitted module (rustc's `unused_imports`). type ImportRule