diff --git a/dag/test/claim/generic_item_clone_bound_witness_test.dag b/dag/test/claim/generic_item_clone_bound_witness_test.dag index 2bf0a415a01..9d0e4b5e240 100644 --- a/dag/test/claim/generic_item_clone_bound_witness_test.dag +++ b/dag/test/claim/generic_item_clone_bound_witness_test.dag @@ -131,3 +131,44 @@ test fn both_triggers_render_single_bound() -> Bool { test fn unbounded_declared_container_negative_control() -> Bool { w_unbounded_declared_container_propagates_nothing() } + +data fn_clone_bound_wf_witness_note: String = "THIRD site, same fixpoint: naming a Clone-bounded declared type in a FN value-param or return type is exactly as ill-formed as naming it in a field (v1_fn_param_wf_needs_clone, extending the same well-formedness trigger onto v1_generic_params_needing_clone_bound rather than a second fixpoint). w_fn_wf_bound_propagates_to_bare_generic is the occurrence_binding_from_candidates specimen shape (deep-heron's honest regen of dag/std/occurrence_binding.dag) reduced to a discriminating fixture: N is named only inside BindingOccurrence (value-param position) and OccurrenceBindingResult (return position), never as a bare N itself, so the structural trigger (v1_type_param_needs_clone_bound) alone would leave it unbounded and only the WF trigger renders `` -- this is the RED-on-pre-fix / GREEN-on-fix witness. w_fn_no_wf_trigger_stays_bare is the negative control: a fn naming only unbounded declared types keeps its generic param bare, proving the WF trigger does not widen to every fn touching a declared type. w_impl_accessor_inherits_item_bound_once locks in the already-correct IMPL-side behavior (accessor impl blocks read the struct's own item-level clone_bounded_type_params via emit_item_type_params_with_clone_bounds, not a fn-grain re-derivation) by asserting the accessor impl header carries the bound exactly once, with doubled-spelling refusals mirroring both_triggers_render_single_bound above." + +fn w_fn_wf_bound_propagates_to_bare_generic() -> Bool { + compile_dag_rust_emit_check( + "module fn_clone_bound_witness.wf_propagate\nimport std.algebra { FreeMonoid }\n\ntype ContainmentPath {\n ancestors: FreeMonoid\n terminal: N\n}\n\ntype BindingOccurrence {\n containment: ContainmentPath\n}\n\ntype OccurrenceBindingResult {\n containment: ContainmentPath\n}\n\nfn occurrence_binding_from_candidate(occurrence: BindingOccurrence) -> OccurrenceBindingResult {\n OccurrenceBindingResult { containment: occurrence.containment }\n}\n", + "src/fn_clone_bound_witness_wf_propagate.rs", + ["fn occurrence_binding_from_candidate"], + ["fn occurrence_binding_from_candidate"] + ) +} + +fn w_fn_no_wf_trigger_stays_bare() -> Bool { + compile_dag_rust_emit_check( + "module fn_clone_bound_witness.wf_negative\n\ntype Holder {\n tag: Int\n}\n\nfn describe_holder(holder: Holder) -> String {\n \"holder\"\n}\n", + "src/fn_clone_bound_witness_wf_negative.rs", + ["fn describe_holder"], + ["fn describe_holder"] + ) +} + +fn w_impl_accessor_inherits_item_bound_once() -> Bool { + compile_dag_rust_emit_check( + "module fn_clone_bound_witness.impl_inherit\nimport std.algebra { FreeMonoid }\n\ntype ContainmentPath {\n ancestors: FreeMonoid\n terminal: N\n}\n\ntype BindingOccurrence {\n containment: ContainmentPath\n}\n\ntype FoldState\n = FoldZero\n | FoldOne { first: BindingOccurrence }\n | FoldMany { first: BindingOccurrence, second: BindingOccurrence }\n", + "src/fn_clone_bound_witness_impl_inherit.rs", + ["impl FoldState"], + ["impl FoldState", "impl FoldState", "impl FoldState", "impl FoldState"] + ) +} + +test fn fn_wf_bound_propagation_positive() -> Bool { + w_fn_wf_bound_propagates_to_bare_generic() +} + +test fn fn_wf_no_trigger_negative_control() -> Bool { + w_fn_no_wf_trigger_stays_bare() +} + +test fn impl_accessor_inherits_item_bound_once() -> Bool { + w_impl_accessor_inherits_item_bound_once() +} diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 709e9d1eb32..f1c2c5c2679 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -4655,6 +4655,13 @@ fn emit_type_params(params: List, source_indices: Map) -> String { + if generic_param_names |> count == 0 { "" } + else { + concat("<", generic_param_names |> map(n => to_pascal(name: n)) |> join(separator: ", "), ">") + } +} + fn emit_type_params_with_clone_bound(params: List, clone_param: String, source_indices: Map) -> String { if params |> count == 0 { "" } else { @@ -5044,7 +5051,8 @@ fn emit_enum_shared_accessors(name: String, type_params: String, generic_param_n concat(" pub fn ", emit_ident(name: fname, target: Rust), "(&self) -> ", ty, " {\n match self {\n", arms_str, "\n }\n }") ) let fns_str = accessor_fns |> join(separator: "\n") - concat("impl", type_params, " ", name, type_params, " {\n", fns_str, "\n}") + let type_application_params = emit_bare_type_params(generic_param_names: generic_param_names) + concat("impl", type_params, " ", name, type_application_params, " {\n", fns_str, "\n}") } fn string_without_prefix(value: String, prefix: String) -> String? { @@ -5318,6 +5326,9 @@ fn emit_fn_def(name: String, params: List, inferred: Node, body: Node, reg return_is_bare_generic: return_is_bare_generic, ret_name: ret_name, body_is_param_ref: body_is_param_ref, + ret: inferred, + bounds: emit_info.clone_bounded_type_params, + type_decl_items: emit_info.type_decl_items, source_indices: si ) let needs_clone_bound = (clone_param_names |> count) > 0 diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index fc248464fd9..72ff5f6c7d9 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -11746,6 +11746,27 @@ pub fn emit_type_params( } } +pub fn emit_bare_type_params(generic_param_names: Rc>) -> String { + if ((generic_param_names.clone().len() as i64) == 0) { + "".to_string() + } else { + v1_rt::concat( + v1_rt::concat( + "<".to_string(), + Rc::new({ + let mut __result = Vec::new(); + for n in generic_param_names.clone().iter().cloned() { + __result.push(to_pascal(n.clone())); + } + __result + }) + .join(&", ".to_string()), + ), + ">".to_string(), + ) + } +} + pub fn emit_type_params_with_clone_bound( params: Rc>>, clone_param: String, @@ -13239,6 +13260,7 @@ pub fn emit_enum_shared_accessors( __result }); let fns_str = accessor_fns.clone().join(&"\n".to_string()); + let type_application_params = emit_bare_type_params(generic_param_names.clone()); v1_rt::concat( v1_rt::concat( v1_rt::concat( @@ -13250,7 +13272,7 @@ pub fn emit_enum_shared_accessors( ), name.clone(), ), - type_params.clone(), + type_application_params.clone(), ), " {\n".to_string(), ), @@ -13939,6 +13961,9 @@ pub fn emit_fn_def( return_is_bare_generic.clone(), ret_name.clone(), body_is_param_ref.clone(), + inferred.clone(), + emit_info.clone_bounded_type_params.clone(), + emit_info.type_decl_items.clone(), si.clone(), ); let needs_clone_bound = ((clone_param_names.clone().len() as i64) > 0); diff --git a/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs b/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs index 8e6aa52a56e..c233a6058bb 100644 --- a/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs +++ b/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs @@ -83,6 +83,15 @@ pub fn trait_derive_emit_item_clone_bound_wf_propagation_note() -> String { CACHED.with(|c: &String| c.clone()) } +pub fn trait_derive_emit_fn_clone_bound_wf_propagation_note() -> String { + thread_local! { + static CACHED: String = { + "THIRD site for the SAME well-formedness trigger (trait_derive_emit_item_clone_bound_wf_propagation_note above), applied to FN declarations rather than item (struct/enum) declarations: naming a Clone-bounded declared type G in a fn's value-param or return type is exactly as ill-formed as naming it in a field, so a fn generic parameter P earns `: Clone` when v1_fn_param_wf_needs_clone finds it occupying a Clone-bounded argument position of some declared type mentioned in a value param or the return type — reusing v1_type_expr_wf_needs_clone_param verbatim against the same v1_clone_bounded_type_params fixpoint (EmitGraphInfo.clone_bounded_type_params), not a second fixpoint or a restated predicate. This is additive to, not a replacement for, the existing structural fn trigger (v1_type_param_needs_clone_bound: bare-generic return or direct container-element usage) computed in v1_generic_params_needing_clone_bound — the two triggers answer different questions (usage-shape vs. naming-a-bounded-declared-type) and a fn param needing either earns the bound. Enum/struct IMPL surfaces (accessor impl blocks, supplemental impls) already inherit their item's bounded type_params string from emit_item_type_params_with_clone_bounds at the struct/enum decl site, so no separate IMPL-side propagation is needed there; the gap closed here is specifically free FN declarations, whose type_params were computed independently of the item-level fixpoint. Discovered live (not hypothetical) in deep-heron's honest regen of dag/std/occurrence_binding.dag: occurrence_binding_from_candidates names BindingOccurrence and BindingCandidate (each well-formedness-bounded because their field ContainmentPath is FreeMonoid-derive-bounded) at value-param position without the fn declaring N: Clone, and OccurrenceBindingResult at the return position — E0277 at both without this trigger.".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + pub fn trait_derive_emit_item_clone_bound_contract_fork_note() -> String { thread_local! { static CACHED: String = { @@ -315,19 +324,29 @@ pub fn v1_generic_params_needing_clone_bound( return_is_bare_generic: bool, ret_name: String, body_is_param_ref: bool, + ret: Rc, + bounds: Rc>>>, + type_decl_items: Rc>>, source_indices: Rc>>, ) -> Rc> { Rc::new({ let mut __result = Vec::new(); for g in generic_param_names.clone().iter().cloned() { - if v1_type_param_needs_clone_bound( + if (v1_type_param_needs_clone_bound( g.clone(), return_is_bare_generic.clone(), ret_name.clone(), body_is_param_ref.clone(), value_params.clone(), source_indices.clone(), - ) { + ) || v1_fn_param_wf_needs_clone( + g.clone(), + value_params.clone(), + ret.clone(), + bounds.clone(), + type_decl_items.clone(), + source_indices.clone(), + )) { __result.push(g); } } @@ -387,6 +406,20 @@ pub fn v1_item_type_param_needs_clone_bound_struct( } } +pub fn v1_wf_child_type_node( + ch: Rc, + source_indices: Rc>>, +) -> Rc { + { + let resolved = child_type_node(ch.clone()); + if (authored_name_at(source_indices.clone(), resolved.clone()) != "".to_string()) { + resolved.clone() + } else { + ch.clone() + } + } +} + pub fn v1_type_expr_is_bare_param( param_name: String, type_expr: Rc, @@ -460,7 +493,7 @@ pub fn v1_type_expr_clone_impl_needs_param( for c in type_expr.children.clone().iter().cloned() { if v1_type_expr_clone_impl_needs_param( param_name.clone(), - child_type_node(c.clone()), + v1_wf_child_type_node(c.clone(), source_indices.clone()), type_decl_items.clone(), source_indices.clone(), ) { @@ -497,7 +530,7 @@ pub fn v1_declared_arg_positions_need_clone_param( generic_param_name_at(decl_param.clone(), source_indices.clone()), ) && v1_type_expr_clone_impl_needs_param( param_name.clone(), - child_type_node(type_arg.clone()), + v1_wf_child_type_node(type_arg.clone(), source_indices.clone()), type_decl_items.clone(), source_indices.clone(), )); @@ -548,7 +581,7 @@ pub fn v1_type_expr_wf_needs_clone_param( for c in type_expr.children.clone().iter().cloned() { if v1_type_expr_wf_needs_clone_param( param_name.clone(), - child_type_node(c.clone()), + v1_wf_child_type_node(c.clone(), source_indices.clone()), bounds.clone(), type_decl_items.clone(), source_indices.clone(), @@ -637,6 +670,38 @@ pub fn v1_item_param_wf_needs_clone( } } +pub fn v1_fn_param_wf_needs_clone( + param_name: String, + value_params: Rc>>, + ret: Rc, + bounds: Rc>>>, + type_decl_items: Rc>>, + source_indices: Rc>>, +) -> bool { + ({ + let mut __found = false; + for vp in value_params.clone().iter().cloned() { + if v1_type_expr_wf_needs_clone_param( + param_name.clone(), + param_node_type_expr(vp.clone()), + bounds.clone(), + type_decl_items.clone(), + source_indices.clone(), + ) { + __found = true; + break; + } + } + __found + } || v1_type_expr_wf_needs_clone_param( + param_name.clone(), + ret.clone(), + bounds.clone(), + type_decl_items.clone(), + source_indices.clone(), + )) +} + pub fn v1_item_clone_undecided_head( item: Rc, type_decl_items: Rc>>, diff --git a/src/v1/trait_derive_emit.dag b/src/v1/trait_derive_emit.dag index 20d655b802f..29d1c710deb 100644 --- a/src/v1/trait_derive_emit.dag +++ b/src/v1/trait_derive_emit.dag @@ -43,6 +43,8 @@ data trait_derive_emit_item_clone_bound_rule_note: String = "Item-level Clone bo data trait_derive_emit_item_clone_bound_wf_propagation_note: String = "SECOND, INDEPENDENT trigger for an item-level Clone bound, distinct from the derive/lowering trigger above and NOT a widening of it: WELL-FORMEDNESS PROPAGATION. Naming a declared generic type G at all requires satisfying G's own declared bounds, so if G's i-th parameter already carries `: Clone` and the i-th argument's Clone impl requires P: Clone, then the item declaring that field is ill-formed without P: Clone -- rustc E0277 at the field, before any derive is considered. This applies to STRUCTS AND ENUMS alike, because it is a property of naming the type, not of deriving Clone for it; the rule note above correctly scopes the DERIVE trigger to structs (derive emits per-impl bounds) and that scoping is unchanged here. The two axes are grounded differently and can disagree: im::Vector carries NO declaration bound (checked against im-15.1.0 vector::Vector), so a container field is a derive-trigger fact only, while Boxed is a well-formedness fact that propagates through Nested { boxed: Boxed } and List> alike. The requirement is a LEAST FIXPOINT over the declared-type graph (v1_clone_bounded_type_params), not a one-field-shape-deep read: each round derives every declared generic type's bounded parameters from the current approximation and stops when a round adds nothing, so a chain Boxed -> Nested -> TwoHop propagates all the way and a recursive type (Cyclic { self_ref: Cyclic? }) saturates instead of diverging. The derive trigger SEEDS the fixpoint (v1_clone_bound_seed_for_item, structs only, reusing v1_item_type_param_needs_clone_bound_struct verbatim rather than restating it) and then propagates, because Boxed earns its bound from the derive trigger and Nested { boxed: Boxed } inherits it from well-formedness. Two sub-predicates, deliberately separate because they answer different questions about the same type expression: v1_type_expr_clone_impl_needs_param asks whether `tau: Clone` requires P: Clone (every derive(Clone) type and every container bounds all of its parameters, so this reduces to `P occurs in tau`), while v1_type_expr_wf_needs_clone_param asks whether NAMING tau requires it (only the argument positions the fixpoint has already bounded count). The undecidable residue is answered by its own total function rather than fused into either Bool: a type application whose head is neither a container nor a declared type in the closure has no readable parameter list, so v1_type_expr_clone_undecided_head names it and the emit site refuses with compile_error!. It is NOT silently treated as `no bound needed` and NOT widened to `bound everything` -- widening would zero the deficit's frequency by construction (DESIGN section 5, absorbing fallback). Dead in corpus as of this landing; kept as the fail-closed arm." +data trait_derive_emit_fn_clone_bound_wf_propagation_note: String = "THIRD site for the SAME well-formedness trigger (trait_derive_emit_item_clone_bound_wf_propagation_note above), applied to FN declarations rather than item (struct/enum) declarations: naming a Clone-bounded declared type G in a fn's value-param or return type is exactly as ill-formed as naming it in a field, so a fn generic parameter P earns `: Clone` when v1_fn_param_wf_needs_clone finds it occupying a Clone-bounded argument position of some declared type mentioned in a value param or the return type — reusing v1_type_expr_wf_needs_clone_param verbatim against the same v1_clone_bounded_type_params fixpoint (EmitGraphInfo.clone_bounded_type_params), not a second fixpoint or a restated predicate. This is additive to, not a replacement for, the existing structural fn trigger (v1_type_param_needs_clone_bound: bare-generic return or direct container-element usage) computed in v1_generic_params_needing_clone_bound — the two triggers answer different questions (usage-shape vs. naming-a-bounded-declared-type) and a fn param needing either earns the bound. Enum/struct IMPL surfaces (accessor impl blocks, supplemental impls) already inherit their item's bounded type_params string from emit_item_type_params_with_clone_bounds at the struct/enum decl site, so no separate IMPL-side propagation is needed there; the gap closed here is specifically free FN declarations, whose type_params were computed independently of the item-level fixpoint. Discovered live (not hypothetical) in deep-heron's honest regen of dag/std/occurrence_binding.dag: occurrence_binding_from_candidates names BindingOccurrence and BindingCandidate (each well-formedness-bounded because their field ContainmentPath is FreeMonoid-derive-bounded) at value-param position without the fn declaring N: Clone, and OccurrenceBindingResult at the return position — E0277 at both without this trigger." + data trait_derive_emit_item_clone_bound_contract_fork_note: String = "MODEL-REALIZATION FORK (counted): supplemental generic bounds are also modeled at v2.std.compilers.target_model target_derive_supplemental_generic_bound_contract (trait_derive_supplemental_generic_bound_contract_test.dag) for target collection representation choices. That carrier has no consumer in v1 seed emit today; it is inhabited with cited upstream impl authorities (v2.extdeps.languages.rust) and consumed by the v2 translate path (v2.compiler.06_translate via target_derive_supplemental_generic_bound_contract_catalog_from_node). This v1 structural rule is a separate interim authority for generic struct item type-param bounds (bare field or direct container element) — an approximation of cited upstream requirements pending v2 emitter subsumption at that grain; dissolution re-grounds onto upstream impl requirements, not a mechanical lift of the same predicate — the two can disagree at the edges." data trait_derive_emit_item_clone_bound_contract_fork_dissolve_on: String = "DISSOLVE-ON: v2 Rust emitter consumes target_derive_supplemental_generic_bound_contract for item-level generic bounds; then delete this structural implementation — do not accumulate beside the contract." @@ -175,6 +177,9 @@ fn v1_generic_params_needing_clone_bound( return_is_bare_generic: Bool, ret_name: String, body_is_param_ref: Bool, + ret: Node, + bounds: Map>, + type_decl_items: Map, source_indices: Map ) -> List { generic_param_names |> filter(g => @@ -185,6 +190,13 @@ fn v1_generic_params_needing_clone_bound( body_is_param_ref: body_is_param_ref, value_params: value_params, source_indices: source_indices + ) || v1_fn_param_wf_needs_clone( + param_name: g, + value_params: value_params, + ret: ret, + bounds: bounds, + type_decl_items: type_decl_items, + source_indices: source_indices ) ) } @@ -219,6 +231,15 @@ fn v1_item_type_param_needs_clone_bound_struct( ) } +fn v1_wf_child_type_node(ch: Node, source_indices: Map) -> Node { + let resolved = child_type_node(ch: ch) + if authored_name_at(source_indices: source_indices, node: resolved) != "" { + resolved + } else { + ch + } +} + fn v1_type_expr_is_bare_param(param_name: String, type_expr: Node, source_indices: Map) -> Bool { authored_name_at(source_indices: source_indices, node: type_expr) == param_name && type_expr.connective == NoConnective @@ -272,7 +293,7 @@ fn v1_type_expr_clone_impl_needs_param( type_expr.children |> any(c => v1_type_expr_clone_impl_needs_param( param_name: param_name, - type_expr: child_type_node(ch: c), + type_expr: v1_wf_child_type_node(ch: c, source_indices: source_indices), type_decl_items: type_decl_items, source_indices: source_indices ) @@ -299,7 +320,7 @@ fn v1_declared_arg_positions_need_clone_param( let here = set_contains(bound_params, generic_param_name_at(n: decl_param, source_indices: source_indices)) && v1_type_expr_clone_impl_needs_param( param_name: param_name, - type_expr: child_type_node(ch: type_arg), + type_expr: v1_wf_child_type_node(ch: type_arg, source_indices: source_indices), type_decl_items: type_decl_items, source_indices: source_indices ) @@ -329,7 +350,7 @@ fn v1_type_expr_wf_needs_clone_param( let nested = type_expr.children |> any(c => v1_type_expr_wf_needs_clone_param( param_name: param_name, - type_expr: child_type_node(ch: c), + type_expr: v1_wf_child_type_node(ch: c, source_indices: source_indices), bounds: bounds, type_decl_items: type_decl_items, source_indices: source_indices @@ -380,6 +401,31 @@ fn v1_item_param_wf_needs_clone( ) } +fn v1_fn_param_wf_needs_clone( + param_name: String, + value_params: List, + ret: Node, + bounds: Map>, + type_decl_items: Map, + source_indices: Map +) -> Bool { + value_params |> any(vp => + v1_type_expr_wf_needs_clone_param( + param_name: param_name, + type_expr: param_node_type_expr(n: vp), + bounds: bounds, + type_decl_items: type_decl_items, + source_indices: source_indices + ) + ) || v1_type_expr_wf_needs_clone_param( + param_name: param_name, + type_expr: ret, + bounds: bounds, + type_decl_items: type_decl_items, + source_indices: source_indices + ) +} + fn v1_item_clone_undecided_head( item: Node, type_decl_items: Map,