diff --git a/src/v3/compiler/src/dag.rs b/src/v3/compiler/src/dag.rs index 276aa5d9a25..cd641916f88 100644 --- a/src/v3/compiler/src/dag.rs +++ b/src/v3/compiler/src/dag.rs @@ -172,6 +172,103 @@ impl ClusterId { } } +/// `std.error_primitives` declares canonical `Result = Ok { value: ok } | Err { value: err }`. +/// Emitters lower it to a target-native `Result` / `struct { Ok; Err }` carrier and must +/// not also emit a second substrate `type Result`. +/// +/// Suppression keys off the **resolved structural fingerprint** of that declaration +/// (name + type-parameter identities + `Ok`/`Err` payload wiring), not `span.file` +/// suffixes — so unrelated modules named `errors.dag` cannot collide, and renaming +/// the std file alone does not silently retarget suppression. +/// +/// **Policy:** the fingerprint is **global**, not std-scoped: any other declaration +/// named `Result` that matches this exact shape is also suppressed (intentional — the +/// substrate owns one canonical `Result` carrier; a user-defined twin with +/// the same fingerprint would not emit as a separate `type Result`). +pub(crate) fn substrate_result_type_decl_suppressed_for_emit( + dag: &Dag, + decl: &Declaration, +) -> bool { + if decl.name.as_deref() != Some("Result") { + return false; + } + let [ok_param, err_param] = match decl.type_params.as_slice() { + [a, b] => [*a, *b], + _ => return false, + }; + let ok_decl = dag.declaration(ok_param); + let err_decl = dag.declaration(err_param); + let ok_param_ok = matches!( + &ok_decl.connective, + TypeConnective::Atom(AtomPayload::TypeParam(name)) if name == "ok" + ); + let err_param_ok = matches!( + &err_decl.connective, + TypeConnective::Atom(AtomPayload::TypeParam(name)) if name == "err" + ); + if !ok_param_ok || !err_param_ok { + return false; + } + let TypeConnective::Disj { variants } = &decl.connective else { + return false; + }; + if variants.len() != 2 { + return false; + } + let Some(ok_field) = variants.iter().find(|v| v.label == "Ok") else { + return false; + }; + let Some(err_field) = variants.iter().find(|v| v.label == "Err") else { + return false; + }; + substrate_result_variant_payload_is_value_of(dag, ok_field.ty, ok_param) + && substrate_result_variant_payload_is_value_of(dag, err_field.ty, err_param) +} + +fn substrate_result_variant_payload_is_value_of( + dag: &Dag, + payload_ty: DeclarationId, + type_param: DeclarationId, +) -> bool { + let payload = dag.declaration(payload_ty); + let TypeConnective::Conj { children } = &payload.connective else { + return false; + }; + children.len() == 1 && children[0].label == "value" && children[0].ty == type_param +} + +/// `std.error_primitives` declares canonical `DivError = DivideByZero | Overflow`. +/// Like `Result`, emit suppression keys off the resolved structural fingerprint, +/// not the declaration source path. A declaration with this exact global shape is +/// the substrate-owned integer-division error carrier and is materialized by the +/// target division prelude when a program actually needs it. +pub(crate) fn substrate_div_error_type_decl_suppressed_for_emit( + dag: &Dag, + decl: &Declaration, +) -> bool { + if decl.name.as_deref() != Some("DivError") || !decl.type_params.is_empty() { + return false; + } + matches!(&decl.connective, TypeConnective::Disj { variants } if { + variants.len() == 2 + && variants.iter().any(|variant| { + variant.label == "DivideByZero" + && substrate_div_error_variant_payload_is_unit(dag, variant.ty) + }) + && variants.iter().any(|variant| { + variant.label == "Overflow" + && substrate_div_error_variant_payload_is_unit(dag, variant.ty) + }) + }) +} + +fn substrate_div_error_variant_payload_is_unit(dag: &Dag, payload_ty: DeclarationId) -> bool { + matches!( + &dag.declaration(payload_ty).connective, + TypeConnective::Conj { children } if children.is_empty() + ) +} + /// A type-system declaration. The unit of the type substrate. /// /// Every named declaration (primitive, algebra, user type, type alias) lives in diff --git a/src/v3/compiler/src/emit.rs b/src/v3/compiler/src/emit.rs index 16eab7cf42b..335b80191b7 100644 --- a/src/v3/compiler/src/emit.rs +++ b/src/v3/compiler/src/emit.rs @@ -54,9 +54,11 @@ impl VariantPayloadBinding { pub use self::python_target::EmitPythonError; use self::rust_target::{EmitError, RealizationCategory, SubstrateMarkerRole}; use crate::dag::{ - ArrowBody, AtomPayload, Behavior, BindNode, BranchNode, BranchPattern, CardinalityBound, - Declaration, DeclarationId, Field, FieldValue, LiteralBits, Path, PortId, TemplateArgument, - TransformNode, TransformTarget, TypeConnective, ValueBody, + substrate_div_error_type_decl_suppressed_for_emit, + substrate_result_type_decl_suppressed_for_emit, ArrowBody, AtomPayload, Behavior, BindNode, + BranchNode, BranchPattern, CardinalityBound, Declaration, DeclarationId, Field, FieldValue, + LiteralBits, Path, PortId, TemplateArgument, TransformNode, TransformTarget, TypeConnective, + ValueBody, }; use crate::infer::strip_refinement_to_base; use crate::operators::OperatorKind; @@ -84,8 +86,6 @@ pub(crate) fn method_emit_template_variant_label( .map(|v| v.label.as_str()) } -const ERROR_PRIMITIVES_AUTHORITY_FILE: &str = "dsl/std/error_primitives.dag"; - /// Result port of a finished `behavior` subgraph — shared by Go and Rust emit /// paths for [`port_is_consumed_from`] (including `Behavior::Loop` body walks). pub(super) fn behavior_result_port(behavior: &Behavior) -> PortId { @@ -98,66 +98,17 @@ pub(super) fn behavior_result_port(behavior: &Behavior) -> PortId { } } -/// `std.error_primitives` declares canonical `Result = Ok { value: ok } | Err { value: err }`. -/// Emitters lower it to a target-native `Result` / `struct { Ok; Err }` carrier and must -/// not also emit a second substrate `type Result`. -/// -/// Suppression keys off the **resolved structural fingerprint** of that declaration -/// (name + type-parameter identities + `Ok`/`Err` payload wiring), not `span.file` -/// suffixes — so unrelated modules named `errors.dag` cannot collide, and renaming -/// the std file alone does not silently retarget suppression. -/// -/// **Policy:** the fingerprint is **global**, not std-scoped: any other declaration -/// named `Result` that matches this exact shape is also suppressed (intentional — the -/// substrate owns one canonical `Result` carrier; a user-defined twin with -/// the same fingerprint would not emit as a separate `type Result`). -pub(crate) fn substrate_result_type_decl_suppressed_for_emit( - dag: &Dag, - decl: &Declaration, -) -> bool { - if decl.name.as_deref() != Some("Result") { - return false; - } - let [ok_param, err_param] = match decl.type_params.as_slice() { - [a, b] => [*a, *b], - _ => return false, - }; - let ok_decl = dag.declaration(ok_param); - let err_decl = dag.declaration(err_param); - let ok_param_ok = matches!( - &ok_decl.connective, - TypeConnective::Atom(AtomPayload::TypeParam(name)) if name == "ok" - ); - let err_param_ok = matches!( - &err_decl.connective, - TypeConnective::Atom(AtomPayload::TypeParam(name)) if name == "err" - ); - if !ok_param_ok || !err_param_ok { - return false; - } - let TypeConnective::Disj { variants } = &decl.connective else { - return false; - }; - let Some(ok_field) = variants.iter().find(|v| v.label == "Ok") else { - return false; - }; - let Some(err_field) = variants.iter().find(|v| v.label == "Err") else { - return false; - }; - substrate_result_variant_payload_is_value_of(dag, ok_field.ty, ok_param) - && substrate_result_variant_payload_is_value_of(dag, err_field.ty, err_param) -} - pub(crate) fn div_prelude_reserved_name_collision<'a>( + dag: &Dag, type_decls: impl IntoIterator, function_decls: impl IntoIterator, top_level_binds: impl IntoIterator, helper_name: &'static str, ) -> Option<&'static str> { - if type_decls - .into_iter() - .any(|decl| decl.name.as_deref() == Some("DivError")) - { + if type_decls.into_iter().any(|decl| { + decl.name.as_deref() == Some("DivError") + && !substrate_div_error_type_decl_suppressed_for_emit(dag, decl) + }) { return Some("DivError"); } function_decls @@ -181,7 +132,7 @@ pub(crate) fn decl_uses_substrate_result_or_div_error( return false; } let decl = dag.declaration(declaration); - if substrate_div_error_type_decl_suppressed_for_emit(decl) { + if substrate_div_error_type_decl_suppressed_for_emit(dag, decl) { return true; } match &decl.connective { @@ -231,20 +182,6 @@ fn dag_needs_go_result_prelude( .any(|decl| decl_uses_substrate_result_or_div_error(dag, decl.id, &mut HashSet::new())) } -pub(crate) fn substrate_div_error_type_decl_suppressed_for_emit(decl: &Declaration) -> bool { - if decl.span.file != ERROR_PRIMITIVES_AUTHORITY_FILE - || decl.name.as_deref() != Some("DivError") - || !decl.type_params.is_empty() - { - return false; - } - matches!(&decl.connective, TypeConnective::Disj { variants } if { - variants.len() == 2 - && variants.iter().any(|variant| variant.label == "DivideByZero") - && variants.iter().any(|variant| variant.label == "Overflow") - }) -} - pub(crate) fn port_uses_substrate_result_or_div_error(dag: &Dag, port: PortId) -> bool { let Some(ty) = dag.port(port).value_type() else { return false; @@ -261,7 +198,7 @@ fn decl_uses_substrate_div_error( return false; } let decl = dag.declaration(declaration); - if substrate_div_error_type_decl_suppressed_for_emit(decl) { + if substrate_div_error_type_decl_suppressed_for_emit(dag, decl) { return true; } match &decl.connective { @@ -313,18 +250,6 @@ pub(crate) fn dag_needs_div_error_prelude( .any(|decl| decl_uses_substrate_div_error(dag, decl.id, &mut HashSet::new())) } -fn substrate_result_variant_payload_is_value_of( - dag: &Dag, - payload_ty: DeclarationId, - type_param: DeclarationId, -) -> bool { - let payload = dag.declaration(payload_ty); - let TypeConnective::Conj { children } = &payload.connective else { - return false; - }; - children.len() == 1 && children[0].label == "value" && children[0].ty == type_param -} - /// Structural port-liveness walk. Returns true if `target` appears as any port /// reachable from `root` via producer→input edges (each behavior visits its /// inputs once; cost is bounded by the body subgraph). @@ -1335,6 +1260,7 @@ fn emit_go_with_mode(dag: &Dag, mode: EmitMode) -> Result { .filter(|decl| !indexes.source_filtering.excludes(&decl.span.file)) .filter(|decl| decl.name.is_some()) .filter(|decl| !substrate_result_type_decl_suppressed_for_emit(dag, decl)) + .filter(|decl| !substrate_div_error_type_decl_suppressed_for_emit(dag, decl)) .filter(|decl| { matches!( decl.connective, @@ -1408,6 +1334,7 @@ fn emit_go_with_mode(dag: &Dag, mode: EmitMode) -> Result { if let (true, Some(name)) = ( needs_int_div_prelude, div_prelude_reserved_name_collision( + dag, type_decls.iter(), function_decls.iter(), top_level_binds.iter(), @@ -3783,6 +3710,179 @@ mod tests { assert_eq!(super::behavior_result_port(behavior), sum); } + #[test] + fn result_and_diverror_emit_suppression_are_structural_not_path_keyed() { + let mut dag = Dag::new(); + let result = dag + .declaration_by_name("Result") + .expect("bootstrap Result") + .id; + let div_error = dag + .declaration_by_name("DivError") + .expect("bootstrap DivError") + .id; + + dag.declaration_mut(result).span.file = "renamed/std/errors.dag".to_string(); + dag.declaration_mut(div_error).span.file = "renamed/std/errors.dag".to_string(); + + assert!(super::substrate_result_type_decl_suppressed_for_emit( + &dag, + dag.declaration(result) + )); + assert!(super::substrate_div_error_type_decl_suppressed_for_emit( + &dag, + dag.declaration(div_error) + )); + } + + #[test] + fn payload_bearing_diverror_is_not_emit_suppressed() { + let mut dag = Dag::new(); + let int = dag.int_shape().expect("bootstrap Int").declaration; + let shadow_div_error = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: shadow_div_error, + name: Some("DivError".to_string()), + connective: TypeConnective::Disj { + variants: vec![ + Field { + label: "DivideByZero".to_string(), + ty: int, + }, + Field { + label: "Overflow".to_string(), + ty: int, + }, + ], + }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: SourceSpan::new("user/errors.dag", 0, 0), + }); + + assert!(!super::substrate_div_error_type_decl_suppressed_for_emit( + &dag, + dag.declaration(shadow_div_error) + )); + } + + #[test] + fn extra_variant_result_is_not_emit_suppressed() { + let mut dag = Dag::new(); + let int = dag.int_shape().expect("bootstrap Int").declaration; + let ok = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: ok, + name: None, + connective: TypeConnective::Atom(AtomPayload::TypeParam("ok".to_string())), + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: SourceSpan::new("user/result_extra.dag", 0, 0), + }); + let err = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: err, + name: None, + connective: TypeConnective::Atom(AtomPayload::TypeParam("err".to_string())), + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: SourceSpan::new("user/result_extra.dag", 0, 0), + }); + let ok_payload = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: ok_payload, + name: None, + connective: TypeConnective::Conj { + children: vec![Field { + label: "value".to_string(), + ty: ok, + }], + }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: SourceSpan::new("user/result_extra.dag", 0, 0), + }); + let err_payload = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: err_payload, + name: None, + connective: TypeConnective::Conj { + children: vec![Field { + label: "value".to_string(), + ty: err, + }], + }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: SourceSpan::new("user/result_extra.dag", 0, 0), + }); + let result = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: result, + name: Some("Result".to_string()), + connective: TypeConnective::Disj { + variants: vec![ + Field { + label: "Ok".to_string(), + ty: ok_payload, + }, + Field { + label: "Err".to_string(), + ty: err_payload, + }, + Field { + label: "Other".to_string(), + ty: int, + }, + ], + }, + type_params: vec![ok, err], + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: SourceSpan::new("user/result_extra.dag", 0, 0), + }); + + assert!(!super::substrate_result_type_decl_suppressed_for_emit( + &dag, + dag.declaration(result) + )); + } + #[test] fn port_is_consumed_from_reaches_operand_ports_from_add_output() { let mut dag = Dag::new(); diff --git a/src/v3/compiler/src/emit/python_target.rs b/src/v3/compiler/src/emit/python_target.rs index 33050bbae06..966c2c8aeac 100644 --- a/src/v3/compiler/src/emit/python_target.rs +++ b/src/v3/compiler/src/emit/python_target.rs @@ -676,6 +676,7 @@ pub(crate) fn emit_python_with_mode( .filter(|decl| !indexes.source_filtering.excludes(&decl.span.file)) .filter(|decl| decl.name.is_some()) .filter(|decl| !super::substrate_result_type_decl_suppressed_for_emit(dag, decl)) + .filter(|decl| !super::substrate_div_error_type_decl_suppressed_for_emit(dag, decl)) .filter(|decl| { matches!( decl.connective, @@ -754,6 +755,7 @@ pub(crate) fn emit_python_with_mode( if let (true, Some(name)) = ( needs_int_div_prelude, div_prelude_reserved_name_collision( + dag, type_decls.iter(), function_decls.iter(), top_level_binds.iter(), diff --git a/src/v3/compiler/src/emit/rust_target.rs b/src/v3/compiler/src/emit/rust_target.rs index fb502207749..6fc5ea62e79 100644 --- a/src/v3/compiler/src/emit/rust_target.rs +++ b/src/v3/compiler/src/emit/rust_target.rs @@ -2840,6 +2840,7 @@ pub(crate) fn emit_rust_with_mode(dag: &Dag, mode: EmitRustMode) -> Result`. Generic `Result` is not // emitted as a Rust `enum` (and would collide with the prelude if it were). .filter(|decl| !super::substrate_result_type_decl_suppressed_for_emit(dag, decl)) + .filter(|decl| !super::substrate_div_error_type_decl_suppressed_for_emit(dag, decl)) .filter(|decl| { matches!( decl.connective, @@ -2918,6 +2919,7 @@ pub(crate) fn emit_rust_with_mode(dag: &Dag, mode: EmitRustMode) -> Result bool { } const WALK_DEPTH_LIMIT: usize = 32; -const ERROR_PRIMITIVES_AUTHORITY_FILE: &str = "dsl/std/error_primitives.dag"; - /// §8.9 operator dispatch via structural algebra walk. /// /// Walks the LHS type's declaration chain (following `Instantiation` @@ -4687,79 +4687,15 @@ fn substitute_receiver( } fn canonical_result_decl(dag: &Dag) -> Option<&Declaration> { - let mut matches = dag.declarations().iter().filter(|decl| { - decl.span.file == ERROR_PRIMITIVES_AUTHORITY_FILE - && substrate_result_decl_has_error_primitive_shape(dag, decl) - }); - let result = matches.next()?; - if matches.next().is_some() { - return None; - } - Some(result) -} - -fn substrate_result_decl_has_error_primitive_shape(dag: &Dag, decl: &Declaration) -> bool { - if decl.name.as_deref() != Some("Result") { - return false; - } - let [ok_param, err_param] = match decl.type_params.as_slice() { - [a, b] => [*a, *b], - _ => return false, - }; - let ok_decl = dag.declaration(ok_param); - let err_decl = dag.declaration(err_param); - let ok_param_ok = matches!( - &ok_decl.connective, - TypeConnective::Atom(AtomPayload::TypeParam(name)) if name == "ok" - ); - let err_param_ok = matches!( - &err_decl.connective, - TypeConnective::Atom(AtomPayload::TypeParam(name)) if name == "err" - ); - if !ok_param_ok || !err_param_ok { - return false; - } - let TypeConnective::Disj { variants } = &decl.connective else { - return false; - }; - let Some(ok_field) = variants.iter().find(|v| v.label == "Ok") else { - return false; - }; - let Some(err_field) = variants.iter().find(|v| v.label == "Err") else { - return false; - }; - substrate_result_variant_payload_is_value_of(dag, ok_field.ty, ok_param) - && substrate_result_variant_payload_is_value_of(dag, err_field.ty, err_param) -} - -fn substrate_result_variant_payload_is_value_of( - dag: &Dag, - payload_ty: DeclarationId, - type_param: DeclarationId, -) -> bool { - let payload = dag.declaration(payload_ty); - let TypeConnective::Conj { children } = &payload.connective else { - return false; - }; - children.len() == 1 && children[0].label == "value" && children[0].ty == type_param + dag.declarations() + .iter() + .find(|decl| substrate_result_type_decl_suppressed_for_emit(dag, decl)) } fn canonical_div_error_decl(dag: &Dag) -> Option<&Declaration> { - let mut matches = dag.declarations().iter().filter(|decl| { - decl.span.file == ERROR_PRIMITIVES_AUTHORITY_FILE - && decl.name.as_deref() == Some("DivError") - && decl.type_params.is_empty() - && matches!(&decl.connective, TypeConnective::Disj { variants } if { - variants.len() == 2 - && variants.iter().any(|variant| variant.label == "DivideByZero") - && variants.iter().any(|variant| variant.label == "Overflow") - }) - }); - let div_error = matches.next()?; - if matches.next().is_some() { - return None; - } - Some(div_error) + dag.declarations() + .iter() + .find(|decl| substrate_div_error_type_decl_suppressed_for_emit(dag, decl)) } /// Dispatch-time invariant check on an `ExternalRealization` target: @@ -6775,6 +6711,48 @@ mod bool_logical_operator_arrow_tests { assert_eq!(arguments[1].value, std_div_error); } + #[test] + fn arithmetic_division_canonical_result_and_diverror_are_structural_not_path_keyed() { + let mut dag = Dag::new(); + let int = dag.int_shape().expect("bootstrap Int").declaration; + let result = canonical_result_decl(&dag).expect("bootstrap Result").id; + let div_error = canonical_div_error_decl(&dag) + .expect("bootstrap DivError") + .id; + + dag.declaration_mut(result).span.file = "renamed/std/errors.dag".to_string(); + dag.declaration_mut(div_error).span.file = "renamed/std/errors.dag".to_string(); + + let resolved = resolve_operator_arrow( + &mut dag, + OperatorKind::Arithmetic(ArithmeticOp::Div), + &TypeShape::new(int), + ) + .expect("division must resolve through structural error primitives"); + let TypeConnective::Instantiation { + template, + arguments, + } = &dag.declaration(resolved.output.declaration).connective + else { + panic!("division output must remain an instantiation"); + }; + + assert_eq!(*template, result); + assert_eq!(arguments.len(), 2); + assert_eq!(arguments[0].value, int); + assert_eq!(arguments[1].value, div_error); + assert_eq!( + canonical_result_decl(&dag).expect("structural Result").id, + result + ); + assert_eq!( + canonical_div_error_decl(&dag) + .expect("structural DivError") + .id, + div_error + ); + } + #[test] fn arithmetic_division_ignores_user_shadowed_result_carriers() { let mut dag = Dag::new(); @@ -6863,6 +6841,13 @@ mod bool_logical_operator_arrow_tests { nominal_opacity: None, span, }); + assert!( + !substrate_div_error_type_decl_suppressed_for_emit( + &dag, + dag.declaration(shadow_div_error) + ), + "payload-bearing DivError twin must not match the canonical unit-variant fingerprint", + ); let resolved = resolve_operator_arrow( &mut dag, @@ -6890,6 +6875,200 @@ mod bool_logical_operator_arrow_tests { ); } + #[test] + fn arithmetic_division_prefers_bootstrap_error_primitives_over_exact_twins() { + let mut dag = Dag::new(); + let int = dag.int_shape().expect("bootstrap Int").declaration; + let std_result = canonical_result_decl(&dag).expect("std Result").id; + let std_div_error = canonical_div_error_decl(&dag).expect("std DivError").id; + let span = synthetic_span(); + + let twin_ok = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: twin_ok, + name: None, + connective: TypeConnective::Atom(AtomPayload::TypeParam("ok".to_string())), + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: span.clone(), + }); + let twin_err = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: twin_err, + name: None, + connective: TypeConnective::Atom(AtomPayload::TypeParam("err".to_string())), + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: span.clone(), + }); + let twin_ok_payload = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: twin_ok_payload, + name: None, + connective: TypeConnective::Conj { + children: vec![Field { + label: "value".to_string(), + ty: twin_ok, + }], + }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: span.clone(), + }); + let twin_err_payload = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: twin_err_payload, + name: None, + connective: TypeConnective::Conj { + children: vec![Field { + label: "value".to_string(), + ty: twin_err, + }], + }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: span.clone(), + }); + let twin_result = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: twin_result, + name: Some("Result".to_string()), + connective: TypeConnective::Disj { + variants: vec![ + Field { + label: "Ok".to_string(), + ty: twin_ok_payload, + }, + Field { + label: "Err".to_string(), + ty: twin_err_payload, + }, + ], + }, + type_params: vec![twin_ok, twin_err], + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: span.clone(), + }); + let twin_divide_by_zero = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: twin_divide_by_zero, + name: None, + connective: TypeConnective::Conj { children: vec![] }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: span.clone(), + }); + let twin_overflow = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: twin_overflow, + name: None, + connective: TypeConnective::Conj { children: vec![] }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span: span.clone(), + }); + let twin_div_error = dag.alloc_declaration_id(); + dag.push_declaration(Declaration { + id: twin_div_error, + name: Some("DivError".to_string()), + connective: TypeConnective::Disj { + variants: vec![ + Field { + label: "DivideByZero".to_string(), + ty: twin_divide_by_zero, + }, + Field { + label: "Overflow".to_string(), + ty: twin_overflow, + }, + ], + }, + type_params: Vec::new(), + phantom_params: Vec::new(), + meta_tag: None, + specialization_parent: None, + inhabits: None, + value_body: None, + refinement: None, + nominal_opacity: None, + span, + }); + + assert!(substrate_result_type_decl_suppressed_for_emit( + &dag, + dag.declaration(twin_result) + )); + assert!(substrate_div_error_type_decl_suppressed_for_emit( + &dag, + dag.declaration(twin_div_error) + )); + assert_eq!(canonical_result_decl(&dag).expect("Result").id, std_result); + assert_eq!( + canonical_div_error_decl(&dag).expect("DivError").id, + std_div_error + ); + + let resolved = resolve_operator_arrow( + &mut dag, + OperatorKind::Arithmetic(ArithmeticOp::Div), + &TypeShape::new(int), + ) + .expect("division must resolve through bootstrap error primitives"); + let TypeConnective::Instantiation { + template, + arguments, + } = &dag.declaration(resolved.output.declaration).connective + else { + panic!("division output must remain an instantiation"); + }; + assert_eq!(*template, std_result); + assert_eq!(arguments[1].value, std_div_error); + assert_ne!(*template, twin_result); + assert_ne!(arguments[1].value, twin_div_error); + } + #[test] fn read_algebra_field_fails_closed_on_unresolved_instantiation_argument() { let mut dag = Dag::new(); diff --git a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs index a796a28b3b2..f35b5340c1b 100644 --- a/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs +++ b/src/v3/compiler/tests/boundary/m1_3_emit_go_test.rs @@ -492,7 +492,7 @@ fn emit_go_checked_division_prelude_maps_overflow() { } #[test] -fn emit_go_omits_div_prelude_for_user_diverror_signature_without_division() { +fn emit_go_structural_diverror_signature_uses_substrate_prelude() { let dag = compile_to_dag( "type DivError = DivideByZero | Overflow\n\ fn passthrough(x: DivError) -> DivError = x\n", @@ -503,12 +503,12 @@ fn passthrough(x: DivError) -> DivError = x\n", .expect("emits go module") .text; assert!( - !out.contains("func v3intdiv"), - "user DivError signatures should not trigger integer division prelude; got: {out}" + out.contains("func v3intdiv"), + "structural DivError signatures use the substrate prelude, independent of source path; got: {out}" ); assert!( out.matches("type DivError").count() == 1, - "user DivError should emit once without colliding with a std prelude; got: {out}" + "structural DivError should materialize once through the substrate prelude; got: {out}" ); } diff --git a/src/v3/compiler/tests/boundary/m1_3_emit_rust_test.rs b/src/v3/compiler/tests/boundary/m1_3_emit_rust_test.rs index 8592f95feea..1a6bd414d3e 100644 --- a/src/v3/compiler/tests/boundary/m1_3_emit_rust_test.rs +++ b/src/v3/compiler/tests/boundary/m1_3_emit_rust_test.rs @@ -300,18 +300,18 @@ let x: Int = 42", } #[test] -fn emit_rust_omits_div_prelude_for_user_diverror_signature_without_division() { +fn emit_rust_structural_diverror_signature_uses_substrate_prelude() { let out = emit_module( "type DivError = DivideByZero | Overflow\n\ fn passthrough(x: DivError) -> DivError = x\n", ); assert!( - !out.contains("__v3_int_div"), - "user DivError signatures should not trigger integer division prelude; got: {out}" + out.contains("__v3_int_div"), + "structural DivError signatures use the substrate prelude, independent of source path; got: {out}" ); assert!( out.matches("pub enum DivError").count() == 1, - "user DivError should emit once without colliding with a std prelude; got: {out}" + "structural DivError should materialize once through the substrate prelude; got: {out}" ); } diff --git a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs index c5ef71ba18f..cee701a869f 100644 --- a/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs +++ b/src/v3/compiler/tests/boundary/m1_4_emit_python_test.rs @@ -228,7 +228,7 @@ fn emit_python_checked_division_prelude_maps_overflow() { } #[test] -fn emit_python_omits_div_prelude_for_user_diverror_signature_without_division() { +fn emit_python_structural_diverror_signature_uses_substrate_prelude() { let dag = compile_to_dag( "type DivError = DivideByZero | Overflow\n\ fn passthrough(x: DivError) -> DivError = x\n", @@ -237,12 +237,12 @@ fn passthrough(x: DivError) -> DivError = x\n", .expect("compiles"); let out = emit_python_module(&dag).expect("emits python module"); assert!( - !out.contains("def __v3_idiv"), - "user DivError signatures should not trigger integer division prelude; got: {out}" + out.contains("def __v3_idiv"), + "structural DivError signatures use the substrate prelude, independent of source path; got: {out}" ); assert!( - out.matches("class DivError:").count() == 1, - "user DivError should emit once without colliding with a std prelude; got: {out}" + out.matches("class DivError(enum.IntEnum):").count() == 1, + "structural DivError should materialize once through the substrate prelude; got: {out}" ); }