Skip to main content

uor_foundation_sdk/
lib.rs

1// @generated by uor-crate from uor-ontology — do not edit manually
2
3//! UOR Foundation SDK — procedural-macro ergonomics for `uor-foundation`.
4//!
5//! Emitted by `codegen/src/sdk_macros.rs` from the ontology. Consumers of
6//! this crate must also depend on `uor-foundation`; the macros emit
7//! absolute-path references (`::uor_foundation::…`) that resolve in the
8//! *consumer's* dependency graph.
9//!
10//! # Macros
11//!
12//! - [`product_shape!`] — emits a `ConstrainedTypeShape` impl and a
13//!   `mint_product_witness` helper for the UOR product type `A × B`
14//!   (PT_1 / PT_2a / PT_3 / PT_4).
15//! - [`coproduct_shape!`] — emits a `ConstrainedTypeShape` impl and a
16//!   `mint_coproduct_witness` helper for the UOR sum type `A + B`
17//!   (ST_1 / ST_2 / ST_6 / ST_7 / ST_8 / ST_9 / ST_10).
18//! - [`cartesian_product_shape!`] — emits a `ConstrainedTypeShape` impl,
19//!   a `CartesianProductShape` marker impl, and a `mint_cartesian_witness`
20//!   helper for the UOR Cartesian-partition product `A ⊠ B`
21//!   (CPT_1 / CPT_2a / CPT_3 / CPT_4 / CPT_5).
22//! - [`prism_model!`] — emits the seal impls (`__sdk_seal::Sealed` for
23//!   the model and the route witness), the `FoundationClosed` impl on
24//!   the route witness, and the `PrismModel<H, B, A>` impl whose
25//!   `forward` body delegates to `pipeline::run_route` (wiki ADR-020 +
26//!   ADR-022 D1, D3, D4, D5).
27//!
28//! # Operand support
29//!
30//! Operand `CONSTRAINTS` arrays may contain every `ConstraintRef`
31//! variant: `Residue`, `Hamming`, `Depth`, `Carry`, `Site`, `Affine`,
32//! `SatClauses`, `Bound`, and `Conjunction`. Phase 17 stores
33//! `Affine.coefficients` as a fixed-size
34//! `[i64; AFFINE_MAX_COEFFS]` array (capacity 8) and limits
35//! `Conjunction.conjuncts` to a `[LeafConstraintRef; CONJUNCTION_MAX_TERMS]`
36//! depth-1 array; both are stable-Rust const-buildable, so the SDK
37//! macros support the full operand catalogue. Inputs exceeding the
38//! caps fail `validate_const()` with a typed `ShapeViolation`.
39
40#![deny(
41    clippy::unwrap_used,
42    clippy::expect_used,
43    clippy::panic,
44    missing_docs,
45    clippy::missing_errors_doc
46)]
47
48use proc_macro::TokenStream;
49use quote::quote;
50use syn::ext::IdentExt;
51use syn::parse::{Parse, ParseStream};
52use syn::{parse_macro_input, Ident, Result, Token};
53
54/// Callsite input for `product_shape!(Name, A, B)` — three identifier
55/// tokens separated by commas.
56struct ShapeArgs {
57    name: Ident,
58    left: Ident,
59    right: Ident,
60}
61
62impl Parse for ShapeArgs {
63    fn parse(input: ParseStream) -> Result<Self> {
64        let name: Ident = input.parse()?;
65        input.parse::<Token![,]>()?;
66        let left: Ident = input.parse()?;
67        input.parse::<Token![,]>()?;
68        let right: Ident = input.parse()?;
69        // Trailing comma permitted for consistency with Rust style.
70        let _ = input.parse::<Token![,]>();
71        Ok(Self { name, left, right })
72    }
73}
74
75/// Product-type shape constructor. See crate-level docs.
76///
77/// # Example
78///
79/// ```
80/// use uor_foundation::pipeline::{ConstrainedTypeShape, ConstraintRef};
81/// use uor_foundation_sdk::product_shape;
82///
83/// pub struct A;
84/// impl ConstrainedTypeShape for A {
85///     const IRI: &'static str = "https://example.org/A";
86///     const SITE_COUNT: usize = 1;
87///     const CONSTRAINTS: &'static [ConstraintRef] = &[
88///         ConstraintRef::Residue { modulus: 7, residue: 3 },
89///     ];
90///     const CYCLE_SIZE: u64 = 7;
91/// }
92///
93/// pub struct B;
94/// impl ConstrainedTypeShape for B {
95///     const IRI: &'static str = "https://example.org/B";
96///     const SITE_COUNT: usize = 1;
97///     const CONSTRAINTS: &'static [ConstraintRef] = &[
98///         ConstraintRef::Hamming { bound: 1 },
99///     ];
100///     const CYCLE_SIZE: u64 = 2;
101/// }
102///
103/// product_shape!(MyProduct, A, B);
104///
105/// assert!(<MyProduct as ConstrainedTypeShape>::IRI.starts_with("urn:uor:product:"));
106/// assert_eq!(<MyProduct as ConstrainedTypeShape>::SITE_COUNT, 2);
107/// ```
108#[proc_macro]
109pub fn product_shape(input: TokenStream) -> TokenStream {
110    let ShapeArgs { name, left, right } = parse_macro_input!(input as ShapeArgs);
111    let iri = format!(
112        "urn:uor:product:{}:{}",
113        lexically_earlier(&left, &right),
114        lexically_later(&left, &right)
115    );
116    // Canonicalize operand ordering by stable token spelling so
117    // `product_shape!(X, A, B)` and `product_shape!(X, B, A)` emit
118    // identical source. Documented as the §4e canonicalization rule the
119    // SDK enforces — the runtime content-fingerprint match §4e describes
120    // is then checked by consumer tests.
121    let (l, r) = canonical_operand_pair(&left, &right);
122    let raw_const = format_ident_suffix(&name, "__CONSTRAINTS_RAW");
123    let len_const = format_ident_suffix(&name, "__CONSTRAINTS_LEN");
124
125    let expansion = quote! {
126        /// UOR ProductType shape, emitted by `product_shape!`.
127        pub struct #name;
128
129        const #raw_const:
130            [::uor_foundation::pipeline::ConstraintRef;
131             2 * ::uor_foundation::enforcement::NERVE_CONSTRAINTS_CAP]
132        = ::uor_foundation::pipeline::sdk_concat_product_constraints::<#l, #r>();
133
134        const #len_const: usize =
135            ::uor_foundation::pipeline::sdk_product_constraints_len::<#l, #r>();
136
137        impl ::uor_foundation::pipeline::ConstrainedTypeShape for #name {
138            const IRI: &'static str = #iri;
139            const SITE_BUDGET: usize =
140                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET
141                + <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET;
142            const SITE_COUNT: usize =
143                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT
144                + <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
145            const CONSTRAINTS: &'static [::uor_foundation::pipeline::ConstraintRef] = {
146                let buf: &'static [::uor_foundation::pipeline::ConstraintRef] = &#raw_const;
147                match buf.split_at_checked(#len_const) {
148                    Some((head, _tail)) => head,
149                    None => &[],
150                }
151            };
152            // ADR-032: product cardinality = saturating product of factors.
153            const CYCLE_SIZE: u64 = ::uor_foundation::pipeline::cycle_size_product(
154                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
155                <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
156            );
157        }
158
159        // Wiki ADR-023 + ADR-027: shape-derived shapes receive the four
160        // sealed-trait impls (`__sdk_seal::Sealed`, `IntoBindingValue`,
161        // `GroundedShape`, plus the `ConstrainedTypeShape` impl above).
162        // The shape struct is a zero-sized type-level marker, so the
163        // canonical byte sequence is empty (MAX_BYTES = 0); applications
164        // that need to carry runtime input data declare a custom
165        // `ConstrainedTypeShape` via the `output_shape!` macro and write a
166        // bespoke `IntoBindingValue` impl.
167        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #name {}
168        impl<'a> ::uor_foundation::pipeline::IntoBindingValue<'a> for #name {
169            fn as_binding_value<const INLINE_BYTES: usize>(
170                &self,
171            ) -> ::uor_foundation::pipeline::TermValue<'a, INLINE_BYTES> {
172                ::uor_foundation::pipeline::TermValue::empty()
173            }
174        }
175        impl ::uor_foundation::enforcement::GroundedShape for #name {}
176
177        // ADR-033 G20: positional-only field directory. Field byte
178        // boundaries are derived from each factor's `SITE_COUNT` (the
179        // foundation convention: one byte per site in the canonical
180        // binding-table serialization). Composite operands inherit
181        // their cumulative SITE_COUNT through the product/coproduct
182        // composition rules.
183        impl ::uor_foundation::pipeline::PartitionProductFields for #name {
184            const FIELDS: &'static [(u32, u32)] = &[
185                (0u32, <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32),
186                (<#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32,
187                 <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32),
188            ];
189            const FIELD_NAMES: &'static [&'static str] = &["", ""];
190        }
191
192        // ADR-033 G4: per-factor static-type carriers — chained access
193        // resolution by the `prism_model!` proc-macro names each
194        // intermediate shape via `<PrevTy as PartitionProductFactor<I>>::Factor`.
195        impl ::uor_foundation::pipeline::PartitionProductFactor<0> for #name {
196            type Factor = #l;
197        }
198        impl ::uor_foundation::pipeline::PartitionProductFactor<1> for #name {
199            type Factor = #r;
200        }
201
202        impl #name {
203            /// Mint a verified [`PartitionProductWitness`] for this shape's
204            /// operand pair. The numeric invariants (Euler characteristics,
205            /// entropy, fingerprints) are caller-supplied because they depend
206            /// on the resolved constraint configuration; shape-derivable
207            /// fields (site budgets / site counts) are read from the
208            /// operand `ConstrainedTypeShape` impls.
209            ///
210            /// # Errors
211            ///
212            /// Returns a `GenericImpossibilityWitness` citing the specific
213            /// failed identity when PT_1, PT_3, PT_4, or the foundation
214            /// layout-width invariant fails.
215            #[allow(clippy::too_many_arguments)]
216            pub fn mint_product_witness(
217                witt_bits: u16,
218                left_fingerprint: ::uor_foundation::ContentFingerprint,
219                right_fingerprint: ::uor_foundation::ContentFingerprint,
220                left_euler: i32,
221                right_euler: i32,
222                left_entropy_nats_bits: u64,
223                right_entropy_nats_bits: u64,
224                combined_euler: i32,
225                combined_entropy_nats_bits: u64,
226                combined_fingerprint: ::uor_foundation::ContentFingerprint,
227            ) -> ::core::result::Result<
228                ::uor_foundation::PartitionProductWitness,
229                ::uor_foundation::enforcement::GenericImpossibilityWitness,
230            > {
231                use ::uor_foundation::pipeline::ConstrainedTypeShape;
232                let inputs = ::uor_foundation::PartitionProductMintInputs {
233                    witt_bits,
234                    left_fingerprint,
235                    right_fingerprint,
236                    left_site_budget: <#l as ConstrainedTypeShape>::SITE_BUDGET as u16,
237                    right_site_budget: <#r as ConstrainedTypeShape>::SITE_BUDGET as u16,
238                    left_total_site_count: <#l as ConstrainedTypeShape>::SITE_COUNT as u16,
239                    right_total_site_count: <#r as ConstrainedTypeShape>::SITE_COUNT as u16,
240                    left_euler,
241                    right_euler,
242                    left_entropy_nats_bits,
243                    right_entropy_nats_bits,
244                    combined_site_budget: <#name as ConstrainedTypeShape>::SITE_BUDGET as u16,
245                    combined_site_count: <#name as ConstrainedTypeShape>::SITE_COUNT as u16,
246                    combined_euler,
247                    combined_entropy_nats_bits,
248                    combined_fingerprint,
249                };
250                <::uor_foundation::PartitionProductWitness
251                    as ::uor_foundation::VerifiedMint>::mint_verified(inputs)
252            }
253        }
254    };
255
256    expansion.into()
257}
258
259/// Coproduct shape constructor. See crate-level docs.
260///
261/// # Example
262///
263/// Demonstrates the post-Phase-17 fixed-array `Affine` operand variant.
264///
265/// ```
266/// use uor_foundation::pipeline::{
267///     AFFINE_MAX_COEFFS, ConstrainedTypeShape, ConstraintRef,
268/// };
269/// use uor_foundation_sdk::coproduct_shape;
270///
271/// const A_COEFFS: [i64; AFFINE_MAX_COEFFS] = {
272///     let mut a = [0i64; AFFINE_MAX_COEFFS];
273///     a[0] = 1;
274///     a
275/// };
276///
277/// pub struct A;
278/// impl ConstrainedTypeShape for A {
279///     const IRI: &'static str = "https://example.org/A";
280///     const SITE_COUNT: usize = 1;
281///     const CONSTRAINTS: &'static [ConstraintRef] = &[
282///         ConstraintRef::Affine {
283///             coefficients: A_COEFFS,
284///             coefficient_count: 1,
285///             bias: 0,
286///         },
287///     ];
288///     const CYCLE_SIZE: u64 = 1;
289/// }
290///
291/// pub struct B;
292/// impl ConstrainedTypeShape for B {
293///     const IRI: &'static str = "https://example.org/B";
294///     const SITE_COUNT: usize = 1;
295///     const CONSTRAINTS: &'static [ConstraintRef] = &[];
296///     const CYCLE_SIZE: u64 = 1;
297/// }
298///
299/// coproduct_shape!(MySum, A, B);
300/// assert!(<MySum as ConstrainedTypeShape>::IRI.starts_with("urn:uor:coproduct:"));
301/// ```
302#[proc_macro]
303pub fn coproduct_shape(input: TokenStream) -> TokenStream {
304    let ShapeArgs { name, left, right } = parse_macro_input!(input as ShapeArgs);
305    let iri = format!(
306        "urn:uor:coproduct:{}:{}",
307        lexically_earlier(&left, &right),
308        lexically_later(&left, &right)
309    );
310    let (l, r) = canonical_operand_pair(&left, &right);
311
312    // Per amendment §4b' + §4d: coproduct layout is
313    //   constraints(A) ∪ {tag-pinner bias=0} ∪ constraints(B) ∪ {tag-pinner bias=-1}
314    // with tag_site = max(SITE_COUNT(A), SITE_COUNT(B)). The foundation
315    // `sdk_concat_product_constraints` helper handles the A / B splice
316    // but the two Affine tag-pinners require construction at macro time,
317    // because the tag_site index is a call-site-specific const expression.
318    //
319    // For a coproduct, the Affine coefficient slices are `&[i64]` with
320    // all-zero entries except position tag_site = 1. Since the macro
321    // cannot allocate `&'static [i64]` slices of arbitrary length, the
322    // emission uses a fixed-size coefficient array that matches
323    // `NERVE_CONSTRAINTS_CAP` (the bound on site indices in SDK shapes).
324    let raw_const = format_ident_suffix(&name, "__CONSTRAINTS_RAW");
325    let len_const = format_ident_suffix(&name, "__CONSTRAINTS_LEN");
326    let tag_coeffs_l = format_ident_suffix(&name, "__TAG_COEFFS_L");
327    let tag_coeffs_r = format_ident_suffix(&name, "__TAG_COEFFS_R");
328    let tag_coeff_count = format_ident_suffix(&name, "__TAG_COEFF_COUNT");
329
330    let expansion = quote! {
331        /// UOR SumType shape, emitted by `coproduct_shape!`.
332        pub struct #name;
333
334        // Two tag-pinning Affine coefficient arrays, one per variant,
335        // each of length 2 * NERVE_CONSTRAINTS_CAP so the single `1` at
336        // the tag_site position fits regardless of how wide the
337        // operands' SITE_COUNTs are (bounded by NERVE_CONSTRAINTS_CAP
338        // each, so tag_site = max(L::SITE_COUNT, R::SITE_COUNT) <
339        // Phase 17: tag-pinner coefficient buffer is now a fixed-size
340        // `[i64; AFFINE_MAX_COEFFS]` array stored inline in the
341        // ConstraintRef::Affine variant. The active prefix runs to
342        // `tag_site + 1`. For coproduct shapes whose
343        // `max(L::SITE_COUNT, R::SITE_COUNT) + 1` exceeds
344        // AFFINE_MAX_COEFFS = 8, the const-eval clamps the tag-pinner
345        // to a no-op (count = 0), and `validate_const` rejects the
346        // shape at admission time as Affine-unsatisfiable.
347        const #tag_coeffs_l: [i64; ::uor_foundation::pipeline::AFFINE_MAX_COEFFS] = {
348            let mut out = [0i64; ::uor_foundation::pipeline::AFFINE_MAX_COEFFS];
349            let tag_site = {
350                let a = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
351                let b = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
352                if a > b { a } else { b }
353            };
354            if tag_site < ::uor_foundation::pipeline::AFFINE_MAX_COEFFS {
355                out[tag_site] = 1;
356            }
357            out
358        };
359        const #tag_coeffs_r: [i64; ::uor_foundation::pipeline::AFFINE_MAX_COEFFS] = #tag_coeffs_l;
360        const #tag_coeff_count: u32 = {
361            let a = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
362            let b = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
363            let tag_site = if a > b { a } else { b };
364            (tag_site as u32).saturating_add(1)
365        };
366
367        // Full constraint buffer: L constraints + L tag-pinner (bias=0)
368        // + R constraints (shifted by L::SITE_COUNT? — per amendment §4d
369        // sum types share the data-site space so R's site references are
370        // NOT shifted). The foundation helper
371        // `sdk_concat_product_constraints` shifts R by A::SITE_COUNT, which
372        // is correct for products but wrong for coproducts. Coproducts
373        // instead splice R's constraints verbatim since the tag site is
374        // the distinguishing bit, not a layout offset.
375        //
376        // This makes coproduct construction diverge from the helper's
377        // assumption, so we emit a per-callsite const fn that does the
378        // correct coproduct splice.
379        const #raw_const:
380            [::uor_foundation::pipeline::ConstraintRef;
381             2 * ::uor_foundation::enforcement::NERVE_CONSTRAINTS_CAP + 2]
382        = {
383            let mut out =
384                [::uor_foundation::pipeline::ConstraintRef::Site { position: u32::MAX };
385                 2 * ::uor_foundation::enforcement::NERVE_CONSTRAINTS_CAP + 2];
386            let left_arr = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CONSTRAINTS;
387            let right_arr = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CONSTRAINTS;
388            let mut i = 0;
389            while i < left_arr.len() {
390                out[i] = left_arr[i];
391                i += 1;
392            }
393            // L's tag-pinner: bias 0.
394            out[i] = ::uor_foundation::pipeline::ConstraintRef::Affine {
395                coefficients: #tag_coeffs_l,
396                coefficient_count: #tag_coeff_count,
397                bias: 0,
398            };
399            let left_boundary = i + 1;
400            let mut j = 0;
401            while j < right_arr.len() {
402                out[left_boundary + j] = right_arr[j];
403                j += 1;
404            }
405            // R's tag-pinner: bias -1.
406            out[left_boundary + right_arr.len()] = ::uor_foundation::pipeline::ConstraintRef::Affine {
407                coefficients: #tag_coeffs_r,
408                coefficient_count: #tag_coeff_count,
409                bias: -1,
410            };
411            out
412        };
413        const #len_const: usize =
414            <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CONSTRAINTS.len()
415            + <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CONSTRAINTS.len()
416            + 2;
417
418        impl ::uor_foundation::pipeline::ConstrainedTypeShape for #name {
419            const IRI: &'static str = #iri;
420            const SITE_BUDGET: usize = {
421                let a = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET;
422                let b = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET;
423                if a > b { a } else { b }
424            };
425            const SITE_COUNT: usize = {
426                let a = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
427                let b = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
428                (if a > b { a } else { b }) + 1
429            };
430            const CONSTRAINTS: &'static [::uor_foundation::pipeline::ConstraintRef] = {
431                let buf: &'static [::uor_foundation::pipeline::ConstraintRef] = &#raw_const;
432                match buf.split_at_checked(#len_const) {
433                    Some((head, _tail)) => head,
434                    None => &[],
435                }
436            };
437            // ADR-032: coproduct cardinality = saturating sum + 1
438            // (the `+ 1` is the discriminant tag's contribution).
439            const CYCLE_SIZE: u64 = ::uor_foundation::pipeline::cycle_size_coproduct(
440                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
441                <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
442            );
443        }
444
445        // Wiki ADR-023 + ADR-027: shape-derived shapes receive the four
446        // sealed-trait impls so they qualify as both Input
447        // (`IntoBindingValue`) and Output (`GroundedShape` +
448        // `IntoBindingValue`) for `PrismModel` (zero-sized marker;
449        // canonical byte sequence is empty).
450        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #name {}
451        impl<'a> ::uor_foundation::pipeline::IntoBindingValue<'a> for #name {
452            fn as_binding_value<const INLINE_BYTES: usize>(
453                &self,
454            ) -> ::uor_foundation::pipeline::TermValue<'a, INLINE_BYTES> {
455                ::uor_foundation::pipeline::TermValue::empty()
456            }
457        }
458        impl ::uor_foundation::enforcement::GroundedShape for #name {}
459
460        impl #name {
461            /// Mint a verified [`PartitionCoproductWitness`] for this shape's
462            /// operand pair. See `product_shape!`'s `mint_product_witness`
463            /// doc comment for the caller-vs-derived field split.
464            ///
465            /// # Errors
466            ///
467            /// Returns a `GenericImpossibilityWitness` citing the specific
468            /// failed identity when ST_1, ST_2, ST_6, ST_7, ST_8, ST_9,
469            /// ST_10, the `CoproductLayoutWidth` layout invariant, or the
470            /// `CoproductTagEncoding` byte-pattern invariant fails.
471            #[allow(clippy::too_many_arguments)]
472            pub fn mint_coproduct_witness(
473                witt_bits: u16,
474                left_fingerprint: ::uor_foundation::ContentFingerprint,
475                right_fingerprint: ::uor_foundation::ContentFingerprint,
476                left_euler: i32,
477                right_euler: i32,
478                left_entropy_nats_bits: u64,
479                right_entropy_nats_bits: u64,
480                left_betti: [u32; ::uor_foundation::enforcement::MAX_BETTI_DIMENSION],
481                right_betti: [u32; ::uor_foundation::enforcement::MAX_BETTI_DIMENSION],
482                combined_euler: i32,
483                combined_entropy_nats_bits: u64,
484                combined_betti: [u32; ::uor_foundation::enforcement::MAX_BETTI_DIMENSION],
485                combined_fingerprint: ::uor_foundation::ContentFingerprint,
486            ) -> ::core::result::Result<
487                ::uor_foundation::PartitionCoproductWitness,
488                ::uor_foundation::enforcement::GenericImpossibilityWitness,
489            > {
490                use ::uor_foundation::pipeline::ConstrainedTypeShape;
491                let left_total_site_count = <#l as ConstrainedTypeShape>::SITE_COUNT as u16;
492                let right_total_site_count = <#r as ConstrainedTypeShape>::SITE_COUNT as u16;
493                let tag_site = if left_total_site_count > right_total_site_count {
494                    left_total_site_count
495                } else {
496                    right_total_site_count
497                };
498                let left_constraint_count =
499                    <#l as ConstrainedTypeShape>::CONSTRAINTS.len() + 1;
500                let inputs = ::uor_foundation::PartitionCoproductMintInputs {
501                    witt_bits,
502                    left_fingerprint,
503                    right_fingerprint,
504                    left_site_budget: <#l as ConstrainedTypeShape>::SITE_BUDGET as u16,
505                    right_site_budget: <#r as ConstrainedTypeShape>::SITE_BUDGET as u16,
506                    left_total_site_count,
507                    right_total_site_count,
508                    left_euler,
509                    right_euler,
510                    left_entropy_nats_bits,
511                    right_entropy_nats_bits,
512                    left_betti,
513                    right_betti,
514                    combined_site_budget: <#name as ConstrainedTypeShape>::SITE_BUDGET as u16,
515                    combined_site_count: <#name as ConstrainedTypeShape>::SITE_COUNT as u16,
516                    combined_euler,
517                    combined_entropy_nats_bits,
518                    combined_betti,
519                    combined_fingerprint,
520                    combined_constraints: <#name as ConstrainedTypeShape>::CONSTRAINTS,
521                    left_constraint_count,
522                    tag_site,
523                };
524                <::uor_foundation::PartitionCoproductWitness
525                    as ::uor_foundation::VerifiedMint>::mint_verified(inputs)
526            }
527        }
528    };
529
530    expansion.into()
531}
532
533/// Cartesian-product shape constructor. See crate-level docs.
534///
535/// # Example
536///
537/// ```
538/// use uor_foundation::pipeline::{
539///     CartesianProductShape, ConstrainedTypeShape, ConstraintRef,
540/// };
541/// use uor_foundation_sdk::cartesian_product_shape;
542///
543/// pub struct A;
544/// impl ConstrainedTypeShape for A {
545///     const IRI: &'static str = "https://example.org/A";
546///     const SITE_COUNT: usize = 1;
547///     const CONSTRAINTS: &'static [ConstraintRef] = &[];
548///     const CYCLE_SIZE: u64 = 1;
549/// }
550/// pub struct B;
551/// impl ConstrainedTypeShape for B {
552///     const IRI: &'static str = "https://example.org/B";
553///     const SITE_COUNT: usize = 1;
554///     const CONSTRAINTS: &'static [ConstraintRef] = &[];
555///     const CYCLE_SIZE: u64 = 1;
556/// }
557///
558/// cartesian_product_shape!(MyCartesian, A, B);
559///
560/// fn assert_marker<T: CartesianProductShape>() {}
561/// assert_marker::<MyCartesian>();
562/// ```
563#[proc_macro]
564pub fn cartesian_product_shape(input: TokenStream) -> TokenStream {
565    let ShapeArgs { name, left, right } = parse_macro_input!(input as ShapeArgs);
566    let iri = format!(
567        "urn:uor:cartesian:{}:{}",
568        lexically_earlier(&left, &right),
569        lexically_later(&left, &right)
570    );
571    let (l, r) = canonical_operand_pair(&left, &right);
572    let raw_const = format_ident_suffix(&name, "__CONSTRAINTS_RAW");
573    let len_const = format_ident_suffix(&name, "__CONSTRAINTS_LEN");
574
575    // CartesianProduct emission mirrors product_shape!'s layout but
576    // additionally implements `CartesianProductShape` so the nerve-Betti
577    // pipeline uses `primitive_cartesian_nerve_betti` (Künneth) instead of
578    // the flat simplicial primitive.
579    let expansion = quote! {
580        /// UOR CartesianPartitionProduct shape, emitted by
581        /// `cartesian_product_shape!`.
582        pub struct #name;
583
584        const #raw_const:
585            [::uor_foundation::pipeline::ConstraintRef;
586             2 * ::uor_foundation::enforcement::NERVE_CONSTRAINTS_CAP]
587        = ::uor_foundation::pipeline::sdk_concat_product_constraints::<#l, #r>();
588
589        const #len_const: usize =
590            ::uor_foundation::pipeline::sdk_product_constraints_len::<#l, #r>();
591
592        impl ::uor_foundation::pipeline::ConstrainedTypeShape for #name {
593            const IRI: &'static str = #iri;
594            const SITE_BUDGET: usize =
595                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET
596                + <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET;
597            const SITE_COUNT: usize =
598                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT
599                + <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
600            const CONSTRAINTS: &'static [::uor_foundation::pipeline::ConstraintRef] = {
601                let buf: &'static [::uor_foundation::pipeline::ConstraintRef] = &#raw_const;
602                match buf.split_at_checked(#len_const) {
603                    Some((head, _tail)) => head,
604                    None => &[],
605                }
606            };
607            // ADR-032: cartesian-product cardinality = saturating product
608            // (the binary `cartesian_product_shape!` is the two-factor case;
609            // ≥3 factors compose left-associatively). Equivalent to
610            // saturating-power for the homogeneous case but expressed via
611            // the binary product since this macro takes two operands.
612            const CYCLE_SIZE: u64 = ::uor_foundation::pipeline::cycle_size_product(
613                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
614                <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
615            );
616        }
617
618        impl ::uor_foundation::pipeline::CartesianProductShape for #name {
619            type Left = #l;
620            type Right = #r;
621        }
622
623        // Wiki ADR-023 + ADR-027: shape-derived shapes receive the four
624        // sealed-trait impls so they qualify as both Input
625        // (`IntoBindingValue`) and Output (`GroundedShape` +
626        // `IntoBindingValue`) for `PrismModel` (zero-sized marker;
627        // canonical byte sequence is empty).
628        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #name {}
629        impl<'a> ::uor_foundation::pipeline::IntoBindingValue<'a> for #name {
630            fn as_binding_value<const INLINE_BYTES: usize>(
631                &self,
632            ) -> ::uor_foundation::pipeline::TermValue<'a, INLINE_BYTES> {
633                ::uor_foundation::pipeline::TermValue::empty()
634            }
635        }
636        impl ::uor_foundation::enforcement::GroundedShape for #name {}
637
638        // ADR-033 G20: positional-only field directory. Field byte
639        // boundaries are derived from each factor's `SITE_COUNT` (the
640        // foundation convention: one byte per site in the canonical
641        // binding-table serialization). Composite operands inherit
642        // their cumulative SITE_COUNT through the product/coproduct
643        // composition rules.
644        impl ::uor_foundation::pipeline::PartitionProductFields for #name {
645            const FIELDS: &'static [(u32, u32)] = &[
646                (0u32, <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32),
647                (<#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32,
648                 <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32),
649            ];
650            const FIELD_NAMES: &'static [&'static str] = &["", ""];
651        }
652
653        // ADR-033 G4: per-factor static-type carriers for chained access.
654        impl ::uor_foundation::pipeline::PartitionProductFactor<0> for #name {
655            type Factor = #l;
656        }
657        impl ::uor_foundation::pipeline::PartitionProductFactor<1> for #name {
658            type Factor = #r;
659        }
660
661        impl #name {
662            /// Mint a verified [`CartesianProductWitness`] for this shape's
663            /// operand pair.
664            ///
665            /// # Errors
666            ///
667            /// Returns a `GenericImpossibilityWitness` citing the specific
668            /// failed identity when CPT_1, CPT_3, CPT_4, CPT_5, or the
669            /// `CartesianLayoutWidth` layout invariant fails.
670            #[allow(clippy::too_many_arguments)]
671            pub fn mint_cartesian_witness(
672                witt_bits: u16,
673                left_fingerprint: ::uor_foundation::ContentFingerprint,
674                right_fingerprint: ::uor_foundation::ContentFingerprint,
675                left_euler: i32,
676                right_euler: i32,
677                left_betti: [u32; ::uor_foundation::enforcement::MAX_BETTI_DIMENSION],
678                right_betti: [u32; ::uor_foundation::enforcement::MAX_BETTI_DIMENSION],
679                left_entropy_nats_bits: u64,
680                right_entropy_nats_bits: u64,
681                combined_euler: i32,
682                combined_betti: [u32; ::uor_foundation::enforcement::MAX_BETTI_DIMENSION],
683                combined_entropy_nats_bits: u64,
684                combined_fingerprint: ::uor_foundation::ContentFingerprint,
685            ) -> ::core::result::Result<
686                ::uor_foundation::CartesianProductWitness,
687                ::uor_foundation::enforcement::GenericImpossibilityWitness,
688            > {
689                use ::uor_foundation::pipeline::ConstrainedTypeShape;
690                let inputs = ::uor_foundation::CartesianProductMintInputs {
691                    witt_bits,
692                    left_fingerprint,
693                    right_fingerprint,
694                    left_site_budget: <#l as ConstrainedTypeShape>::SITE_BUDGET as u16,
695                    right_site_budget: <#r as ConstrainedTypeShape>::SITE_BUDGET as u16,
696                    left_total_site_count: <#l as ConstrainedTypeShape>::SITE_COUNT as u16,
697                    right_total_site_count: <#r as ConstrainedTypeShape>::SITE_COUNT as u16,
698                    left_euler,
699                    right_euler,
700                    left_betti,
701                    right_betti,
702                    left_entropy_nats_bits,
703                    right_entropy_nats_bits,
704                    combined_site_budget: <#name as ConstrainedTypeShape>::SITE_BUDGET as u16,
705                    combined_site_count: <#name as ConstrainedTypeShape>::SITE_COUNT as u16,
706                    combined_euler,
707                    combined_betti,
708                    combined_entropy_nats_bits,
709                    combined_fingerprint,
710                };
711                <::uor_foundation::CartesianProductWitness
712                    as ::uor_foundation::VerifiedMint>::mint_verified(inputs)
713            }
714        }
715    };
716
717    expansion.into()
718}
719
720// =====================================================================
721// `partition_product!` and `partition_coproduct!` — wiki ADR-026 G17 + G18.
722//
723// ADR-026 specifies these as type-level operators recognised in
724// `type Input` / `type Output` positions as variadic forms
725// `partition_product!(<A>, <B>, …)`. On stable Rust 1.83 the in-position
726// variadic form requires `generic_const_exprs` (unstable) to compute
727// `IRI`/`CONSTRAINTS` from operand type-parameters at the trait level;
728// the architecturally-equivalent stable-Rust surface is the same macros
729// at item position with a name argument followed by the operand list:
730//
731//   `partition_product!(<Name>, <A>, <B>, [<C>, …]);`
732//   `partition_coproduct!(<Name>, <A>, <B>, [<C>, …]);`
733//
734// Three or more operands fold left-associatively: `partition_product!(N, A, B, C)`
735// emits the chain `((A × B) × C)` via repeated PT_3 composition. The
736// emitted `ConstrainedTypeShape` impl carries the canonically-joined
737// `CONSTRAINTS` per ADR-025's PT_3 rule and the IRI per ADR-017's
738// content-deterministic naming.
739
740/// Variadic input: `partition_product!(Name, A, B, [C, …])`.
741struct VariadicShapeArgs {
742    name: Ident,
743    /// Operands as full `syn::Type` since v0.4.11 — admits both bare
744    /// identifiers (`LeafA`) and generic-parameter-bearing types
745    /// (`BigIntShape<128>`, `MerkleRoot<H, 32>`). The latter resolves
746    /// the implementer-reported const-generic leaf depth-2 access
747    /// gap by letting closure-body field-access expressions thread the
748    /// full type through `<#ty as PartitionProductFields>::FIELDS[idx]`
749    /// const-eval lookups.
750    operands: Vec<syn::Type>,
751    /// ADR-033 G3: per-operand field names, parallel to `operands`. Each
752    /// entry is `Some(ident)` for the named-field form
753    /// `partition_product!(N, lhs: A, rhs: B)` and `None` for positional
754    /// `partition_product!(N, A, B)`. The two forms must not be mixed in
755    /// a single invocation.
756    field_names: Vec<Option<Ident>>,
757    /// ADR-057 recurse markers, parallel to `operands`. `None` indicates
758    /// a normal (non-recursive) operand whose constraints inline at the
759    /// operand's position; `Some(bound)` indicates a `recurse:T` or
760    /// `recurse(bound):T` operand that lowers to `ConstraintRef::Recurse
761    /// { shape_iri: <T>::IRI, descent_bound: bound }` at the operand's
762    /// position. The descent_bound is a `proc_macro2::TokenStream` so the
763    /// bound expression can be any const-evaluable u32 expression
764    /// (literal, const reference, `<AppBounds as HostBounds>` associated
765    /// const, etc.); when the `recurse:T` form omits the bound, the
766    /// expression defaults to `u32::MAX` (saturated descent).
767    recurse_bounds: Vec<Option<proc_macro2::TokenStream>>,
768}
769
770impl Parse for VariadicShapeArgs {
771    fn parse(input: ParseStream) -> Result<Self> {
772        let name: Ident = input.parse()?;
773        input.parse::<Token![,]>()?;
774        let mut operands: Vec<syn::Type> = Vec::new();
775        let mut field_names: Vec<Option<Ident>> = Vec::new();
776        let mut recurse_bounds: Vec<Option<proc_macro2::TokenStream>> = Vec::new();
777        // Helper: try to parse one operand, possibly with a `name:` prefix
778        // and possibly with a `recurse:` or `recurse(<bound>):` prefix.
779        // The decision tree:
780        //   - `recurse:T`              → positional + recurse, bound = u32::MAX
781        //   - `recurse(<bound>):T`     → positional + recurse, bound = <bound>
782        //   - `<name>: recurse:T`      → named + recurse, bound = u32::MAX
783        //   - `<name>: recurse(<bound>):T` → named + recurse, bound = <bound>
784        //   - `<name>:T`               → named, non-recurse (pre-ADR-057 form)
785        //   - `T`                      → positional, non-recurse (pre-ADR-057 form)
786        // Returns (field_name, operand_type, recurse_bound).
787        fn parse_one(
788            input: ParseStream,
789        ) -> Result<(Option<Ident>, syn::Type, Option<proc_macro2::TokenStream>)> {
790            // ADR-033 G3 + ADR-057 dispatch:
791            //   `recurse:T`              → positional + recurse (saturated)
792            //   `recurse(<b>):T`         → positional + recurse (bounded)
793            //   `<name>:recurse:T`       → named + recurse (saturated)
794            //   `<name>:recurse(<b>):T`  → named + recurse (bounded)
795            //   `<name>:T`               → named (non-recurse)
796            //   `T`                      → positional (non-recurse)
797            //
798            // The named-field detection forks past the leading ident +
799            // `:`. We MUST exclude `recurse` from the named-field
800            // interpretation: `recurse:T` is a positional recurse marker,
801            // not a field named "recurse" containing a `T`-typed scalar.
802            // The wiki's ADR-057 operand grammar reserves `recurse` as a
803            // marker keyword.
804            let forked = input.fork();
805            let mut field_name: Option<Ident> = None;
806            if let Ok(maybe_name) = forked.parse::<Ident>() {
807                if maybe_name != "recurse" && forked.peek(Token![:]) {
808                    // Confirmed named-field form (and the name is not the
809                    // reserved `recurse` marker). Consume the ident + colon
810                    // from the real stream; the operand-position parser
811                    // below then handles either a `recurse` marker or a
812                    // plain type.
813                    field_name = Some(input.parse::<Ident>()?);
814                    input.parse::<Token![:]>()?;
815                }
816            }
817            // Check for `recurse` operand marker at operand position.
818            let recurse_bound = parse_optional_recurse_marker(input)?;
819            // Parse the operand type.
820            let ty: syn::Type = input.parse()?;
821            Ok((field_name, ty, recurse_bound))
822        }
823        let (n0, t0, r0) = parse_one(input)?;
824        field_names.push(n0);
825        operands.push(t0);
826        recurse_bounds.push(r0);
827        while input.peek(Token![,]) {
828            input.parse::<Token![,]>()?;
829            if input.is_empty() {
830                break;
831            }
832            let (ni, ti, ri) = parse_one(input)?;
833            field_names.push(ni);
834            operands.push(ti);
835            recurse_bounds.push(ri);
836        }
837        if operands.len() < 2 {
838            return Err(syn::Error::new(
839                name.span(),
840                "partition_product!/partition_coproduct! require at least two operands",
841            ));
842        }
843        // ADR-033 G3: enforce all-or-nothing — either every operand has
844        // a name or none does. Mixed forms are a closure violation.
845        let any_named = field_names.iter().any(|n| n.is_some());
846        let all_named = field_names.iter().all(|n| n.is_some());
847        if any_named && !all_named {
848            return Err(syn::Error::new(
849                name.span(),
850                "partition_product!/partition_coproduct!: named-field form must name every operand (or use the positional form for all)",
851            ));
852        }
853        Ok(Self {
854            name,
855            operands,
856            field_names,
857            recurse_bounds,
858        })
859    }
860}
861
862/// ADR-057: parse an optional `recurse` or `recurse(<bound>)` marker
863/// preceding an operand type. Returns `Some(bound_tokens)` when the
864/// marker is present (defaulting to `u32::MAX` when no `(<bound>)`
865/// group is supplied), `None` when no marker is present.
866fn parse_optional_recurse_marker(input: ParseStream) -> Result<Option<proc_macro2::TokenStream>> {
867    // Speculative parse: peek for an `Ident` whose string is "recurse"
868    // followed by either `:` (the `recurse:T` form, but we'll consume
869    // `recurse` here and let the caller's `:` handling in parse_one
870    // distinguish it from a name-prefix `:`) or `(...)` for the bounded
871    // form. We use a fork to look ahead without consuming if the marker
872    // isn't present.
873    let forked = input.fork();
874    let Ok(marker_ident) = forked.parse::<Ident>() else {
875        return Ok(None);
876    };
877    if marker_ident != "recurse" {
878        return Ok(None);
879    }
880    // We confirmed `recurse`. Look at what follows:
881    //   `recurse(<bound>):T` — paren group then colon
882    //   `recurse:T`          — bare colon
883    // Anything else is a parse error.
884    if forked.peek(syn::token::Paren) {
885        // Bounded form. Consume the ident, the paren group, and the colon.
886        input.parse::<Ident>()?;
887        let content;
888        syn::parenthesized!(content in input);
889        let bound_expr: syn::Expr = content.parse()?;
890        input.parse::<Token![:]>()?;
891        Ok(Some(quote! { (#bound_expr) as u32 }))
892    } else if forked.peek(Token![:]) {
893        // Saturated form. Consume the ident and the colon.
894        input.parse::<Ident>()?;
895        input.parse::<Token![:]>()?;
896        Ok(Some(quote! { u32::MAX }))
897    } else {
898        // Not a recurse marker after all — the identifier is the
899        // operand's own type (a struct named `recurse` would shadow
900        // the marker, but that's an architectural violation —
901        // `recurse` is a reserved operand-grammar identifier per
902        // ADR-057). Fall through to treat the ident as a type.
903        Ok(None)
904    }
905}
906
907/// `partition_product!` — wiki ADR-026 G17 type-level shape constructor.
908/// Emits a `ConstrainedTypeShape` impl whose `CONSTRAINTS` carry the
909/// canonically-joined PT_3 form (algebraic-product per ADR-025), with
910/// `SITE_COUNT = Σ operands' SITE_COUNT`.
911#[proc_macro]
912pub fn partition_product(input: TokenStream) -> TokenStream {
913    let parsed = parse_macro_input!(input as VariadicShapeArgs);
914    expand_partition_product(
915        parsed.name,
916        &parsed.operands,
917        &parsed.field_names,
918        &parsed.recurse_bounds,
919    )
920}
921
922fn expand_partition_product(
923    name: Ident,
924    operands: &[syn::Type],
925    field_names: &[Option<Ident>],
926    recurse_bounds: &[Option<proc_macro2::TokenStream>],
927) -> TokenStream {
928    // ADR-057: any operand marked `recurse:T` or `recurse(<bound>):T`
929    // triggers the recurse-aware emission path. The binary form uses
930    // `sdk_concat_product_constraints_v2`; CYCLE_SIZE saturates at
931    // `u64::MAX` per the ADR's "shape with Recurse constraint saturates"
932    // rule.
933    let any_recurse = recurse_bounds.iter().any(|b| b.is_some());
934
935    if operands.len() == 2 {
936        // Binary form — delegate to the existing product_shape! semantics
937        // by emitting the same ConstrainedTypeShape impl pattern with the
938        // PT_3 canonical-join `CONSTRAINTS`.
939        let l_orig = &operands[0];
940        let r_orig = &operands[1];
941        let l_recurse = &recurse_bounds[0];
942        let r_recurse = &recurse_bounds[1];
943        let iri = format!(
944            "urn:uor:product:{}:{}",
945            lexically_earlier_ty(l_orig, r_orig),
946            lexically_later_ty(l_orig, r_orig),
947        );
948        // ADR-057: canonical ordering is preserved, but when an operand
949        // is recurse-marked the marker must travel with the operand. We
950        // canonicalize the (type, recurse_bound) pair together.
951        let (l, r, l_recurse, r_recurse) = if type_token_string(l_orig) <= type_token_string(r_orig)
952        {
953            (
954                l_orig.clone(),
955                r_orig.clone(),
956                l_recurse.clone(),
957                r_recurse.clone(),
958            )
959        } else {
960            (
961                r_orig.clone(),
962                l_orig.clone(),
963                r_recurse.clone(),
964                l_recurse.clone(),
965            )
966        };
967        let _ = canonical_operand_pair_ty; // suppress unused-import lint if generated
968        let raw_const = format_ident_suffix(&name, "__CONSTRAINTS_RAW");
969        let len_const = format_ident_suffix(&name, "__CONSTRAINTS_LEN");
970        // ADR-033 G3: per-factor names — empty string for positional, the
971        // user-supplied identifier for named.
972        let l_name_lit = field_names
973            .first()
974            .and_then(|n| n.as_ref())
975            .map(|i| i.to_string())
976            .unwrap_or_default();
977        let r_name_lit = field_names
978            .get(1)
979            .and_then(|n| n.as_ref())
980            .map(|i| i.to_string())
981            .unwrap_or_default();
982        // ADR-033 G4: per-positional-index `PartitionProductFactor` impls
983        // emit the static type of each factor so chained field-access
984        // expressions in `prism_model!` closures can synthesize further
985        // `<NextSource as PartitionProductFields>::FIELDS` lookups.
986        //
987        // ADR-057: when any operand is recurse-marked, switch to the v2
988        // emission path (`sdk_concat_product_constraints_v2`) and saturate
989        // CYCLE_SIZE at u64::MAX per the wiki commitment.
990        let (raw_const_init, len_const_init, cycle_size_init): (
991            proc_macro2::TokenStream,
992            proc_macro2::TokenStream,
993            proc_macro2::TokenStream,
994        ) = if any_recurse {
995            let l_recurse_expr = match &l_recurse {
996                Some(bound) => quote! { ::core::option::Option::Some(#bound) },
997                None => quote! { ::core::option::Option::None },
998            };
999            let r_recurse_expr = match &r_recurse {
1000                Some(bound) => quote! { ::core::option::Option::Some(#bound) },
1001                None => quote! { ::core::option::Option::None },
1002            };
1003            let l_recurse_flag = l_recurse.is_some();
1004            let r_recurse_flag = r_recurse.is_some();
1005            (
1006                quote! {
1007                    ::uor_foundation::pipeline::sdk_concat_product_constraints_v2::<#l, #r>(
1008                        #l_recurse_expr,
1009                        #r_recurse_expr,
1010                    )
1011                },
1012                quote! {
1013                    ::uor_foundation::pipeline::sdk_product_constraints_v2_len::<#l, #r>(
1014                        #l_recurse_flag,
1015                        #r_recurse_flag,
1016                    )
1017                },
1018                quote! { u64::MAX },
1019            )
1020        } else {
1021            (
1022                quote! { ::uor_foundation::pipeline::sdk_concat_product_constraints::<#l, #r>() },
1023                quote! { ::uor_foundation::pipeline::sdk_product_constraints_len::<#l, #r>() },
1024                quote! {
1025                    ::uor_foundation::pipeline::cycle_size_product(
1026                        <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
1027                        <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
1028                    )
1029                },
1030            )
1031        };
1032        let expansion = quote! {
1033            /// UOR ADR-026 G17 partition-product shape (binary form).
1034            pub struct #name;
1035
1036            const #raw_const:
1037                [::uor_foundation::pipeline::ConstraintRef;
1038                 2 * ::uor_foundation::enforcement::NERVE_CONSTRAINTS_CAP]
1039            = #raw_const_init;
1040
1041            const #len_const: usize = #len_const_init;
1042
1043            impl ::uor_foundation::pipeline::ConstrainedTypeShape for #name {
1044                const IRI: &'static str = #iri;
1045                const SITE_BUDGET: usize =
1046                    <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET
1047                    + <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET;
1048                const SITE_COUNT: usize =
1049                    <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT
1050                    + <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
1051                const CONSTRAINTS: &'static [::uor_foundation::pipeline::ConstraintRef] = {
1052                    let buf: &'static [::uor_foundation::pipeline::ConstraintRef] = &#raw_const;
1053                    match buf.split_at_checked(#len_const) {
1054                        Some((head, _tail)) => head,
1055                        None => &[],
1056                    }
1057                };
1058                // ADR-032 + ADR-057: cardinality saturates at u64::MAX when
1059                // any operand is recurse-marked (the runtime resolves the
1060                // recursive expansion against the registered shape's
1061                // `<T>::CYCLE_SIZE`, which itself may be saturated).
1062                const CYCLE_SIZE: u64 = #cycle_size_init;
1063            }
1064
1065            impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #name {}
1066            impl<'a> ::uor_foundation::pipeline::IntoBindingValue<'a> for #name {
1067                fn as_binding_value<const INLINE_BYTES: usize>(
1068                    &self,
1069                ) -> ::uor_foundation::pipeline::TermValue<'a, INLINE_BYTES> {
1070                    ::uor_foundation::pipeline::TermValue::empty()
1071                }
1072            }
1073            impl ::uor_foundation::enforcement::GroundedShape for #name {}
1074
1075            // ADR-033 G20: factor-field directory. FIELD_NAMES carries the
1076            // named-form identifiers when supplied, "" otherwise.
1077            impl ::uor_foundation::pipeline::PartitionProductFields for #name {
1078                const FIELDS: &'static [(u32, u32)] = &[
1079                    (0u32, <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32),
1080                    (<#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32,
1081                     <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32),
1082                ];
1083                const FIELD_NAMES: &'static [&'static str] = &[#l_name_lit, #r_name_lit];
1084            }
1085
1086            // ADR-033 G4: per-factor static-type carriers — used by the
1087            // `prism_model!` proc-macro to thread the static type of a
1088            // chained field access (`input.outer.inner`) so the inner
1089            // ProjectField's offset/length lookup names the right
1090            // `PartitionProductFields` impl.
1091            impl ::uor_foundation::pipeline::PartitionProductFactor<0> for #name {
1092                type Factor = #l;
1093            }
1094            impl ::uor_foundation::pipeline::PartitionProductFactor<1> for #name {
1095                type Factor = #r;
1096            }
1097        };
1098        expansion.into()
1099    } else {
1100        // Variadic form, ≥3 operands.
1101        let any_named = field_names.iter().any(|n| n.is_some());
1102        if any_named {
1103            // ADR-033 G3 named-variadic form: emit the final shape as a
1104            // flat N-factor product with FIELDS/FIELD_NAMES exposing each
1105            // operand directly. The byte offsets are running sums of
1106            // SITE_COUNT.
1107            return expand_partition_product_named_flat(
1108                name,
1109                operands,
1110                field_names,
1111                recurse_bounds,
1112            );
1113        }
1114        // Positional variadic: keep the existing left-associative
1115        // helper-step chain; FIELD_NAMES stays positional ("","").
1116        // ADR-057: each helper step folds an operand's recurse_bound into
1117        // the binary product; intermediate step types are non-recurse
1118        // (they're synthesized partition products).
1119        let mut intermediate_names: Vec<Ident> = Vec::with_capacity(operands.len() - 1);
1120        for i in 0..operands.len() - 2 {
1121            intermediate_names.push(Ident::new(&format!("{}PpStep{}", name, i), name.span()));
1122        }
1123        let mut chain: Vec<proc_macro2::TokenStream> = Vec::with_capacity(operands.len() - 1);
1124        // First step: A × B → PpStep0
1125        let mut prev = if operands.len() > 2 {
1126            intermediate_names[0].clone()
1127        } else {
1128            name.clone()
1129        };
1130        let first_a = &operands[0];
1131        let first_b = &operands[1];
1132        let first_step_call = expand_partition_product_helper(
1133            prev.clone(),
1134            first_a,
1135            first_b,
1136            &recurse_bounds[0],
1137            &recurse_bounds[1],
1138        );
1139        chain.push(first_step_call);
1140        // Each subsequent step: prev × operand[i+1] → next. `prev` is
1141        // always a synthesized intermediate Ident; wrap as Type::Path so
1142        // the helper's `&syn::Type` signature accepts it. The
1143        // intermediate-step type is never recurse-marked; operand[i]'s
1144        // recurse bound flows into the right side.
1145        for i in 2..operands.len() {
1146            let next = if i == operands.len() - 1 {
1147                name.clone()
1148            } else {
1149                intermediate_names[i - 1].clone()
1150            };
1151            let prev_ty: syn::Type = syn::parse_quote! { #prev };
1152            let next_call = expand_partition_product_helper(
1153                next.clone(),
1154                &prev_ty,
1155                &operands[i],
1156                &None,
1157                &recurse_bounds[i],
1158            );
1159            chain.push(next_call);
1160            prev = next;
1161        }
1162        let combined = quote! { #( #chain )* };
1163        combined.into()
1164    }
1165}
1166
1167/// ADR-033 G3 named-variadic emission: `partition_product!(N, a: A, b: B, c: C)`
1168/// produces a flat N-factor shape whose `FIELDS` and `FIELD_NAMES` expose
1169/// each operand directly (running-sum offsets over SITE_COUNT). This is
1170/// the architecturally-correct form for named variadic — the alternative
1171/// (left-associative helper steps) would force users to access fields
1172/// via `input.0.0` rather than the names they supplied.
1173fn expand_partition_product_named_flat(
1174    name: Ident,
1175    operands: &[syn::Type],
1176    field_names: &[Option<Ident>],
1177    recurse_bounds: &[Option<proc_macro2::TokenStream>],
1178) -> TokenStream {
1179    let any_recurse = recurse_bounds.iter().any(|b| b.is_some());
1180    // Build the FIELDS array: `(running_sum, operand_i.SITE_COUNT)`.
1181    let mut fields_entries: Vec<proc_macro2::TokenStream> = Vec::with_capacity(operands.len());
1182    for (i, op) in operands.iter().enumerate() {
1183        // Running sum: Σ_{j<i} operand_j.SITE_COUNT
1184        let running_sum: proc_macro2::TokenStream = if i == 0 {
1185            quote::quote! { 0u32 }
1186        } else {
1187            let prior = &operands[..i];
1188            quote::quote! {
1189                0u32 #(
1190                    + <#prior as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32
1191                )*
1192            }
1193        };
1194        fields_entries.push(quote::quote! {
1195            (#running_sum, <#op as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32)
1196        });
1197    }
1198    // FIELD_NAMES: the user-supplied identifiers as string literals.
1199    let name_lits: Vec<String> = field_names
1200        .iter()
1201        .map(|n| n.as_ref().map(|i| i.to_string()).unwrap_or_default())
1202        .collect();
1203    // Per-index PartitionProductFactor impls.
1204    let factor_impls: Vec<proc_macro2::TokenStream> = operands
1205        .iter()
1206        .enumerate()
1207        .map(|(i, op)| {
1208            quote::quote! {
1209                impl ::uor_foundation::pipeline::PartitionProductFactor<#i> for #name {
1210                    type Factor = #op;
1211                }
1212            }
1213        })
1214        .collect();
1215    // SITE_COUNT and SITE_BUDGET: sums.
1216    let site_count_sum: proc_macro2::TokenStream = quote::quote! {
1217        0usize #(
1218            + <#operands as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT
1219        )*
1220    };
1221    let site_budget_sum: proc_macro2::TokenStream = quote::quote! {
1222        0usize #(
1223            + <#operands as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET
1224        )*
1225    };
1226    // CYCLE_SIZE: left-associative product fold via cycle_size_product.
1227    // ADR-057: when any operand is recurse-marked, CYCLE_SIZE saturates
1228    // at u64::MAX.
1229    let cycle_size_expr: proc_macro2::TokenStream = if any_recurse {
1230        quote::quote! { u64::MAX }
1231    } else {
1232        let mut acc = quote::quote! { 1u64 };
1233        for op in operands {
1234            acc = quote::quote! {
1235                ::uor_foundation::pipeline::cycle_size_product(
1236                    #acc,
1237                    <#op as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
1238                )
1239            };
1240        }
1241        acc
1242    };
1243    // IRI: enumerate operands lexicographically (by token-stream-string,
1244    // which incorporates generic-arg lists) for stability.
1245    let mut sorted: Vec<&syn::Type> = operands.iter().collect();
1246    sorted.sort_by_key(|t| type_token_string(t));
1247    let iri_body: String = sorted
1248        .iter()
1249        .map(|t| type_token_string(t))
1250        .collect::<Vec<_>>()
1251        .join(":");
1252    let iri = format!("urn:uor:product:{iri_body}");
1253    let raw_const = format_ident_suffix(&name, "__CONSTRAINTS_RAW");
1254    let len_const = format_ident_suffix(&name, "__CONSTRAINTS_LEN");
1255    // CONSTRAINTS: build via the binary helper applied to the first two
1256    // operands' (type, recurse_bound) pair. For ≥3 operands the
1257    // architectural fold (left-associative product) makes the
1258    // first-pair representation a structural witness; downstream
1259    // preflight consumes this slice. ADR-057-marked operands flow
1260    // through the v2 helper.
1261    let l = &operands[0];
1262    let r = &operands[1];
1263    let l_recurse = &recurse_bounds[0];
1264    let r_recurse = &recurse_bounds[1];
1265    let (raw_const_init, len_const_init): (proc_macro2::TokenStream, proc_macro2::TokenStream) =
1266        if l_recurse.is_some() || r_recurse.is_some() {
1267            let l_recurse_expr = match l_recurse {
1268                Some(bound) => quote::quote! { ::core::option::Option::Some(#bound) },
1269                None => quote::quote! { ::core::option::Option::None },
1270            };
1271            let r_recurse_expr = match r_recurse {
1272                Some(bound) => quote::quote! { ::core::option::Option::Some(#bound) },
1273                None => quote::quote! { ::core::option::Option::None },
1274            };
1275            let l_recurse_flag = l_recurse.is_some();
1276            let r_recurse_flag = r_recurse.is_some();
1277            (
1278                quote::quote! {
1279                    ::uor_foundation::pipeline::sdk_concat_product_constraints_v2::<#l, #r>(
1280                        #l_recurse_expr,
1281                        #r_recurse_expr,
1282                    )
1283                },
1284                quote::quote! {
1285                    ::uor_foundation::pipeline::sdk_product_constraints_v2_len::<#l, #r>(
1286                        #l_recurse_flag,
1287                        #r_recurse_flag,
1288                    )
1289                },
1290            )
1291        } else {
1292            (
1293                quote::quote! {
1294                    ::uor_foundation::pipeline::sdk_concat_product_constraints::<#l, #r>()
1295                },
1296                quote::quote! {
1297                    ::uor_foundation::pipeline::sdk_product_constraints_len::<#l, #r>()
1298                },
1299            )
1300        };
1301    let expansion = quote::quote! {
1302        /// UOR ADR-026 G17 partition-product shape (named variadic form).
1303        pub struct #name;
1304
1305        const #raw_const:
1306            [::uor_foundation::pipeline::ConstraintRef;
1307             2 * ::uor_foundation::enforcement::NERVE_CONSTRAINTS_CAP]
1308        = #raw_const_init;
1309
1310        const #len_const: usize = #len_const_init;
1311
1312        impl ::uor_foundation::pipeline::ConstrainedTypeShape for #name {
1313            const IRI: &'static str = #iri;
1314            const SITE_BUDGET: usize = #site_budget_sum;
1315            const SITE_COUNT: usize = #site_count_sum;
1316            const CONSTRAINTS: &'static [::uor_foundation::pipeline::ConstraintRef] = {
1317                let buf: &'static [::uor_foundation::pipeline::ConstraintRef] = &#raw_const;
1318                match buf.split_at_checked(#len_const) {
1319                    Some((head, _tail)) => head,
1320                    None => &[],
1321                }
1322            };
1323            const CYCLE_SIZE: u64 = #cycle_size_expr;
1324        }
1325
1326        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #name {}
1327        impl<'a> ::uor_foundation::pipeline::IntoBindingValue<'a> for #name {
1328            fn as_binding_value<const INLINE_BYTES: usize>(
1329                &self,
1330            ) -> ::uor_foundation::pipeline::TermValue<'a, INLINE_BYTES> {
1331                ::uor_foundation::pipeline::TermValue::empty()
1332            }
1333        }
1334        impl ::uor_foundation::enforcement::GroundedShape for #name {}
1335
1336        impl ::uor_foundation::pipeline::PartitionProductFields for #name {
1337            const FIELDS: &'static [(u32, u32)] = &[ #( #fields_entries ),* ];
1338            const FIELD_NAMES: &'static [&'static str] = &[ #( #name_lits ),* ];
1339        }
1340
1341        #( #factor_impls )*
1342    };
1343    expansion.into()
1344}
1345
1346/// Emit the `ConstrainedTypeShape` + supporting impl-block for a
1347/// binary partition-product step. Used by `expand_partition_product`'s
1348/// left-associative variadic chain.
1349fn expand_partition_product_helper(
1350    name: Ident,
1351    left: &syn::Type,
1352    right: &syn::Type,
1353    left_recurse: &Option<proc_macro2::TokenStream>,
1354    right_recurse: &Option<proc_macro2::TokenStream>,
1355) -> proc_macro2::TokenStream {
1356    let iri = format!(
1357        "urn:uor:product:{}:{}",
1358        lexically_earlier_ty(left, right),
1359        lexically_later_ty(left, right),
1360    );
1361    // ADR-057: canonical ordering pairs the (type, recurse_bound) together
1362    // so the recurse marker travels with the operand it qualifies.
1363    let (l, r, l_recurse, r_recurse) = if type_token_string(left) <= type_token_string(right) {
1364        (
1365            left.clone(),
1366            right.clone(),
1367            left_recurse.clone(),
1368            right_recurse.clone(),
1369        )
1370    } else {
1371        (
1372            right.clone(),
1373            left.clone(),
1374            right_recurse.clone(),
1375            left_recurse.clone(),
1376        )
1377    };
1378    let _ = canonical_operand_pair_ty; // helper still exported; suppress unused
1379    let any_recurse = l_recurse.is_some() || r_recurse.is_some();
1380    let raw_const = format_ident_suffix(&name, "__CONSTRAINTS_RAW");
1381    let len_const = format_ident_suffix(&name, "__CONSTRAINTS_LEN");
1382    let (raw_const_init, len_const_init, cycle_size_init): (
1383        proc_macro2::TokenStream,
1384        proc_macro2::TokenStream,
1385        proc_macro2::TokenStream,
1386    ) = if any_recurse {
1387        let l_recurse_expr = match &l_recurse {
1388            Some(bound) => quote! { ::core::option::Option::Some(#bound) },
1389            None => quote! { ::core::option::Option::None },
1390        };
1391        let r_recurse_expr = match &r_recurse {
1392            Some(bound) => quote! { ::core::option::Option::Some(#bound) },
1393            None => quote! { ::core::option::Option::None },
1394        };
1395        let l_recurse_flag = l_recurse.is_some();
1396        let r_recurse_flag = r_recurse.is_some();
1397        (
1398            quote! {
1399                ::uor_foundation::pipeline::sdk_concat_product_constraints_v2::<#l, #r>(
1400                    #l_recurse_expr,
1401                    #r_recurse_expr,
1402                )
1403            },
1404            quote! {
1405                ::uor_foundation::pipeline::sdk_product_constraints_v2_len::<#l, #r>(
1406                    #l_recurse_flag,
1407                    #r_recurse_flag,
1408                )
1409            },
1410            quote! { u64::MAX },
1411        )
1412    } else {
1413        (
1414            quote! { ::uor_foundation::pipeline::sdk_concat_product_constraints::<#l, #r>() },
1415            quote! { ::uor_foundation::pipeline::sdk_product_constraints_len::<#l, #r>() },
1416            quote! {
1417                ::uor_foundation::pipeline::cycle_size_product(
1418                    <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
1419                    <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
1420                )
1421            },
1422        )
1423    };
1424    quote! {
1425        /// UOR ADR-026 G17 partition-product step.
1426        pub struct #name;
1427
1428        const #raw_const:
1429            [::uor_foundation::pipeline::ConstraintRef;
1430             2 * ::uor_foundation::enforcement::NERVE_CONSTRAINTS_CAP]
1431        = #raw_const_init;
1432
1433        const #len_const: usize = #len_const_init;
1434
1435        impl ::uor_foundation::pipeline::ConstrainedTypeShape for #name {
1436            const IRI: &'static str = #iri;
1437            const SITE_BUDGET: usize =
1438                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET
1439                + <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET;
1440            const SITE_COUNT: usize =
1441                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT
1442                + <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
1443            const CONSTRAINTS: &'static [::uor_foundation::pipeline::ConstraintRef] = {
1444                let buf: &'static [::uor_foundation::pipeline::ConstraintRef] = &#raw_const;
1445                match buf.split_at_checked(#len_const) {
1446                    Some((head, _tail)) => head,
1447                    None => &[],
1448                }
1449            };
1450            // ADR-032 + ADR-057: cardinality saturates at u64::MAX when
1451            // any operand is recurse-marked (the runtime expansion against
1452            // the registry resolves the recursive contribution).
1453            const CYCLE_SIZE: u64 = #cycle_size_init;
1454        }
1455
1456        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #name {}
1457        impl<'a> ::uor_foundation::pipeline::IntoBindingValue<'a> for #name {
1458            fn as_binding_value<const INLINE_BYTES: usize>(
1459                &self,
1460            ) -> ::uor_foundation::pipeline::TermValue<'a, INLINE_BYTES> {
1461                ::uor_foundation::pipeline::TermValue::empty()
1462            }
1463        }
1464        impl ::uor_foundation::enforcement::GroundedShape for #name {}
1465
1466        // ADR-033 G20: positional-only field directory for the helper
1467        // step. SITE_COUNT-derived (see ADR-033 conventions in the
1468        // emit_term_for_field helper).
1469        impl ::uor_foundation::pipeline::PartitionProductFields for #name {
1470            const FIELDS: &'static [(u32, u32)] = &[
1471                (0u32, <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32),
1472                (<#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32,
1473                 <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT as u32),
1474            ];
1475            const FIELD_NAMES: &'static [&'static str] = &["", ""];
1476        }
1477
1478        // ADR-033 G4: per-factor static-type carriers for chained access
1479        // resolution by the `prism_model!` proc-macro.
1480        impl ::uor_foundation::pipeline::PartitionProductFactor<0> for #name {
1481            type Factor = #l;
1482        }
1483        impl ::uor_foundation::pipeline::PartitionProductFactor<1> for #name {
1484            type Factor = #r;
1485        }
1486    }
1487}
1488
1489/// `partition_coproduct!` — wiki ADR-026 G18 type-level shape constructor.
1490/// Variadic named form. Folds left-associatively into binary
1491/// coproducts. Each binary step emits the same shape `coproduct_shape!`
1492/// produces (ST_10 canonical structure).
1493#[proc_macro]
1494pub fn partition_coproduct(input: TokenStream) -> TokenStream {
1495    let parsed = parse_macro_input!(input as VariadicShapeArgs);
1496    if parsed.field_names.iter().any(|n| n.is_some()) {
1497        return syn::Error::new(
1498            parsed.name.span(),
1499            "partition_coproduct! does not accept named-field form (coproducts discriminate by tag, not by named field; ADR-033 G3 named-field form applies to partition_product!)",
1500        )
1501        .to_compile_error()
1502        .into();
1503    }
1504    expand_partition_coproduct(parsed.name, &parsed.operands, &parsed.recurse_bounds)
1505}
1506
1507fn expand_partition_coproduct(
1508    name: Ident,
1509    operands: &[syn::Type],
1510    recurse_bounds: &[Option<proc_macro2::TokenStream>],
1511) -> TokenStream {
1512    if operands.len() == 2 {
1513        let combined = expand_partition_coproduct_helper(
1514            name,
1515            &operands[0],
1516            &operands[1],
1517            &recurse_bounds[0],
1518            &recurse_bounds[1],
1519        );
1520        return combined.into();
1521    }
1522    let mut intermediate_names: Vec<Ident> = Vec::with_capacity(operands.len() - 1);
1523    for i in 0..operands.len() - 2 {
1524        intermediate_names.push(Ident::new(&format!("{}PcStep{}", name, i), name.span()));
1525    }
1526    let mut chain: Vec<proc_macro2::TokenStream> = Vec::with_capacity(operands.len() - 1);
1527    let mut prev = if operands.len() > 2 {
1528        intermediate_names[0].clone()
1529    } else {
1530        name.clone()
1531    };
1532    let first_step = expand_partition_coproduct_helper(
1533        prev.clone(),
1534        &operands[0],
1535        &operands[1],
1536        &recurse_bounds[0],
1537        &recurse_bounds[1],
1538    );
1539    chain.push(first_step);
1540    for i in 2..operands.len() {
1541        let next = if i == operands.len() - 1 {
1542            name.clone()
1543        } else {
1544            intermediate_names[i - 1].clone()
1545        };
1546        let prev_ty: syn::Type = syn::parse_quote! { #prev };
1547        // The intermediate-step type is never recurse-marked.
1548        let step = expand_partition_coproduct_helper(
1549            next.clone(),
1550            &prev_ty,
1551            &operands[i],
1552            &None,
1553            &recurse_bounds[i],
1554        );
1555        chain.push(step);
1556        prev = next;
1557    }
1558    let combined = quote! { #( #chain )* };
1559    combined.into()
1560}
1561
1562fn expand_partition_coproduct_helper(
1563    name: Ident,
1564    left: &syn::Type,
1565    right: &syn::Type,
1566    left_recurse: &Option<proc_macro2::TokenStream>,
1567    right_recurse: &Option<proc_macro2::TokenStream>,
1568) -> proc_macro2::TokenStream {
1569    let iri = format!(
1570        "urn:uor:coproduct:{}:{}",
1571        lexically_earlier_ty(left, right),
1572        lexically_later_ty(left, right),
1573    );
1574    // ADR-057: canonical ordering pairs (type, recurse_bound) together.
1575    let (l, r, l_recurse, r_recurse) = if type_token_string(left) <= type_token_string(right) {
1576        (
1577            left.clone(),
1578            right.clone(),
1579            left_recurse.clone(),
1580            right_recurse.clone(),
1581        )
1582    } else {
1583        (
1584            right.clone(),
1585            left.clone(),
1586            right_recurse.clone(),
1587            left_recurse.clone(),
1588        )
1589    };
1590    let _ = canonical_operand_pair_ty;
1591    let any_recurse = l_recurse.is_some() || r_recurse.is_some();
1592    let raw_const = format_ident_suffix(&name, "__CONSTRAINTS_RAW");
1593    let len_const = format_ident_suffix(&name, "__CONSTRAINTS_LEN");
1594    let tag_coeffs_l = format_ident_suffix(&name, "__TAG_COEFFS_L");
1595    let tag_coeffs_r = format_ident_suffix(&name, "__TAG_COEFFS_R");
1596    let tag_coeff_count = format_ident_suffix(&name, "__TAG_COEFF_COUNT");
1597
1598    // ADR-057: per-operand contribution blocks. Each block is a
1599    // statement-list emitted inside the const-init for #raw_const; it
1600    // advances `i` by the operand's number of constraint entries
1601    // (`<T>::CONSTRAINTS.len()` for inline, `1` for recurse).
1602    let left_block: proc_macro2::TokenStream = match &l_recurse {
1603        Some(bound) => quote! {
1604            out[i] = ::uor_foundation::pipeline::ConstraintRef::Recurse {
1605                shape_iri: <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::IRI,
1606                descent_bound: #bound,
1607            };
1608            i += 1;
1609        },
1610        None => quote! {
1611            let left_arr = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CONSTRAINTS;
1612            let mut li = 0;
1613            while li < left_arr.len() {
1614                out[i] = left_arr[li];
1615                i += 1;
1616                li += 1;
1617            }
1618        },
1619    };
1620    let right_block: proc_macro2::TokenStream = match &r_recurse {
1621        Some(bound) => quote! {
1622            out[i] = ::uor_foundation::pipeline::ConstraintRef::Recurse {
1623                shape_iri: <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::IRI,
1624                descent_bound: #bound,
1625            };
1626            i += 1;
1627        },
1628        None => quote! {
1629            let right_arr = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CONSTRAINTS;
1630            let mut ri = 0;
1631            while ri < right_arr.len() {
1632                out[i] = right_arr[ri];
1633                i += 1;
1634                ri += 1;
1635            }
1636        },
1637    };
1638    // Length is left-contribution + 1 (tag) + right-contribution + 1 (tag).
1639    let len_const_init: proc_macro2::TokenStream = {
1640        let left_len = match &l_recurse {
1641            Some(_) => quote! { 1usize },
1642            None => quote! {
1643                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CONSTRAINTS.len()
1644            },
1645        };
1646        let right_len = match &r_recurse {
1647            Some(_) => quote! { 1usize },
1648            None => quote! {
1649                <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CONSTRAINTS.len()
1650            },
1651        };
1652        quote! { #left_len + #right_len + 2 }
1653    };
1654    let cycle_size_init: proc_macro2::TokenStream = if any_recurse {
1655        quote! { u64::MAX }
1656    } else {
1657        quote! {
1658            ::uor_foundation::pipeline::cycle_size_coproduct(
1659                <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
1660                <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
1661            )
1662        }
1663    };
1664
1665    quote! {
1666        /// UOR ADR-026 G18 partition-coproduct step.
1667        pub struct #name;
1668
1669        const #tag_coeffs_l: [i64; ::uor_foundation::pipeline::AFFINE_MAX_COEFFS] = {
1670            let mut out = [0i64; ::uor_foundation::pipeline::AFFINE_MAX_COEFFS];
1671            let tag_site = {
1672                let a = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
1673                let b = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
1674                if a > b { a } else { b }
1675            };
1676            if tag_site < ::uor_foundation::pipeline::AFFINE_MAX_COEFFS {
1677                out[tag_site] = 1;
1678            }
1679            out
1680        };
1681        const #tag_coeffs_r: [i64; ::uor_foundation::pipeline::AFFINE_MAX_COEFFS] = #tag_coeffs_l;
1682        const #tag_coeff_count: u32 = {
1683            let a = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
1684            let b = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
1685            let tag_site = if a > b { a } else { b };
1686            (tag_site as u32).saturating_add(1)
1687        };
1688
1689        const #raw_const:
1690            [::uor_foundation::pipeline::ConstraintRef;
1691             2 * ::uor_foundation::enforcement::NERVE_CONSTRAINTS_CAP + 2]
1692        = {
1693            let mut out =
1694                [::uor_foundation::pipeline::ConstraintRef::Site { position: u32::MAX };
1695                 2 * ::uor_foundation::enforcement::NERVE_CONSTRAINTS_CAP + 2];
1696            // ADR-057: per-operand contribution is either the operand's
1697            // inline `CONSTRAINTS` array (default) or a single Recurse
1698            // entry referencing the operand's shape IRI.
1699            let mut i = 0;
1700            #left_block
1701            out[i] = ::uor_foundation::pipeline::ConstraintRef::Affine {
1702                coefficients: #tag_coeffs_l,
1703                coefficient_count: #tag_coeff_count,
1704                bias: 0,
1705            };
1706            i += 1;
1707            #right_block
1708            out[i] = ::uor_foundation::pipeline::ConstraintRef::Affine {
1709                coefficients: #tag_coeffs_r,
1710                coefficient_count: #tag_coeff_count,
1711                bias: -1,
1712            };
1713            out
1714        };
1715        const #len_const: usize = #len_const_init;
1716
1717        impl ::uor_foundation::pipeline::ConstrainedTypeShape for #name {
1718            const IRI: &'static str = #iri;
1719            const SITE_BUDGET: usize = {
1720                let a = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET;
1721                let b = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_BUDGET;
1722                if a > b { a } else { b }
1723            };
1724            const SITE_COUNT: usize = {
1725                let a = <#l as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
1726                let b = <#r as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT;
1727                (if a > b { a } else { b }) + 1
1728            };
1729            const CONSTRAINTS: &'static [::uor_foundation::pipeline::ConstraintRef] = {
1730                let buf: &'static [::uor_foundation::pipeline::ConstraintRef] = &#raw_const;
1731                match buf.split_at_checked(#len_const) {
1732                    Some((head, _tail)) => head,
1733                    None => &[],
1734                }
1735            };
1736            // ADR-032 + ADR-057: coproduct cardinality = saturating sum + 1
1737            // for non-recurse; saturates at u64::MAX when any operand
1738            // carries a Recurse reference.
1739            const CYCLE_SIZE: u64 = #cycle_size_init;
1740        }
1741
1742        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #name {}
1743        impl<'a> ::uor_foundation::pipeline::IntoBindingValue<'a> for #name {
1744            fn as_binding_value<const INLINE_BYTES: usize>(
1745                &self,
1746            ) -> ::uor_foundation::pipeline::TermValue<'a, INLINE_BYTES> {
1747                ::uor_foundation::pipeline::TermValue::empty()
1748            }
1749        }
1750        impl ::uor_foundation::enforcement::GroundedShape for #name {}
1751    }
1752}
1753
1754// =====================================================================
1755// `prism_model!` — wiki ADR-020 + ADR-022 D3.
1756//
1757// The macro accepts the closure-bodied form the wiki specifies as the
1758// maximally-Rust-native syntax for declaring a Prism model:
1759//
1760// ```text
1761// prism_model! {
1762//     pub struct MyModel;
1763//     pub struct MyRoute;
1764//     impl PrismModel<HType, BType, AType> for MyModel {
1765//         type Input  = InputShape;
1766//         type Output = OutputShape;
1767//         type Route  = MyRoute;
1768//         fn route(input: Self::Input) -> Self::Output {
1769//             // closure body — Rust expression syntax parsed by the macro
1770//             // into a Term tree at expansion time. The body never executes
1771//             // as Rust at runtime; it is consumed at macro time, mapped to
1772//             // the term-tree representation, and the macro emits both the
1773//             // term tree (as a `&'static [Term]` slice) and the
1774//             // closure-checked `FoundationClosed` impl.
1775//             //
1776//             // Recognised foundation-vocabulary forms:
1777//             //   - integer literals          → Term::Literal
1778//             //   - identifier `input`        → Term::Variable
1779//             //   - lowercase PrimitiveOp     → Term::Application
1780//             //     names: add, sub, mul, xor, and, or
1781//             //           neg, bnot, succ, pred
1782//             // Anything else fails to compile, pointing at the offending
1783//             // call site (a closure violation per ADR-020).
1784//         }
1785//     }
1786// }
1787// ```
1788//
1789// Emissions (per ADR-022):
1790//   D1: `impl __sdk_seal::Sealed for MyModel`,
1791//       `impl __sdk_seal::Sealed for MyRoute`
1792//   D2: `const ROUTE_TERMS_<MODEL>: &'static [Term] = &[…]` (fully const
1793//       — `TermArena::from_slice(ROUTE_TERMS_<MODEL>)` is a `const fn`)
1794//   D5: `impl FoundationClosed for MyRoute { arena_slice() → ROUTE_TERMS_<MODEL> }`
1795//   D4: `impl PrismModel<H,B,A> for MyModel { …; fn forward(input) { run_route::<H,B,A,Self>(input) } }`
1796
1797/// Parsed shape of the macro input — a struct declaration for the model,
1798/// optionally one for the route witness, and an `impl PrismModel<…> for
1799/// <Model>` block carrying the three (or four, when ADR-036's ResolverTuple
1800/// substrate parameter is selected) associated types and the closure-bodied
1801/// `route` function. The optional `fn resolvers() -> R { … }` clause
1802/// supplies the ResolverTuple instance for routes that walk the resolver-
1803/// bound ψ-Term variants (ADR-035 + ADR-036).
1804struct PrismModelInput {
1805    model_vis: syn::Visibility,
1806    model_name: Ident,
1807    route_vis: syn::Visibility,
1808    route_name: Ident,
1809    h_ty: syn::Type,
1810    b_ty: syn::Type,
1811    a_ty: syn::Type,
1812    /// `Some(R)` for `impl PrismModel<H, B, A, R> for Model` (ADR-036
1813    /// four-position form); `None` for the three-position form
1814    /// (R defaults to `NullResolverTuple` on the trait).
1815    r_ty: Option<syn::Type>,
1816    /// `Some(C)` for `impl PrismModel<H, B, A, R, C> for Model` (ADR-048
1817    /// five-position form); `None` defaults to `EmptyCommitment` per the
1818    /// `PrismModel` trait's default-type parameter. Wiki ADR-048 commits
1819    /// the 5th substrate parameter as the cost-model commitment surface.
1820    c_ty: Option<syn::Type>,
1821    input_ty: syn::Type,
1822    output_ty: syn::Type,
1823    route_input_ident: Ident,
1824    route_body: syn::Block,
1825    /// Optional `fn resolvers() -> R { <expr> }` clause supplying the
1826    /// ResolverTuple instance the macro-emitted `forward` body borrows.
1827    /// Present iff the user includes the clause; foundation derives the
1828    /// default (`R::default()` for `R: Default`, falling back to
1829    /// `NullResolverTuple` when `r_ty` is `None`) at expansion time.
1830    resolvers_body: Option<syn::Block>,
1831    /// Optional `fn commitment() -> C { <expr> }` clause supplying the
1832    /// TypedCommitment instance the macro-emitted `forward` body borrows.
1833    /// Present iff the user includes the clause; foundation derives the
1834    /// default (`C::default()` for `C: Default`, falling back to
1835    /// `EmptyCommitment` when `c_ty` is `None`) at expansion time.
1836    commitment_body: Option<syn::Block>,
1837}
1838
1839impl Parse for PrismModelInput {
1840    fn parse(input: ParseStream) -> Result<Self> {
1841        // Model struct: `pub struct ModelName;`
1842        let model_vis: syn::Visibility = input.parse()?;
1843        input.parse::<Token![struct]>()?;
1844        let model_name: Ident = input.parse()?;
1845        input.parse::<Token![;]>()?;
1846
1847        // Route witness struct: `pub struct RouteName;`
1848        let route_vis: syn::Visibility = input.parse()?;
1849        input.parse::<Token![struct]>()?;
1850        let route_name: Ident = input.parse()?;
1851        input.parse::<Token![;]>()?;
1852
1853        // `impl PrismModel<H, B, A[, R]> for ModelName`
1854        input.parse::<Token![impl]>()?;
1855        let trait_ident: Ident = input.parse()?;
1856        if trait_ident != "PrismModel" {
1857            return Err(syn::Error::new(
1858                trait_ident.span(),
1859                "prism_model! expects an `impl PrismModel<H, B, A[, R[, C]]> for <Model>` block",
1860            ));
1861        }
1862        input.parse::<Token![<]>()?;
1863        let h_ty: syn::Type = input.parse()?;
1864        input.parse::<Token![,]>()?;
1865        let b_ty: syn::Type = input.parse()?;
1866        input.parse::<Token![,]>()?;
1867        let a_ty: syn::Type = input.parse()?;
1868        // ADR-036: optional fourth substrate-parameter position (R: ResolverTuple).
1869        let r_ty: Option<syn::Type> = if input.peek(Token![,]) {
1870            input.parse::<Token![,]>()?;
1871            Some(input.parse::<syn::Type>()?)
1872        } else {
1873            None
1874        };
1875        // ADR-048: optional fifth substrate-parameter position (C: TypedCommitment).
1876        let c_ty: Option<syn::Type> = if input.peek(Token![,]) {
1877            input.parse::<Token![,]>()?;
1878            Some(input.parse::<syn::Type>()?)
1879        } else {
1880            None
1881        };
1882        input.parse::<Token![>]>()?;
1883        input.parse::<Token![for]>()?;
1884        let impl_target: Ident = input.parse()?;
1885        if impl_target != model_name {
1886            return Err(syn::Error::new(
1887                impl_target.span(),
1888                "prism_model!'s `impl PrismModel<…> for <Model>` target must match the declared model struct",
1889            ));
1890        }
1891
1892        // Body of the impl block:
1893        //   { type Input = …; type Output = …; type Route = …;
1894        //     fn route(…) { … } [ fn resolvers() -> R { … } ] }
1895        let body;
1896        syn::braced!(body in input);
1897
1898        // type Input = …;
1899        body.parse::<Token![type]>()?;
1900        let input_kw: Ident = body.parse()?;
1901        if input_kw != "Input" {
1902            return Err(syn::Error::new(
1903                input_kw.span(),
1904                "expected `type Input = …;`",
1905            ));
1906        }
1907        body.parse::<Token![=]>()?;
1908        let input_ty: syn::Type = body.parse()?;
1909        body.parse::<Token![;]>()?;
1910
1911        // type Output = …;
1912        body.parse::<Token![type]>()?;
1913        let output_kw: Ident = body.parse()?;
1914        if output_kw != "Output" {
1915            return Err(syn::Error::new(
1916                output_kw.span(),
1917                "expected `type Output = …;`",
1918            ));
1919        }
1920        body.parse::<Token![=]>()?;
1921        let output_ty: syn::Type = body.parse()?;
1922        body.parse::<Token![;]>()?;
1923
1924        // type Route = …;
1925        body.parse::<Token![type]>()?;
1926        let route_kw: Ident = body.parse()?;
1927        if route_kw != "Route" {
1928            return Err(syn::Error::new(
1929                route_kw.span(),
1930                "expected `type Route = …;`",
1931            ));
1932        }
1933        body.parse::<Token![=]>()?;
1934        let route_ty_ident: Ident = body.parse()?;
1935        if route_ty_ident != route_name {
1936            return Err(syn::Error::new(
1937                route_ty_ident.span(),
1938                "prism_model!'s `type Route = <RouteName>;` must match the declared route struct",
1939            ));
1940        }
1941        body.parse::<Token![;]>()?;
1942
1943        // fn route(input: Self::Input) -> Self::Output { … }
1944        body.parse::<Token![fn]>()?;
1945        let fn_kw: Ident = body.parse()?;
1946        if fn_kw != "route" {
1947            return Err(syn::Error::new(
1948                fn_kw.span(),
1949                "expected `fn route(input: Self::Input) -> Self::Output { … }`",
1950            ));
1951        }
1952        let params;
1953        syn::parenthesized!(params in body);
1954        let route_input_ident: Ident = params.parse()?;
1955        params.parse::<Token![:]>()?;
1956        let _input_param_ty: syn::Type = params.parse()?;
1957        // Discard return-type position — we already know it from `type Output`.
1958        body.parse::<Token![->]>()?;
1959        let _output_param_ty: syn::Type = body.parse()?;
1960        let route_body: syn::Block = body.parse()?;
1961
1962        // Optional `fn resolvers() -> R { … }` (ADR-036) followed by
1963        // optional `fn commitment() -> C { … }` (ADR-048). The two
1964        // clauses are independent and order-flexible — the parser
1965        // consumes whichever `fn` it sees and matches by identifier.
1966        let mut resolvers_body: Option<syn::Block> = None;
1967        let mut commitment_body: Option<syn::Block> = None;
1968        while body.peek(Token![fn]) {
1969            body.parse::<Token![fn]>()?;
1970            let method_kw: Ident = body.parse()?;
1971            let method_name = method_kw.to_string();
1972            if method_name != "resolvers" && method_name != "commitment" {
1973                return Err(syn::Error::new(
1974                    method_kw.span(),
1975                    "the only optional methods after `fn route` are `fn resolvers() -> R { … }` (ADR-036) and `fn commitment() -> C { … }` (ADR-048)",
1976                ));
1977            }
1978            let method_params;
1979            syn::parenthesized!(method_params in body);
1980            if !method_params.is_empty() {
1981                return Err(syn::Error::new(
1982                    method_kw.span(),
1983                    "`fn resolvers()` / `fn commitment()` take no parameters — their return value is the per-call substrate instance",
1984                ));
1985            }
1986            body.parse::<Token![->]>()?;
1987            let _ret_ty: syn::Type = body.parse()?;
1988            let block: syn::Block = body.parse()?;
1989            if method_name == "resolvers" {
1990                if resolvers_body.is_some() {
1991                    return Err(syn::Error::new(
1992                        method_kw.span(),
1993                        "duplicate `fn resolvers()` clause in `prism_model!` body",
1994                    ));
1995                }
1996                resolvers_body = Some(block);
1997            } else {
1998                if commitment_body.is_some() {
1999                    return Err(syn::Error::new(
2000                        method_kw.span(),
2001                        "duplicate `fn commitment()` clause in `prism_model!` body",
2002                    ));
2003                }
2004                commitment_body = Some(block);
2005            }
2006        }
2007
2008        Ok(Self {
2009            model_vis,
2010            model_name,
2011            route_vis,
2012            route_name,
2013            h_ty,
2014            b_ty,
2015            a_ty,
2016            r_ty,
2017            c_ty,
2018            input_ty,
2019            output_ty,
2020            route_input_ident,
2021            route_body,
2022            resolvers_body,
2023            commitment_body,
2024        })
2025    }
2026}
2027
2028/// One spec entry in the term arena being built. Maps to a `Term::*`
2029/// variant token stream at emission time.
2030/// Wiki ADR-035 receiver-shape kinds for ψ-chain compatibility checks.
2031/// The closure-body parser tags each emitted TermSpec with its produced
2032/// shape and validates ψ-chain operand-shape mismatches at proc-macro
2033/// expansion (`chain_complex` requires a simplicial-complex operand,
2034/// `homology_groups` requires a chain-complex operand, etc.).
2035#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2036enum PsiShape {
2037    /// Byte sequence — Literal, Variable, Application, ProjectField,
2038    /// Try, Recurse, Unfold, Match, Lift, Project. Also the output of
2039    /// `Term::Betti` (the β-vector serialization, a byte sequence) and
2040    /// `Term::KInvariants` (the κ-label byte serialization).
2041    Byte,
2042    /// SimplicialComplex — output of `Term::Nerve` (ψ_1).
2043    SimplicialComplex,
2044    /// ChainComplex — output of `Term::ChainComplex` (ψ_2).
2045    ChainComplex,
2046    /// HomologyGroups — output of `Term::HomologyGroups` (ψ_3).
2047    HomologyGroups,
2048    /// CochainComplex — output of `Term::CochainComplex` (ψ_5).
2049    CochainComplex,
2050    /// CohomologyGroups — output of `Term::CohomologyGroups` (ψ_6).
2051    CohomologyGroups,
2052    /// PostnikovTower — output of `Term::PostnikovTower` (ψ_7).
2053    PostnikovTower,
2054    /// HomotopyGroups — output of `Term::HomotopyGroups` (ψ_8).
2055    HomotopyGroups,
2056}
2057
2058impl PsiShape {
2059    /// Human-readable description used in receiver-shape violation
2060    /// error messages.
2061    fn describe(self) -> &'static str {
2062        match self {
2063            PsiShape::Byte => "byte-shaped",
2064            PsiShape::SimplicialComplex => "simplicial-complex-shaped",
2065            PsiShape::ChainComplex => "chain-complex-shaped",
2066            PsiShape::HomologyGroups => "homology-groups-shaped",
2067            PsiShape::CochainComplex => "cochain-complex-shaped",
2068            PsiShape::CohomologyGroups => "cohomology-groups-shaped",
2069            PsiShape::PostnikovTower => "Postnikov-tower-shaped",
2070            PsiShape::HomotopyGroups => "homotopy-groups-shaped",
2071        }
2072    }
2073}
2074
2075/// Determine the produced shape of a TermSpec. Combinatorial Term
2076/// variants produce byte-shaped values; ψ-Term variants produce their
2077/// per-stage ontology-defined shape (per ADR-035's nine ψ-maps).
2078fn term_spec_shape(spec: &TermSpec) -> PsiShape {
2079    match spec {
2080        TermSpec::Nerve { .. } => PsiShape::SimplicialComplex,
2081        TermSpec::ChainComplex { .. } => PsiShape::ChainComplex,
2082        TermSpec::HomologyGroups { .. } => PsiShape::HomologyGroups,
2083        TermSpec::CochainComplex { .. } => PsiShape::CochainComplex,
2084        TermSpec::CohomologyGroups { .. } => PsiShape::CohomologyGroups,
2085        TermSpec::PostnikovTower { .. } => PsiShape::PostnikovTower,
2086        TermSpec::HomotopyGroups { .. } => PsiShape::HomotopyGroups,
2087        // `Betti` extracts a β-vector serialization — byte-shaped at the
2088        // catamorphism's TermValue level. `KInvariants` produces the
2089        // κ-label byte serialization — byte-shaped. All other Term
2090        // variants are byte-shaped per the existing closure-body
2091        // grammar (ADR-022 D3 G1..G20).
2092        _ => PsiShape::Byte,
2093    }
2094}
2095
2096/// Receiver-shape compatibility per wiki ADR-035:
2097///   - `nerve` (G21): operand must be byte-shaped (per-value bytes).
2098///   - All other ψ-Term variants (G22..G29): operand must be the exact
2099///     upstream ψ-stage shape — receiver and operand match exactly.
2100fn psi_shape_compatible(expected: PsiShape, actual: PsiShape) -> bool {
2101    expected == actual
2102}
2103
2104// `TermSpec::AxisInvocation`, `TermSpec::FirstAdmit`,
2105// `TermSpec::FirstAdmitIdxPlaceholder`, and `TermSpec::LiteralExpr` are
2106// retained as inert variants for substrate compatibility with prior
2107// macro-emitter shapes. Under the wiki ADR-035 ψ-residuals discipline,
2108// the closure-body parser rejects `first_admit(...)`, `hash(...)`, and
2109// byte-comparison/concat operators at emission time, so these variants
2110// are not constructed by the current verb-body emitter. The match arms
2111// against them survive in clone/render paths to keep the enum
2112// total-functioned should non-verb-body callers (conformance generators,
2113// trace replay) emit a TermSpec arena containing them.
2114#[allow(dead_code)]
2115enum TermSpec {
2116    /// `Term::Literal { value, level: WittLevel::W8 }`
2117    Literal(u64),
2118    /// `Term::Variable { name_index: 0 }` (the macro recognises `input`
2119    /// as the sole bound name; future iterations support `let` bindings).
2120    Variable,
2121    /// `Term::Application { operator, args: TermList { start, len } }`
2122    Application {
2123        operator: proc_macro2::TokenStream,
2124        args_start: u32,
2125        args_len: u32,
2126    },
2127    /// `Term::AxisInvocation { axis_index, kernel_id, input_index }` —
2128    /// wiki ADR-030 (replaces ADR-026 G19's HasherProjection). G19's
2129    /// `hash(input)` form lowers to AxisInvocation against axis 0
2130    /// (the application's HashAxis position) and kernel 0 (HashAxis::KERNEL_HASH).
2131    AxisInvocation {
2132        axis_index: u32,
2133        kernel_id: u32,
2134        input_index: u32,
2135    },
2136    /// `Term::ProjectField { source_index, byte_offset, byte_length }`
2137    /// — wiki ADR-033 G20. Byte-slice projection over a partition_product
2138    /// source; offsets/lengths are const-eval token streams referencing
2139    /// `<RouteInputTy as PartitionProductFields>::FIELDS[idx]` so the
2140    /// values are computed by the trait impl at the consumer's compile
2141    /// time (positional and named forms both lower to indexed lookup).
2142    ProjectField {
2143        source_index: u32,
2144        byte_offset: proc_macro2::TokenStream,
2145        byte_length: proc_macro2::TokenStream,
2146    },
2147    /// `Term::FirstAdmit { domain_size_index, predicate_index }` — wiki
2148    /// ADR-034 Mechanism 2. Bounded structural search over a domain of
2149    /// size N (read from the domain type's `CYCLE_SIZE` per ADR-032);
2150    /// the catamorphism iterates `idx` ascending and short-circuits on
2151    /// the first non-zero predicate result.
2152    FirstAdmit {
2153        domain_size_index: u32,
2154        predicate_index: u32,
2155    },
2156    /// FirstAdmit candidate-value placeholder — wiki ADR-034 Mechanism 2.
2157    /// Lowers to `Term::Variable { name_index: FIRST_ADMIT_IDX_NAME_INDEX }`.
2158    /// The catamorphism's Variable handler returns the threaded
2159    /// `first_admit_idx_value` (the current candidate `idx` packed at the
2160    /// domain's byte width).
2161    FirstAdmitIdxPlaceholder,
2162    /// Recurse iteration-counter placeholder — wiki ADR-034 Mechanism 1.
2163    /// Lowers to `Term::Variable { name_index: RECURSE_IDX_NAME_INDEX }`.
2164    /// The catamorphism's Variable handler returns the threaded
2165    /// `recurse_idx_value` (the current measure value, i.e. the
2166    /// iteration counter at this descent).
2167    RecurseIdxPlaceholder,
2168    /// ψ_1 (wiki ADR-035 G21): `Term::Nerve { value_index }`.
2169    Nerve { value_index: u32 },
2170    /// ψ_2 (wiki ADR-035 G22): `Term::ChainComplex { simplicial_index }`.
2171    ChainComplex { simplicial_index: u32 },
2172    /// ψ_3 (wiki ADR-035 G23): `Term::HomologyGroups { chain_index }`.
2173    HomologyGroups { chain_index: u32 },
2174    /// ψ_4 (wiki ADR-035 G24): `Term::Betti { homology_index }`.
2175    Betti { homology_index: u32 },
2176    /// ψ_5 (wiki ADR-035 G25): `Term::CochainComplex { chain_index }`.
2177    CochainComplex { chain_index: u32 },
2178    /// ψ_6 (wiki ADR-035 G26): `Term::CohomologyGroups { cochain_index }`.
2179    CohomologyGroups { cochain_index: u32 },
2180    /// ψ_7 (wiki ADR-035 G27): `Term::PostnikovTower { simplicial_index }`.
2181    PostnikovTower { simplicial_index: u32 },
2182    /// ψ_8 (wiki ADR-035 G28): `Term::HomotopyGroups { postnikov_index }`.
2183    HomotopyGroups { postnikov_index: u32 },
2184    /// ψ_9 (wiki ADR-035 G29): `Term::KInvariants { homotopy_index }`.
2185    KInvariants { homotopy_index: u32 },
2186    /// `Term::Literal { value: <const-eval expr>, level: <expr> }` — a
2187    /// Literal whose value is a const-eval token stream rather than a
2188    /// macro-time u64. Used by ADR-032's `first_admit` lowering to read
2189    /// the descent measure from `<DomainTy as ConstrainedTypeShape>::CYCLE_SIZE`
2190    /// at the consumer's compile time.
2191    LiteralExpr {
2192        value: proc_macro2::TokenStream,
2193        level: proc_macro2::TokenStream,
2194    },
2195    /// ADR-051 wide-Witt literal carrier (Dependency 2 in v0.4.10): a
2196    /// `Term::Literal` whose `value` is a byte sequence (rather than a
2197    /// u64). Surfaces as `literal_bytes(&[u8], WittLevel)` in verb bodies.
2198    /// Renders to `pipeline::literal_bytes(<bytes_expr>, <level_expr>)`
2199    /// at the consumer's compile time, producing a const Term::Literal
2200    /// whose TermValue carrier holds the full byte sequence.
2201    ///
2202    /// Required for wide Witt-level literals (W128+) that don't fit in
2203    /// a u64 — secp256k1 P_LITERAL (W256), AES round constants (W128),
2204    /// FHE plaintext-coefficient tables, etc.
2205    LiteralBytesExpr {
2206        bytes: proc_macro2::TokenStream,
2207        level: proc_macro2::TokenStream,
2208    },
2209    /// Compile-time verb splice — wiki ADR-024.
2210    ///
2211    /// Inlines the verb's `&'static [Term]` fragment into the host
2212    /// arena at expansion time via `inline_verb_fragment` (with each
2213    /// internal arena index shifted by the host's current length) plus
2214    /// substitution: every `Term::Variable { name_index: 0 }` in the
2215    /// fragment is replaced by the host arena's term at `arg_root_idx`.
2216    /// The substitution makes the verb's `input` parameter bind to the
2217    /// caller's argument expression.
2218    ///
2219    /// `arg_root_idx` is the TermSpec arena index whose result position
2220    /// the verb's input substitutes to. The render layer translates
2221    /// TermSpec indices to dynamic host positions via per-spec
2222    /// `pos_<N>` const-let bindings emitted in the arena builder.
2223    VerbSplice {
2224        arg_root_idx: u32,
2225        fragment_path: proc_macro2::TokenStream,
2226    },
2227    /// `Term::Lift { operand_index, target }` — wiki ADR-022 D3 G4.
2228    /// Canonical injection from the operand's Witt level to a strictly
2229    /// higher target level (lossless zero-extension).
2230    Lift {
2231        operand_index: u32,
2232        target_witt: proc_macro2::TokenStream,
2233    },
2234    /// `Term::Project { operand_index, target }` — wiki ADR-022 D3 G5.
2235    /// Canonical surjection from the operand's Witt level to a strictly
2236    /// lower target level (lossy truncation).
2237    Project {
2238        operand_index: u32,
2239        target_witt: proc_macro2::TokenStream,
2240    },
2241    /// `Term::Try { body_index, handler_index: u32::MAX }` — wiki
2242    /// ADR-022 D3 G9. The postfix `?` operator on a sub-expression.
2243    /// Foundation's catamorphism interprets the `u32::MAX` sentinel as
2244    /// "propagate the failure unchanged through `PipelineFailure`".
2245    Try { body_index: u32 },
2246    /// `Term::Recurse { measure_index, base_index, step_index }` — wiki
2247    /// ADR-022 D3 G7. Bounded recursion guarded by a descent measure.
2248    Recurse {
2249        measure_index: u32,
2250        base_index: u32,
2251        step_index: u32,
2252    },
2253    /// `Term::Unfold { seed_index, step_index }` — wiki ADR-022 D3 G8.
2254    /// Anamorphism step.
2255    Unfold { seed_index: u32, step_index: u32 },
2256    /// `Term::Match { scrutinee_index, arms }` — wiki ADR-022 D3 G6.
2257    /// Arms alternate (pattern, body) per the convention foundation's
2258    /// catamorphism reads to dispatch.
2259    Match {
2260        scrutinee_index: u32,
2261        arms_start: u32,
2262        arms_len: u32,
2263    },
2264    /// Wildcard pattern sentinel — wiki ADR-022 D3 G6 (and G9 default
2265    /// handler). Lowers to `Term::Variable { name_index: u32::MAX }`.
2266    WildcardSentinel,
2267    /// Recurse-placeholder sentinel — wiki ADR-029. Lowers to
2268    /// `Term::Variable { name_index: RECURSE_PLACEHOLDER_NAME_INDEX }`.
2269    /// The catamorphism's Variable handler returns the threaded
2270    /// `recurse_value` (the previous iteration's accumulator) when the
2271    /// name_index matches `pipeline::RECURSE_PLACEHOLDER_NAME_INDEX`.
2272    RecursePlaceholder,
2273    /// Unfold-placeholder sentinel — wiki ADR-029. Lowers to
2274    /// `Term::Variable { name_index: UNFOLD_PLACEHOLDER_NAME_INDEX }`.
2275    /// The catamorphism's Variable handler returns the threaded
2276    /// `unfold_value` (the unfold's current state) when the name_index
2277    /// matches `pipeline::UNFOLD_PLACEHOLDER_NAME_INDEX`. The Term::Unfold
2278    /// fold-rule iterates step with the placeholder bound to the
2279    /// accumulated state until a Kleene fixpoint or
2280    /// `pipeline::UNFOLD_MAX_ITERATIONS` is reached.
2281    UnfoldPlaceholder,
2282}
2283
2284/// Per-scope binding table the closure-body parser maintains. Maps a
2285/// `let`-introduced identifier to the arena index where that binding's
2286/// value-tree's root lives, so identifier references inside the
2287/// `let`'s scope resolve to that root. Per wiki ADR-022 D3 G10 the
2288/// macro builds this incrementally as it descends through `let`
2289/// statements; the route's input parameter is its own special case
2290/// (`Term::Variable { name_index: 0 }`) and lives outside this table.
2291#[derive(Default, Clone)]
2292struct BindingScope {
2293    bindings: Vec<(Ident, usize)>,
2294    /// ADR-033 G20: the route input type, threaded through the closure-
2295    /// body parser so field-access expressions can synthesize const-eval
2296    /// lookups against `<RouteInputTy as PartitionProductFields>::FIELDS`.
2297    /// Set for `prism_model!` route bodies, `verb!` bodies (since v0.4.9),
2298    /// and `axis!` body clauses (since v0.4.9).
2299    route_input_ty: Option<syn::Type>,
2300    /// ADR-056 scope refinement: the ψ-residuals discipline (rejection of
2301    /// `concat`/`hash`/`first_admit` calls + `Le`/`Lt`/`Ge`/`Gt`
2302    /// binary-op syntax) applies ONLY to the route body's syntactic
2303    /// surface (the `prism_model!`-declared `route` function's closure
2304    /// body). Verb bodies declared via `verb!` and axis impl bodies
2305    /// declared via `axis!`'s `body = …;` clause are unrestricted within
2306    /// the substrate vocabulary per ADR-024 + ADR-055; they may use the
2307    /// full `PrimitiveOp` catalog (including `Concat`, `Le`, `Lt`, `Ge`,
2308    /// `Gt`), `Term::FirstAdmit`, and `Term::AxisInvocation`.
2309    ///
2310    /// `true` only for `prism_model!`'s route body; `false` for `verb!`
2311    /// bodies and `axis!` body clauses (and the default).
2312    in_route_body: bool,
2313}
2314
2315impl BindingScope {
2316    fn lookup(&self, ident: &Ident) -> Option<usize> {
2317        // Iterate in reverse so inner shadowing (G10 forbids it but the
2318        // lookup works either way) finds the latest declaration.
2319        self.bindings
2320            .iter()
2321            .rev()
2322            .find(|(name, _)| name == ident)
2323            .map(|(_, idx)| *idx)
2324    }
2325
2326    fn shadow_check(&self, ident: &Ident) -> Result<()> {
2327        if self.bindings.iter().any(|(name, _)| name == ident) {
2328            return Err(syn::Error::new(
2329                ident.span(),
2330                format!(
2331                    "closure violation: shadowing `{ident}` (ADR-022 D3 G10 forbids declaring two `let`s with the same identifier in the same scope)"
2332                ),
2333            ));
2334        }
2335        Ok(())
2336    }
2337
2338    fn push(&mut self, ident: Ident, root_idx: usize) {
2339        self.bindings.push((ident, root_idx));
2340    }
2341}
2342
2343/// Recursively walk a Rust expression and append the terms that compute
2344/// it to `arena`, returning the index where this expression's *root*
2345/// term lands. `route_input` is the name of the route's bound input
2346/// parameter (mapped to `Term::Variable { name_index: 0 }`); `scope`
2347/// carries `let`-introduced bindings (G10) the parser has accumulated.
2348fn emit_term_for_expr(
2349    expr: &syn::Expr,
2350    route_input: &Ident,
2351    arena: &mut Vec<TermSpec>,
2352    scope: &mut BindingScope,
2353) -> Result<usize> {
2354    match expr {
2355        syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(int_lit), .. }) => {
2356            let value: u64 = int_lit.base10_parse().map_err(|e| {
2357                syn::Error::new(int_lit.span(), format!("integer literal out of u64 range: {e}"))
2358            })?;
2359            let idx = arena.len();
2360            arena.push(TermSpec::Literal(value));
2361            Ok(idx)
2362        }
2363        syn::Expr::Path(path_expr) if path_expr.path.get_ident() == Some(route_input) => {
2364            let idx = arena.len();
2365            arena.push(TermSpec::Variable);
2366            Ok(idx)
2367        }
2368        syn::Expr::Path(path_expr) => {
2369            // ADR-022 D3 G10: a bare identifier may be a `let`-introduced
2370            // binding from the surrounding scope. The macro splices the
2371            // binding's value-tree root by emitting a duplicate path that
2372            // shares the same arena root index — semantically identical
2373            // because Term values are content-determined.
2374            if let Some(name) = path_expr.path.get_ident() {
2375                if let Some(root) = scope.lookup(name) {
2376                    return Ok(root);
2377                }
2378            }
2379            Err(syn::Error::new_spanned(
2380                path_expr,
2381                "closure violation: identifier is not a foundation-vocabulary name (only the route's `input` parameter, `let`-introduced bindings, and reserved macro-vocabulary identifiers are recognised)",
2382            ))
2383        }
2384        syn::Expr::Call(call_expr) => emit_term_for_call(call_expr, route_input, arena, scope),
2385        // ADR-013/TR-08 substrate amendment: byte-level comparison operators
2386        // <= < >= > == != lower to Term::Application(Le/Lt/Ge/Gt/Eq/Ne, [lhs, rhs]).
2387        syn::Expr::Binary(bin_expr) => emit_term_for_binary(bin_expr, route_input, arena, scope),
2388        syn::Expr::Block(block_expr) => emit_term_for_block(&block_expr.block, route_input, arena, scope),
2389        syn::Expr::Paren(paren_expr) => {
2390            emit_term_for_expr(&paren_expr.expr, route_input, arena, scope)
2391        }
2392        // ADR-022 D3 G9: postfix `?` operator. Emits Term::Try with the
2393        // default-propagation handler (`u32::MAX`) — the catamorphism
2394        // propagates the body's failure unchanged through PipelineFailure.
2395        syn::Expr::Try(try_expr) => {
2396            let body_root = emit_term_for_expr(&try_expr.expr, route_input, arena, scope)?;
2397            let idx = arena.len();
2398            arena.push(TermSpec::Try { body_index: body_root as u32 });
2399            Ok(idx)
2400        }
2401        // ADR-022 D3 G6: `match <scrutinee> { <pat> => <arm>, …, _ => <default> }`.
2402        syn::Expr::Match(match_expr) => emit_term_for_match(match_expr, route_input, arena, scope),
2403        // ADR-033 G20: field-access projection over a partition_product
2404        // input. `<expr>.<index>` (positional) or `<expr>.<field_name>`
2405        // (named, requires partition_product! to use the named-field
2406        // form). Lowers to `Term::ProjectField` whose offset/length are
2407        // const-eval lookups against `<SourceTy as PartitionProductFields>::FIELDS`.
2408        syn::Expr::Field(field_expr) => emit_term_for_field(field_expr, route_input, arena, scope),
2409        other => Err(syn::Error::new_spanned(
2410            other,
2411            "closure violation: expression form is not in foundation vocabulary (recognised forms: integer literals, the route's `input` parameter, `let`-introduced bindings, postfix `?`, `match`, field access on partition_product inputs, and macro-vocabulary function calls — PrimitiveOps, hash, lift, project, recurse, unfold, plus implementation verbs)",
2412        )),
2413    }
2414}
2415
2416/// ADR-033 G20: emit `Term::ProjectField` for a `<expr>.<member>` form.
2417/// `<member>` is either a positional index (numeric) or a field name.
2418/// The byte_offset and byte_length are emitted as const-eval token
2419/// streams against `<SourceTy as PartitionProductFields>::FIELDS`. For
2420/// chained access (e.g. `input.outer.inner`), each `.<member>` emits one
2421/// `Term::ProjectField` whose `source_index` is the prior projection's
2422/// arena root, and whose static source type is resolved through
2423/// `<PrevTy as PartitionProductFactor<I>>::Factor` (per ADR-033 G4).
2424///
2425/// Source-type resolution proceeds as follows:
2426///   - if the receiver is the route input ident, the source type is the
2427///     route input type (read from `scope.route_input_ty`);
2428///   - if the receiver is a chained `<expr>.<member>` field-access, the
2429///     source type is `<PrevTy as PartitionProductFactor<INDEX>>::Factor`
2430///     where `INDEX` is the const-eval index of the prior member (literal
2431///     for positional, `field_index_by_name(...)` for named);
2432///   - if the receiver is anything else (let-binding, etc.), we cannot
2433///     determine the source type at proc-macro time and reject.
2434fn emit_term_for_field(
2435    field_expr: &syn::ExprField,
2436    route_input: &Ident,
2437    arena: &mut Vec<TermSpec>,
2438    scope: &mut BindingScope,
2439) -> Result<usize> {
2440    // Emit the source expression first; capture its arena root.
2441    let source_root = emit_term_for_expr(&field_expr.base, route_input, arena, scope)?;
2442    // Determine the source's static type for the FIELDS lookup.
2443    let source_ty: syn::Type = resolve_field_receiver_type(&field_expr.base, route_input, scope)?;
2444    // Resolve the member to a const-eval index expression — either a
2445    // literal index (G20 positional) or a name lookup against FIELD_NAMES
2446    // (G20 named).
2447    let index_expr = field_index_expr(&field_expr.member, &source_ty);
2448    let offset_expr = quote::quote! {
2449        <#source_ty as ::uor_foundation::pipeline::PartitionProductFields>::FIELDS[#index_expr].0
2450    };
2451    let length_expr = quote::quote! {
2452        <#source_ty as ::uor_foundation::pipeline::PartitionProductFields>::FIELDS[#index_expr].1
2453    };
2454    let idx = arena.len();
2455    arena.push(TermSpec::ProjectField {
2456        source_index: source_root as u32,
2457        byte_offset: offset_expr,
2458        byte_length: length_expr,
2459    });
2460    Ok(idx)
2461}
2462
2463/// ADR-033 G4 helper: given a field-access member (`Unnamed(i)` or
2464/// `Named(name)`), produce the const-eval token stream that evaluates to
2465/// the factor index in the source type's `PartitionProductFields::FIELDS`
2466/// directory. For positional access this is a literal; for named it is a
2467/// call to `field_index_by_name(<name>)` on the source type.
2468fn field_index_expr(member: &syn::Member, source_ty: &syn::Type) -> proc_macro2::TokenStream {
2469    match member {
2470        syn::Member::Unnamed(idx) => {
2471            let i = idx.index as usize;
2472            quote::quote! { #i }
2473        }
2474        syn::Member::Named(name) => {
2475            let name_lit = name.to_string();
2476            // ADR-033 G3/G4: invoke the free `const fn`
2477            // `field_index_by_name_in` against the source's FIELD_NAMES
2478            // (stable Rust 1.83 substitute for a const trait method).
2479            quote::quote! {
2480                ::uor_foundation::pipeline::field_index_by_name_in(
2481                    <#source_ty as ::uor_foundation::pipeline::PartitionProductFields>::FIELD_NAMES,
2482                    #name_lit,
2483                )
2484            }
2485        }
2486    }
2487}
2488
2489/// ADR-033 G4 helper: resolve the static type of a chained-field-access
2490/// receiver. For the route-input base case the type comes from the
2491/// closure scope. For each chained step `<inner>.<member>` the type is
2492/// `<InnerTy as PartitionProductFactor<INDEX>>::Factor`. INDEX is the
2493/// const-eval expression returned by [`field_index_expr`].
2494fn resolve_field_receiver_type(
2495    base: &syn::Expr,
2496    route_input: &Ident,
2497    scope: &BindingScope,
2498) -> Result<syn::Type> {
2499    match base {
2500        syn::Expr::Path(path_expr) if path_expr.path.get_ident() == Some(route_input) => {
2501            match &scope.route_input_ty {
2502                Some(ty) => Ok(ty.clone()),
2503                None => Err(syn::Error::new_spanned(
2504                    base,
2505                    "closure violation: ADR-033 G20 field access requires the route input type to be known (only `prism_model!` and macros that pin `route_input_ty` admit field-access expressions)",
2506                )),
2507            }
2508        }
2509        syn::Expr::Paren(paren_expr) => {
2510            resolve_field_receiver_type(&paren_expr.expr, route_input, scope)
2511        }
2512        syn::Expr::Field(inner) => {
2513            // Recursively resolve the inner receiver's type, then descend
2514            // through `<InnerTy as PartitionProductFactor<INDEX>>::Factor`.
2515            let inner_ty = resolve_field_receiver_type(&inner.base, route_input, scope)?;
2516            let inner_index = field_index_expr(&inner.member, &inner_ty);
2517            // Synthesize the qualified path
2518            //   <#inner_ty as PartitionProductFactor<{#inner_index}>>::Factor
2519            // as a syn::Type. We stitch it together via parse_quote!.
2520            let synth: syn::Type = syn::parse_quote! {
2521                <#inner_ty as ::uor_foundation::pipeline::PartitionProductFactor<{#inner_index}>>::Factor
2522            };
2523            Ok(synth)
2524        }
2525        other => Err(syn::Error::new_spanned(
2526            other,
2527            "closure violation: ADR-033 G20 field access receiver must be the route's `input` parameter or another field-access (let-binding receivers are not supported)",
2528        )),
2529    }
2530}
2531
2532/// Handle `match <scrutinee> { <lit_pat> => <body>, …, _ => <default> }`
2533/// per wiki ADR-022 D3 G6. Each arm's pattern is an atomic term
2534/// (Literal or WildcardSentinel); each arm's body is an arbitrary
2535/// expression whose sub-tree lives in the arena. The wiki specifies the
2536/// arms span as a contiguous block of `2 * num_arms` terms alternating
2537/// (pattern, body); the body in that span is a *copy of the body's root
2538/// term* so the catamorphism's evaluator can dispatch by reading
2539/// `arena[start + 2k]` (pattern) and `arena[start + 2k + 1]` (body
2540/// root). The body's sub-tree lives at lower indices.
2541fn emit_term_for_match(
2542    match_expr: &syn::ExprMatch,
2543    route_input: &Ident,
2544    arena: &mut Vec<TermSpec>,
2545    scope: &mut BindingScope,
2546) -> Result<usize> {
2547    let scrutinee_root = emit_term_for_expr(&match_expr.expr, route_input, arena, scope)?;
2548    if match_expr.arms.is_empty() {
2549        return Err(syn::Error::new_spanned(
2550            match_expr,
2551            "closure violation: `match` (G6) must have at least one arm; non-exhaustive matches are closure violations",
2552        ));
2553    }
2554    let last_arm = &match_expr.arms[match_expr.arms.len() - 1];
2555    let last_is_wildcard = matches!(last_arm.pat, syn::Pat::Wild(_));
2556    if !last_is_wildcard {
2557        return Err(syn::Error::new_spanned(
2558            &last_arm.pat,
2559            "closure violation: `match` (G6) MUST end with a wildcard arm `_ => <default>`; non-exhaustive matches are closure violations",
2560        ));
2561    }
2562
2563    // First pass: emit each arm's body sub-tree, recording the pattern
2564    // spec and body-root arena index. The pattern is atomic so we
2565    // construct the spec inline rather than reserving an arena slot.
2566    let mut arm_pairs: Vec<(TermSpec, usize)> = Vec::with_capacity(match_expr.arms.len());
2567    for arm in &match_expr.arms {
2568        if arm.guard.is_some() {
2569            return Err(syn::Error::new_spanned(
2570                arm,
2571                "closure violation: match arm guards are not in the closure-body grammar (G6)",
2572            ));
2573        }
2574        let pattern_spec = match &arm.pat {
2575            syn::Pat::Lit(lit_pat) => {
2576                if let syn::Lit::Int(int_lit) = &lit_pat.lit {
2577                    let value: u64 = int_lit.base10_parse().map_err(|e| {
2578                        syn::Error::new(
2579                            int_lit.span(),
2580                            format!("integer literal out of u64 range: {e}"),
2581                        )
2582                    })?;
2583                    TermSpec::Literal(value)
2584                } else {
2585                    return Err(syn::Error::new_spanned(
2586                        lit_pat,
2587                        "closure violation: match patterns must be integer literals or `_` (G6)",
2588                    ));
2589                }
2590            }
2591            syn::Pat::Wild(_) => TermSpec::WildcardSentinel,
2592            other => {
2593                return Err(syn::Error::new_spanned(
2594                    other,
2595                    "closure violation: match patterns must be integer literals or `_` (G6)",
2596                ));
2597            }
2598        };
2599        let body_root = emit_term_for_expr(&arm.body, route_input, arena, scope)?;
2600        arm_pairs.push((pattern_spec, body_root));
2601    }
2602
2603    // Second pass: emit the contiguous arms span with alternating
2604    // (pattern, body_root_copy). The body_root_copy duplicates the
2605    // TermSpec at the body's root so the arms span is exactly
2606    // `2 * num_arms` terms — the layout the catamorphism's evaluator
2607    // expects per ADR-029's `Term::Match` fold rule.
2608    let arms_start = arena.len();
2609    for (pattern_spec, body_root_idx) in arm_pairs {
2610        arena.push(pattern_spec);
2611        let body_copy = clone_term_spec(&arena[body_root_idx]);
2612        arena.push(body_copy);
2613    }
2614    let arms_len = arena.len() - arms_start;
2615    let idx = arena.len();
2616    arena.push(TermSpec::Match {
2617        scrutinee_index: scrutinee_root as u32,
2618        arms_start: arms_start as u32,
2619        arms_len: arms_len as u32,
2620    });
2621    Ok(idx)
2622}
2623
2624/// Build the closure-violation error for a ψ-residual op emission per
2625/// wiki ADR-035. The error spans the offending syntactic form and
2626/// names the ψ-residual category + the architectural reason +
2627/// the canonical alternative (ψ-chain composition).
2628fn reject_psi_residual_op<E: quote::ToTokens>(
2629    expr: &E,
2630    op_syntax: &str,
2631    op_name: &str,
2632) -> Result<usize> {
2633    let message = format!(
2634        "ψ-residual violation (wiki ADR-035): byte-comparison `{op_syntax}` \
2635         (`PrimitiveOp::{op_name}`) is excluded from verb-body composition. \
2636         The canonical compiled form is structural — admission is a property \
2637         of the value's k-invariant signature, not a comparison predicate. \
2638         Express the admission relation through the ψ-chain (G21..G29) \
2639         instead: e.g. `k_invariants(homotopy_groups(postnikov_tower(nerve(input))))` \
2640         produces the κ-label that classifies the input's homotopy type."
2641    );
2642    Err(syn::Error::new_spanned(expr, message))
2643}
2644
2645/// Build the closure-violation error for a ψ-residual call form.
2646fn reject_psi_residual_call<E: quote::ToTokens>(
2647    expr: &E,
2648    form: &str,
2649    term_variant: &str,
2650    rationale: &str,
2651) -> Result<usize> {
2652    let message = format!(
2653        "ψ-residual violation (wiki ADR-035): `{form}` lowers to `Term::{term_variant}` \
2654         and is excluded from verb-body composition. {rationale}"
2655    );
2656    Err(syn::Error::new_spanned(expr, message))
2657}
2658
2659/// Map a binary operator expression to a `Term::Application` with the
2660/// corresponding `PrimitiveOp` discriminant. Wiki ADR-013/TR-08 substrate
2661/// amendment: foundation extends `PrimitiveOp` with `Le`, `Lt`, `Ge`,
2662/// `Gt` (byte-level comparison) — those discriminants remain in the
2663/// substrate for non-verb-body contexts but are rejected here per
2664/// ADR-035's ψ-residuals discipline.
2665fn emit_term_for_binary(
2666    expr: &syn::ExprBinary,
2667    route_input: &Ident,
2668    arena: &mut Vec<TermSpec>,
2669    scope: &mut BindingScope,
2670) -> Result<usize> {
2671    let operator = match expr.op {
2672        // ADR-035 ψ-residuals discipline scoped per ADR-056: byte-comparison
2673        // PrimitiveOps (Le, Lt, Ge, Gt) are ψ-enumeration residuals of
2674        // search-based admission predicates and excluded from the route
2675        // body's syntactic surface. Per ADR-056 the rejection applies ONLY
2676        // to `prism_model!` route bodies (`in_route_body == true`); verb!
2677        // bodies and axis! body clauses may use the full substrate
2678        // vocabulary including these comparisons (SHA padding length
2679        // comparison, tensor saturation clamp bounds, etc.).
2680        syn::BinOp::Le(_) if scope.in_route_body => {
2681            return reject_psi_residual_op(expr, "<=", "Le")
2682        }
2683        syn::BinOp::Lt(_) if scope.in_route_body => return reject_psi_residual_op(expr, "<", "Lt"),
2684        syn::BinOp::Ge(_) if scope.in_route_body => {
2685            return reject_psi_residual_op(expr, ">=", "Ge")
2686        }
2687        syn::BinOp::Gt(_) if scope.in_route_body => return reject_psi_residual_op(expr, ">", "Gt"),
2688        syn::BinOp::Le(_) => quote! { ::uor_foundation::PrimitiveOp::Le },
2689        syn::BinOp::Lt(_) => quote! { ::uor_foundation::PrimitiveOp::Lt },
2690        syn::BinOp::Ge(_) => quote! { ::uor_foundation::PrimitiveOp::Ge },
2691        syn::BinOp::Gt(_) => quote! { ::uor_foundation::PrimitiveOp::Gt },
2692        syn::BinOp::Add(_) => quote! { ::uor_foundation::PrimitiveOp::Add },
2693        syn::BinOp::Sub(_) => quote! { ::uor_foundation::PrimitiveOp::Sub },
2694        syn::BinOp::Mul(_) => quote! { ::uor_foundation::PrimitiveOp::Mul },
2695        syn::BinOp::BitXor(_) => quote! { ::uor_foundation::PrimitiveOp::Xor },
2696        syn::BinOp::BitAnd(_) => quote! { ::uor_foundation::PrimitiveOp::And },
2697        syn::BinOp::BitOr(_) => quote! { ::uor_foundation::PrimitiveOp::Or },
2698        _ => {
2699            return Err(syn::Error::new_spanned(
2700                expr,
2701                "closure violation: binary operator is not in the closure-body grammar; recognised operators are arithmetic (+, -, *) and bitwise (^, &, |). Byte-level comparison (<=, <, >=, >) is admissible in verb bodies and axis impl bodies per ADR-056; it is excluded only from the route body's syntactic surface (per ADR-035/ADR-056).",
2702            ));
2703        }
2704    };
2705    let lhs_root = emit_term_for_expr(&expr.left, route_input, arena, scope)?;
2706    let rhs_root = emit_term_for_expr(&expr.right, route_input, arena, scope)?;
2707    let already_contiguous = rhs_root == lhs_root + 1;
2708    let (args_start, args_len) = if already_contiguous {
2709        (lhs_root as u32, 2u32)
2710    } else {
2711        let start = arena.len();
2712        let lhs_dup = clone_term_spec(&arena[lhs_root]);
2713        arena.push(lhs_dup);
2714        let rhs_dup = clone_term_spec(&arena[rhs_root]);
2715        arena.push(rhs_dup);
2716        (start as u32, 2u32)
2717    };
2718    let idx = arena.len();
2719    arena.push(TermSpec::Application {
2720        operator,
2721        args_start,
2722        args_len,
2723    });
2724    Ok(idx)
2725}
2726
2727/// Duplicate a `TermSpec`. Used when emitting `match` arm-spans where
2728/// the body's root term must be copied into the arms range alongside
2729/// its pattern.
2730fn clone_term_spec(spec: &TermSpec) -> TermSpec {
2731    match spec {
2732        TermSpec::Literal(v) => TermSpec::Literal(*v),
2733        TermSpec::Variable => TermSpec::Variable,
2734        TermSpec::Application {
2735            operator,
2736            args_start,
2737            args_len,
2738        } => TermSpec::Application {
2739            operator: operator.clone(),
2740            args_start: *args_start,
2741            args_len: *args_len,
2742        },
2743        TermSpec::AxisInvocation {
2744            axis_index,
2745            kernel_id,
2746            input_index,
2747        } => TermSpec::AxisInvocation {
2748            axis_index: *axis_index,
2749            kernel_id: *kernel_id,
2750            input_index: *input_index,
2751        },
2752        TermSpec::ProjectField {
2753            source_index,
2754            byte_offset,
2755            byte_length,
2756        } => TermSpec::ProjectField {
2757            source_index: *source_index,
2758            byte_offset: byte_offset.clone(),
2759            byte_length: byte_length.clone(),
2760        },
2761        TermSpec::LiteralExpr { value, level } => TermSpec::LiteralExpr {
2762            value: value.clone(),
2763            level: level.clone(),
2764        },
2765        TermSpec::LiteralBytesExpr { bytes, level } => TermSpec::LiteralBytesExpr {
2766            bytes: bytes.clone(),
2767            level: level.clone(),
2768        },
2769        TermSpec::VerbSplice {
2770            arg_root_idx,
2771            fragment_path,
2772        } => TermSpec::VerbSplice {
2773            arg_root_idx: *arg_root_idx,
2774            fragment_path: fragment_path.clone(),
2775        },
2776        TermSpec::Lift {
2777            operand_index,
2778            target_witt,
2779        } => TermSpec::Lift {
2780            operand_index: *operand_index,
2781            target_witt: target_witt.clone(),
2782        },
2783        TermSpec::Project {
2784            operand_index,
2785            target_witt,
2786        } => TermSpec::Project {
2787            operand_index: *operand_index,
2788            target_witt: target_witt.clone(),
2789        },
2790        TermSpec::Try { body_index } => TermSpec::Try {
2791            body_index: *body_index,
2792        },
2793        TermSpec::Recurse {
2794            measure_index,
2795            base_index,
2796            step_index,
2797        } => TermSpec::Recurse {
2798            measure_index: *measure_index,
2799            base_index: *base_index,
2800            step_index: *step_index,
2801        },
2802        TermSpec::Unfold {
2803            seed_index,
2804            step_index,
2805        } => TermSpec::Unfold {
2806            seed_index: *seed_index,
2807            step_index: *step_index,
2808        },
2809        TermSpec::Match {
2810            scrutinee_index,
2811            arms_start,
2812            arms_len,
2813        } => TermSpec::Match {
2814            scrutinee_index: *scrutinee_index,
2815            arms_start: *arms_start,
2816            arms_len: *arms_len,
2817        },
2818        TermSpec::WildcardSentinel => TermSpec::WildcardSentinel,
2819        TermSpec::RecursePlaceholder => TermSpec::RecursePlaceholder,
2820        TermSpec::UnfoldPlaceholder => TermSpec::UnfoldPlaceholder,
2821        TermSpec::FirstAdmit {
2822            domain_size_index,
2823            predicate_index,
2824        } => TermSpec::FirstAdmit {
2825            domain_size_index: *domain_size_index,
2826            predicate_index: *predicate_index,
2827        },
2828        TermSpec::FirstAdmitIdxPlaceholder => TermSpec::FirstAdmitIdxPlaceholder,
2829        TermSpec::RecurseIdxPlaceholder => TermSpec::RecurseIdxPlaceholder,
2830        TermSpec::Nerve { value_index } => TermSpec::Nerve {
2831            value_index: *value_index,
2832        },
2833        TermSpec::ChainComplex { simplicial_index } => TermSpec::ChainComplex {
2834            simplicial_index: *simplicial_index,
2835        },
2836        TermSpec::HomologyGroups { chain_index } => TermSpec::HomologyGroups {
2837            chain_index: *chain_index,
2838        },
2839        TermSpec::Betti { homology_index } => TermSpec::Betti {
2840            homology_index: *homology_index,
2841        },
2842        TermSpec::CochainComplex { chain_index } => TermSpec::CochainComplex {
2843            chain_index: *chain_index,
2844        },
2845        TermSpec::CohomologyGroups { cochain_index } => TermSpec::CohomologyGroups {
2846            cochain_index: *cochain_index,
2847        },
2848        TermSpec::PostnikovTower { simplicial_index } => TermSpec::PostnikovTower {
2849            simplicial_index: *simplicial_index,
2850        },
2851        TermSpec::HomotopyGroups { postnikov_index } => TermSpec::HomotopyGroups {
2852            postnikov_index: *postnikov_index,
2853        },
2854        TermSpec::KInvariants { homotopy_index } => TermSpec::KInvariants {
2855            homotopy_index: *homotopy_index,
2856        },
2857    }
2858}
2859
2860/// Handle a `{ <stmts>; <tail_expr> }` block — wiki ADR-022 D3 G10 + G11.
2861/// `let` statements bind identifiers to the `let`'s value-tree root; the
2862/// final expression is the block's value.
2863fn emit_term_for_block(
2864    block: &syn::Block,
2865    route_input: &Ident,
2866    arena: &mut Vec<TermSpec>,
2867    scope: &mut BindingScope,
2868) -> Result<usize> {
2869    if block.stmts.is_empty() {
2870        return Err(syn::Error::new_spanned(
2871            block,
2872            "closure violation: block expressions must contain at least one statement (G11) — empty blocks are unreachable in the closure-body grammar",
2873        ));
2874    }
2875    let mut local_scope = scope.clone();
2876    let last = block.stmts.len() - 1;
2877    for (i, stmt) in block.stmts.iter().enumerate() {
2878        match stmt {
2879            syn::Stmt::Local(local) => {
2880                if i == last {
2881                    return Err(syn::Error::new_spanned(
2882                        stmt,
2883                        "closure violation: block must end with an expression statement (G11), not a `let` binding",
2884                    ));
2885                }
2886                let ident = match &local.pat {
2887                    syn::Pat::Ident(pat_ident) => {
2888                        if pat_ident.by_ref.is_some() || pat_ident.mutability.is_some() {
2889                            return Err(syn::Error::new_spanned(
2890                                pat_ident,
2891                                "closure violation: `let` binding patterns must be plain identifiers (no `ref`, no `mut`) per ADR-022 D3 G10",
2892                            ));
2893                        }
2894                        if pat_ident.subpat.is_some() {
2895                            return Err(syn::Error::new_spanned(
2896                                pat_ident,
2897                                "closure violation: `let` binding patterns must be plain identifiers per ADR-022 D3 G10",
2898                            ));
2899                        }
2900                        pat_ident.ident.clone()
2901                    }
2902                    other => {
2903                        return Err(syn::Error::new_spanned(
2904                            other,
2905                            "closure violation: `let` binding patterns must be plain identifiers per ADR-022 D3 G10",
2906                        ));
2907                    }
2908                };
2909                local_scope.shadow_check(&ident)?;
2910                let init = local.init.as_ref().ok_or_else(|| {
2911                    syn::Error::new_spanned(
2912                        local,
2913                        "closure violation: `let` bindings must have an initializer (`let <name> = <expr>;`)",
2914                    )
2915                })?;
2916                if init.diverge.is_some() {
2917                    return Err(syn::Error::new_spanned(
2918                        local,
2919                        "closure violation: `let ... else` is not in the closure-body grammar (G10)",
2920                    ));
2921                }
2922                let value_root =
2923                    emit_term_for_expr(&init.expr, route_input, arena, &mut local_scope)?;
2924                local_scope.push(ident, value_root);
2925            }
2926            syn::Stmt::Expr(inner, semi) => {
2927                if i == last {
2928                    if semi.is_some() {
2929                        return Err(syn::Error::new_spanned(
2930                            stmt,
2931                            "closure violation: block must end with a tail expression (G11), no trailing `;`",
2932                        ));
2933                    }
2934                    return emit_term_for_expr(inner, route_input, arena, &mut local_scope);
2935                }
2936                return Err(syn::Error::new_spanned(
2937                    stmt,
2938                    "closure violation: only `let` statements may precede the block's tail expression (G10/G11)",
2939                ));
2940            }
2941            other => {
2942                return Err(syn::Error::new_spanned(
2943                    other,
2944                    "closure violation: block statement is not in the closure-body grammar (G10/G11 admits only `let` and a final expression)",
2945                ));
2946            }
2947        }
2948    }
2949    // Unreachable in well-typed flow — the loop always returns or errors.
2950    Err(syn::Error::new_spanned(
2951        block,
2952        "closure violation: block lacks a tail expression (G11)",
2953    ))
2954}
2955
2956/// Map a function-call expression to a `Term::Application` (G3),
2957/// `Term::Lift` (G4), `Term::Project` (G5), `Term::AxisInvocation`
2958/// (G19, ADR-030 — replaces the legacy `HasherProjection`),
2959/// `Term::Recurse` (G7), `Term::Unfold` (G8), or — for
2960/// non-reserved identifiers — a `TermSpec::VerbSplice` that the
2961/// `prism_model!` const-fn arena builder inlines at compile time per
2962/// ADR-024. Rejects anything else as a closure violation.
2963fn emit_term_for_call(
2964    call: &syn::ExprCall,
2965    route_input: &Ident,
2966    arena: &mut Vec<TermSpec>,
2967    scope: &mut BindingScope,
2968) -> Result<usize> {
2969    // ADR-022 D3 G4 / G5: `lift::<W{n}>(operand)` and `project::<W{n}>(operand)`
2970    // — the call target is a path with a generic Witt-level argument.
2971    if let syn::Expr::Path(path_expr) = call.func.as_ref() {
2972        let segments = &path_expr.path.segments;
2973        if segments.len() == 1 {
2974            let segment = &segments[0];
2975            let last_ident = &segment.ident;
2976            if last_ident == "lift" || last_ident == "project" {
2977                let target_witt = match &segment.arguments {
2978                    syn::PathArguments::AngleBracketed(args) if args.args.len() == 1 => {
2979                        match &args.args[0] {
2980                            syn::GenericArgument::Type(syn::Type::Path(tp)) => tp.path.clone(),
2981                            other => {
2982                                return Err(syn::Error::new_spanned(
2983                                    other,
2984                                    "closure violation: lift/project's generic argument must be a Witt-level type (e.g., `WittLevel::W32`)",
2985                                ));
2986                            }
2987                        }
2988                    }
2989                    _ => {
2990                        return Err(syn::Error::new_spanned(
2991                            segment,
2992                            format!(
2993                                "closure violation: `{last_ident}` requires a generic Witt-level argument: `{last_ident}::<WittLevel::W{{n}}>(operand)`"
2994                            ),
2995                        ));
2996                    }
2997                };
2998                if call.args.len() != 1 {
2999                    return Err(syn::Error::new(
3000                        last_ident.span(),
3001                        format!(
3002                            "closure violation: `{last_ident}` (G4/G5) expects 1 argument, got {}",
3003                            call.args.len()
3004                        ),
3005                    ));
3006                }
3007                let operand_root = emit_term_for_expr(&call.args[0], route_input, arena, scope)?;
3008                let target_witt_ts = quote! { #target_witt };
3009                let idx = arena.len();
3010                let spec = if last_ident == "lift" {
3011                    TermSpec::Lift {
3012                        operand_index: operand_root as u32,
3013                        target_witt: target_witt_ts,
3014                    }
3015                } else {
3016                    TermSpec::Project {
3017                        operand_index: operand_root as u32,
3018                        target_witt: target_witt_ts,
3019                    }
3020                };
3021                arena.push(spec);
3022                return Ok(idx);
3023            }
3024        }
3025    }
3026
3027    // All other call shapes require a bare identifier callee.
3028    let func_ident = match call.func.as_ref() {
3029        syn::Expr::Path(p) => p.path.get_ident().cloned().ok_or_else(|| {
3030            syn::Error::new_spanned(
3031                &call.func,
3032                "closure violation: call target must be a bare identifier matching a PrimitiveOp name, the `hash` verb form, or a declared verb identifier",
3033            )
3034        })?,
3035        other => {
3036            return Err(syn::Error::new_spanned(
3037                other,
3038                "closure violation: call target must be a bare identifier",
3039            ));
3040        }
3041    };
3042
3043    // ADR-024 verb invocation: any non-reserved, non-PrimitiveOp
3044    // identifier in call position is treated as a verb call. The macro
3045    // records a `TermSpec::VerbSplice` referencing `VERB_TERMS_<NAME>`;
3046    // when the route emits its arena, `render_const_fn_arena_builder`
3047    // inlines the verb's fragment at compile time via foundation's
3048    // `inline_verb_fragment` const-fn helper. Rust's name resolution
3049    // surfaces "cannot find value" at the verb-call span if the
3050    // referenced const isn't in scope.
3051    // ADR-053: `mod` is a Rust keyword so closure-body authors write
3052    // `r#mod(a, b)`; `syn::Ident::to_string()` on a raw ident includes
3053    // the `r#` prefix, so we normalize before matching.
3054    let unraw_name = func_ident.unraw().to_string();
3055    let verb_resolution = match unraw_name.as_str() {
3056        // Exclude all reserved + PrimitiveOp identifiers from verb
3057        // resolution; these have dedicated handling below.
3058        "add" | "sub" | "mul" | "xor" | "and" | "or" | "neg" | "bnot" | "succ" | "pred"
3059        // ADR-053: Div/Mod/Pow ring-axis completion + Concat byte-packing.
3060        | "div" | "mod" | "pow"
3061        | "hash" | "parallel" | "fold_n" | "tree_fold" | "first_admit"
3062        | "recurse" | "unfold" | "concat"
3063        // ADR-051: wide-value literal carriers (Dependency 2).
3064        | "literal_u64" | "literal_bytes"
3065        // ADR-035 G21..G29 ψ-chain identifiers.
3066        | "nerve" | "chain_complex" | "homology_groups" | "betti" | "cochain_complex"
3067        | "cohomology_groups" | "postnikov_tower" | "homotopy_groups" | "k_invariants" => false,
3068        _ => true,
3069    };
3070    if verb_resolution {
3071        if call.args.len() != 1 {
3072            return Err(syn::Error::new(
3073                func_ident.span(),
3074                format!(
3075                    "verb invocation `{}` expects 1 argument (the verb's input value), got {}",
3076                    func_ident,
3077                    call.args.len()
3078                ),
3079            ));
3080        }
3081        let arg_root = emit_term_for_expr(&call.args[0], route_input, arena, scope)?;
3082        let const_name = Ident::new(
3083            &format!("VERB_TERMS_{}", to_screaming_snake(&func_ident.to_string())),
3084            func_ident.span(),
3085        );
3086        let fragment_path = quote! { #const_name };
3087        let idx = arena.len();
3088        arena.push(TermSpec::VerbSplice {
3089            arg_root_idx: arg_root as u32,
3090            fragment_path,
3091        });
3092        return Ok(idx);
3093    }
3094
3095    // ADR-026 G14: `fold_n(n, init, |state, idx| step)`. Lowers to an
3096    // unrolled `Term::Application`-style chain when `n` is a const
3097    // literal at or below `pipeline::FOLD_UNROLL_THRESHOLD`; lowers to
3098    // `Term::Recurse` otherwise.
3099    if func_ident == "fold_n" {
3100        if call.args.len() != 3 {
3101            return Err(syn::Error::new(
3102                func_ident.span(),
3103                format!(
3104                    "closure violation: `fold_n` (G14) expects 3 arguments (count, init, step closure), got {}",
3105                    call.args.len()
3106                ),
3107            ));
3108        }
3109        // The count must be a const expression. The macro recognises
3110        // const integer literals (the unroll path) and any other
3111        // expression (the Term::Recurse path, with the count's tree as
3112        // the descent measure).
3113        let count_lit: Option<u64> = match &call.args[0] {
3114            syn::Expr::Lit(syn::ExprLit {
3115                lit: syn::Lit::Int(int_lit),
3116                ..
3117            }) => int_lit.base10_parse::<u64>().ok(),
3118            _ => None,
3119        };
3120        let init_root = emit_term_for_expr(&call.args[1], route_input, arena, scope)?;
3121        let step_closure = match &call.args[2] {
3122            syn::Expr::Closure(c) => c,
3123            other => {
3124                return Err(syn::Error::new_spanned(
3125                    other,
3126                    "closure violation: `fold_n`'s third argument must be a closure `|state, idx| <step_expr>` (G14)",
3127                ));
3128            }
3129        };
3130        if step_closure.inputs.len() != 2 {
3131            return Err(syn::Error::new_spanned(
3132                step_closure,
3133                "closure violation: `fold_n`'s step closure expects exactly 2 parameters (state, idx) per G14",
3134            ));
3135        }
3136        let state_ident = match &step_closure.inputs[0] {
3137            syn::Pat::Ident(p) => p.ident.clone(),
3138            other => {
3139                return Err(syn::Error::new_spanned(
3140                    other,
3141                    "closure violation: `fold_n`'s state parameter must be a plain identifier (G14)",
3142                ));
3143            }
3144        };
3145        let idx_ident = match &step_closure.inputs[1] {
3146            syn::Pat::Ident(p) => p.ident.clone(),
3147            other => {
3148                return Err(syn::Error::new_spanned(
3149                    other,
3150                    "closure violation: `fold_n`'s idx parameter must be a plain identifier (G14)",
3151                ));
3152            }
3153        };
3154        // Unroll path: const literal at or below threshold.
3155        const FOLD_UNROLL_THRESHOLD: u64 = 8;
3156        if let Some(n) = count_lit {
3157            if n <= FOLD_UNROLL_THRESHOLD {
3158                let mut state_root = init_root;
3159                for i in 0..n {
3160                    let idx_root = arena.len();
3161                    arena.push(TermSpec::Literal(i));
3162                    let mut iter_scope = scope.clone();
3163                    iter_scope.shadow_check(&state_ident)?;
3164                    iter_scope.shadow_check(&idx_ident)?;
3165                    iter_scope.push(state_ident.clone(), state_root);
3166                    iter_scope.push(idx_ident.clone(), idx_root);
3167                    state_root = emit_term_for_expr(
3168                        &step_closure.body,
3169                        route_input,
3170                        arena,
3171                        &mut iter_scope,
3172                    )?;
3173                }
3174                return Ok(state_root);
3175            }
3176        }
3177        // Recurse path: count is parametric or exceeds the threshold.
3178        // Lower to Term::Recurse with the count's tree as descent measure,
3179        // init as base, and the step body as the step subtree per
3180        // ADR-029. State binds to the RecursePlaceholder Variable so the
3181        // catamorphism's recurse_value threading resolves it.
3182        let measure_root = emit_term_for_expr(&call.args[0], route_input, arena, scope)?;
3183        let mut step_scope = scope.clone();
3184        step_scope.shadow_check(&state_ident)?;
3185        step_scope.shadow_check(&idx_ident)?;
3186        let placeholder_idx = arena.len();
3187        arena.push(TermSpec::RecursePlaceholder);
3188        step_scope.push(state_ident, placeholder_idx);
3189        step_scope.push(idx_ident, measure_root);
3190        let step_root =
3191            emit_term_for_expr(&step_closure.body, route_input, arena, &mut step_scope)?;
3192        let idx = arena.len();
3193        arena.push(TermSpec::Recurse {
3194            measure_index: measure_root as u32,
3195            base_index: init_root as u32,
3196            step_index: step_root as u32,
3197        });
3198        return Ok(idx);
3199    }
3200
3201    // ADR-022 D3 G7: `recurse(measure, base, |self_ident| step)`.
3202    if func_ident == "recurse" {
3203        if call.args.len() != 3 {
3204            return Err(syn::Error::new(
3205                func_ident.span(),
3206                format!(
3207                    "closure violation: `recurse` (G7) expects 3 arguments (measure, base, step closure), got {}",
3208                    call.args.len()
3209                ),
3210            ));
3211        }
3212        let measure_root = emit_term_for_expr(&call.args[0], route_input, arena, scope)?;
3213        let base_root = emit_term_for_expr(&call.args[1], route_input, arena, scope)?;
3214        // Third arg is a closure `|self_ident| step`.
3215        let step_closure = match &call.args[2] {
3216            syn::Expr::Closure(c) => c,
3217            other => {
3218                return Err(syn::Error::new_spanned(
3219                    other,
3220                    "closure violation: `recurse`'s third argument must be a closure `|self_ident| <step_expr>` (G7)",
3221                ));
3222            }
3223        };
3224        // ADR-022 D3 G7 + ADR-034 Mechanism 1: the step closure admits
3225        // either 1 parameter (`|self_ident|`, the recursive-call
3226        // placeholder) or 2 parameters (`|self_ident, idx_ident|`, the
3227        // recursive-call placeholder + the iteration counter).
3228        if step_closure.inputs.is_empty() || step_closure.inputs.len() > 2 {
3229            return Err(syn::Error::new_spanned(
3230                step_closure,
3231                "closure violation: `recurse`'s step closure expects 1 parameter (`|self_ident| <step>` per G7) or 2 parameters (`|self_ident, idx_ident| <step>` per ADR-034 Mechanism 1)",
3232            ));
3233        }
3234        let self_ident = match &step_closure.inputs[0] {
3235            syn::Pat::Ident(p) => p.ident.clone(),
3236            other => {
3237                return Err(syn::Error::new_spanned(
3238                    other,
3239                    "closure violation: `recurse`'s step parameter must be a plain identifier (G7)",
3240                ));
3241            }
3242        };
3243        let idx_ident_opt: Option<Ident> = if step_closure.inputs.len() == 2 {
3244            match &step_closure.inputs[1] {
3245                syn::Pat::Ident(p) => Some(p.ident.clone()),
3246                other => {
3247                    return Err(syn::Error::new_spanned(
3248                        other,
3249                        "closure violation: `recurse`'s iteration-counter parameter must be a plain identifier (ADR-034 Mechanism 1)",
3250                    ));
3251                }
3252            }
3253        } else {
3254            None
3255        };
3256        // Wiki ADR-029: emit a `Term::Variable { name_index =
3257        // RECURSE_PLACEHOLDER_NAME_INDEX }` BEFORE the step body and
3258        // bind `self_ident` to that Variable's arena index. ADR-034 M1:
3259        // when the two-parameter form is used, also emit a
3260        // `Term::Variable { name_index = RECURSE_IDX_NAME_INDEX }`
3261        // placeholder and bind `idx_ident` to it.
3262        let mut step_scope = scope.clone();
3263        step_scope.shadow_check(&self_ident)?;
3264        let self_placeholder_idx = arena.len();
3265        arena.push(TermSpec::RecursePlaceholder);
3266        step_scope.push(self_ident, self_placeholder_idx);
3267        if let Some(idx_ident) = idx_ident_opt {
3268            step_scope.shadow_check(&idx_ident)?;
3269            let idx_placeholder_idx = arena.len();
3270            arena.push(TermSpec::RecurseIdxPlaceholder);
3271            step_scope.push(idx_ident, idx_placeholder_idx);
3272        }
3273        let step_root =
3274            emit_term_for_expr(&step_closure.body, route_input, arena, &mut step_scope)?;
3275        let idx = arena.len();
3276        arena.push(TermSpec::Recurse {
3277            measure_index: measure_root as u32,
3278            base_index: base_root as u32,
3279            step_index: step_root as u32,
3280        });
3281        return Ok(idx);
3282    }
3283
3284    // ADR-022 D3 G8: `unfold(seed, |state_ident| step)`.
3285    if func_ident == "unfold" {
3286        if call.args.len() != 2 {
3287            return Err(syn::Error::new(
3288                func_ident.span(),
3289                format!(
3290                    "closure violation: `unfold` (G8) expects 2 arguments (seed, step closure), got {}",
3291                    call.args.len()
3292                ),
3293            ));
3294        }
3295        let seed_root = emit_term_for_expr(&call.args[0], route_input, arena, scope)?;
3296        let step_closure = match &call.args[1] {
3297            syn::Expr::Closure(c) => c,
3298            other => {
3299                return Err(syn::Error::new_spanned(
3300                    other,
3301                    "closure violation: `unfold`'s second argument must be a closure `|state_ident| <step_expr>` (G8)",
3302                ));
3303            }
3304        };
3305        if step_closure.inputs.len() != 1 {
3306            return Err(syn::Error::new_spanned(
3307                step_closure,
3308                "closure violation: `unfold`'s step closure expects exactly 1 parameter (the state placeholder, G8)",
3309            ));
3310        }
3311        let state_ident = match &step_closure.inputs[0] {
3312            syn::Pat::Ident(p) => p.ident.clone(),
3313            other => {
3314                return Err(syn::Error::new_spanned(
3315                    other,
3316                    "closure violation: `unfold`'s step parameter must be a plain identifier (G8)",
3317                ));
3318            }
3319        };
3320        // ADR-029 anamorphism: emit a `Variable { name_index =
3321        // UNFOLD_PLACEHOLDER_NAME_INDEX }` BEFORE the step body and bind
3322        // state_ident to that placeholder's arena index, so any reference
3323        // to state_ident inside step lowers to the placeholder Variable.
3324        // The catamorphism's Term::Unfold fold-rule iterates step with
3325        // the placeholder bound to the current accumulated state until a
3326        // Kleene fixpoint or UNFOLD_MAX_ITERATIONS.
3327        let mut step_scope = scope.clone();
3328        step_scope.shadow_check(&state_ident)?;
3329        let placeholder_idx = arena.len();
3330        arena.push(TermSpec::UnfoldPlaceholder);
3331        step_scope.push(state_ident, placeholder_idx);
3332        let step_root =
3333            emit_term_for_expr(&step_closure.body, route_input, arena, &mut step_scope)?;
3334        let idx = arena.len();
3335        arena.push(TermSpec::Unfold {
3336            seed_index: seed_root as u32,
3337            step_index: step_root as u32,
3338        });
3339        return Ok(idx);
3340    }
3341
3342    // ADR-026 G13: `parallel(f, g)` produces the parallel-composed
3343    // route. The result's term tree is the partition-product of f's
3344    // and g's term trees: each operand's subtree is emitted, and the
3345    // composite is realised as a binary `Term::Application` whose
3346    // operator is the structural-combine `Or` (the foundation-default
3347    // partition-product byte combiner per the ten-Term-variant
3348    // commitment of ADR-029; implementations override the runtime per
3349    // ADR-024's three-way split if they need parallel-execution
3350    // semantics). The macro recognises `parallel(f, g)` so the verb-
3351    // closure check + the operator set's closure both hold.
3352    if func_ident == "parallel" {
3353        if call.args.len() != 2 {
3354            return Err(syn::Error::new(
3355                func_ident.span(),
3356                format!(
3357                    "closure violation: `parallel` (G13) expects 2 routes (left, right), got {}",
3358                    call.args.len()
3359                ),
3360            ));
3361        }
3362        let lhs_root = emit_term_for_expr(&call.args[0], route_input, arena, scope)?;
3363        let rhs_root = emit_term_for_expr(&call.args[1], route_input, arena, scope)?;
3364        // The args block must be contiguous in the arena per ADR-022 D2.
3365        // Build a contiguous duplicate block at the end if the operands
3366        // aren't already adjacent.
3367        let already_contiguous = rhs_root == lhs_root + 1;
3368        let (args_start, args_len) = if already_contiguous {
3369            (lhs_root as u32, 2u32)
3370        } else {
3371            let start = arena.len();
3372            let lhs_dup = clone_term_spec(&arena[lhs_root]);
3373            arena.push(lhs_dup);
3374            let rhs_dup = clone_term_spec(&arena[rhs_root]);
3375            arena.push(rhs_dup);
3376            (start as u32, 2u32)
3377        };
3378        let idx = arena.len();
3379        arena.push(TermSpec::Application {
3380            operator: quote! { ::uor_foundation::PrimitiveOp::Or },
3381            args_start,
3382            args_len,
3383        });
3384        return Ok(idx);
3385    }
3386
3387    // ADR-026 G15: `tree_fold(reducer, [a, b, c, …])` lowers to a
3388    // pairwise reduction chain — at each level, the reducer is applied
3389    // to adjacent operand pairs, halving the count. For a power-of-two
3390    // count `n`, the output is a balanced tree of depth `log2(n)`. For
3391    // odd levels, the unpaired leaf is carried forward. The reducer is
3392    // a binary identifier (PrimitiveOp or verb).
3393    if func_ident == "tree_fold" {
3394        if call.args.len() != 2 {
3395            return Err(syn::Error::new(
3396                func_ident.span(),
3397                format!(
3398                    "closure violation: `tree_fold` (G15) expects 2 arguments (reducer, leaves array), got {}",
3399                    call.args.len()
3400                ),
3401            ));
3402        }
3403        // The reducer is an identifier (PrimitiveOp like `add` or a verb).
3404        let reducer_ident = match &call.args[0] {
3405            syn::Expr::Path(p) => p.path.get_ident().cloned().ok_or_else(|| {
3406                syn::Error::new_spanned(
3407                    &call.args[0],
3408                    "closure violation: `tree_fold`'s reducer must be a bare identifier (PrimitiveOp or verb)",
3409                )
3410            })?,
3411            other => {
3412                return Err(syn::Error::new_spanned(
3413                    other,
3414                    "closure violation: `tree_fold`'s reducer must be a bare identifier",
3415                ));
3416            }
3417        };
3418        // The leaves are an array literal `[expr, expr, …]`.
3419        let leaves_array = match &call.args[1] {
3420            syn::Expr::Array(a) => a,
3421            other => {
3422                return Err(syn::Error::new_spanned(
3423                    other,
3424                    "closure violation: `tree_fold`'s leaves must be an array literal `[a, b, c, …]`",
3425                ));
3426            }
3427        };
3428        if leaves_array.elems.is_empty() {
3429            return Err(syn::Error::new_spanned(
3430                leaves_array,
3431                "closure violation: `tree_fold`'s leaves array must be non-empty (G15)",
3432            ));
3433        }
3434        // Emit each leaf's subtree, recording its root index.
3435        let mut current_level: Vec<usize> = Vec::with_capacity(leaves_array.elems.len());
3436        for leaf_expr in &leaves_array.elems {
3437            current_level.push(emit_term_for_expr(leaf_expr, route_input, arena, scope)?);
3438        }
3439        // The reducer is rendered via emit_term_for_call's path (via a
3440        // synthesized two-arg call) so the same identifier-resolution
3441        // applies (PrimitiveOp / verb / closure violation).
3442        let reducer_op_or_verb = match reducer_ident.unraw().to_string().as_str() {
3443            "add" => Some(quote! { ::uor_foundation::PrimitiveOp::Add }),
3444            "sub" => Some(quote! { ::uor_foundation::PrimitiveOp::Sub }),
3445            "mul" => Some(quote! { ::uor_foundation::PrimitiveOp::Mul }),
3446            "xor" => Some(quote! { ::uor_foundation::PrimitiveOp::Xor }),
3447            "and" => Some(quote! { ::uor_foundation::PrimitiveOp::And }),
3448            "or" => Some(quote! { ::uor_foundation::PrimitiveOp::Or }),
3449            // ADR-053 ring-axis completion as tree_fold reducers.
3450            "div" => Some(quote! { ::uor_foundation::PrimitiveOp::Div }),
3451            "mod" => Some(quote! { ::uor_foundation::PrimitiveOp::Mod }),
3452            "pow" => Some(quote! { ::uor_foundation::PrimitiveOp::Pow }),
3453            _ => None,
3454        };
3455        // Pairwise reduction: at each level, fold adjacent pairs.
3456        while current_level.len() > 1 {
3457            let mut next_level: Vec<usize> = Vec::with_capacity(current_level.len().div_ceil(2));
3458            let mut i = 0;
3459            while i + 1 < current_level.len() {
3460                let l_idx = current_level[i];
3461                let r_idx = current_level[i + 1];
3462                // Build the reducer application; args must be contiguous.
3463                let already_contiguous = r_idx == l_idx + 1;
3464                let (args_start, args_len) = if already_contiguous {
3465                    (l_idx as u32, 2u32)
3466                } else {
3467                    let start = arena.len();
3468                    let l_dup = clone_term_spec(&arena[l_idx]);
3469                    arena.push(l_dup);
3470                    let r_dup = clone_term_spec(&arena[r_idx]);
3471                    arena.push(r_dup);
3472                    (start as u32, 2u32)
3473                };
3474                let app_idx = arena.len();
3475                if let Some(op) = &reducer_op_or_verb {
3476                    arena.push(TermSpec::Application {
3477                        operator: op.clone(),
3478                        args_start,
3479                        args_len,
3480                    });
3481                } else {
3482                    // Verb-style reducer: emit a VerbSplice. The verb's
3483                    // term-tree fragment will be inlined at compile time.
3484                    // The verb call takes the LEFT operand as its arg per
3485                    // ADR-024's substitution semantics; the right operand
3486                    // is fed via a wrapping Application (Or as combine).
3487                    // For tree_fold over verbs, the verb must be unary; if
3488                    // used here as binary, we wrap via Or.
3489                    let const_name = Ident::new(
3490                        &format!(
3491                            "VERB_TERMS_{}",
3492                            to_screaming_snake(&reducer_ident.to_string())
3493                        ),
3494                        reducer_ident.span(),
3495                    );
3496                    let fragment_path = quote! { #const_name };
3497                    arena.push(TermSpec::VerbSplice {
3498                        arg_root_idx: l_idx as u32,
3499                        fragment_path,
3500                    });
3501                    let _ = (args_start, args_len, r_idx);
3502                }
3503                next_level.push(app_idx);
3504                i += 2;
3505            }
3506            // Carry an unpaired final leaf to the next level.
3507            if i < current_level.len() {
3508                next_level.push(current_level[i]);
3509            }
3510            current_level = next_level;
3511        }
3512        return Ok(current_level[0]);
3513    }
3514
3515    // ADR-051 wide-Witt literal embedding (Dependency 2 in v0.4.10):
3516    // `literal_u64(<value>, <level>)` lowers to `TermSpec::LiteralExpr`
3517    // which renders to a const-evaluated `pipeline::literal_u64(...)`
3518    // call producing a `Term::Literal { value: TermValue, level }` at
3519    // the consumer's compile time. The u64 form is sufficient for
3520    // widths up to W64; the level argument controls the byte-width of
3521    // the resulting TermValue.
3522    if unraw_name == "literal_u64" {
3523        if call.args.len() != 2 {
3524            return Err(syn::Error::new(
3525                func_ident.span(),
3526                format!(
3527                    "`literal_u64(<value>, <level>)` expects 2 arguments, got {}",
3528                    call.args.len()
3529                ),
3530            ));
3531        }
3532        let value_tokens = quote::ToTokens::to_token_stream(&call.args[0]);
3533        let level_tokens = quote::ToTokens::to_token_stream(&call.args[1]);
3534        let idx = arena.len();
3535        arena.push(TermSpec::LiteralExpr {
3536            value: value_tokens,
3537            level: level_tokens,
3538        });
3539        return Ok(idx);
3540    }
3541
3542    // ADR-051 wide-Witt literal embedding (Dependency 2 in v0.4.10):
3543    // `literal_bytes(<bytes>, <level>)` lowers to `TermSpec::LiteralBytesExpr`
3544    // which renders to a const-evaluated `pipeline::literal_bytes(...)`
3545    // call producing a `Term::Literal { value: TermValue, level }` whose
3546    // TermValue carrier holds the full byte sequence. Required for wide
3547    // Witt-level literals (W128+) that don't fit in a u64 — secp256k1
3548    // P_LITERAL (W256), AES round constants (W128), FHE plaintext-
3549    // coefficient tables, etc.
3550    if unraw_name == "literal_bytes" {
3551        if call.args.len() != 2 {
3552            return Err(syn::Error::new(
3553                func_ident.span(),
3554                format!(
3555                    "`literal_bytes(<bytes>, <level>)` expects 2 arguments, got {}",
3556                    call.args.len()
3557                ),
3558            ));
3559        }
3560        let bytes_tokens = quote::ToTokens::to_token_stream(&call.args[0]);
3561        let level_tokens = quote::ToTokens::to_token_stream(&call.args[1]);
3562        let idx = arena.len();
3563        arena.push(TermSpec::LiteralBytesExpr {
3564            bytes: bytes_tokens,
3565            level: level_tokens,
3566        });
3567        return Ok(idx);
3568    }
3569
3570    // ADR-035 ψ-residuals discipline scoped per ADR-056: `first_admit`
3571    // lowers to `Term::FirstAdmit` — ψ-enumeration over a counter domain.
3572    // Per ADR-056 the rejection applies only when the syntactic emission
3573    // is in the route body (`in_route_body == true`); verb! bodies and
3574    // axis! body clauses may use `first_admit` for bounded search per
3575    // ADR-034 (its canonical use site).
3576    if func_ident == "first_admit" && scope.in_route_body {
3577        return reject_psi_residual_call(
3578            call,
3579            "first_admit(<domain>, |idx| <pred>)",
3580            "FirstAdmit",
3581            "The canonical compiled form is structural — admission is a \
3582             property of the value's k-invariant signature, not a search \
3583             predicate enumerated over a counter domain. Express the \
3584             admission relation through the ψ-chain (G21..G29): \
3585             e.g. `k_invariants(homotopy_groups(postnikov_tower(nerve(input))))` \
3586             produces the κ-label that classifies the input's homotopy type. \
3587             Per ADR-056 this restriction applies only to the route body's \
3588             syntactic surface; verb and axis impl bodies admit `first_admit` \
3589             directly.",
3590        );
3591    }
3592
3593    // ADR-056: `concat` is admissible in verb! / axis! bodies per the
3594    // ADR-035 scope refinement. In route bodies (in_route_body == true),
3595    // the rejection remains — admission predicates may not depend on
3596    // byte-level concatenation. In verb / axis body contexts, concat
3597    // emits `Term::Application { operator: PrimitiveOp::Concat, … }`
3598    // and is the canonical realization of SHA padding, HMAC composition,
3599    // Merkle tree internal-node combination, etc.
3600    if func_ident == "concat" {
3601        if scope.in_route_body {
3602            return reject_psi_residual_call(
3603                call,
3604                "concat(<lhs>, <rhs>)",
3605                "Application { operator: PrimitiveOp::Concat }",
3606                "The canonical compiled form is structural — admission is a \
3607                 property of the value's k-invariant signature, not a \
3608                 byte-shape predicate. If byte-concatenation is the input's \
3609                 structural decomposition, use `partition_product!` to \
3610                 declare the typed shape and `Term::ProjectField` (G20) to \
3611                 extract sub-byte ranges; pipe each component through the \
3612                 ψ-chain instead of byte-manipulating before admission. \
3613                 Per ADR-056 this restriction applies only to the route \
3614                 body's syntactic surface; verb and axis impl bodies admit \
3615                 `concat` directly.",
3616            );
3617        }
3618        // Verb / axis body: emit `Term::Application { operator: Concat }`.
3619        if call.args.len() != 2 {
3620            return Err(syn::Error::new(
3621                func_ident.span(),
3622                format!(
3623                    "PrimitiveOp `concat` expects 2 arguments (lhs, rhs), got {}",
3624                    call.args.len()
3625                ),
3626            ));
3627        }
3628        let lhs_root = emit_term_for_expr(&call.args[0], route_input, arena, scope)?;
3629        let rhs_root = emit_term_for_expr(&call.args[1], route_input, arena, scope)?;
3630        let already_contiguous = rhs_root == lhs_root + 1;
3631        let (args_start, args_len) = if already_contiguous {
3632            (lhs_root as u32, 2u32)
3633        } else {
3634            let start = arena.len();
3635            let lhs_dup = clone_term_spec(&arena[lhs_root]);
3636            arena.push(lhs_dup);
3637            let rhs_dup = clone_term_spec(&arena[rhs_root]);
3638            arena.push(rhs_dup);
3639            (start as u32, 2u32)
3640        };
3641        let idx = arena.len();
3642        arena.push(TermSpec::Application {
3643            operator: quote! { ::uor_foundation::PrimitiveOp::Concat },
3644            args_start,
3645            args_len,
3646        });
3647        return Ok(idx);
3648    }
3649
3650    // ADR-056: `hash(input)` lowers to `Term::AxisInvocation` (axis 0 =
3651    // canonical hash axis, kernel 0 = `fold_bytes` ∘ `finalize`). In
3652    // route bodies it's rejected (the route's surface composes ψ-chain
3653    // forms, not axis dispatch). In verb / axis bodies it's admissible
3654    // — canonical hash invocations are exactly how SHA, BLAKE3, etc.
3655    // expose themselves to compound operations like HMAC and Merkle.
3656    if func_ident == "hash" {
3657        if scope.in_route_body {
3658            return reject_psi_residual_call(
3659                call,
3660                "hash(<value>)",
3661                "AxisInvocation",
3662                "The canonical hash axis is consumed by resolvers and verb \
3663                 bodies, not directly by the route body (ADR-036 + ADR-056). \
3664                 Move the `hash(...)` call into a `NerveResolver` (or other \
3665                 resolver-trait impl) where the per-value content fingerprint \
3666                 is computed as part of the resolver's internal resolution \
3667                 semantics — or into a verb body, which admits `hash(...)` \
3668                 freely per ADR-056. The route body composes ψ-chain forms \
3669                 (G21..G29) over the input's structural decomposition.",
3670            );
3671        }
3672        // Verb / axis body: emit `Term::AxisInvocation { axis 0, kernel 0 }`.
3673        if call.args.len() != 1 {
3674            return Err(syn::Error::new(
3675                func_ident.span(),
3676                format!(
3677                    "`hash(<value>)` expects 1 argument, got {}",
3678                    call.args.len()
3679                ),
3680            ));
3681        }
3682        let value_root = emit_term_for_expr(&call.args[0], route_input, arena, scope)?;
3683        let idx = arena.len();
3684        arena.push(TermSpec::AxisInvocation {
3685            axis_index: 0,
3686            kernel_id: 0,
3687            input_index: value_root as u32,
3688        });
3689        return Ok(idx);
3690    }
3691
3692    // Wiki ADR-035 G21..G29: closure-body grammar identifiers for the
3693    // nine ψ-chain Term variants. Each takes a single operand whose
3694    // arena root becomes the corresponding `*_index` field. The
3695    // receiver-shape check (wiki ADR-035: G22 takes simplicial-complex,
3696    // G23/G25 take chain-complex, G24 takes homology-groups, G26 takes
3697    // cochain-complex, G27 takes simplicial-complex, G28 takes
3698    // Postnikov-tower, G29 takes homotopy-groups; G21 takes byte) is
3699    // enforced at proc-macro expansion by walking the operand TermSpec
3700    // and asserting its produced shape matches.
3701    let psi_chain: &[(&str, &str, PsiShape)] = &[
3702        ("nerve", "G21", PsiShape::Byte),
3703        ("chain_complex", "G22", PsiShape::SimplicialComplex),
3704        ("homology_groups", "G23", PsiShape::ChainComplex),
3705        ("betti", "G24", PsiShape::HomologyGroups),
3706        ("cochain_complex", "G25", PsiShape::ChainComplex),
3707        ("cohomology_groups", "G26", PsiShape::CochainComplex),
3708        ("postnikov_tower", "G27", PsiShape::SimplicialComplex),
3709        ("homotopy_groups", "G28", PsiShape::PostnikovTower),
3710        ("k_invariants", "G29", PsiShape::HomotopyGroups),
3711    ];
3712    for (name, grammar, expected_shape) in psi_chain {
3713        if func_ident == name {
3714            if call.args.len() != 1 {
3715                return Err(syn::Error::new(
3716                    func_ident.span(),
3717                    format!(
3718                        "closure violation: `{name}` (ADR-035 {grammar}) expects 1 argument, got {}",
3719                        call.args.len()
3720                    ),
3721                ));
3722            }
3723            let operand_root = emit_term_for_expr(&call.args[0], route_input, arena, scope)?;
3724            let operand_shape = term_spec_shape(&arena[operand_root]);
3725            if !psi_shape_compatible(*expected_shape, operand_shape) {
3726                return Err(syn::Error::new_spanned(
3727                    &call.args[0],
3728                    format!(
3729                        "receiver-shape violation (wiki ADR-035 {grammar}): `{name}` expects \
3730                         a {expected} receiver, but the operand produces a {actual} value. \
3731                         The ψ-chain stages compose along the ontology's identity maps \
3732                         (ψ_1 → ψ_2 → … → ψ_9); receiver-shape mismatches break the \
3733                         canonical compiled form's structural-witness chain.",
3734                        expected = expected_shape.describe(),
3735                        actual = operand_shape.describe(),
3736                    ),
3737                ));
3738            }
3739            let idx = arena.len();
3740            let spec = match *name {
3741                "nerve" => TermSpec::Nerve {
3742                    value_index: operand_root as u32,
3743                },
3744                "chain_complex" => TermSpec::ChainComplex {
3745                    simplicial_index: operand_root as u32,
3746                },
3747                "homology_groups" => TermSpec::HomologyGroups {
3748                    chain_index: operand_root as u32,
3749                },
3750                "betti" => TermSpec::Betti {
3751                    homology_index: operand_root as u32,
3752                },
3753                "cochain_complex" => TermSpec::CochainComplex {
3754                    chain_index: operand_root as u32,
3755                },
3756                "cohomology_groups" => TermSpec::CohomologyGroups {
3757                    cochain_index: operand_root as u32,
3758                },
3759                "postnikov_tower" => TermSpec::PostnikovTower {
3760                    simplicial_index: operand_root as u32,
3761                },
3762                "homotopy_groups" => TermSpec::HomotopyGroups {
3763                    postnikov_index: operand_root as u32,
3764                },
3765                "k_invariants" => TermSpec::KInvariants {
3766                    homotopy_index: operand_root as u32,
3767                },
3768                _ => unreachable!(),
3769            };
3770            arena.push(spec);
3771            return Ok(idx);
3772        }
3773    }
3774
3775    let (operator, expected_arity) = match func_ident.unraw().to_string().as_str() {
3776        "add" => (quote! { ::uor_foundation::PrimitiveOp::Add }, 2usize),
3777        "sub" => (quote! { ::uor_foundation::PrimitiveOp::Sub }, 2),
3778        "mul" => (quote! { ::uor_foundation::PrimitiveOp::Mul }, 2),
3779        "xor" => (quote! { ::uor_foundation::PrimitiveOp::Xor }, 2),
3780        "and" => (quote! { ::uor_foundation::PrimitiveOp::And }, 2),
3781        "or" => (quote! { ::uor_foundation::PrimitiveOp::Or }, 2),
3782        "neg" => (quote! { ::uor_foundation::PrimitiveOp::Neg }, 1),
3783        "bnot" => (quote! { ::uor_foundation::PrimitiveOp::Bnot }, 1),
3784        "succ" => (quote! { ::uor_foundation::PrimitiveOp::Succ }, 1),
3785        "pred" => (quote! { ::uor_foundation::PrimitiveOp::Pred }, 1),
3786        // ADR-053 ring-axis completion: Div/Mod/Pow as substrate primitives.
3787        // ADR-054 cites these in canonical body composition examples
3788        // (rotr decomposition, pad-and-finalize via Concat). Per ADR-053's
3789        // catalog the runtime catamorphism evaluates them as
3790        // folding-transformations over Z/(2^n)Z.
3791        //
3792        // Note: `mod` is a Rust keyword, so closure-body authors invoke it as
3793        // a raw identifier `r#mod(a, b)`; `Ident::unraw().to_string()`
3794        // returns "mod" which the arm matches.
3795        "div" => (quote! { ::uor_foundation::PrimitiveOp::Div }, 2),
3796        "mod" => (quote! { ::uor_foundation::PrimitiveOp::Mod }, 2),
3797        "pow" => (quote! { ::uor_foundation::PrimitiveOp::Pow }, 2),
3798        // ADR-026 reserved macro-vocabulary identifiers (G13–G18). The
3799        // closure-body lowering for these forms is specified
3800        // architecturally; the substrate-level macro recognises them so
3801        // they never fall through to the closure-violation branch as
3802        // unknown identifiers, but the structural lowering is owned by
3803        // the implementation (per ADR-024's three-way responsibility split
3804        // between substrate, prism, and implementation). Implementations
3805        // that need these forms supply their own SDK macros that desugar
3806        // into the substrate primitives; the substrate macro reserves the
3807        // identifiers so they cannot be re-used as `verb!` names.
3808        "partition_product" | "partition_coproduct" => {
3809            return Err(syn::Error::new(
3810                func_ident.span(),
3811                format!(
3812                    "closure violation: `{}` (ADR-026 G17/G18) is a type-level shape constructor — invoke it at item position via the named SDK form `partition_product!(<Name>, <A>, <B>)` or `partition_coproduct!(<Name>, <A>, <B>)`, then reference `<Name>` in `type Input` / `type Output`",
3813                    func_ident
3814                ),
3815            ));
3816        }
3817        other => {
3818            return Err(syn::Error::new(
3819                func_ident.span(),
3820                format!(
3821                    "closure violation: `{other}` is not a foundation PrimitiveOp (recognised: add, sub, mul, div, r#mod, pow, xor, and, or, neg, bnot, succ, pred, concat), nor an ADR-026 macro-vocabulary identifier (hash/parallel/fold_n/tree_fold/first_admit/recurse/unfold), nor a declared verb"
3822                ),
3823            ));
3824        }
3825    };
3826
3827    if call.args.len() != expected_arity {
3828        return Err(syn::Error::new(
3829            func_ident.span(),
3830            format!(
3831                "PrimitiveOp `{}` expects {} argument(s), got {}",
3832                func_ident,
3833                expected_arity,
3834                call.args.len()
3835            ),
3836        ));
3837    }
3838
3839    // Emit each arg's subtree and record its root index.
3840    let mut arg_root_indices: Vec<usize> = Vec::with_capacity(call.args.len());
3841    for arg in call.args.iter() {
3842        arg_root_indices.push(emit_term_for_expr(arg, route_input, arena, scope)?);
3843    }
3844
3845    // ADR-022 D2: the args block must be CONTIGUOUS in the arena. If
3846    // the arg roots already are (the common case for leaf args or for
3847    // the canonical post-order layout), use them directly. Otherwise
3848    // duplicate each arg's root term into a fresh contiguous block —
3849    // a duplicate carries the same `Term` value (same operator + same
3850    // args.start), so it is semantically identical.
3851    let already_contiguous = arg_root_indices.windows(2).all(|w| w[1] == w[0] + 1);
3852
3853    let (args_start, args_len) = if already_contiguous && !arg_root_indices.is_empty() {
3854        let len = arg_root_indices.len();
3855        let start = arg_root_indices[0];
3856        (start as u32, len as u32)
3857    } else {
3858        // Build a contiguous duplicate block at the end of the arena.
3859        let start = arena.len();
3860        for &idx in &arg_root_indices {
3861            // Clone the spec at idx into a new slot. Since TermSpec is
3862            // not Clone, hand-build a fresh entry referring to the same
3863            // contents. (Operators are token streams; clone via .clone()
3864            // on the field.)
3865            let dup = match &arena[idx] {
3866                TermSpec::Literal(v) => TermSpec::Literal(*v),
3867                TermSpec::Variable => TermSpec::Variable,
3868                TermSpec::Application {
3869                    operator,
3870                    args_start,
3871                    args_len,
3872                } => TermSpec::Application {
3873                    operator: operator.clone(),
3874                    args_start: *args_start,
3875                    args_len: *args_len,
3876                },
3877                TermSpec::AxisInvocation {
3878                    axis_index,
3879                    kernel_id,
3880                    input_index,
3881                } => TermSpec::AxisInvocation {
3882                    axis_index: *axis_index,
3883                    kernel_id: *kernel_id,
3884                    input_index: *input_index,
3885                },
3886                TermSpec::ProjectField {
3887                    source_index,
3888                    byte_offset,
3889                    byte_length,
3890                } => TermSpec::ProjectField {
3891                    source_index: *source_index,
3892                    byte_offset: byte_offset.clone(),
3893                    byte_length: byte_length.clone(),
3894                },
3895                TermSpec::LiteralExpr { value, level } => TermSpec::LiteralExpr {
3896                    value: value.clone(),
3897                    level: level.clone(),
3898                },
3899                TermSpec::LiteralBytesExpr { bytes, level } => TermSpec::LiteralBytesExpr {
3900                    bytes: bytes.clone(),
3901                    level: level.clone(),
3902                },
3903                TermSpec::VerbSplice {
3904                    arg_root_idx,
3905                    fragment_path,
3906                } => TermSpec::VerbSplice {
3907                    arg_root_idx: *arg_root_idx,
3908                    fragment_path: fragment_path.clone(),
3909                },
3910                TermSpec::Lift {
3911                    operand_index,
3912                    target_witt,
3913                } => TermSpec::Lift {
3914                    operand_index: *operand_index,
3915                    target_witt: target_witt.clone(),
3916                },
3917                TermSpec::Project {
3918                    operand_index,
3919                    target_witt,
3920                } => TermSpec::Project {
3921                    operand_index: *operand_index,
3922                    target_witt: target_witt.clone(),
3923                },
3924                TermSpec::Try { body_index } => TermSpec::Try {
3925                    body_index: *body_index,
3926                },
3927                TermSpec::Recurse {
3928                    measure_index,
3929                    base_index,
3930                    step_index,
3931                } => TermSpec::Recurse {
3932                    measure_index: *measure_index,
3933                    base_index: *base_index,
3934                    step_index: *step_index,
3935                },
3936                TermSpec::Unfold {
3937                    seed_index,
3938                    step_index,
3939                } => TermSpec::Unfold {
3940                    seed_index: *seed_index,
3941                    step_index: *step_index,
3942                },
3943                TermSpec::Match {
3944                    scrutinee_index,
3945                    arms_start,
3946                    arms_len,
3947                } => TermSpec::Match {
3948                    scrutinee_index: *scrutinee_index,
3949                    arms_start: *arms_start,
3950                    arms_len: *arms_len,
3951                },
3952                TermSpec::WildcardSentinel => TermSpec::WildcardSentinel,
3953                TermSpec::RecursePlaceholder => TermSpec::RecursePlaceholder,
3954                TermSpec::UnfoldPlaceholder => TermSpec::UnfoldPlaceholder,
3955                TermSpec::FirstAdmit {
3956                    domain_size_index,
3957                    predicate_index,
3958                } => TermSpec::FirstAdmit {
3959                    domain_size_index: *domain_size_index,
3960                    predicate_index: *predicate_index,
3961                },
3962                TermSpec::FirstAdmitIdxPlaceholder => TermSpec::FirstAdmitIdxPlaceholder,
3963                TermSpec::RecurseIdxPlaceholder => TermSpec::RecurseIdxPlaceholder,
3964                TermSpec::Nerve { value_index } => TermSpec::Nerve {
3965                    value_index: *value_index,
3966                },
3967                TermSpec::ChainComplex { simplicial_index } => TermSpec::ChainComplex {
3968                    simplicial_index: *simplicial_index,
3969                },
3970                TermSpec::HomologyGroups { chain_index } => TermSpec::HomologyGroups {
3971                    chain_index: *chain_index,
3972                },
3973                TermSpec::Betti { homology_index } => TermSpec::Betti {
3974                    homology_index: *homology_index,
3975                },
3976                TermSpec::CochainComplex { chain_index } => TermSpec::CochainComplex {
3977                    chain_index: *chain_index,
3978                },
3979                TermSpec::CohomologyGroups { cochain_index } => TermSpec::CohomologyGroups {
3980                    cochain_index: *cochain_index,
3981                },
3982                TermSpec::PostnikovTower { simplicial_index } => TermSpec::PostnikovTower {
3983                    simplicial_index: *simplicial_index,
3984                },
3985                TermSpec::HomotopyGroups { postnikov_index } => TermSpec::HomotopyGroups {
3986                    postnikov_index: *postnikov_index,
3987                },
3988                TermSpec::KInvariants { homotopy_index } => TermSpec::KInvariants {
3989                    homotopy_index: *homotopy_index,
3990                },
3991            };
3992            arena.push(dup);
3993        }
3994        (start as u32, arg_root_indices.len() as u32)
3995    };
3996
3997    let app_idx = arena.len();
3998    arena.push(TermSpec::Application {
3999        operator,
4000        args_start,
4001        args_len,
4002    });
4003    Ok(app_idx)
4004}
4005
4006/// Emit the macro-time-built term arena as a sequence of `Term::*`
4007/// constructor expressions, ready to splice into a `&'static [Term]`
4008/// const-array literal.
4009fn render_arena(arena: &[TermSpec]) -> Vec<proc_macro2::TokenStream> {
4010    arena
4011        .iter()
4012        .map(|spec| match spec {
4013            TermSpec::Literal(value) => quote! {
4014                ::uor_foundation::pipeline::literal_u64(
4015                    #value,
4016                    ::uor_foundation::WittLevel::W8,
4017                )
4018            },
4019            TermSpec::LiteralExpr { value, level } => quote! {
4020                ::uor_foundation::pipeline::literal_u64(
4021                    #value,
4022                    #level,
4023                )
4024            },
4025            TermSpec::LiteralBytesExpr { bytes, level } => quote! {
4026                ::uor_foundation::pipeline::literal_bytes(
4027                    #bytes,
4028                    #level,
4029                )
4030            },
4031            TermSpec::Variable => quote! {
4032                ::uor_foundation::enforcement::Term::Variable { name_index: 0u32 }
4033            },
4034            TermSpec::Application {
4035                operator,
4036                args_start,
4037                args_len,
4038            } => {
4039                let s = *args_start;
4040                let l = *args_len;
4041                quote! {
4042                    ::uor_foundation::enforcement::Term::Application {
4043                        operator: #operator,
4044                        args: ::uor_foundation::enforcement::TermList {
4045                            start: #s,
4046                            len: #l,
4047                        },
4048                    }
4049                }
4050            }
4051            TermSpec::AxisInvocation {
4052                axis_index,
4053                kernel_id,
4054                input_index,
4055            } => {
4056                let a = *axis_index;
4057                let k = *kernel_id;
4058                let i = *input_index;
4059                quote! {
4060                    ::uor_foundation::enforcement::Term::AxisInvocation {
4061                        axis_index: #a,
4062                        kernel_id: #k,
4063                        input_index: #i,
4064                    }
4065                }
4066            }
4067            TermSpec::ProjectField {
4068                source_index,
4069                byte_offset,
4070                byte_length,
4071            } => {
4072                let s = *source_index;
4073                quote! {
4074                    ::uor_foundation::enforcement::Term::ProjectField {
4075                        source_index: #s,
4076                        byte_offset: (#byte_offset) as u32,
4077                        byte_length: (#byte_length) as u32,
4078                    }
4079                }
4080            }
4081            TermSpec::VerbSplice { .. } => {
4082                // VerbSplice never reaches the slice-literal renderer:
4083                // when an arena contains a VerbSplice the caller falls
4084                // back to `render_const_fn_arena_builder` which inlines
4085                // the verb fragment via foundation's const-fn helper
4086                // `inline_verb_fragment` at const-eval time per
4087                // ADR-024. Reaching this branch indicates a logic bug
4088                // in the macro's emission selection.
4089                quote! {
4090                    compile_error!(
4091                        "internal error: VerbSplice reached the slice-literal renderer; \
4092                         render_const_fn_arena_builder should have been chosen"
4093                    )
4094                }
4095            }
4096            TermSpec::Lift {
4097                operand_index,
4098                target_witt,
4099            } => {
4100                let i = *operand_index;
4101                quote! {
4102                    ::uor_foundation::enforcement::Term::Lift {
4103                        operand_index: #i,
4104                        target: #target_witt,
4105                    }
4106                }
4107            }
4108            TermSpec::Project {
4109                operand_index,
4110                target_witt,
4111            } => {
4112                let i = *operand_index;
4113                quote! {
4114                    ::uor_foundation::enforcement::Term::Project {
4115                        operand_index: #i,
4116                        target: #target_witt,
4117                    }
4118                }
4119            }
4120            TermSpec::Try { body_index } => {
4121                let i = *body_index;
4122                quote! {
4123                    ::uor_foundation::enforcement::Term::Try {
4124                        body_index: #i,
4125                        handler_index: u32::MAX,
4126                    }
4127                }
4128            }
4129            TermSpec::Recurse {
4130                measure_index,
4131                base_index,
4132                step_index,
4133            } => {
4134                let m = *measure_index;
4135                let b = *base_index;
4136                let s = *step_index;
4137                quote! {
4138                    ::uor_foundation::enforcement::Term::Recurse {
4139                        measure_index: #m,
4140                        base_index: #b,
4141                        step_index: #s,
4142                    }
4143                }
4144            }
4145            TermSpec::Unfold {
4146                seed_index,
4147                step_index,
4148            } => {
4149                let s = *seed_index;
4150                let st = *step_index;
4151                quote! {
4152                    ::uor_foundation::enforcement::Term::Unfold {
4153                        seed_index: #s,
4154                        step_index: #st,
4155                    }
4156                }
4157            }
4158            TermSpec::Match {
4159                scrutinee_index,
4160                arms_start,
4161                arms_len,
4162            } => {
4163                let s = *scrutinee_index;
4164                let st = *arms_start;
4165                let l = *arms_len;
4166                quote! {
4167                    ::uor_foundation::enforcement::Term::Match {
4168                        scrutinee_index: #s,
4169                        arms: ::uor_foundation::enforcement::TermList {
4170                            start: #st,
4171                            len: #l,
4172                        },
4173                    }
4174                }
4175            }
4176            TermSpec::WildcardSentinel => quote! {
4177                ::uor_foundation::enforcement::Term::Variable {
4178                    name_index: u32::MAX,
4179                }
4180            },
4181            TermSpec::RecursePlaceholder => quote! {
4182                ::uor_foundation::enforcement::Term::Variable {
4183                    name_index: ::uor_foundation::pipeline::RECURSE_PLACEHOLDER_NAME_INDEX,
4184                }
4185            },
4186            TermSpec::UnfoldPlaceholder => quote! {
4187                ::uor_foundation::enforcement::Term::Variable {
4188                    name_index: ::uor_foundation::pipeline::UNFOLD_PLACEHOLDER_NAME_INDEX,
4189                }
4190            },
4191            TermSpec::FirstAdmit {
4192                domain_size_index,
4193                predicate_index,
4194            } => {
4195                let d = *domain_size_index;
4196                let p = *predicate_index;
4197                quote! {
4198                    ::uor_foundation::enforcement::Term::FirstAdmit {
4199                        domain_size_index: #d,
4200                        predicate_index: #p,
4201                    }
4202                }
4203            }
4204            TermSpec::FirstAdmitIdxPlaceholder => quote! {
4205                ::uor_foundation::enforcement::Term::Variable {
4206                    name_index: ::uor_foundation::pipeline::FIRST_ADMIT_IDX_NAME_INDEX,
4207                }
4208            },
4209            TermSpec::RecurseIdxPlaceholder => quote! {
4210                ::uor_foundation::enforcement::Term::Variable {
4211                    name_index: ::uor_foundation::pipeline::RECURSE_IDX_NAME_INDEX,
4212                }
4213            },
4214            TermSpec::Nerve { value_index } => {
4215                let v = *value_index;
4216                quote! {
4217                    ::uor_foundation::enforcement::Term::Nerve { value_index: #v }
4218                }
4219            }
4220            TermSpec::ChainComplex { simplicial_index } => {
4221                let s = *simplicial_index;
4222                quote! {
4223                    ::uor_foundation::enforcement::Term::ChainComplex { simplicial_index: #s }
4224                }
4225            }
4226            TermSpec::HomologyGroups { chain_index } => {
4227                let c = *chain_index;
4228                quote! {
4229                    ::uor_foundation::enforcement::Term::HomologyGroups { chain_index: #c }
4230                }
4231            }
4232            TermSpec::Betti { homology_index } => {
4233                let h = *homology_index;
4234                quote! {
4235                    ::uor_foundation::enforcement::Term::Betti { homology_index: #h }
4236                }
4237            }
4238            TermSpec::CochainComplex { chain_index } => {
4239                let c = *chain_index;
4240                quote! {
4241                    ::uor_foundation::enforcement::Term::CochainComplex { chain_index: #c }
4242                }
4243            }
4244            TermSpec::CohomologyGroups { cochain_index } => {
4245                let c = *cochain_index;
4246                quote! {
4247                    ::uor_foundation::enforcement::Term::CohomologyGroups { cochain_index: #c }
4248                }
4249            }
4250            TermSpec::PostnikovTower { simplicial_index } => {
4251                let s = *simplicial_index;
4252                quote! {
4253                    ::uor_foundation::enforcement::Term::PostnikovTower { simplicial_index: #s }
4254                }
4255            }
4256            TermSpec::HomotopyGroups { postnikov_index } => {
4257                let p = *postnikov_index;
4258                quote! {
4259                    ::uor_foundation::enforcement::Term::HomotopyGroups { postnikov_index: #p }
4260                }
4261            }
4262            TermSpec::KInvariants { homotopy_index } => {
4263                let h = *homotopy_index;
4264                quote! {
4265                    ::uor_foundation::enforcement::Term::KInvariants { homotopy_index: #h }
4266                }
4267            }
4268        })
4269        .collect()
4270}
4271
4272/// Render a single non-VerbSplice TermSpec as a `Term::*` constructor
4273/// expression that uses the dynamic `len` variable for index fields
4274/// when those fields reference the spec at the given TermSpec index.
4275/// `spec_pos[i]` is the TokenStream for spec `i`'s result-position
4276/// const-let (e.g., `pos_3` for atomic specs, `len - 1` for verb
4277/// splices' last term).
4278fn render_atomic_term_in_builder(
4279    spec: &TermSpec,
4280    spec_pos: &[proc_macro2::TokenStream],
4281) -> proc_macro2::TokenStream {
4282    let pos_at = |idx: u32| -> proc_macro2::TokenStream {
4283        let i = idx as usize;
4284        if i < spec_pos.len() {
4285            spec_pos[i].clone()
4286        } else {
4287            // Out-of-bounds index — emit u32::MAX so const-eval surfaces a
4288            // recognizable arena-bounds error.
4289            quote! { u32::MAX }
4290        }
4291    };
4292    match spec {
4293        TermSpec::Literal(value) => {
4294            let v = *value;
4295            quote! {
4296                ::uor_foundation::pipeline::literal_u64(
4297                    #v,
4298                    ::uor_foundation::WittLevel::W8,
4299                )
4300            }
4301        }
4302        TermSpec::LiteralExpr { value, level } => quote! {
4303            ::uor_foundation::pipeline::literal_u64(
4304                #value,
4305                #level,
4306            )
4307        },
4308        TermSpec::LiteralBytesExpr { bytes, level } => quote! {
4309            ::uor_foundation::pipeline::literal_bytes(
4310                #bytes,
4311                #level,
4312            )
4313        },
4314        TermSpec::Variable => quote! {
4315            ::uor_foundation::enforcement::Term::Variable { name_index: 0u32 }
4316        },
4317        TermSpec::Application {
4318            operator,
4319            args_start,
4320            args_len,
4321        } => {
4322            let s = pos_at(*args_start);
4323            let l = *args_len;
4324            quote! {
4325                ::uor_foundation::enforcement::Term::Application {
4326                    operator: #operator,
4327                    args: ::uor_foundation::enforcement::TermList {
4328                        start: (#s) as u32,
4329                        len: #l,
4330                    },
4331                }
4332            }
4333        }
4334        TermSpec::AxisInvocation {
4335            axis_index,
4336            kernel_id,
4337            input_index,
4338        } => {
4339            let a = *axis_index;
4340            let k = *kernel_id;
4341            let i = pos_at(*input_index);
4342            quote! {
4343                ::uor_foundation::enforcement::Term::AxisInvocation {
4344                    axis_index: #a,
4345                    kernel_id: #k,
4346                    input_index: (#i) as u32,
4347                }
4348            }
4349        }
4350        TermSpec::ProjectField {
4351            source_index,
4352            byte_offset,
4353            byte_length,
4354        } => {
4355            let s = pos_at(*source_index);
4356            quote! {
4357                ::uor_foundation::enforcement::Term::ProjectField {
4358                    source_index: (#s) as u32,
4359                    byte_offset: (#byte_offset) as u32,
4360                    byte_length: (#byte_length) as u32,
4361                }
4362            }
4363        }
4364        TermSpec::Lift {
4365            operand_index,
4366            target_witt,
4367        } => {
4368            let i = pos_at(*operand_index);
4369            quote! {
4370                ::uor_foundation::enforcement::Term::Lift {
4371                    operand_index: (#i) as u32,
4372                    target: #target_witt,
4373                }
4374            }
4375        }
4376        TermSpec::Project {
4377            operand_index,
4378            target_witt,
4379        } => {
4380            let i = pos_at(*operand_index);
4381            quote! {
4382                ::uor_foundation::enforcement::Term::Project {
4383                    operand_index: (#i) as u32,
4384                    target: #target_witt,
4385                }
4386            }
4387        }
4388        TermSpec::Try { body_index } => {
4389            let i = pos_at(*body_index);
4390            quote! {
4391                ::uor_foundation::enforcement::Term::Try {
4392                    body_index: (#i) as u32,
4393                    handler_index: u32::MAX,
4394                }
4395            }
4396        }
4397        TermSpec::Recurse {
4398            measure_index,
4399            base_index,
4400            step_index,
4401        } => {
4402            let m = pos_at(*measure_index);
4403            let b = pos_at(*base_index);
4404            let s = pos_at(*step_index);
4405            quote! {
4406                ::uor_foundation::enforcement::Term::Recurse {
4407                    measure_index: (#m) as u32,
4408                    base_index: (#b) as u32,
4409                    step_index: (#s) as u32,
4410                }
4411            }
4412        }
4413        TermSpec::Unfold {
4414            seed_index,
4415            step_index,
4416        } => {
4417            let s = pos_at(*seed_index);
4418            let st = pos_at(*step_index);
4419            quote! {
4420                ::uor_foundation::enforcement::Term::Unfold {
4421                    seed_index: (#s) as u32,
4422                    step_index: (#st) as u32,
4423                }
4424            }
4425        }
4426        TermSpec::Match {
4427            scrutinee_index,
4428            arms_start,
4429            arms_len,
4430        } => {
4431            let sc = pos_at(*scrutinee_index);
4432            let st = *arms_start;
4433            let l = *arms_len;
4434            // For Match, arms are emitted as a contiguous span at fixed
4435            // positional offsets (the macro emits them sequentially in
4436            // the host arena); arms.start references the first such
4437            // position which lives at the static spec-index `st`.
4438            let st_pos = if (st as usize) < spec_pos.len() {
4439                spec_pos[st as usize].clone()
4440            } else {
4441                quote! { u32::MAX }
4442            };
4443            quote! {
4444                ::uor_foundation::enforcement::Term::Match {
4445                    scrutinee_index: (#sc) as u32,
4446                    arms: ::uor_foundation::enforcement::TermList {
4447                        start: (#st_pos) as u32,
4448                        len: #l,
4449                    },
4450                }
4451            }
4452        }
4453        TermSpec::WildcardSentinel => quote! {
4454            ::uor_foundation::enforcement::Term::Variable { name_index: u32::MAX }
4455        },
4456        TermSpec::RecursePlaceholder => quote! {
4457            ::uor_foundation::enforcement::Term::Variable {
4458                name_index: ::uor_foundation::pipeline::RECURSE_PLACEHOLDER_NAME_INDEX,
4459            }
4460        },
4461        TermSpec::UnfoldPlaceholder => quote! {
4462            ::uor_foundation::enforcement::Term::Variable {
4463                name_index: ::uor_foundation::pipeline::UNFOLD_PLACEHOLDER_NAME_INDEX,
4464            }
4465        },
4466        TermSpec::FirstAdmit {
4467            domain_size_index,
4468            predicate_index,
4469        } => {
4470            let d = pos_at(*domain_size_index);
4471            let p = pos_at(*predicate_index);
4472            quote! {
4473                ::uor_foundation::enforcement::Term::FirstAdmit {
4474                    domain_size_index: (#d) as u32,
4475                    predicate_index: (#p) as u32,
4476                }
4477            }
4478        }
4479        TermSpec::FirstAdmitIdxPlaceholder => quote! {
4480            ::uor_foundation::enforcement::Term::Variable {
4481                name_index: ::uor_foundation::pipeline::FIRST_ADMIT_IDX_NAME_INDEX,
4482            }
4483        },
4484        TermSpec::RecurseIdxPlaceholder => quote! {
4485            ::uor_foundation::enforcement::Term::Variable {
4486                name_index: ::uor_foundation::pipeline::RECURSE_IDX_NAME_INDEX,
4487            }
4488        },
4489        TermSpec::Nerve { value_index } => {
4490            let v = pos_at(*value_index);
4491            quote! {
4492                ::uor_foundation::enforcement::Term::Nerve { value_index: (#v) as u32 }
4493            }
4494        }
4495        TermSpec::ChainComplex { simplicial_index } => {
4496            let s = pos_at(*simplicial_index);
4497            quote! {
4498                ::uor_foundation::enforcement::Term::ChainComplex {
4499                    simplicial_index: (#s) as u32,
4500                }
4501            }
4502        }
4503        TermSpec::HomologyGroups { chain_index } => {
4504            let c = pos_at(*chain_index);
4505            quote! {
4506                ::uor_foundation::enforcement::Term::HomologyGroups {
4507                    chain_index: (#c) as u32,
4508                }
4509            }
4510        }
4511        TermSpec::Betti { homology_index } => {
4512            let h = pos_at(*homology_index);
4513            quote! {
4514                ::uor_foundation::enforcement::Term::Betti {
4515                    homology_index: (#h) as u32,
4516                }
4517            }
4518        }
4519        TermSpec::CochainComplex { chain_index } => {
4520            let c = pos_at(*chain_index);
4521            quote! {
4522                ::uor_foundation::enforcement::Term::CochainComplex {
4523                    chain_index: (#c) as u32,
4524                }
4525            }
4526        }
4527        TermSpec::CohomologyGroups { cochain_index } => {
4528            let c = pos_at(*cochain_index);
4529            quote! {
4530                ::uor_foundation::enforcement::Term::CohomologyGroups {
4531                    cochain_index: (#c) as u32,
4532                }
4533            }
4534        }
4535        TermSpec::PostnikovTower { simplicial_index } => {
4536            let s = pos_at(*simplicial_index);
4537            quote! {
4538                ::uor_foundation::enforcement::Term::PostnikovTower {
4539                    simplicial_index: (#s) as u32,
4540                }
4541            }
4542        }
4543        TermSpec::HomotopyGroups { postnikov_index } => {
4544            let p = pos_at(*postnikov_index);
4545            quote! {
4546                ::uor_foundation::enforcement::Term::HomotopyGroups {
4547                    postnikov_index: (#p) as u32,
4548                }
4549            }
4550        }
4551        TermSpec::KInvariants { homotopy_index } => {
4552            let h = pos_at(*homotopy_index);
4553            quote! {
4554                ::uor_foundation::enforcement::Term::KInvariants {
4555                    homotopy_index: (#h) as u32,
4556                }
4557            }
4558        }
4559        TermSpec::VerbSplice { .. } => quote! {
4560            compile_error!("VerbSplice handled separately in render_const_fn_arena_builder")
4561        },
4562    }
4563}
4564
4565/// Render the TermSpec arena as a const-fn arena builder when the arena
4566/// contains verb splices (wiki ADR-024). The builder emits a sequence
4567/// of statements in a const block that:
4568///
4569///   - allocates a fixed-capacity `[Term; CAP]` buffer
4570///   - emits each TermSpec as either a direct `buf[len] = Term::...; len += 1;`
4571///     (for atomic specs) or a `inline_verb_fragment` call (for verb splices)
4572///   - tracks each spec's result position via `pos_<N>` const-let bindings
4573///     so subsequent specs reference verb-spliced positions correctly
4574///   - returns `(buf, len)` and exposes `&buf[..len]` as the route's
4575///     `&'static [Term]` slice
4576fn render_const_fn_arena_builder(
4577    arena: &[TermSpec],
4578    inline_bytes: &proc_macro2::TokenStream,
4579) -> proc_macro2::TokenStream {
4580    // Step 1: compute each spec's result-position TokenStream. For
4581    // atomic specs, the position is a fresh `pos_<N>` const-let. For
4582    // VerbSplice specs, the position is `len - 1` evaluated AFTER the
4583    // splice (the last term of the spliced fragment is the verb's
4584    // result root per ADR-024).
4585    let spec_pos: Vec<proc_macro2::TokenStream> = (0..arena.len())
4586        .map(|i| {
4587            let id = Ident::new(&format!("pos_{}", i), proc_macro2::Span::call_site());
4588            quote! { #id }
4589        })
4590        .collect();
4591
4592    // Step 2: emit the build-step statements per spec.
4593    let mut stmts: Vec<proc_macro2::TokenStream> = Vec::with_capacity(arena.len());
4594    for (i, spec) in arena.iter().enumerate() {
4595        let pos_id = &spec_pos[i];
4596        match spec {
4597            TermSpec::VerbSplice {
4598                arg_root_idx,
4599                fragment_path,
4600            } => {
4601                // The caller's argument expression's root position is
4602                // captured in `pos_<arg_root_idx>`. inline_verb_fragment
4603                // substitutes Variable(0) in the verb's body with a copy
4604                // of `buf[arg_pos]` and shifts non-Variable(0) terms by
4605                // the host's current length per ADR-024.
4606                let arg_pos = &spec_pos[*arg_root_idx as usize];
4607                stmts.push(quote! {
4608                    let __spliced = ::uor_foundation::enforcement::inline_verb_fragment(
4609                        buf,
4610                        len,
4611                        #fragment_path::<#inline_bytes>(),
4612                        (#arg_pos) as u32,
4613                    );
4614                    buf = __spliced.0;
4615                    len = __spliced.1;
4616                    let #pos_id: usize = len - 1;
4617                });
4618            }
4619            other => {
4620                let term_expr = render_atomic_term_in_builder(other, &spec_pos);
4621                stmts.push(quote! {
4622                    buf[len] = #term_expr;
4623                    let #pos_id: usize = len;
4624                    len += 1;
4625                });
4626            }
4627        }
4628    }
4629
4630    // Step 3: assemble the const-fn block. Cap the arena at a generous
4631    // foundation default; const-eval will reject overflows.
4632    quote! {
4633        {
4634            const ROUTE_ARENA_CAP: usize = 256;
4635            const fn __build_arena() -> ([::uor_foundation::enforcement::Term<'static, #inline_bytes>; ROUTE_ARENA_CAP], usize) {
4636                let mut buf: [::uor_foundation::enforcement::Term<'static, #inline_bytes>; ROUTE_ARENA_CAP] =
4637                    [::uor_foundation::enforcement::Term::Variable { name_index: 0u32 }; ROUTE_ARENA_CAP];
4638                let mut len: usize = 0;
4639                #( #stmts )*
4640                (buf, len)
4641            }
4642            const ROUTE_BUILT: ([::uor_foundation::enforcement::Term<'static, #inline_bytes>; ROUTE_ARENA_CAP], usize) =
4643                __build_arena();
4644            const ROUTE_LEN: usize = ROUTE_BUILT.1;
4645            // Slice the active prefix; const split_at_checked is stable on Rust 1.83.
4646            match ROUTE_BUILT.0.split_at_checked(ROUTE_LEN) {
4647                Some((head, _)) => head,
4648                None => &[],
4649            }
4650        }
4651    }
4652}
4653
4654/// `prism_model!` — wiki ADR-020 + ADR-022 D3 closure-bodied form.
4655///
4656/// Parses the model declaration (struct, route witness struct, impl block
4657/// with `Input` / `Output` / `Route` associated types and a closure-bodied
4658/// `route` function), maps the closure body to a foundation-vocabulary
4659/// term tree at expansion time, and emits:
4660///
4661/// - `pub struct <Model>;` and `pub struct <Route>;` (re-emitted from input)
4662/// - `const ROUTE_TERMS_<MODEL>: &'static [Term] = &[…];` (the term tree)
4663/// - `impl __sdk_seal::Sealed for <Model>` and `for <Route>` (D1)
4664/// - `impl FoundationClosed for <Route> { arena_slice() → ROUTE_TERMS_<MODEL> }` (D5)
4665/// - `impl PrismModel<H, B, A> for <Model>` with `forward` body delegating
4666///   to `pipeline::run_route::<H, B, A, Self>(input)` (D4 + D5)
4667///
4668/// A function call to a name not in the foundation PrimitiveOp catalogue
4669/// (add, sub, mul, xor, and, or, neg, bnot, succ, pred) fails to compile,
4670/// pointing at the offending span — the wiki's closure-violation
4671/// enforcement (ADR-020).
4672#[proc_macro]
4673pub fn prism_model(input: TokenStream) -> TokenStream {
4674    let parsed = parse_macro_input!(input as PrismModelInput);
4675    let PrismModelInput {
4676        model_vis,
4677        model_name,
4678        route_vis,
4679        route_name,
4680        h_ty,
4681        b_ty,
4682        a_ty,
4683        r_ty,
4684        c_ty,
4685        input_ty,
4686        output_ty,
4687        route_input_ident,
4688        route_body,
4689        resolvers_body,
4690        commitment_body,
4691    } = parsed;
4692
4693    // ADR-036 resolver-tuple wiring:
4694    //   - When the user names a fourth substrate parameter R, the impl
4695    //     binds it explicitly and `forward` constructs an R instance
4696    //     either from the optional `fn resolvers() -> R { … }` clause or
4697    //     via `<R as Default>::default()` when the clause is omitted.
4698    //   - When R is omitted (the 3-position form), R defaults to
4699    //     `NullResolverTuple` and `forward` borrows the foundation's
4700    //     zero-sized null tuple — RESOLVER_ABSENT propagation per
4701    //     ADR-022 D3 G9 for any resolver-bound ψ-Term encountered.
4702    let (resolver_ty_tokens, resolver_construction) = match (&r_ty, &resolvers_body) {
4703        (Some(r), Some(block)) => (quote! { #r }, quote! { #block }),
4704        (Some(r), None) => (
4705            quote! { #r },
4706            quote! { <#r as ::core::default::Default>::default() },
4707        ),
4708        (None, _) => (
4709            quote! { ::uor_foundation::pipeline::NullResolverTuple },
4710            quote! { ::uor_foundation::pipeline::NullResolverTuple },
4711        ),
4712    };
4713
4714    // ADR-048 typed-commitment wiring — prism's cost-model surface:
4715    //   - When the user names a fifth substrate parameter C, the impl
4716    //     binds it explicitly and `forward` constructs a C instance
4717    //     either from the optional `fn commitment() -> C { … }` clause
4718    //     or via `<C as Default>::default()` when the clause is omitted.
4719    //   - When C is omitted (3- or 4-position form), C defaults to
4720    //     `EmptyCommitment` and the catamorphism's post-resolver
4721    //     `evaluate(kappa_label)` consultation accepts unconditionally
4722    //     (the bare base-admission semantics from ADR-035).
4723    let (commitment_ty_tokens, commitment_construction) = match (&c_ty, &commitment_body) {
4724        (Some(c), Some(block)) => (quote! { #c }, quote! { #block }),
4725        (Some(c), None) => (
4726            quote! { #c },
4727            quote! { <#c as ::core::default::Default>::default() },
4728        ),
4729        (None, _) => (
4730            quote! { ::uor_foundation::pipeline::EmptyCommitment },
4731            quote! { ::uor_foundation::pipeline::EmptyCommitment },
4732        ),
4733    };
4734
4735    // Walk the closure body — the macro-time mapping that ADR-020 / D3
4736    // names. The body is processed via the block handler so `let`
4737    // bindings (G10) and the trailing tail expression (G11) are both
4738    // recognised.
4739    //
4740    // ADR-033 G20: pin the route input type into the binding scope so
4741    // field-access expressions can synthesize the const-eval lookup
4742    // against `<RouteInputTy as PartitionProductFields>::FIELDS`.
4743    //
4744    // ADR-056: this is the `prism_model!` route body — the only syntactic
4745    // surface the ψ-residuals discipline applies to. Verb! bodies and
4746    // axis! body clauses keep `in_route_body == false` (the default).
4747    let mut arena: Vec<TermSpec> = Vec::new();
4748    let mut scope = BindingScope {
4749        route_input_ty: Some(input_ty.clone()),
4750        in_route_body: true,
4751        ..BindingScope::default()
4752    };
4753    if let Err(e) = emit_term_for_block(&route_body, &route_input_ident, &mut arena, &mut scope) {
4754        return e.to_compile_error().into();
4755    }
4756
4757    // ADR-060: the foundation-derived inline carrier width for this model's
4758    // selected `HostBounds` (#b_ty). `#b_ty` is concrete at the call site, so
4759    // `carrier_inline_bytes::<#b_ty>()` is a concrete `const` expression
4760    // admissible as a const-generic argument on stable Rust. Threaded through
4761    // `Term`/`TermArena`, `FoundationClosed`, `PrismModel`, `run_route`,
4762    // `Grounded`, and the verb-splice arena builder.
4763    let inline_bytes = quote! {
4764        { ::uor_foundation::pipeline::carrier_inline_bytes::<#b_ty>() }
4765    };
4766
4767    // ADR-018/060: the application's selected fingerprint width, read from its
4768    // `HostBounds` (#b_ty). `#b_ty` is concrete at the call site so this is a
4769    // concrete `const` expression, admissible as a const-generic argument on
4770    // stable Rust (no `generic_const_exprs`). Threaded through `PrismModel`,
4771    // `Grounded`, and `run_route` exactly parallel to `inline_bytes`, so the
4772    // application's substituted `Hasher` width flows end-to-end without the
4773    // author writing it — ergonomically identical to the `=32`-default form.
4774    let fp_max = quote! {
4775        { <#b_ty as ::uor_foundation::HostBounds>::FINGERPRINT_MAX_BYTES }
4776    };
4777
4778    // Per wiki ADR-024, verb fragments are inlined into the route's
4779    // arena at compile time via the const-fn arena builder when any
4780    // verb invocation is present. Pure (verb-free) routes use the
4781    // simple slice-literal form.
4782    let route_has_verb_splices = arena
4783        .iter()
4784        .any(|s| matches!(s, TermSpec::VerbSplice { .. }));
4785    let route_arena_expr = if route_has_verb_splices {
4786        render_const_fn_arena_builder(&arena, &inline_bytes)
4787    } else {
4788        let term_specs = render_arena(&arena);
4789        quote! { &[ #( #term_specs ),* ] }
4790    };
4791
4792    // Synthesize a unique const name from the model's identifier so two
4793    // models in the same module don't clash on `ROUTE_TERMS`.
4794    let route_terms_const = Ident::new(
4795        &format!(
4796            "ROUTE_TERMS_FOR_{}",
4797            to_screaming_snake(&model_name.to_string())
4798        ),
4799        model_name.span(),
4800    );
4801
4802    let expansion = quote! {
4803        // Re-emit the model + route witness structs from the input.
4804        #model_vis struct #model_name;
4805        #route_vis struct #route_name;
4806
4807        // ADR-022 D2 + ADR-024: const term-tree slice. Macro-time-built,
4808        // with verb fragments inlined at compile time per ADR-024 (the
4809        // catamorphism walks a flat arena over the ten Term variants —
4810        // no runtime depth guard). `pipeline::run_route` reads the
4811        // slice via `FoundationClosed::arena_slice`.
4812        #[allow(non_upper_case_globals, dead_code)]
4813        const #route_terms_const: &[::uor_foundation::enforcement::Term<'static, #inline_bytes>] =
4814            #route_arena_expr;
4815
4816        // ADR-022 D1: seal impls. Foundation-internal macro is the
4817        // only sanctioned producer outside foundation itself.
4818        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #model_name {}
4819        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #route_name {}
4820
4821        // ADR-022 D5 + ADR-060: FoundationClosed impl returning the parsed
4822        // term-tree, const-generic over the foundation-derived inline width.
4823        impl ::uor_foundation::pipeline::FoundationClosed<#inline_bytes> for #route_name {
4824            fn arena_slice() -> &'static [::uor_foundation::enforcement::Term<'static, #inline_bytes>] {
4825                #route_terms_const
4826            }
4827        }
4828
4829        // ADR-020 + ADR-022 D4 + ADR-036 + ADR-048 + ADR-060: PrismModel impl.
4830        // The 4th slot is the foundation-derived INLINE_BYTES carrier width
4831        // (ADR-060); the 5th binds the route's ResolverTuple per ADR-036
4832        // (NullResolverTuple default); the 6th binds the model's
4833        // TypedCommitment per ADR-048 (EmptyCommitment default). The
4834        // macro-emitted `forward` body constructs both substrate instances
4835        // and threads them to `pipeline::run_route` per ADR-022 D5.
4836        impl<'a> ::uor_foundation::pipeline::PrismModel<'a, #h_ty, #b_ty, #a_ty, #inline_bytes, #fp_max, #resolver_ty_tokens, #commitment_ty_tokens> for #model_name {
4837            type Input = #input_ty;
4838            type Output = #output_ty;
4839            type Route = #route_name;
4840
4841            fn forward(
4842                input: <Self as ::uor_foundation::pipeline::PrismModel<'a, #h_ty, #b_ty, #a_ty, #inline_bytes, #fp_max, #resolver_ty_tokens, #commitment_ty_tokens>>::Input,
4843            ) -> ::core::result::Result<
4844                ::uor_foundation::enforcement::Grounded<
4845                    'a,
4846                    <Self as ::uor_foundation::pipeline::PrismModel<'a, #h_ty, #b_ty, #a_ty, #inline_bytes, #fp_max, #resolver_ty_tokens, #commitment_ty_tokens>>::Output,
4847                    #inline_bytes,
4848                    #fp_max,
4849                >,
4850                ::uor_foundation::PipelineFailure,
4851            > {
4852                let __resolvers: #resolver_ty_tokens = #resolver_construction;
4853                let __commitment: #commitment_ty_tokens = #commitment_construction;
4854                ::uor_foundation::pipeline::run_route::<
4855                    #h_ty,
4856                    #b_ty,
4857                    #a_ty,
4858                    Self,
4859                    #resolver_ty_tokens,
4860                    #commitment_ty_tokens,
4861                    #inline_bytes,
4862                    #fp_max,
4863                >(input, &__resolvers, &__commitment)
4864            }
4865        }
4866    };
4867
4868    expansion.into()
4869}
4870
4871// =====================================================================
4872// `output_shape!` — wiki ADR-027.
4873//
4874// Generalizes `GroundedShape`'s seal: the foundation source seals the
4875// trait via `__sdk_seal::Sealed`, and the `output_shape!` SDK macro
4876// emits — alongside the application's `ConstrainedTypeShape` impl — the
4877// `__sdk_seal::Sealed`, `GroundedShape`, and `IntoBindingValue` impls
4878// gated on the seal. This widens the path applications can declare a
4879// custom Output shape without lifting the seal entirely.
4880//
4881// Macro form:
4882//
4883// ```text
4884// output_shape! {
4885//     pub struct OutputHash;
4886//     impl ConstrainedTypeShape for OutputHash {
4887//         const IRI: &'static str = "https://prism.btc/shape/OutputHash";
4888//         const SITE_COUNT: usize = 32;
4889//         const CONSTRAINTS: &'static [ConstraintRef] = &[];
4890//     }
4891// }
4892// ```
4893//
4894// Emissions (per ADR-027):
4895//   - `pub struct <Name>;` (re-emitted)
4896//   - `impl ConstrainedTypeShape for <Name>` (re-emitted)
4897//   - `impl __sdk_seal::Sealed for <Name>`
4898//   - `impl GroundedShape for <Name>`
4899//   - `impl IntoBindingValue for <Name>` with MAX_BYTES = SITE_COUNT
4900//     (byte-level granularity default; shapes whose Witt level is
4901//     greater than 8 bits use a wider per-site multiplier the
4902//     application sets via a custom `IntoBindingValue` impl).
4903
4904/// Parsed shape of the macro input.
4905struct OutputShapeInput {
4906    struct_vis: syn::Visibility,
4907    struct_name: Ident,
4908    impl_iri: syn::LitStr,
4909    impl_site_count: syn::Expr,
4910    impl_constraints: syn::Expr,
4911    /// ADR-032: optional explicit `const CYCLE_SIZE: u64 = …`. If
4912    /// omitted, the macro defaults to `cycle_size_power(256, SITE_COUNT)`
4913    /// (saturating to `u64::MAX` for SITE_COUNT ≥ 8 bytes).
4914    impl_cycle_size: Option<syn::Expr>,
4915}
4916
4917impl Parse for OutputShapeInput {
4918    fn parse(input: ParseStream) -> Result<Self> {
4919        // `pub struct OutputHash;`
4920        let struct_vis: syn::Visibility = input.parse()?;
4921        input.parse::<Token![struct]>()?;
4922        let struct_name: Ident = input.parse()?;
4923        input.parse::<Token![;]>()?;
4924
4925        // `impl ConstrainedTypeShape for OutputHash { ... }`
4926        input.parse::<Token![impl]>()?;
4927        let trait_ident: Ident = input.parse()?;
4928        if trait_ident != "ConstrainedTypeShape" {
4929            return Err(syn::Error::new(
4930                trait_ident.span(),
4931                "output_shape! expects `impl ConstrainedTypeShape for <Name>`",
4932            ));
4933        }
4934        input.parse::<Token![for]>()?;
4935        let target: Ident = input.parse()?;
4936        if target != struct_name {
4937            return Err(syn::Error::new(
4938                target.span(),
4939                "output_shape!'s `impl ConstrainedTypeShape for <Name>` target must match the declared struct",
4940            ));
4941        }
4942
4943        let body;
4944        syn::braced!(body in input);
4945
4946        // `const IRI: &'static str = "...";`
4947        body.parse::<Token![const]>()?;
4948        let kw_iri: Ident = body.parse()?;
4949        if kw_iri != "IRI" {
4950            return Err(syn::Error::new(
4951                kw_iri.span(),
4952                "expected `const IRI: &'static str = ...`",
4953            ));
4954        }
4955        body.parse::<Token![:]>()?;
4956        let _ty: syn::Type = body.parse()?;
4957        body.parse::<Token![=]>()?;
4958        let impl_iri: syn::LitStr = body.parse()?;
4959        body.parse::<Token![;]>()?;
4960
4961        // `const SITE_COUNT: usize = ...;`
4962        body.parse::<Token![const]>()?;
4963        let kw_sc: Ident = body.parse()?;
4964        if kw_sc != "SITE_COUNT" {
4965            return Err(syn::Error::new(
4966                kw_sc.span(),
4967                "expected `const SITE_COUNT: usize = ...`",
4968            ));
4969        }
4970        body.parse::<Token![:]>()?;
4971        let _ty: syn::Type = body.parse()?;
4972        body.parse::<Token![=]>()?;
4973        let impl_site_count: syn::Expr = body.parse()?;
4974        body.parse::<Token![;]>()?;
4975
4976        // `const CONSTRAINTS: &'static [ConstraintRef] = ...;`
4977        body.parse::<Token![const]>()?;
4978        let kw_cn: Ident = body.parse()?;
4979        if kw_cn != "CONSTRAINTS" {
4980            return Err(syn::Error::new(
4981                kw_cn.span(),
4982                "expected `const CONSTRAINTS: &'static [ConstraintRef] = ...`",
4983            ));
4984        }
4985        body.parse::<Token![:]>()?;
4986        let _ty: syn::Type = body.parse()?;
4987        body.parse::<Token![=]>()?;
4988        let impl_constraints: syn::Expr = body.parse()?;
4989        body.parse::<Token![;]>()?;
4990
4991        // ADR-032: optional `const CYCLE_SIZE: u64 = ...;` — if absent,
4992        // the expansion defaults to `cycle_size_power(256, SITE_COUNT)`.
4993        let impl_cycle_size: Option<syn::Expr> = if body.peek(Token![const]) {
4994            body.parse::<Token![const]>()?;
4995            let kw_cs: Ident = body.parse()?;
4996            if kw_cs != "CYCLE_SIZE" {
4997                return Err(syn::Error::new(
4998                    kw_cs.span(),
4999                    "expected `const CYCLE_SIZE: u64 = ...` (the only optional const recognised by output_shape! is CYCLE_SIZE per ADR-032)",
5000                ));
5001            }
5002            body.parse::<Token![:]>()?;
5003            let _ty: syn::Type = body.parse()?;
5004            body.parse::<Token![=]>()?;
5005            let expr: syn::Expr = body.parse()?;
5006            body.parse::<Token![;]>()?;
5007            Some(expr)
5008        } else {
5009            None
5010        };
5011
5012        Ok(Self {
5013            struct_vis,
5014            struct_name,
5015            impl_iri,
5016            impl_site_count,
5017            impl_constraints,
5018            impl_cycle_size,
5019        })
5020    }
5021}
5022
5023/// `output_shape!` — wiki ADR-027 custom Output shape declaration.
5024///
5025/// Emits the application-named struct, the `ConstrainedTypeShape` impl
5026/// (re-emitted from the user's body), and the additional impls
5027/// `__sdk_seal::Sealed`, `GroundedShape`, and `IntoBindingValue` so the
5028/// shape qualifies as a `PrismModel::Output`.
5029#[proc_macro]
5030pub fn output_shape(input: TokenStream) -> TokenStream {
5031    let parsed = parse_macro_input!(input as OutputShapeInput);
5032    let OutputShapeInput {
5033        struct_vis,
5034        struct_name,
5035        impl_iri,
5036        impl_site_count,
5037        impl_constraints,
5038        impl_cycle_size,
5039    } = parsed;
5040
5041    // ADR-032: explicit CYCLE_SIZE if supplied, else saturating
5042    // `256.pow(SITE_COUNT)` (the byte-shaped upper bound).
5043    let cycle_size_tokens = match impl_cycle_size {
5044        Some(expr) => quote! { #expr },
5045        None => quote! {
5046            ::uor_foundation::pipeline::cycle_size_power(256, #impl_site_count)
5047        },
5048    };
5049
5050    let expansion = quote! {
5051        // Re-emit the user's struct and ConstrainedTypeShape impl.
5052        #struct_vis struct #struct_name;
5053
5054        impl ::uor_foundation::pipeline::ConstrainedTypeShape for #struct_name {
5055            const IRI: &'static str = #impl_iri;
5056            const SITE_COUNT: usize = #impl_site_count;
5057            const CONSTRAINTS: &'static [::uor_foundation::pipeline::ConstraintRef] =
5058                #impl_constraints;
5059            // ADR-032: explicit CYCLE_SIZE if supplied, else default.
5060            const CYCLE_SIZE: u64 = #cycle_size_tokens;
5061        }
5062
5063        // ADR-027 emissions: the four sealed-trait impls.
5064        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #struct_name {}
5065        impl ::uor_foundation::enforcement::GroundedShape for #struct_name {}
5066        impl<'a> ::uor_foundation::pipeline::IntoBindingValue<'a> for #struct_name {
5067            fn as_binding_value<const INLINE_BYTES: usize>(
5068                &self,
5069            ) -> ::uor_foundation::pipeline::TermValue<'a, INLINE_BYTES> {
5070                // The output shape carries the catamorphism's evaluation result, not a
5071                // user-supplied input, so the input-side carrier is the empty Inline
5072                // carrier. Applications that re-use the output shape as a downstream
5073                // model's Input write a bespoke `IntoBindingValue` impl reflecting the
5074                // bytes they emit at runtime.
5075                ::uor_foundation::pipeline::TermValue::empty()
5076            }
5077        }
5078    };
5079
5080    expansion.into()
5081}
5082
5083// =====================================================================
5084// `verb!` — wiki ADR-024 Layer-3 implementation closure.
5085//
5086// An implementation declares a named, reusable composition of prism
5087// operators applied to substrate primitives. The macro:
5088//
5089//   - parses a closure-bodied function declaration
5090//     (`pub fn name(input: T) -> U { … }`)
5091//   - emits a `&'static [Term]` slice carrying the verb's term-tree
5092//     fragment (built via the same G1–G19 closure-body grammar as
5093//     `prism_model!`)
5094//   - emits a `pub fn name_term_arena() -> &'static [Term]` accessor
5095//     so `prism_model!` can reference the verb by name during
5096//     route-closure expansion
5097//
5098// Form:
5099//
5100// ```text
5101// verb! {
5102//     pub fn sha256_compression(input: BlockInput) -> CompressionState {
5103//         // closure body — same G1–G19 grammar as prism_model!
5104//         hash(input)
5105//     }
5106// }
5107// ```
5108//
5109// Per ADR-024's three-way responsibility split, the verb's runtime
5110// is implementation-owned: the term-tree fragment is the structural
5111// declaration; how an implementation evaluates it (sequential,
5112// parallel, optimised) is the implementation's choice. Foundation's
5113// `pipeline::run_route` evaluates verb-reachable Term trees per the
5114// per-variant fold-rules (ADR-029).
5115
5116/// Parsed shape of the `verb!` macro input.
5117struct VerbInput {
5118    fn_vis: syn::Visibility,
5119    fn_name: Ident,
5120    input_param: Ident,
5121    input_ty: syn::Type,
5122    output_ty: syn::Type,
5123    body: syn::Block,
5124}
5125
5126impl Parse for VerbInput {
5127    fn parse(input: ParseStream) -> Result<Self> {
5128        let fn_vis: syn::Visibility = input.parse()?;
5129        input.parse::<Token![fn]>()?;
5130        let fn_name: Ident = input.parse()?;
5131        let params;
5132        syn::parenthesized!(params in input);
5133        let input_param: Ident = params.parse()?;
5134        params.parse::<Token![:]>()?;
5135        let input_ty: syn::Type = params.parse()?;
5136        input.parse::<Token![->]>()?;
5137        let output_ty: syn::Type = input.parse()?;
5138        let body: syn::Block = input.parse()?;
5139        Ok(Self {
5140            fn_vis,
5141            fn_name,
5142            input_param,
5143            input_ty,
5144            output_ty,
5145            body,
5146        })
5147    }
5148}
5149
5150/// `verb!` — wiki ADR-024 Layer-3 verb declaration. Emits a const
5151/// term-tree fragment and a public accessor.
5152#[proc_macro]
5153pub fn verb(input: TokenStream) -> TokenStream {
5154    let parsed = parse_macro_input!(input as VerbInput);
5155    let VerbInput {
5156        fn_vis,
5157        fn_name,
5158        input_param,
5159        input_ty,
5160        output_ty,
5161        body,
5162    } = parsed;
5163
5164    let mut arena: Vec<TermSpec> = Vec::new();
5165    // ADR-033 G20: pin the verb's input type into the binding scope so
5166    // field-access expressions can synthesize const-eval lookups
5167    // (including depth-2 chains like `input.0.0` per Dependency 1).
5168    //
5169    // ADR-056: verb bodies are NOT subject to the ψ-residuals discipline
5170    // (which applies only to the route body's syntactic surface). Verbs
5171    // may use the full substrate vocabulary — `Concat`, `Le`/`Lt`/`Ge`/
5172    // `Gt`, `hash(...)` axis invocations, `first_admit(...)` bounded
5173    // search — to realize compound operations like SHA padding, HMAC,
5174    // Merkle tree construction, and tensor saturation.
5175    let mut scope = BindingScope {
5176        route_input_ty: Some(input_ty.clone()),
5177        in_route_body: false,
5178        ..BindingScope::default()
5179    };
5180    if let Err(e) = emit_term_for_block(&body, &input_param, &mut arena, &mut scope) {
5181        return e.to_compile_error().into();
5182    }
5183
5184    // Wiki ADR-024 verb-closure check: a verb's body must not directly
5185    // reference itself. Self-recursion is the local cycle the macro can
5186    // detect at expansion time; cross-verb cycles surface at the
5187    // application's compile time during const-eval of the splicing
5188    // const-fn calls (Rust's const-eval rejects infinite recursion via
5189    // its const-step ceiling).
5190    let self_const_name = format!("VERB_TERMS_{}", to_screaming_snake(&fn_name.to_string()));
5191    for spec in &arena {
5192        if let TermSpec::VerbSplice { fragment_path, .. } = spec {
5193            if fragment_path.to_string().trim() == self_const_name {
5194                return syn::Error::new(
5195                    fn_name.span(),
5196                    format!(
5197                        "verb-closure violation (ADR-024): `{}`'s body references itself directly; the verb-reference graph through non-`recurse` operators must be acyclic. Lift the recursion through `recurse(...)` (G7) instead.",
5198                        fn_name
5199                    ),
5200                )
5201                .to_compile_error()
5202                .into();
5203            }
5204        }
5205    }
5206
5207    // Determine the term-tree fragment representation: if the verb
5208    // body contains nested verb splices, we must emit a const-fn
5209    // builder that resolves the splices at const-eval time. For pure
5210    // (no nested splices) bodies, the simple slice literal works.
5211    let body_has_verb_splices = arena
5212        .iter()
5213        .any(|s| matches!(s, TermSpec::VerbSplice { .. }));
5214    let verb_fragment_expr = if body_has_verb_splices {
5215        render_const_fn_arena_builder(&arena, &quote! { INLINE_BYTES })
5216    } else {
5217        let term_specs = render_arena(&arena);
5218        quote! { &[ #( #term_specs ),* ] }
5219    };
5220
5221    let const_name = Ident::new(
5222        &format!("VERB_TERMS_{}", to_screaming_snake(&fn_name.to_string())),
5223        fn_name.span(),
5224    );
5225    let accessor_name = Ident::new(&format!("{}_term_arena", fn_name), fn_name.span());
5226    // ADR-060: a generic-`INLINE_BYTES` `&'static` term slice cannot be
5227    // returned by a generic `const fn` via rvalue static promotion (the
5228    // promoted array's type depends on the const-generic parameter). The
5229    // stable idiom is a zero-sized holder type with an **associated const**,
5230    // which is `'static` by construction. The verb's const fn / accessor read
5231    // that associated const.
5232    let frag_holder = Ident::new(
5233        &format!("__VerbFrag_{}", to_screaming_snake(&fn_name.to_string())),
5234        fn_name.span(),
5235    );
5236
5237    let expansion = quote! {
5238        // ADR-024 + ADR-060: the verb's term-tree fragment, held as an
5239        // associated const on a const-generic holder type so the
5240        // `&'static [Term<'static, INLINE_BYTES>]` slice is well-formed for any
5241        // consuming model's foundation-derived inline carrier width. A verb is
5242        // model-independent; the consuming `prism_model!` route reads the
5243        // fragment at its own `carrier_inline_bytes::<B>()` width.
5244        #[doc(hidden)]
5245        #[allow(non_camel_case_types)]
5246        #fn_vis struct #frag_holder<const INLINE_BYTES: usize>;
5247        #[allow(dead_code)]
5248        impl<const INLINE_BYTES: usize> #frag_holder<INLINE_BYTES> {
5249            #fn_vis const TERMS: &'static [::uor_foundation::enforcement::Term<'static, INLINE_BYTES>] =
5250                #verb_fragment_expr;
5251        }
5252
5253        // `VERB_TERMS_<NAME>` const fn — names the fragment for `prism_model!`
5254        // splices (after `use_verbs!` re-export) and the verb's own accessor.
5255        #[allow(non_snake_case, dead_code)]
5256        #fn_vis const fn #const_name<const INLINE_BYTES: usize>(
5257        ) -> &'static [::uor_foundation::enforcement::Term<'static, INLINE_BYTES>] {
5258            #frag_holder::<INLINE_BYTES>::TERMS
5259        }
5260
5261        // Public accessor for the verb's term-tree fragment. Per
5262        // ADR-024, the verb is a structural declaration — its
5263        // runtime is implementation-owned.
5264        #fn_vis const fn #accessor_name<const INLINE_BYTES: usize>(
5265        ) -> &'static [::uor_foundation::enforcement::Term<'static, INLINE_BYTES>] {
5266            #frag_holder::<INLINE_BYTES>::TERMS
5267        }
5268
5269        // Marker `pub fn name(_: InputTy) -> OutputTy` so the verb
5270        // appears at the consumer's name-resolution surface. The body
5271        // never executes as Rust at runtime; foundation's catamorphism
5272        // walks the term-tree fragment per ADR-029.
5273        #[allow(unused_variables, unreachable_code)]
5274        #fn_vis fn #fn_name(#input_param: #input_ty) -> #output_ty {
5275            // Verb bodies are catamorphism-evaluated; the Rust function
5276            // form exists for name-resolution and macro-time reference.
5277            // Implementations that invoke a verb directly use foundation's
5278            // `evaluate_term_tree` against the verb's term-tree slice.
5279            let _ = #input_param;
5280            unimplemented!(
5281                "verb `{}` body is catamorphism-evaluated by foundation's pipeline; \
5282                 callers reach it through the term-tree accessor `{}_term_arena()`, \
5283                 not by direct Rust invocation",
5284                stringify!(#fn_name),
5285                stringify!(#fn_name),
5286            )
5287        }
5288    };
5289
5290    expansion.into()
5291}
5292
5293// =====================================================================
5294// `use_verbs!` — wiki ADR-024 cross-implementation verb imports.
5295//
5296// Re-exports verbs from another crate's `verb!` emissions. The
5297// importing implementation's verb-closure check treats imported
5298// verbs as opaque atoms; the imported crate's own `verb!` macro
5299// performed that crate's closure check.
5300//
5301// Form:
5302//
5303// ```text
5304// use_verbs! {
5305//     from other_implementation_crate {
5306//         verb_name_a,
5307//         verb_name_b,
5308//     };
5309// }
5310// ```
5311
5312/// Parsed shape of the `use_verbs!` macro input.
5313struct UseVerbsInput {
5314    crate_path: syn::Path,
5315    verb_names: Vec<Ident>,
5316}
5317
5318impl Parse for UseVerbsInput {
5319    fn parse(input: ParseStream) -> Result<Self> {
5320        // `from <crate_path>`
5321        let from_kw: Ident = input.parse()?;
5322        if from_kw != "from" {
5323            return Err(syn::Error::new(
5324                from_kw.span(),
5325                "expected `from <crate_path>`",
5326            ));
5327        }
5328        let crate_path: syn::Path = input.parse()?;
5329
5330        // `{ verb_a, verb_b, ... }`
5331        let body;
5332        syn::braced!(body in input);
5333        let mut verb_names: Vec<Ident> = Vec::new();
5334        while !body.is_empty() {
5335            verb_names.push(body.parse()?);
5336            if body.peek(Token![,]) {
5337                body.parse::<Token![,]>()?;
5338            }
5339        }
5340
5341        // Optional trailing semicolon.
5342        let _ = input.parse::<Token![;]>();
5343
5344        Ok(Self {
5345            crate_path,
5346            verb_names,
5347        })
5348    }
5349}
5350
5351/// `use_verbs!` — wiki ADR-024 cross-implementation verb imports.
5352#[proc_macro]
5353pub fn use_verbs(input: TokenStream) -> TokenStream {
5354    let parsed = parse_macro_input!(input as UseVerbsInput);
5355    let UseVerbsInput {
5356        crate_path,
5357        verb_names,
5358    } = parsed;
5359
5360    let mut imports: Vec<proc_macro2::TokenStream> = Vec::with_capacity(verb_names.len() * 3);
5361    for name in &verb_names {
5362        let arena_name = Ident::new(&format!("{}_term_arena", name), name.span());
5363        let const_name = Ident::new(
5364            &format!("VERB_TERMS_{}", to_screaming_snake(&name.to_string())),
5365            name.span(),
5366        );
5367        imports.push(quote! { pub use #crate_path::#name; });
5368        imports.push(quote! { pub use #crate_path::#arena_name; });
5369        imports.push(quote! { pub use #crate_path::#const_name; });
5370    }
5371
5372    let expansion = quote! {
5373        #( #imports )*
5374    };
5375
5376    expansion.into()
5377}
5378
5379// =====================================================================
5380// `register_shape!` — wiki ADR-057 SDK macro: registers application shapes
5381// in the foundation's shape-IRI registry per ADR-057's bounded recursive
5382// structural typing surface.
5383//
5384// Form:
5385//
5386// ```text
5387// register_shape!(MyAppRegistry, Shape1, Shape2, Shape3);
5388// ```
5389//
5390// `MyAppRegistry` is a fresh marker type (struct + sealed impl). The
5391// remaining identifiers (or types — generic shapes admitted via the
5392// same Type-parser path that `partition_product!` uses) are the shapes
5393// to register; each must implement `ConstrainedTypeShape`.
5394//
5395// Emits:
5396//
5397// 1. `pub struct MyAppRegistry;` marker type.
5398// 2. `impl __sdk_seal::Sealed for MyAppRegistry`.
5399// 3. `impl ShapeRegistryProvider for MyAppRegistry` whose `REGISTRY`
5400//    const is a `&'static [RegisteredShape]` aggregating one entry per
5401//    shape, with each entry populated from the shape's `ConstrainedTypeShape`
5402//    associated items.
5403//
5404// Application code consults the registry via:
5405//
5406// ```text
5407// pipeline::shape_iri_registry::lookup_shape_in::<MyAppRegistry>(iri)
5408// ```
5409//
5410// The trait-based, const-aggregated registry is foundation's no_std-safe
5411// + zero-`unsafe` realization of ADR-057's wiki-committed registry surface.
5412
5413struct RegisterShapeInput {
5414    registry_name: Ident,
5415    shapes: Vec<syn::Type>,
5416}
5417
5418impl Parse for RegisterShapeInput {
5419    fn parse(input: ParseStream) -> Result<Self> {
5420        let registry_name: Ident = input.parse()?;
5421        input.parse::<Token![,]>()?;
5422        let mut shapes: Vec<syn::Type> = Vec::new();
5423        let first: syn::Type = input.parse()?;
5424        shapes.push(first);
5425        while input.peek(Token![,]) {
5426            input.parse::<Token![,]>()?;
5427            if input.is_empty() {
5428                break;
5429            }
5430            let next: syn::Type = input.parse()?;
5431            shapes.push(next);
5432        }
5433        if shapes.is_empty() {
5434            return Err(syn::Error::new(
5435                registry_name.span(),
5436                "register_shape! requires at least one shape after the registry name",
5437            ));
5438        }
5439        Ok(Self {
5440            registry_name,
5441            shapes,
5442        })
5443    }
5444}
5445
5446/// `register_shape!` — wiki ADR-057 shape-IRI registration. Emits a
5447/// marker type + `ShapeRegistryProvider` impl whose `REGISTRY` const
5448/// carries one `RegisteredShape` entry per shape, populated from each
5449/// shape's `ConstrainedTypeShape` associated items.
5450#[proc_macro]
5451pub fn register_shape(input: TokenStream) -> TokenStream {
5452    let parsed = parse_macro_input!(input as RegisterShapeInput);
5453    let RegisterShapeInput {
5454        registry_name,
5455        shapes,
5456    } = parsed;
5457
5458    let entries: Vec<proc_macro2::TokenStream> = shapes
5459        .iter()
5460        .map(|shape| {
5461            quote! {
5462                ::uor_foundation::pipeline::shape_iri_registry::RegisteredShape {
5463                    iri: <#shape as ::uor_foundation::pipeline::ConstrainedTypeShape>::IRI,
5464                    site_count: <#shape as ::uor_foundation::pipeline::ConstrainedTypeShape>::SITE_COUNT,
5465                    constraints: <#shape as ::uor_foundation::pipeline::ConstrainedTypeShape>::CONSTRAINTS,
5466                    cycle_size: <#shape as ::uor_foundation::pipeline::ConstrainedTypeShape>::CYCLE_SIZE,
5467                }
5468            }
5469        })
5470        .collect();
5471
5472    let registry_const = Ident::new(
5473        &format!("{}_SHAPES", to_screaming_snake(&registry_name.to_string())),
5474        registry_name.span(),
5475    );
5476
5477    let expansion = quote! {
5478        /// ADR-057 application shape registry. Const-aggregated through the
5479        /// SDK `register_shape!` macro; consulted by `ψ_1` NerveResolver via
5480        /// `lookup_shape_in::<#registry_name>(iri)` during `Term::Recurse`
5481        /// expansion.
5482        #[derive(::core::fmt::Debug, ::core::clone::Clone, ::core::marker::Copy, ::core::default::Default)]
5483        pub struct #registry_name;
5484
5485        impl ::uor_foundation::pipeline::__sdk_seal::Sealed for #registry_name {}
5486
5487        /// ADR-057 const-aggregated shape registry for [`#registry_name`].
5488        pub const #registry_const:
5489            &[::uor_foundation::pipeline::shape_iri_registry::RegisteredShape] = &[
5490                #( #entries ),*
5491            ];
5492
5493        impl ::uor_foundation::pipeline::shape_iri_registry::ShapeRegistryProvider for #registry_name {
5494            const REGISTRY:
5495                &'static [::uor_foundation::pipeline::shape_iri_registry::RegisteredShape] =
5496                #registry_const;
5497        }
5498    };
5499
5500    expansion.into()
5501}
5502
5503// Helpers — canonical operand ordering and suffix-identifier construction.
5504
5505/// Returns the lexically-earlier identifier of two — stable string compare.
5506/// Used to canonicalize operand ordering in emitted IRIs so
5507/// `product_shape!(X, A, B)` and `product_shape!(X, B, A)` produce the
5508/// same `IRI` constant.
5509fn lexically_earlier(a: &Ident, b: &Ident) -> String {
5510    let a_s = a.to_string();
5511    let b_s = b.to_string();
5512    if a_s.as_str() <= b_s.as_str() {
5513        a_s
5514    } else {
5515        b_s
5516    }
5517}
5518
5519fn lexically_later(a: &Ident, b: &Ident) -> String {
5520    let a_s = a.to_string();
5521    let b_s = b.to_string();
5522    if a_s.as_str() > b_s.as_str() {
5523        a_s
5524    } else {
5525        b_s
5526    }
5527}
5528
5529/// Returns the operand pair in canonical order. The caller's original
5530/// (left, right) is reordered so the lexically-earlier identifier is
5531/// returned first. Amendment §4e canonicalization — token-string-based
5532/// proxy for the runtime content fingerprint, documented in the plan.
5533fn canonical_operand_pair(a: &Ident, b: &Ident) -> (Ident, Ident) {
5534    let a_s = a.to_string();
5535    let b_s = b.to_string();
5536    if a_s.as_str() <= b_s.as_str() {
5537        (a.clone(), b.clone())
5538    } else {
5539        (b.clone(), a.clone())
5540    }
5541}
5542
5543/// Type-aware variants of the canonical-ordering helpers. Used by the
5544/// `partition_product!` / `partition_coproduct!` / `cartesian_product_shape!`
5545/// variadic parsers (since v0.4.11) so operands carrying const-generic or
5546/// turbofish arguments (`BigIntShape<128>`, `MerkleRoot<H, 32>`,
5547/// `Tensor<f32, [3, 4]>`) round-trip through the same canonicalization
5548/// rules as bare-Ident operands.
5549fn type_token_string(ty: &syn::Type) -> String {
5550    // Stable string form: the full token stream so generic params and
5551    // path qualifiers participate in canonical ordering.
5552    quote::ToTokens::to_token_stream(ty).to_string()
5553}
5554
5555fn lexically_earlier_ty(a: &syn::Type, b: &syn::Type) -> String {
5556    let a_s = type_token_string(a);
5557    let b_s = type_token_string(b);
5558    if a_s.as_str() <= b_s.as_str() {
5559        a_s
5560    } else {
5561        b_s
5562    }
5563}
5564
5565fn lexically_later_ty(a: &syn::Type, b: &syn::Type) -> String {
5566    let a_s = type_token_string(a);
5567    let b_s = type_token_string(b);
5568    if a_s.as_str() > b_s.as_str() {
5569        a_s
5570    } else {
5571        b_s
5572    }
5573}
5574
5575fn canonical_operand_pair_ty(a: &syn::Type, b: &syn::Type) -> (syn::Type, syn::Type) {
5576    let a_s = type_token_string(a);
5577    let b_s = type_token_string(b);
5578    if a_s.as_str() <= b_s.as_str() {
5579        (a.clone(), b.clone())
5580    } else {
5581        (b.clone(), a.clone())
5582    }
5583}
5584
5585/// Build a new identifier for a `const` declaration. Converts the base
5586/// name to SCREAMING_SNAKE_CASE (so a PascalCase shape name like
5587/// `LeafA_Times_LeafB` becomes `LEAF_A_TIMES_LEAF_B`) and appends the
5588/// suffix, preserving the original's call-site span for error reporting.
5589/// This satisfies Rust's `non_upper_case_globals` lint uniformly
5590/// regardless of the casing style the caller uses for shape names.
5591fn format_ident_suffix(base: &Ident, suffix: &str) -> Ident {
5592    let upper_base = to_screaming_snake(&base.to_string());
5593    let joined = format!("{upper_base}{suffix}");
5594    Ident::new(&joined, base.span())
5595}
5596
5597/// Converts a PascalCase / camelCase / snake_case string to
5598/// SCREAMING_SNAKE_CASE. Handles runs of uppercase letters, interior
5599/// underscores, and digit boundaries so the result is idiomatic
5600/// SCREAMING_SNAKE.
5601/// Convert CamelCase / PascalCase to lower_snake_case.
5602fn camel_to_snake(s: &str) -> String {
5603    to_screaming_snake(s).to_ascii_lowercase()
5604}
5605
5606fn to_screaming_snake(s: &str) -> String {
5607    let mut out = String::with_capacity(s.len() + 4);
5608    let chars: Vec<char> = s.chars().collect();
5609    for (i, ch) in chars.iter().enumerate() {
5610        if *ch == '_' {
5611            if !out.ends_with('_') && !out.is_empty() {
5612                out.push('_');
5613            }
5614            continue;
5615        }
5616        if ch.is_ascii_uppercase() {
5617            // Insert a separator before an uppercase letter when:
5618            //  (a) previous char was lowercase or digit
5619            //  (b) previous was uppercase but next is lowercase (end of a
5620            //      run like `HTTPServer` → `HTTP_Server`).
5621            let prev_lower_or_digit = i > 0
5622                && chars[i - 1] != '_'
5623                && (chars[i - 1].is_ascii_lowercase() || chars[i - 1].is_ascii_digit());
5624            let run_ending = i > 0
5625                && chars[i - 1].is_ascii_uppercase()
5626                && i + 1 < chars.len()
5627                && chars[i + 1].is_ascii_lowercase();
5628            if (prev_lower_or_digit || run_ending) && !out.ends_with('_') && !out.is_empty() {
5629                out.push('_');
5630            }
5631            out.push(*ch);
5632        } else {
5633            out.push(ch.to_ascii_uppercase());
5634        }
5635    }
5636    out
5637}
5638
5639// =====================================================================
5640// `axis!` — wiki ADR-030 substrate-extension axis declaration.
5641//
5642// Declares a sealed `AxisExtension`-bounded trait whose author-supplied
5643// methods become per-method `KERNEL_*` ids; emits the trait, a blanket
5644// `__sdk_seal::Sealed` for any implementor, and a blanket `AxisExtension`
5645// impl whose `dispatch_kernel` routes by `kernel_id` to the matching
5646// trait method.
5647//
5648// Form (the wiki's canonical syntax — lines 3144-3155 of 09-Architecture-
5649// Decisions.md):
5650//
5651// ```text
5652// axis! {
5653//     pub trait MyAxis: ::uor_foundation::pipeline::AxisExtension {
5654//         const AXIS_ADDRESS: &'static str = "https://example.org/axis/MyAxis";
5655//         const MAX_OUTPUT_BYTES: usize = 32;
5656//         fn kernel_one(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
5657//         fn kernel_two(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>;
5658//     }
5659// }
5660// ```
5661//
5662// Emissions:
5663//   - the trait declaration (verbatim re-emit)
5664//   - per-method `pub const KERNEL_<METHOD_UPPER>: u32 = i;` ids
5665//   - a blanket `impl<T: MyAxis> AxisExtension for T` whose `dispatch_kernel`
5666//     matches on `kernel_id` and dispatches to the corresponding method
5667//
5668// Per ADR-030's closure-check (lines 3242-3251), all axis methods MUST
5669// take `(input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation>`
5670// signatures; non-conforming methods produce a closure-violation error.
5671
5672struct AxisInput {
5673    trait_decl: syn::ItemTrait,
5674    /// ADR-055 body clause: the substrate-Term decomposition of the axis
5675    /// kernel, written as a closure `|input| { … }`. The closure body is
5676    /// lowered to a `Term` arena via the standard closure-body grammar
5677    /// (ADR-022 D3 + ADR-026 + ADR-033 + ADR-034 + ADR-035 + ADR-053);
5678    /// the macro emits `impl SubstrateTermBody` returning the arena.
5679    ///
5680    /// Wiki ADR-055 shows the clause placed inside the trait
5681    /// (`fn body = |input| { … };`); that placement is not Rust-valid
5682    /// trait-item syntax, so the SDK exposes it as a `body = |input|
5683    /// { … };` clause after the trait declaration. The two surfaces are
5684    /// semantically identical.
5685    body: Option<syn::ExprClosure>,
5686}
5687
5688impl Parse for AxisInput {
5689    fn parse(input: ParseStream) -> Result<Self> {
5690        let trait_decl: syn::ItemTrait = input.parse()?;
5691        let mut body = None;
5692        if !input.is_empty() {
5693            // ADR-055 body clause syntax: `body = |input| { ... };`
5694            let body_kw: Ident = input.parse()?;
5695            if body_kw != "body" {
5696                return Err(syn::Error::new(
5697                    body_kw.span(),
5698                    "expected `body = |input| { … };` clause after the axis trait declaration (ADR-055)",
5699                ));
5700            }
5701            input.parse::<Token![=]>()?;
5702            let closure: syn::ExprClosure = input.parse()?;
5703            input.parse::<Token![;]>()?;
5704            body = Some(closure);
5705        }
5706        Ok(Self { trait_decl, body })
5707    }
5708}
5709
5710/// `axis!` — wiki ADR-030 substrate-extension axis declaration.
5711#[proc_macro]
5712pub fn axis(input: TokenStream) -> TokenStream {
5713    let parsed = parse_macro_input!(input as AxisInput);
5714    let mut trait_decl = parsed.trait_decl;
5715    // ADR-060: `AxisExtension` is now const-generic over `INLINE_BYTES`, so it
5716    // can no longer be a plain supertrait of the axis trait (the axis trait
5717    // isn't generic over `INLINE_BYTES`). Strip the `: AxisExtension`
5718    // supertrait from the re-emitted trait; the `AxisExtension<INLINE_BYTES>`
5719    // relationship is provided per-struct (blanket over `INLINE_BYTES`) by the
5720    // macro-emitted companion impl, not by a trait-level supertrait bound.
5721    trait_decl.supertraits = syn::punctuated::Punctuated::new();
5722    trait_decl.colon_token = None;
5723    let trait_name = trait_decl.ident.clone();
5724    let body_clause = parsed.body;
5725    // Collect method idents (kernel names).
5726    let mut kernel_idents: Vec<Ident> = Vec::new();
5727    for item in &trait_decl.items {
5728        if let syn::TraitItem::Fn(fn_item) = item {
5729            kernel_idents.push(fn_item.sig.ident.clone());
5730        }
5731    }
5732    // Emit per-method kernel id consts.
5733    let mut kernel_consts: Vec<proc_macro2::TokenStream> = Vec::new();
5734    for (i, ident) in kernel_idents.iter().enumerate() {
5735        let upper_name = ident.to_string().to_ascii_uppercase();
5736        let const_name = Ident::new(&format!("KERNEL_{upper_name}"), ident.span());
5737        let id = i as u32;
5738        kernel_consts.push(quote! {
5739            pub const #const_name: u32 = #id;
5740        });
5741    }
5742    // Emit dispatch_kernel arms. The arms live INSIDE the per-struct
5743    // companion macro's macro_rules body, so the implementing type is
5744    // the `$struct_ident` metavariable (not a generic `T`). The
5745    // proc-macro emits the literal token sequence `$ struct_ident` —
5746    // quote!'s `$` handling preserves it for macro_rules to substitute
5747    // at the companion-macro call site.
5748    let dollar = proc_macro2::Punct::new('$', proc_macro2::Spacing::Joint);
5749    let struct_ident_meta: proc_macro2::TokenStream = quote!(#dollar struct_ident);
5750    let struct_ty_meta: proc_macro2::TokenStream = quote!(#dollar struct_ty);
5751    let dispatch_arms: Vec<proc_macro2::TokenStream> = kernel_idents
5752        .iter()
5753        .enumerate()
5754        .map(|(i, ident)| {
5755            let id = i as u32;
5756            quote! {
5757                #id => <#struct_ident_meta as #trait_name>::#ident(input, out),
5758            }
5759        })
5760        .collect();
5761    let dispatch_arms_generic: Vec<proc_macro2::TokenStream> = kernel_idents
5762        .iter()
5763        .enumerate()
5764        .map(|(i, ident)| {
5765            let id = i as u32;
5766            quote! {
5767                #id => <#struct_ty_meta as #trait_name>::#ident(input, out),
5768            }
5769        })
5770        .collect();
5771
5772    // Wiki ADR-030: the axis trait has `AxisExtension` as a supertrait.
5773    // The blanket `impl<T: MyAxis> AxisExtension for T` cannot be emitted
5774    // here without violating Rust's orphan rule (AxisExtension is a
5775    // foundation-foreign trait when the macro is invoked from an
5776    // application crate). Instead, the macro emits a companion
5777    // `axis_extension_impl_for_<lower_snake_case>!(<StructIdent>)` macro
5778    // the user invokes per implementing struct; that macro produces an
5779    // `impl AxisExtension for <StructIdent>` with the kernel-routed
5780    // dispatch arms. The companion-macro pattern is the standard
5781    // ecosystem idiom for foreign-trait blanket-implementation
5782    // emission (cf. `serde::__impl_de_unsized_impl_block!`).
5783    //
5784    // Foundation-internal axis declarations (foundation-private crates
5785    // invoking `axis!`) get the orphan rule for free because
5786    // `AxisExtension` is local to foundation; the companion macro is
5787    // optional in that context but emitted regardless for surface
5788    // uniformity.
5789    let trait_name_lower = camel_to_snake(&trait_name.to_string());
5790    let companion_macro_ident = Ident::new(
5791        &format!("axis_extension_impl_for_{trait_name_lower}"),
5792        trait_name.span(),
5793    );
5794
5795    // ADR-055 body clause lowering. When the macro's input carries a
5796    // `body = |input| { … };` clause, lower the closure body to a Term
5797    // arena via the standard closure-body grammar (the same path
5798    // `prism_model!` uses for route bodies). The resulting arena lives
5799    // in a const at the axis declaration's surrounding module so the
5800    // companion-macro emission can reference it.
5801    //
5802    // When no body clause is provided, the companion macros emit
5803    // `body_arena() -> &[]` (the primitive-fast-path interpretation per
5804    // ADR-055 — byte-output-equivalent to `dispatch_kernel`).
5805    // ADR-060: a generic-`INLINE_BYTES` `&'static` term slice cannot be
5806    // returned by a generic `const fn` nor stored in a plain const via rvalue
5807    // static promotion (the promoted array's type depends on the const-generic
5808    // parameter). Mirror the verb fix: hold the body arena as an associated
5809    // const on a const-generic zero-sized holder type so the
5810    // `&'static [Term<'static, INLINE_BYTES>]` slice is well-formed for any
5811    // consuming model's foundation-derived inline carrier width.
5812    let body_arena_holder = Ident::new(
5813        &format!("__AxisBody_{}", to_screaming_snake(&trait_name.to_string())),
5814        trait_name.span(),
5815    );
5816    let (body_arena_const, body_arena_expr): (proc_macro2::TokenStream, proc_macro2::TokenStream) =
5817        if let Some(closure) = body_clause {
5818            // The closure must have exactly one input named `input`.
5819            if closure.inputs.len() != 1 {
5820                let err = syn::Error::new_spanned(
5821                    &closure,
5822                    "ADR-055 body clause: closure must have exactly one input named `input`",
5823                );
5824                return err.to_compile_error().into();
5825            }
5826            let input_pat = &closure.inputs[0];
5827            let input_ident: Ident = match input_pat {
5828                syn::Pat::Ident(pi) => pi.ident.clone(),
5829                other => {
5830                    let err = syn::Error::new_spanned(
5831                        other,
5832                        "ADR-055 body clause: closure input must be a bare identifier `input`",
5833                    );
5834                    return err.to_compile_error().into();
5835                }
5836            };
5837            // Lower the closure body. We accept either a block body or an
5838            // expression body; emit_term_for_block handles blocks, while a
5839            // bare-expression body wraps as a single tail expr.
5840            let body_block: syn::Block = match closure.body.as_ref() {
5841                syn::Expr::Block(eb) => eb.block.clone(),
5842                other => {
5843                    let span = quote::ToTokens::to_token_stream(other);
5844                    let parsed: syn::Block = match syn::parse2(quote! { { #span } }) {
5845                        Ok(b) => b,
5846                        Err(e) => return e.to_compile_error().into(),
5847                    };
5848                    parsed
5849                }
5850            };
5851            let mut arena: Vec<TermSpec> = Vec::new();
5852            let mut scope = BindingScope::default();
5853            if let Err(e) = emit_term_for_block(&body_block, &input_ident, &mut arena, &mut scope) {
5854                return e.to_compile_error().into();
5855            }
5856            let has_verb_splices = arena
5857                .iter()
5858                .any(|s| matches!(s, TermSpec::VerbSplice { .. }));
5859            let arena_expr = if has_verb_splices {
5860                render_const_fn_arena_builder(&arena, &quote! { INLINE_BYTES })
5861            } else {
5862                let term_specs = render_arena(&arena);
5863                quote! { &[ #( #term_specs ),* ] }
5864            };
5865            (
5866                quote! {
5867                    #[doc(hidden)]
5868                    #[allow(non_camel_case_types)]
5869                    struct #body_arena_holder<const INLINE_BYTES: usize>;
5870                    #[allow(dead_code)]
5871                    impl<const INLINE_BYTES: usize> #body_arena_holder<INLINE_BYTES> {
5872                        const TERMS: &'static [::uor_foundation::enforcement::Term<'static, INLINE_BYTES>] =
5873                            #arena_expr;
5874                    }
5875                },
5876                quote! { #body_arena_holder::<INLINE_BYTES>::TERMS },
5877            )
5878        } else {
5879            // No body clause — primitive-fast-path interpretation.
5880            (quote! {}, quote! { &[] })
5881        };
5882    let expansion = quote! {
5883        #trait_decl
5884
5885        #(#kernel_consts)*
5886
5887        // ADR-055 body-arena const (emitted when a `body = …;` clause is
5888        // present; absent otherwise — companion-macro uses the empty-arena
5889        // primitive-fast-path interpretation).
5890        #body_arena_const
5891
5892        /// Wiki ADR-030 companion macro: instantiate `AxisExtension`
5893        /// for a concrete struct implementing this axis trait. The
5894        /// macro emits an `impl AxisExtension for <StructIdent>` block
5895        /// that delegates `dispatch_kernel` to the axis-trait methods
5896        /// per `KERNEL_*` id. The orphan-rule-conformant per-struct
5897        /// impl mechanism replacing the blanket
5898        /// `impl<T: <axis>> AxisExtension for T` (which would violate
5899        /// Rust's orphan rule from any crate that does not define
5900        /// `AxisExtension`).
5901        #[macro_export]
5902        macro_rules! #companion_macro_ident {
5903            // Non-generic form: simple struct ident.
5904            ($struct_ident:ident) => {
5905                // ADR-055: SubstrateTermBody supertrait. The body_arena
5906                // resolves to the `BODY_ARENA_<AXIS>` const if the axis
5907                // declaration carried a `body = …;` clause, otherwise to
5908                // the empty slice (primitive-fast-path interpretation
5909                // where dispatch_kernel is byte-output-equivalent).
5910                impl ::uor_foundation::pipeline::__sdk_seal::Sealed for $struct_ident {}
5911                impl<const INLINE_BYTES: usize> ::uor_foundation::pipeline::SubstrateTermBody<INLINE_BYTES> for $struct_ident {
5912                    fn body_arena() -> &'static [::uor_foundation::enforcement::Term<'static, INLINE_BYTES>] {
5913                        #body_arena_expr
5914                    }
5915                }
5916                impl<const INLINE_BYTES: usize, const FP_MAX: usize> ::uor_foundation::pipeline::AxisExtension<INLINE_BYTES, FP_MAX> for $struct_ident {
5917                    const AXIS_ADDRESS: &'static str =
5918                        <$struct_ident as #trait_name>::AXIS_ADDRESS;
5919                    const MAX_OUTPUT_BYTES: usize =
5920                        <$struct_ident as #trait_name>::MAX_OUTPUT_BYTES;
5921                    fn dispatch_kernel(
5922                        kernel_id: u32,
5923                        input: &[u8],
5924                        out: &mut [u8],
5925                    ) -> ::core::result::Result<
5926                        usize,
5927                        ::uor_foundation::enforcement::ShapeViolation,
5928                    > {
5929                        match kernel_id {
5930                            #(#dispatch_arms)*
5931                            _ => Err(::uor_foundation::enforcement::ShapeViolation {
5932                                shape_iri:
5933                                    "https://uor.foundation/axis/AxisExtensionShape",
5934                                constraint_iri:
5935                                    "https://uor.foundation/axis/AxisExtensionShape/kernelId",
5936                                property_iri:
5937                                    "https://uor.foundation/axis/kernelId",
5938                                expected_range:
5939                                    "https://uor.foundation/axis/RecognisedKernelId",
5940                                min_count: 0,
5941                                max_count: 0,
5942                                kind: ::uor_foundation::ViolationKind::ValueCheck,
5943                            }),
5944                        }
5945                    }
5946                }
5947            };
5948            // ADR-052 @generic form: parametric Layer-3 axis implementations.
5949            // Accepts the implementing type plus generic parameter list and
5950            // optional where-clauses, emitting the `AxisExtension` impl with
5951            // the same kernel-id-dispatch surface.
5952            //
5953            // Examples:
5954            //   axis_extension_impl_for_x!(@generic MyImpl<T>, [T], where [T: Bound]);
5955            //   axis_extension_impl_for_x!(@generic Wrap<T, const N: usize>, [T, const N: usize]);
5956            //
5957            // The first repeating group is the generic parameter list as
5958            // it would appear after `impl<...>`. The optional `where [...]`
5959            // group carries any predicates.
5960            (@generic $struct_ty:ty, [$($generic_params:tt)*] $(, where [$($where_clauses:tt)*])?) => {
5961                impl<$($generic_params)*> ::uor_foundation::pipeline::__sdk_seal::Sealed for $struct_ty
5962                $(where $($where_clauses)*)?
5963                {}
5964                impl<const INLINE_BYTES: usize, $($generic_params)*> ::uor_foundation::pipeline::SubstrateTermBody<INLINE_BYTES> for $struct_ty
5965                $(where $($where_clauses)*)?
5966                {
5967                    fn body_arena() -> &'static [::uor_foundation::enforcement::Term<'static, INLINE_BYTES>] {
5968                        #body_arena_expr
5969                    }
5970                }
5971                impl<const INLINE_BYTES: usize, const FP_MAX: usize, $($generic_params)*> ::uor_foundation::pipeline::AxisExtension<INLINE_BYTES, FP_MAX> for $struct_ty
5972                $(where $($where_clauses)*)?
5973                {
5974                    const AXIS_ADDRESS: &'static str =
5975                        <$struct_ty as #trait_name>::AXIS_ADDRESS;
5976                    const MAX_OUTPUT_BYTES: usize =
5977                        <$struct_ty as #trait_name>::MAX_OUTPUT_BYTES;
5978                    fn dispatch_kernel(
5979                        kernel_id: u32,
5980                        input: &[u8],
5981                        out: &mut [u8],
5982                    ) -> ::core::result::Result<
5983                        usize,
5984                        ::uor_foundation::enforcement::ShapeViolation,
5985                    > {
5986                        match kernel_id {
5987                            #(#dispatch_arms_generic)*
5988                            _ => Err(::uor_foundation::enforcement::ShapeViolation {
5989                                shape_iri:
5990                                    "https://uor.foundation/axis/AxisExtensionShape",
5991                                constraint_iri:
5992                                    "https://uor.foundation/axis/AxisExtensionShape/kernelId",
5993                                property_iri:
5994                                    "https://uor.foundation/axis/kernelId",
5995                                expected_range:
5996                                    "https://uor.foundation/axis/RecognisedKernelId",
5997                                min_count: 0,
5998                                max_count: 0,
5999                                kind: ::uor_foundation::ViolationKind::ValueCheck,
6000                            }),
6001                        }
6002                    }
6003                }
6004            };
6005        }
6006    };
6007    expansion.into()
6008}
6009
6010// =====================================================================
6011// `resolver!` — wiki ADR-036 ResolverTuple declaration macro.
6012//
6013// Application authors declare ResolverTuple impls through this macro
6014// paralleling `axis!` per ADR-030. The macro recognizes eight canonical
6015// field names — one per `ResolverCategory` variant — and emits the
6016// `ResolverTuple` impl, the per-category accessor methods, the
6017// `Has<Category>Resolver<H>` satisfactions, and the seal.
6018//
6019// Form:
6020//
6021// ```text
6022// resolver! {
6023//     pub struct MyApplicationResolvers<H: ::uor_foundation::enforcement::Hasher> {
6024//         nerve: MyNerveResolver<H>,
6025//         chain_complex: MyChainComplexResolver<H>,
6026//         // ... only the fields the application's verbs require;
6027//         //     missing categories default to foundation's Null impls
6028//         //     (which propagate RESOLVER_ABSENT through Term::Try).
6029//     }
6030// }
6031// ```
6032//
6033// Recognised field names (each maps to one `ResolverCategory` + one
6034// resolver trait):
6035//   - nerve              → NerveResolver<H>
6036//   - chain_complex      → ChainComplexResolver<H>
6037//   - homology_groups    → HomologyGroupResolver<H>
6038//   - cochain_complex    → CochainComplexResolver<H>
6039//   - cohomology_groups  → CohomologyGroupResolver<H>
6040//   - postnikov          → PostnikovResolver<H>
6041//   - homotopy_groups    → HomotopyGroupResolver<H>
6042//   - k_invariants       → KInvariantResolver<H>
6043//
6044// Unrecognised field names fail with a closure-violation error at
6045// proc-macro expansion citing the recognised field-name set.
6046
6047struct ResolverInput {
6048    struct_vis: syn::Visibility,
6049    struct_name: Ident,
6050    hasher_param: Ident,
6051    fields: Vec<(Ident, syn::Type)>,
6052    /// ADR-057: optional shape-registry marker type. When the application's
6053    /// `resolver!` declaration carries a `shape_registry: MyRegistry` clause,
6054    /// the emitted `ResolverTuple` impl sets `type ShapeRegistry = MyRegistry`
6055    /// — ψ_1's `NerveResolver` consults `R::REGISTRY` plus foundation's
6056    /// built-in registry when expanding `ConstraintRef::Recurse`. Defaults
6057    /// to `EmptyShapeRegistry` (foundation built-in registry only) when the
6058    /// clause is absent.
6059    shape_registry: Option<syn::Type>,
6060}
6061
6062impl Parse for ResolverInput {
6063    fn parse(input: ParseStream) -> Result<Self> {
6064        let struct_vis: syn::Visibility = input.parse()?;
6065        input.parse::<Token![struct]>()?;
6066        let struct_name: Ident = input.parse()?;
6067        // Parse the generic Hasher parameter `<H: ::uor_foundation::enforcement::Hasher>`
6068        // — we only need the parameter's identifier (H or whatever name)
6069        // for emitting the impls. The bound is enforced by the trait
6070        // definitions in foundation.
6071        input.parse::<Token![<]>()?;
6072        let hasher_param: Ident = input.parse()?;
6073        // Skip past the trait bound (`:` followed by a path).
6074        if input.peek(Token![:]) {
6075            input.parse::<Token![:]>()?;
6076            let _: syn::Type = input.parse()?;
6077        }
6078        input.parse::<Token![>]>()?;
6079        let body;
6080        syn::braced!(body in input);
6081        let mut fields: Vec<(Ident, syn::Type)> = Vec::new();
6082        let mut shape_registry: Option<syn::Type> = None;
6083        while !body.is_empty() {
6084            // `pub` is admitted but optional — discard.
6085            if body.peek(Token![pub]) {
6086                let _: syn::Visibility = body.parse()?;
6087            }
6088            let field_name: Ident = body.parse()?;
6089            body.parse::<Token![:]>()?;
6090            let field_ty: syn::Type = body.parse()?;
6091            // ADR-057: the reserved `shape_registry` clause names the
6092            // application's ShapeRegistryProvider marker type; it is NOT
6093            // a resolver-category field.
6094            if field_name == "shape_registry" {
6095                if shape_registry.is_some() {
6096                    return Err(syn::Error::new_spanned(
6097                        &field_name,
6098                        "`resolver!` shape_registry clause specified twice",
6099                    ));
6100                }
6101                shape_registry = Some(field_ty);
6102            } else {
6103                fields.push((field_name, field_ty));
6104            }
6105            if body.peek(Token![,]) {
6106                body.parse::<Token![,]>()?;
6107            }
6108        }
6109        Ok(Self {
6110            struct_vis,
6111            struct_name,
6112            hasher_param,
6113            fields,
6114            shape_registry,
6115        })
6116    }
6117}
6118
6119const RESOLVER_FIELD_TABLE: &[(&str, &str, &str, &str, &str)] = &[
6120    // (field_name, ResolverCategory, ResolverTrait, MarkerTrait, accessor_method)
6121    (
6122        "nerve",
6123        "Nerve",
6124        "NerveResolver",
6125        "HasNerveResolver",
6126        "nerve_resolver",
6127    ),
6128    (
6129        "chain_complex",
6130        "ChainComplex",
6131        "ChainComplexResolver",
6132        "HasChainComplexResolver",
6133        "chain_complex_resolver",
6134    ),
6135    (
6136        "homology_groups",
6137        "HomologyGroup",
6138        "HomologyGroupResolver",
6139        "HasHomologyGroupResolver",
6140        "homology_group_resolver",
6141    ),
6142    (
6143        "cochain_complex",
6144        "CochainComplex",
6145        "CochainComplexResolver",
6146        "HasCochainComplexResolver",
6147        "cochain_complex_resolver",
6148    ),
6149    (
6150        "cohomology_groups",
6151        "CohomologyGroup",
6152        "CohomologyGroupResolver",
6153        "HasCohomologyGroupResolver",
6154        "cohomology_group_resolver",
6155    ),
6156    (
6157        "postnikov",
6158        "Postnikov",
6159        "PostnikovResolver",
6160        "HasPostnikovResolver",
6161        "postnikov_resolver",
6162    ),
6163    (
6164        "homotopy_groups",
6165        "HomotopyGroup",
6166        "HomotopyGroupResolver",
6167        "HasHomotopyGroupResolver",
6168        "homotopy_group_resolver",
6169    ),
6170    (
6171        "k_invariants",
6172        "KInvariant",
6173        "KInvariantResolver",
6174        "HasKInvariantResolver",
6175        "k_invariant_resolver",
6176    ),
6177];
6178
6179/// `resolver!` — wiki ADR-036 ResolverTuple declaration macro.
6180///
6181/// Declares a sealed `ResolverTuple` impl from a struct-bodied field
6182/// list. Each field name MUST be one of the eight canonical resolver
6183/// categories; the field type is the application-author's resolver
6184/// trait impl for that category (parameterized by the model's Hasher).
6185///
6186/// The macro emits all eight `Has<Category>Resolver<H>` impls so the
6187/// resulting struct satisfies `run_route`'s where-clause unconditionally.
6188/// Declared fields delegate to the user's resolver value; undeclared
6189/// categories delegate to `NullResolverTuple`, whose `resolve` returns
6190/// the `RESOLVER_ABSENT` shape violation (recoverable through
6191/// `Term::Try`'s default-propagation handler per ADR-022 D3 G9).
6192#[proc_macro]
6193pub fn resolver(input: TokenStream) -> TokenStream {
6194    let parsed = parse_macro_input!(input as ResolverInput);
6195    let ResolverInput {
6196        struct_vis,
6197        struct_name,
6198        hasher_param,
6199        fields,
6200        shape_registry,
6201    } = parsed;
6202    // Validate field names against the canonical table.
6203    let recognised: Vec<&str> = RESOLVER_FIELD_TABLE.iter().map(|t| t.0).collect();
6204    for (name, _) in &fields {
6205        let name_str = name.to_string();
6206        if !recognised.contains(&name_str.as_str()) {
6207            let recognised_csv = recognised.join(", ");
6208            return syn::Error::new_spanned(
6209                name,
6210                format!(
6211                    "closure violation: `resolver!` field `{name_str}` is not a recognised resolver category. Recognised: {recognised_csv}"
6212                ),
6213            )
6214            .to_compile_error()
6215            .into();
6216        }
6217    }
6218    // Build the struct fields token stream.
6219    let struct_fields: Vec<proc_macro2::TokenStream> = fields
6220        .iter()
6221        .map(|(name, ty)| quote::quote! { pub #name: #ty, })
6222        .collect();
6223    // Build the CATEGORIES const array.
6224    let category_idents: Vec<proc_macro2::TokenStream> = fields
6225        .iter()
6226        .map(|(name, _)| {
6227            // Field name was validated above against RESOLVER_FIELD_TABLE,
6228            // so `find` is guaranteed to return Some. Fall back to the
6229            // first entry on the impossible None branch to avoid `.expect`.
6230            let entry = RESOLVER_FIELD_TABLE
6231                .iter()
6232                .find(|t| t.0 == name.to_string().as_str())
6233                .unwrap_or(&RESOLVER_FIELD_TABLE[0]);
6234            let cat = syn::Ident::new(entry.1, name.span());
6235            quote::quote! { ::uor_foundation::pipeline::ResolverCategory::#cat }
6236        })
6237        .collect();
6238    let arity = fields.len();
6239    // ADR-036: emit ALL 8 Has<Category>Resolver<H> impls so user-declared
6240    // ResolverTuple structs satisfy `run_route`'s where-clause regardless
6241    // of which categories the application chose to populate. For each
6242    // category:
6243    //   - declared field: delegate to `&self.<field_name>`
6244    //   - undeclared:    delegate to `&NullResolverTuple` (foundation's
6245    //                    Null impls satisfy every resolver trait for any
6246    //                    `H: Hasher`, emitting RESOLVER_ABSENT at resolve
6247    //                    time per ADR-022 D3 G9).
6248    let has_impls: Vec<proc_macro2::TokenStream> = RESOLVER_FIELD_TABLE
6249        .iter()
6250        .map(|entry| {
6251            let cat_field = entry.0;
6252            let resolver_trait = syn::Ident::new(entry.2, struct_name.span());
6253            let marker = syn::Ident::new(entry.3, struct_name.span());
6254            let accessor = syn::Ident::new(entry.4, struct_name.span());
6255            if let Some((field_name, field_ty)) = fields
6256                .iter()
6257                .find(|(n, _)| *n == cat_field)
6258            {
6259                // Declared category: where-clause asserts the user's
6260                // field type impls the resolver trait, and the accessor
6261                // returns a borrow of that field. ADR-060: blanket over the
6262                // carrier inline width `INLINE_BYTES` so the tuple satisfies
6263                // any model's `run_route` regardless of its `HostBounds`.
6264                quote::quote! {
6265                    impl<const INLINE_BYTES: usize, #hasher_param: ::uor_foundation::enforcement::Hasher>
6266                        ::uor_foundation::pipeline::#marker<INLINE_BYTES, #hasher_param>
6267                        for #struct_name<#hasher_param>
6268                    where
6269                        #field_ty: ::uor_foundation::pipeline::#resolver_trait<INLINE_BYTES, #hasher_param>,
6270                    {
6271                        fn #accessor(&self) -> &dyn ::uor_foundation::pipeline::#resolver_trait<INLINE_BYTES, #hasher_param> {
6272                            &self.#field_name
6273                        }
6274                    }
6275                }
6276            } else {
6277                // Undeclared category: accessor returns a static borrow
6278                // of `NullResolverTuple` (unit-struct const-promotion).
6279                // `NullResolverTuple` already impls every resolver trait
6280                // for any `H: Hasher` and `INLINE_BYTES` (foundation,
6281                // ADR-036 + ADR-060), so the `&dyn` coercion is direct.
6282                quote::quote! {
6283                    impl<const INLINE_BYTES: usize, #hasher_param: ::uor_foundation::enforcement::Hasher>
6284                        ::uor_foundation::pipeline::#marker<INLINE_BYTES, #hasher_param>
6285                        for #struct_name<#hasher_param>
6286                    {
6287                        fn #accessor(&self) -> &dyn ::uor_foundation::pipeline::#resolver_trait<INLINE_BYTES, #hasher_param> {
6288                            &::uor_foundation::pipeline::NullResolverTuple
6289                        }
6290                    }
6291                }
6292            }
6293        })
6294        .collect();
6295    // Build a `Default::default()`-per-field initializer list so the
6296    // macro-emitted `Default` impl can construct the tuple struct without
6297    // referencing user types directly. The Default impl carries explicit
6298    // `where #ty: Default` bounds for each declared field, so field types
6299    // that aren't `Default` simply make the impl uninstantiable (the
6300    // user supplies `fn resolvers() -> R` in `prism_model!` instead).
6301    let default_field_inits: Vec<proc_macro2::TokenStream> = fields
6302        .iter()
6303        .map(|(name, _)| quote::quote! { #name: ::core::default::Default::default(), })
6304        .collect();
6305    let default_where_clauses: Vec<proc_macro2::TokenStream> = fields
6306        .iter()
6307        .map(|(_, ty)| quote::quote! { #ty: ::core::default::Default, })
6308        .collect();
6309    // ADR-057: resolve the ShapeRegistry associated type — either the
6310    // user's `shape_registry: MyRegistry` clause, or foundation's
6311    // EmptyShapeRegistry default.
6312    let shape_registry_ty: proc_macro2::TokenStream = match shape_registry {
6313        Some(ty) => quote::quote! { #ty },
6314        None => quote::quote! {
6315            ::uor_foundation::pipeline::shape_iri_registry::EmptyShapeRegistry
6316        },
6317    };
6318    let expansion = quote::quote! {
6319        #struct_vis struct #struct_name<#hasher_param: ::uor_foundation::enforcement::Hasher> {
6320            #(#struct_fields)*
6321            #[doc(hidden)]
6322            pub _phantom: ::core::marker::PhantomData<#hasher_param>,
6323        }
6324
6325        impl<#hasher_param: ::uor_foundation::enforcement::Hasher>
6326            ::uor_foundation::pipeline::__sdk_seal::Sealed
6327            for #struct_name<#hasher_param> {}
6328
6329        impl<#hasher_param: ::uor_foundation::enforcement::Hasher>
6330            ::uor_foundation::pipeline::ResolverTuple
6331            for #struct_name<#hasher_param>
6332        {
6333            const ARITY: usize = #arity;
6334            const CATEGORIES: &'static [::uor_foundation::pipeline::ResolverCategory] =
6335                &[#(#category_idents),*];
6336            // ADR-057: ψ_1's NerveResolver consults this registry when
6337            // expanding ConstraintRef::Recurse. Defaults to foundation's
6338            // EmptyShapeRegistry when the application's `resolver!`
6339            // declaration omits the `shape_registry: MyRegistry` clause.
6340            type ShapeRegistry = #shape_registry_ty;
6341        }
6342
6343        // ADR-036 + prism_model!-default-construction interop: emit
6344        // `Default` so the macro-emitted `forward` body can call
6345        // `<R as Default>::default()` when the user does not supply an
6346        // explicit `fn resolvers() -> R` clause. Each declared field
6347        // type appears in the `where`-clause; fields that don't impl
6348        // `Default` make this impl uninstantiable at use-site (the
6349        // application supplies the explicit clause instead).
6350        impl<#hasher_param: ::uor_foundation::enforcement::Hasher>
6351            ::core::default::Default
6352            for #struct_name<#hasher_param>
6353        where
6354            #(#default_where_clauses)*
6355        {
6356            fn default() -> Self {
6357                Self {
6358                    #(#default_field_inits)*
6359                    _phantom: ::core::marker::PhantomData,
6360                }
6361            }
6362        }
6363
6364        #(#has_impls)*
6365    };
6366    expansion.into()
6367}