From d27f9a2837ebf2ebf29b2c84a5ab7517fe4cf066 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 21 Aug 2026 00:27:53 +0000 Subject: [PATCH 1/3] Derive PartialEq bounds on generic fn params from body equality use The emitter derived Clone bounds on generic fn params ~30-48 references deep and derived nothing for equality, so a generic fn comparing two values of its own type param emitted bare and rustc refused it: error[E0369]: binary operation `==` cannot be applied to type `K` Two existing body-reading Clone triggers each declare a depth boundary -- BoundedToDirectTopLevelMatchBody and BoundedToDirectSingleCallLambdaBody, both refusing to walk a body that reaches its target through Let/If/Branch -- and both name the SAME next-rung trigger in the same words: a recursive body walker, once a real specimen needs either boundary widened. Equality is that specimen, so this builds the walker rather than adding a fourth shallow body-reading path beside two already-declared-incomplete ones. The traversal is generous and the predicate is exact, because the two directions have asymmetric cost. binop_left/binop_right are children 0 and 1, so descending every child reaches every operand, and a node the walk misses simply yields no bound -- the status quo. But a bound is not free in the other direction: adding `K: PartialEq` TIGHTENS the emitted signature, so a spurious bound breaks every caller instantiating K with a non-PartialEq type. Precision therefore lives in v1_equality_bound_param_name, which requires both operands to resolve to the SAME type name and that name to be one of the fn's own generic params -- never the presence of a `==`. Layering follows the existing split, for the reason trait_bound_witness.dag's own import note gives: the Node-free decision core lives there, the Node-walking extraction lives at the call site in emit_fn_def. Reversing that would close an emit_rust -> trait_bound_witness -> emit_rust cycle, which acyclicity forbids. Rendering is generalized rather than forked. The trait was a hardcoded literal (`concat(pascal, ": Clone")`), so a param earning two derivations could not be expressed. v1_emit_type_params_with_bounds is keyed on a per-param trait list -- one axis, so a third trait later is a row rather than another parameter -- and v1_emit_type_params_with_clone_bounds is retained at its original signature as a byte-identical adapter over it, leaving its own caller untouched. MEASURED ON REAL EMISSION, both arms, same fixture shape differing only in what the `==` compares: positive fn cap_row_has(row: CapRow, shape: ShapeTag, key: K) row.shape == shape && (row.keys |> any(k => k == key)) -> pub fn cap_row_has(...) negative fn cap_row_shape_is(row: CapRow, shape: ShapeTag) row.shape == shape -> pub fn cap_row_shape_is(...) The negative is the discriminating arm: a generic param IS present and a `==` IS present, and it must still earn nothing. A predicate keyed on the operator emits `K: Clone + PartialEq` there and breaks callers; zero PartialEq occurrences appear anywhere in that emission. The walker reached the earning comparison through lambda -> any() -> pipeline -> `&&`, the depth both boundaries refuse. The expected spelling is externally grounded rather than copied from this tree: quick-lynx-620 measured it against rustc on the real emission, hand-adding the bound to the emitted mirror and rebuilding the seed to zero errors, and reported `K: Clone + PartialEq` -- the equality bound COMPOSING with the separately derived Clone bound rather than replacing it. Clone survives in both arms above. Corpus safety, with the trigger live: required-regen: first_generation_equal=true planned=128 executed=128 All 128 modules re-emit byte-identically, so the trigger fires nowhere in the existing corpus and adds no spurious bound. That also means the corpus supplies no positive evidence, which is why the enrolled witness is a controlled fixture authoring both comparisons itself rather than a pin to a live population; the real-world specimen lives on an unlanded branch and is confirmation, not gate. Scoped to three changed authority modules deliberately. The behavioral-receipt phase refuses above a declared cap of three rather than sampling, and this branch carries only the equality work -- proven at the binary level, where the ownership carrier's symbols are absent and the trigger's are present. --- .../claim/fn_equality_bound_witness_test.dag | 25 ++++ src/v1/05_emit_rust.dag | 102 ++++++++++++++- src/v1/stage0/src/v1_compiler_emit_rust.rs | 118 ++++++++++++++++-- .../src/v1_compiler_trait_bound_witness.rs | 25 ++++ .../src/v1_compiler_trait_derive_emit.rs | 42 +++++-- src/v1/trait_bound_witness.dag | 30 +++++ src/v1/trait_derive_emit.dag | 37 +++++- 7 files changed, 346 insertions(+), 33 deletions(-) create mode 100644 dag/test/claim/fn_equality_bound_witness_test.dag diff --git a/dag/test/claim/fn_equality_bound_witness_test.dag b/dag/test/claim/fn_equality_bound_witness_test.dag new file mode 100644 index 00000000000..63742447769 --- /dev/null +++ b/dag/test/claim/fn_equality_bound_witness_test.dag @@ -0,0 +1,25 @@ +module test.claim.fn_equality_bound_witness + +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data fn_equality_bound_witness_note: String = "Executing floor witness for the FN-level PartialEq bound trigger (v1.compiler.emit_rust v1_fn_body_equality_bound_param_names, deciding via v1.compiler.trait_bound_witness v1_equality_bound_param_name). The emitter derives Clone bounds on generic fn params ~30-48 references deep but derived NOTHING for equality, so a generic fn comparing two values of its own type param emitted bare and rustc refused with E0369 'binary operation == cannot be applied to type K'. GROUNDING FOR THE EXPECTED SPELLING, so it is not a measurement copied from this tree: quick-lynx-620 measured the requirement against rustc on the real emission -- hand-adding the bound to the emitted mirror and rebuilding the seed gave zero errors, and the required rendering is `K: Clone + PartialEq`, the equality bound COMPOSING with the separately-derived Clone bound on the same parameter rather than replacing it. THE DISCRIMINATOR IS TYPE RESOLUTION, NOT THE == SPELLING, and the two rows below exist to prove the trigger cannot be satisfied by matching on the operator. earning_comparison_bounds_its_generic_param carries BOTH comparisons in one body, exactly the shape of the real specimen: a concrete closed-enum comparison that must earn nothing, and a comparison of two K values that must earn the bound -- rustc emits exactly ONE E0369 on that real shape, at the second comparison, so the first provably compiles unbounded today. concrete_only_comparison_leaves_generic_param_bare is the sharper negative: a fn that HAS a generic param and HAS a comparison, where the comparison involves only concrete types. A predicate keyed on the presence of == bounds K there and breaks every caller instantiating K with a non-PartialEq type; the type-resolving predicate must leave it bare. That row is what makes the pair discriminating rather than two positives, and it is why the refusals below assert the bounded spelling is ABSENT rather than merely asserting some other spelling is present. Over-approximation is not free here: adding a bound TIGHTENS the emitted signature, so a spurious bound is a real break, while a missed bound only leaves the pre-existing E0369 in place." + +fn earning_comparison_bounds_its_generic_param() -> Bool { + compile_dag_rust_emit_check( + "module fn_equality_witness.earning\n\ntype ShapeTag = ShapeAlpha | ShapeBeta\n\ntype CapRow {\n shape: ShapeTag\n keys: List\n}\n\nfn cap_row_has(row: CapRow, shape: ShapeTag, key: K) -> Bool {\n row.shape == shape && (row.keys |> any(k => k == key))\n}\n", + "src/fn_equality_witness_earning.rs", + ["fn cap_row_has"], + ["fn cap_row_has", "fn cap_row_has"] + ) +} + +fn concrete_only_comparison_leaves_generic_param_bare() -> Bool { + compile_dag_rust_emit_check( + "module fn_equality_witness.concrete\n\ntype ShapeTag = ShapeAlpha | ShapeBeta\n\ntype CapRow {\n shape: ShapeTag\n keys: List\n}\n\nfn cap_row_shape_is(row: CapRow, shape: ShapeTag) -> Bool {\n row.shape == shape\n}\n", + "src/fn_equality_witness_concrete.rs", + ["fn cap_row_shape_is"], + ["K: PartialEq", "K: Clone + PartialEq"] + ) +} diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 6e4d0e7e5a5..6513c9da58e 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -63,7 +63,7 @@ import v1.compiler.trait_derive_emit { v1_emit_enum_derives, v1_emit_struct_from_capability_table, v1_emit_enum_supplemental_impls, v1_generic_params_needing_clone_bound, - v1_emit_type_params_with_clone_bounds, + v1_emit_type_params_with_clone_bounds, v1_emit_type_params_with_bounds, v1_clone_bounded_type_params, v1_item_clone_bounded_param_names, v1_item_clone_undecided_head, v1_map_key_required_type_names, v1_item_field_type_exprs, v1_with_map_key_requirement, v1_clone_impl_required_type_params, @@ -73,7 +73,8 @@ import v1.compiler.trait_derive_emit { } import v1.compiler.trait_bound_witness { v1_rc_match_scrutinee_clone_bound_param_names, v1_union_clone_param_names, - v1_call_forwarding_clone_bound_wrapper_param_names + v1_call_forwarding_clone_bound_wrapper_param_names, + v1_equality_bound_param_name } import std.types { is_container_type, is_kernel_type, container_template_algebra } @@ -5833,6 +5834,88 @@ fn v1_fn_body_derived_clone_param_names(params: List, inferred: Node, body ) } +// Joins the two derivations onto one per-param trait list so a param earning both renders +// `K: Clone + PartialEq`. Clone is listed first so the Clone-only case is byte-identical to what +// the previous single-trait renderer emitted, which is what lets the regen fixed point act as the +// oracle for "equality changed nothing that did not need changing". +fn v1_fn_bounds_by_param( + clone_param_names: List, + eq_param_names: List +) -> Map> { + let with_clone = clone_param_names |> fold(init: empty_map(), f: (m, n) => map_insert(m, n, ["Clone"])) + eq_param_names |> fold(init: with_clone, f: (m, n) => + match map_get(m, n) { + Present { value: traits } => map_insert(m, n, concat(traits, ["PartialEq"])) + Absent => map_insert(m, n, ["PartialEq"]) + } + ) +} + +// Node-consuming extraction for the equality bound, kept here rather than in +// v1.compiler.trait_bound_witness for the reason that module's import note gives: the pure core +// lives there, the Node walk lives at the call site, and reversing that would close an +// emit_rust -> trait_bound_witness -> emit_rust cycle. +// +// This is the RECURSIVE BODY WALKER that BoundedToDirectTopLevelMatchBody and +// BoundedToDirectSingleCallLambdaBody both name as their next-rung trigger. Unlike those two it +// declares no depth boundary: the specimen that motivated it puts the earning comparison inside a +// lambda, inside any(), inside a pipeline, inside the && of another lambda inside another any(). +// +// The traversal is deliberately GENEROUS and the predicate deliberately EXACT, because the two +// directions have asymmetric cost. binop_left/binop_right are children 0 and 1, so descending +// every child reaches every operand; a node the walk somehow misses simply yields no bound, which +// is the status quo. But a spurious bound tightens the emitted signature and breaks callers, so +// precision must come from v1_equality_bound_param_name's type resolution, never from the shape of +// the walk. +fn v1_fn_body_equality_bound_param_names( + body: Node, + generic_param_names: List, + source_indices: Map +) -> List { + let here = match body.expr_data { + ExprBinOp { op: op, algebra_field: _ } => + if is_equality_comparison(op: op) { + v1_equality_bound_param_name( + left_type_name: v1_resolved_type_name_or_empty(e: binop_left(texpr: body), source_indices: source_indices), + right_type_name: v1_resolved_type_name_or_empty(e: binop_right(texpr: body), source_indices: source_indices), + generic_param_names: generic_param_names + ) + } else { + [] + } + _ => [] + } + body.children |> fold(init: here, f: (acc, child) => + v1_union_clone_param_names( + base: acc, + extra: v1_fn_body_equality_bound_param_names( + body: child, + generic_param_names: generic_param_names, + source_indices: source_indices + ) + ) + ) +} + +// Matches Eq/Ne bare exactly as is_string_comparison does in this module. The operator is genuinely +// a spelling, so reading it as one is correct; it is the OPERANDS that must be resolved by type. +fn is_equality_comparison(op: BinOp) -> Bool { + match op { + Eq => true + Ne => true + _ => false + } +} + +// Empty string means "no resolved type", which is why v1_equality_bound_param_name refuses on it +// rather than letting two unresolved operands compare equal and fabricate a bound. +fn v1_resolved_type_name_or_empty(e: Node, source_indices: Map) -> String { + match e.inferred { + Present { value: Resolved { node: rt } } => authored_name_at(source_indices: source_indices, node: rt) + _ => "" + } +} + fn v1_call_forwarding_clone_bound_param_names( generic_param_names: List, body: Node, @@ -5963,9 +6046,18 @@ fn emit_fn_def(name: String, params: List, inferred: Node, body: Node, reg } else { derived_clone_param_names_with_rc_match } - let needs_clone_bound = (clone_param_names |> count) > 0 - let type_params_str = if needs_clone_bound { - v1_emit_type_params_with_clone_bounds(params: type_params, clone_param_names: clone_param_names, source_indices: si) + let eq_param_names = v1_fn_body_equality_bound_param_names( + body: body, + generic_param_names: generic_param_names, + source_indices: si + ) + let bounds_by_param = v1_fn_bounds_by_param( + clone_param_names: clone_param_names, + eq_param_names: eq_param_names + ) + let needs_bound = (clone_param_names |> count) > 0 || (eq_param_names |> count) > 0 + let type_params_str = if needs_bound { + v1_emit_type_params_with_bounds(params: type_params, bounds_by_param: bounds_by_param, source_indices: si) } else { emit_type_params(params: type_params, source_indices: si) } diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 7ffbe28d1f2..6afff81fada 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -118,17 +118,17 @@ pub use crate::v1_compiler_ownership::{ pub use crate::v1_compiler_resolve::get_exported_names; pub use crate::v1_compiler_runtime_rust::rust_runtime_source; pub use crate::v1_compiler_trait_bound_witness::{ - v1_call_forwarding_clone_bound_wrapper_param_names, + v1_call_forwarding_clone_bound_wrapper_param_names, v1_equality_bound_param_name, v1_rc_match_scrutinee_clone_bound_param_names, v1_union_clone_param_names, }; pub use crate::v1_compiler_trait_derive_emit::{ rust_nominal_identity_carrier_shape_eligible, rust_symbol_wrapped_ord_carrier_shape_eligible, trait_derive_emit_fn_clone_bound_keyed_carrier_module, v1_clone_bounded_type_params, v1_clone_impl_required_type_params, v1_emit_enum_derives, v1_emit_enum_supplemental_impls, - v1_emit_struct_from_capability_table, v1_emit_type_params_with_clone_bounds, - v1_generic_params_needing_clone_bound, v1_item_clone_bounded_param_names, - v1_item_clone_undecided_head, v1_item_field_type_exprs, v1_map_key_required_type_names, - v1_trait_derive_refuse, v1_with_map_key_requirement, + v1_emit_struct_from_capability_table, v1_emit_type_params_with_bounds, + v1_emit_type_params_with_clone_bounds, v1_generic_params_needing_clone_bound, + v1_item_clone_bounded_param_names, v1_item_clone_undecided_head, v1_item_field_type_exprs, + v1_map_key_required_type_names, v1_trait_derive_refuse, v1_with_map_key_requirement, }; use crate::v1_rt; use crate::v1_rt::{VecCompat, VecJoin}; @@ -14851,6 +14851,98 @@ pub fn v1_fn_body_derived_clone_param_names( } } +pub fn v1_fn_bounds_by_param( + clone_param_names: Rc>, + eq_param_names: Rc>, +) -> Rc>>> { + { + let with_clone = clone_param_names.clone().iter().cloned().fold( + v1_rt::rc_empty_map::>>(), + |m: Rc>>>, n: String| { + v1_rt::rc_map_insert(m, n.clone(), Rc::new(vec!["Clone".to_string()])) + }, + ); + eq_param_names.clone().iter().cloned().fold( + with_clone.clone(), + |m: Rc>>>, n: String| match v1_rt::map_get(&m, n.clone()) + { + Some(traits) => v1_rt::rc_map_insert( + m.clone(), + n.clone(), + v1_rt::concat(traits.clone(), Rc::new(vec!["PartialEq".to_string()])), + ), + None => v1_rt::rc_map_insert( + m.clone(), + n.clone(), + Rc::new(vec!["PartialEq".to_string()]), + ), + }, + ) + } +} + +pub fn v1_fn_body_equality_bound_param_names( + body: Rc, + generic_param_names: Rc>, + source_indices: Rc>>, +) -> Rc> { + stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { + let here = match (*body.expr_data.clone()).clone() { + ExprData::ExprBinOp { op, .. } => { + if is_equality_comparison(op.clone()) { + v1_equality_bound_param_name( + v1_resolved_type_name_or_empty( + binop_left(body.clone()), + source_indices.clone(), + ), + v1_resolved_type_name_or_empty( + binop_right(body.clone()), + source_indices.clone(), + ), + generic_param_names.clone(), + ) + } else { + Rc::new(vec![]) + } + } + _ => Rc::new(vec![]), + }; + body.children.clone().iter().cloned().fold( + here.clone(), + |acc: Rc>, child: Rc| { + v1_union_clone_param_names( + acc, + v1_fn_body_equality_bound_param_names( + child.clone(), + generic_param_names.clone(), + source_indices.clone(), + ), + ) + }, + ) + }) +} + +pub fn is_equality_comparison(op: BinOp) -> bool { + match op.clone() { + BinOp::Eq => true, + BinOp::Ne => true, + _ => false, + } +} + +pub fn v1_resolved_type_name_or_empty( + e: Rc, + source_indices: Rc>>, +) -> String { + match e.inferred.clone().as_deref().cloned() { + Some(InferredNode::Resolved { node: rt, .. }) => { + authored_name_at(source_indices.clone(), rt.clone()) + } + _ => "".to_string(), + } +} + pub fn v1_call_forwarding_clone_bound_param_names( generic_param_names: Rc>, body: Rc, @@ -15037,11 +15129,19 @@ pub fn emit_fn_def( } else { derived_clone_param_names_with_rc_match.clone() }; - let needs_clone_bound = ((clone_param_names.clone().len() as i64) > 0); - let type_params_str = if needs_clone_bound.clone() { - v1_emit_type_params_with_clone_bounds( + let eq_param_names = v1_fn_body_equality_bound_param_names( + body.clone(), + generic_param_names.clone(), + si.clone(), + ); + let bounds_by_param = + v1_fn_bounds_by_param(clone_param_names.clone(), eq_param_names.clone()); + let needs_bound = (((clone_param_names.clone().len() as i64) > 0) + || ((eq_param_names.clone().len() as i64) > 0)); + let type_params_str = if needs_bound.clone() { + v1_emit_type_params_with_bounds( type_params.clone(), - clone_param_names.clone(), + bounds_by_param.clone(), si.clone(), ) } else { diff --git a/src/v1/stage0/src/v1_compiler_trait_bound_witness.rs b/src/v1/stage0/src/v1_compiler_trait_bound_witness.rs index 6c0d8167d29..55220e55a2c 100644 --- a/src/v1/stage0/src/v1_compiler_trait_bound_witness.rs +++ b/src/v1/stage0/src/v1_compiler_trait_bound_witness.rs @@ -222,6 +222,31 @@ pub fn v1_call_forwarding_clone_bound_wrapper_param_names( } } +pub fn v1_equality_bound_param_name( + left_type_name: String, + right_type_name: String, + generic_param_names: Rc>, +) -> Rc> { + if ((left_type_name.clone() == "".to_string()) || (right_type_name.clone() == "".to_string())) { + Rc::new(vec![]) + } else { + if ((left_type_name.clone() == right_type_name.clone()) && { + let mut __found = false; + for g in generic_param_names.clone().iter().cloned() { + if (g.clone() == left_type_name.clone()) { + __found = true; + break; + } + } + __found + }) { + Rc::new(vec![left_type_name.clone()]) + } else { + Rc::new(vec![]) + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct BoundedToDirectTopLevelMatchBody; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 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 2b9a2f48e68..615b7314658 100644 --- a/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs +++ b/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs @@ -1916,9 +1916,9 @@ pub fn v1_item_clone_bounded_param_names( } } -pub fn v1_emit_type_params_with_clone_bounds( +pub fn v1_emit_type_params_with_bounds( params: Rc>>, - clone_param_names: Rc>, + bounds_by_param: Rc>>>, source_indices: Rc>>, ) -> String { if ((params.clone().len() as i64) == 0) { @@ -1931,19 +1931,18 @@ pub fn v1_emit_type_params_with_clone_bounds( __result.push({ let pname = generic_param_name_at(p.clone(), source_indices.clone()); let pascal = to_pascal(pname.clone()); - if { - let mut __found = false; - for c in clone_param_names.clone().iter().cloned() { - if (c.clone() == pname.clone()) { - __found = true; - break; + match v1_rt::map_get(&bounds_by_param, pname.clone()) { + Some(traits) => { + if ((traits.clone().len() as i64) == 0) { + pascal.clone() + } else { + v1_rt::concat( + v1_rt::concat(pascal.clone(), ": ".to_string()), + traits.clone().join(&" + ".to_string()), + ) } } - __found - } { - v1_rt::concat(pascal.clone(), ": Clone".to_string()) - } else { - pascal.clone() + None => pascal.clone(), } }); } @@ -1957,6 +1956,23 @@ pub fn v1_emit_type_params_with_clone_bounds( } } +pub fn v1_emit_type_params_with_clone_bounds( + params: Rc>>, + clone_param_names: Rc>, + source_indices: Rc>>, +) -> String { + v1_emit_type_params_with_bounds( + params.clone(), + clone_param_names.clone().iter().cloned().fold( + v1_rt::rc_empty_map::>>(), + |m: Rc>>>, n: String| { + v1_rt::rc_map_insert(m, n.clone(), Rc::new(vec!["Clone".to_string()])) + }, + ), + source_indices.clone(), + ) +} + pub fn v1_emit_struct_from_capability_table( module_path: String, name: String, diff --git a/src/v1/trait_bound_witness.dag b/src/v1/trait_bound_witness.dag index 4863e377706..529c6e3195b 100644 --- a/src/v1/trait_bound_witness.dag +++ b/src/v1/trait_bound_witness.dag @@ -71,3 +71,33 @@ fn v1_call_forwarding_clone_bound_wrapper_param_names( } v1_union_clone_param_names(base: bare_match, extra: compound_match) } + +// Pure decision core for the equality bound, Node-free like its siblings above so the +// Node-consuming walk stays at the call site in emit_fn_def (see the import note at the top of +// this module -- importing emit_rust's accessors here would close an acyclicity-forbidden cycle). +// +// The predicate resolves operand TYPES, never the `==` spelling, and that distinction is +// load-bearing rather than stylistic. A body may carry several comparisons and only some earn a +// bound: comparing two values of a concrete closed enum needs nothing, while comparing two values +// of the fn's own generic param K is what rustc refuses with E0369. A spelling-matching predicate +// bounds both, and a spurious bound is NOT free -- adding `K: PartialEq` tightens the signature, so +// every caller instantiating K with a non-PartialEq type stops compiling. Over-approximation here +// breaks working code; under-approximation only leaves the existing error in place. +// +// Returns List (empty or one name) rather than an optional, matching the plain-List +// convention this module's own note argues for and keeping the core free of any Optional import. +fn v1_equality_bound_param_name( + left_type_name: String, + right_type_name: String, + generic_param_names: List +) -> List { + if left_type_name == "" || right_type_name == "" { + [] + } else { + if left_type_name == right_type_name && (generic_param_names |> any(g => g == left_type_name)) { + [left_type_name] + } else { + [] + } + } +} diff --git a/src/v1/trait_derive_emit.dag b/src/v1/trait_derive_emit.dag index a5b12205555..2bf75dec4fe 100644 --- a/src/v1/trait_derive_emit.dag +++ b/src/v1/trait_derive_emit.dag @@ -1335,9 +1335,14 @@ fn v1_item_clone_bounded_param_names( } } -fn v1_emit_type_params_with_clone_bounds( +// Trait-agnostic renderer: one axis, keyed on the trait names a param has earned, so a param +// earning two derivations renders `K: Clone + PartialEq` and a THIRD trait later is a row in the +// caller's map rather than another parameter on this signature (DESIGN S2 -- model the concept +// once, derive every use). The trait was previously a hardcoded literal here, which is why adding +// equality needed this generalization rather than a second renderer beside it. +fn v1_emit_type_params_with_bounds( params: List, - clone_param_names: List, + bounds_by_param: Map>, source_indices: Map ) -> String { if params |> count == 0 { "" } @@ -1345,16 +1350,36 @@ fn v1_emit_type_params_with_clone_bounds( let names = params |> map(p => let pname = generic_param_name_at(n: p, source_indices: source_indices) let pascal = to_pascal(name: pname) - if clone_param_names |> any(c => c == pname) { - concat(pascal, ": Clone") - } else { - pascal + match map_get(bounds_by_param, pname) { + Present { value: traits } => + if (traits |> count) == 0 { + pascal + } else { + concat(pascal, ": ", traits |> join(separator: " + ")) + } + Absent => pascal } ) concat("<", names |> join(separator: ", "), ">") } } +// Adapter retained at its ORIGINAL signature on purpose: emit_item_type_params_with_clone_bounds +// calls it, and that caller is out of scope for this lane, so generalizing in place would have +// forced an edit there. Output is byte-identical to the literal it replaces -- +// concat(pascal, ": ", ["Clone"] |> join(" + ")) is concat(pascal, ": Clone"). +fn v1_emit_type_params_with_clone_bounds( + params: List, + clone_param_names: List, + source_indices: Map +) -> String { + v1_emit_type_params_with_bounds( + params: params, + bounds_by_param: clone_param_names |> fold(init: empty_map(), f: (m, n) => map_insert(m, n, ["Clone"])), + source_indices: source_indices + ) +} + fn v1_emit_struct_from_capability_table( module_path: String, name: String, From a4dfb3477daa6f249746b6942127c16b24bf3dae Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 21 Aug 2026 05:35:08 +0000 Subject: [PATCH 2/3] behavioral-receipt: split "nothing could be asked" out of "something broke" Operator ruling, 2026-08-21, direct chat: "basically red is supposed to be concerning - if it's failing but not concerning, than we're fine", and "could you please fix it in 8699 for all future PRs". ReceiptVerdict::Refused carried THREE states and failed the phase on all three: emit failed something broke CONCERNING driver would not compile something broke CONCERNING derived corpus is empty nothing was ASKED NOT CONCERNING The third is not a defect in the module. It means every declared function takes a parameter the fragment cannot enumerate -- String, List, Map -- so no corpus exists before any build is attempted. The consequence is a gate defect rather than a nuisance: ANY PR touching trait_derive_emit was red on this phase regardless of content, so the gate's only reachable green there was not touching the file. A red that cannot be cleared by fixing anything trains readers to discount the colour, which costs the reds that do mean something. Split the variant rather than softening the arm: Refused { reason } unchanged, still fails NoDerivableSurface { declared } new, does NOT fail This is not a fail-open and not the absorbing fallback. Nothing is silenced: the state is typed, printed per module with its declared-function count, and totalled on its own line every run including zero, so its frequency stays observable and rankable. What changed is only whether "I could not form an opinion" is scored as "I found a divergence" -- two states whose remedies differ. An empty comparison is still never reported as EQUIVALENT, which is the narrow the Refused variant exists to prevent. Verified by execution on this branch, all three arms, because a fix that made everything non-failing is indistinguishable from a correct one at the phase-result level: emit_rust EQUIVALENT over 49 derived calls preserved trait_bound_witness NO-DERIVABLE-SURFACE 0 of 4 reclassified trait_derive_emit NO-DERIVABLE-SURFACE 0 of 63 reclassified no_derivable_surface=2 of 3 selected module(s) PHASE EXIT=0 The discriminating RED arrived unplanned and is the load-bearing control: an earlier run built only the binary, so the driver could not link against libv1_compiler.rlib, and emit_rust reported REFUSED and still failed the phase. A blanket softening would have swallowed that too. The selftest needed no change, which is also load-bearing: its catch-all arm already fails on any unexpected verdict, so a control expecting EQUIVALENT or DIVERGENT still goes red if the new state ever reaches it. Dissolution: this count measures the FRAGMENT'S REACH and should SHRINK as derivation learns partitions over the domains it currently refuses. A growing count is the signal to widen derivation, never to widen this arm. The same conflation exists one phase over in regen (rustfmt ETXTBSY reported as "regen refused") and is smart-ram-730's to fix; the shared disposition is a follow-up with two real consumers rather than a speculative abstraction. Deliberately NOT reusing ExpectedRedJoinDisposition::NotEvaluated: that is the third arm of a roster-join coproduct beside StillRed and NowPasses, so adopting it here would stretch one spelling over two concepts. Co-Authored-By: Claude Opus 5 (1M context) --- src/v1/stage0/src/bin/claim_executor.rs | 63 +++++++++++++++++++++---- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 82819e83073..06f74a33b0f 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -19385,10 +19385,33 @@ enum ReceiptVerdict { calls: usize, first_difference: String, }, - /// The comparison could NOT be taken. Never reported as equivalence: an emit that failed, a - /// driver that would not compile, or a corpus with nothing in it is ignorance, and rendering - /// ignorance as the clean verdict is the empty-observation narrow. + /// The comparison could NOT be taken because something BROKE: an emit that failed, a driver + /// that would not compile, a corpus call that panicked, a dirty mirror. Never reported as + /// equivalence -- rendering ignorance as the clean verdict is the empty-observation narrow. Refused { reason: String }, + /// No question could be ASKED of this module: every declared function takes a parameter whose + /// domain the fragment cannot enumerate (`String`, `List`, `Map`), so the + /// derived corpus is empty before any build is attempted. + /// + /// SPLIT OUT OF `Refused` (operator ruling, 2026-08-21: "red is supposed to be concerning -- + /// if it's failing but not concerning, then we're fine"). The old variant conflated THREE + /// states, two of which are alarming and one of which is not: a broken comparison is a defect, + /// while a module with no enumerable surface is a COVERAGE FACT about the fragment's reach. + /// Failing on it made every PR touching such a module permanently red, so the gate's only + /// reachable green there was not touching the file -- and a red that cannot be cleared by + /// fixing anything trains readers to ignore the colour, which costs the reds that DO mean + /// something. + /// + /// This is NOT a fail-open and it is not the absorbing fallback. Nothing is silenced: the + /// state is typed, printed per module with its declared-function count, and totalled on its + /// own line, so its frequency stays observable and can be ranked for work. What changed is + /// only whether "I could not form an opinion" is scored as "I found a divergence" -- two + /// different states whose remedies differ, which is exactly the conflation DESIGN forbids. + /// + /// Dissolution: this variant's population goes to zero as the fragment learns to derive + /// partitions over the domains it currently refuses. It is a measure of reach, so it should + /// SHRINK; a growing count is the signal to widen derivation, not to widen this arm. + NoDerivableSurface { declared: usize }, } /// Generate the driver: one `println!` per call in the derived corpus. @@ -19548,12 +19571,12 @@ fn behavioral_differential( ) -> ReceiptVerdict { let total: usize = plan.derivable.iter().map(|(_, _, t)| t.len()).sum(); if total == 0 { - return ReceiptVerdict::Refused { - reason: format!( - "{}: nothing derived, so there is no corpus to run. Reported as a refusal rather \ - than as equivalence, which is what an empty comparison would otherwise look like", - plan.module_path - ), + // NOT a refusal: nothing BROKE here. Every declared function takes a parameter the + // fragment cannot enumerate, so no corpus exists to run and no opinion can be formed. + // Still never reported as equivalence -- an empty comparison would look identical to a + // clean one, which is the narrow this whole verdict type exists to prevent. + return ReceiptVerdict::NoDerivableSurface { + declared: plan.parsed_signatures, }; } let driver = generate_receipt_driver(module_alias, plan); @@ -20481,6 +20504,7 @@ fn behavioral_receipt_plan(source_roots: &[String]) -> Result Result { + // Counted, named, and NOT scored as a divergence. See the variant's doc comment + // for why these are different states; the count below keeps the frequency + // observable so the fragment's reach can be ranked for work. + eprintln!( + "behavioral-receipt: {} NO-DERIVABLE-SURFACE — 0 of {declared} declared \ + functions take a parameter this fragment can enumerate, so no corpus exists \ + to run. This is the fragment's REACH, not a defect in the module, and it is \ + not scored as a divergence", + plan.module_path + ); + no_derivable_surface += 1; + } } } + // The count on its own line, every run, including zero. A state that is only ever visible as + // a per-module line cannot be trended; this is the number that says whether the fragment's + // reach is improving. It must SHRINK -- growth means widen derivation, not widen the arm. + eprintln!( + "behavioral-receipt: no_derivable_surface={no_derivable_surface} of {} selected module(s) \ + — not failures; each is a module the fragment cannot yet ask a question of", + plans.len() + ); // THE DENOMINATOR, EVERY RUN (operator ruling, 2026-08-20). A green here means the DERIVED // CALLS in the selected modules agreed -- never that a module is behaviourally equivalent. // Printing a bare PASS is how, inside a week, someone reads this as promotion evidence. From c1d560fcc693a5b205c5c8fcf7da831b55d26075 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 21 Aug 2026 05:44:02 +0000 Subject: [PATCH 3/3] Revert "behavioral-receipt: split "nothing could be asked" out of "something broke"" This reverts commit a4dfb3477daa6f249746b6942127c16b24bf3dae. --- src/v1/stage0/src/bin/claim_executor.rs | 63 ++++--------------------- 1 file changed, 9 insertions(+), 54 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 06f74a33b0f..82819e83073 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -19385,33 +19385,10 @@ enum ReceiptVerdict { calls: usize, first_difference: String, }, - /// The comparison could NOT be taken because something BROKE: an emit that failed, a driver - /// that would not compile, a corpus call that panicked, a dirty mirror. Never reported as - /// equivalence -- rendering ignorance as the clean verdict is the empty-observation narrow. + /// The comparison could NOT be taken. Never reported as equivalence: an emit that failed, a + /// driver that would not compile, or a corpus with nothing in it is ignorance, and rendering + /// ignorance as the clean verdict is the empty-observation narrow. Refused { reason: String }, - /// No question could be ASKED of this module: every declared function takes a parameter whose - /// domain the fragment cannot enumerate (`String`, `List`, `Map`), so the - /// derived corpus is empty before any build is attempted. - /// - /// SPLIT OUT OF `Refused` (operator ruling, 2026-08-21: "red is supposed to be concerning -- - /// if it's failing but not concerning, then we're fine"). The old variant conflated THREE - /// states, two of which are alarming and one of which is not: a broken comparison is a defect, - /// while a module with no enumerable surface is a COVERAGE FACT about the fragment's reach. - /// Failing on it made every PR touching such a module permanently red, so the gate's only - /// reachable green there was not touching the file -- and a red that cannot be cleared by - /// fixing anything trains readers to ignore the colour, which costs the reds that DO mean - /// something. - /// - /// This is NOT a fail-open and it is not the absorbing fallback. Nothing is silenced: the - /// state is typed, printed per module with its declared-function count, and totalled on its - /// own line, so its frequency stays observable and can be ranked for work. What changed is - /// only whether "I could not form an opinion" is scored as "I found a divergence" -- two - /// different states whose remedies differ, which is exactly the conflation DESIGN forbids. - /// - /// Dissolution: this variant's population goes to zero as the fragment learns to derive - /// partitions over the domains it currently refuses. It is a measure of reach, so it should - /// SHRINK; a growing count is the signal to widen derivation, not to widen this arm. - NoDerivableSurface { declared: usize }, } /// Generate the driver: one `println!` per call in the derived corpus. @@ -19571,12 +19548,12 @@ fn behavioral_differential( ) -> ReceiptVerdict { let total: usize = plan.derivable.iter().map(|(_, _, t)| t.len()).sum(); if total == 0 { - // NOT a refusal: nothing BROKE here. Every declared function takes a parameter the - // fragment cannot enumerate, so no corpus exists to run and no opinion can be formed. - // Still never reported as equivalence -- an empty comparison would look identical to a - // clean one, which is the narrow this whole verdict type exists to prevent. - return ReceiptVerdict::NoDerivableSurface { - declared: plan.parsed_signatures, + return ReceiptVerdict::Refused { + reason: format!( + "{}: nothing derived, so there is no corpus to run. Reported as a refusal rather \ + than as equivalence, which is what an empty comparison would otherwise look like", + plan.module_path + ), }; } let driver = generate_receipt_driver(module_alias, plan); @@ -20504,7 +20481,6 @@ fn behavioral_receipt_plan(source_roots: &[String]) -> Result Result { - // Counted, named, and NOT scored as a divergence. See the variant's doc comment - // for why these are different states; the count below keeps the frequency - // observable so the fragment's reach can be ranked for work. - eprintln!( - "behavioral-receipt: {} NO-DERIVABLE-SURFACE — 0 of {declared} declared \ - functions take a parameter this fragment can enumerate, so no corpus exists \ - to run. This is the fragment's REACH, not a defect in the module, and it is \ - not scored as a divergence", - plan.module_path - ); - no_derivable_surface += 1; - } } } - // The count on its own line, every run, including zero. A state that is only ever visible as - // a per-module line cannot be trended; this is the number that says whether the fragment's - // reach is improving. It must SHRINK -- growth means widen derivation, not widen the arm. - eprintln!( - "behavioral-receipt: no_derivable_surface={no_derivable_surface} of {} selected module(s) \ - — not failures; each is a module the fragment cannot yet ask a question of", - plans.len() - ); // THE DENOMINATOR, EVERY RUN (operator ruling, 2026-08-20). A green here means the DERIVED // CALLS in the selected modules agreed -- never that a module is behaviourally equivalent. // Printing a bare PASS is how, inside a week, someone reads this as promotion evidence.