Skip to main content

prismqueer_projections/
lib.rs

1//! Derive macros for prism-core.
2//!
3//! `#[derive(Prism)]` with `#[oid("@something")]` generates:
4//! - `Addressable` impl (Oid from the `@name` string, deterministic)
5//! - `Display` impl (prints the `@name`)
6//!
7//! Field-level optic annotations generate accessor structs:
8//! - `#[lens]` — total, bidirectional. Generates `FieldNameLens` with `view`/`set`.
9//! - `#[prism]` — partial access. Generates `FieldNamePrism` with `extract`/`review`.
10//! - `#[traversal]` — multiple targets. Generates `FieldNameTraversal` with `traverse`/`traverse_mut`.
11//! - `#[iso]` — round-trip lossless. Generates `FieldNameIso` with `forward`/`backward`.
12//!
13//! Also generates `optic_fields()` returning `&'static [FieldOptic]` metadata.
14
15extern crate proc_macro;
16
17use proc_macro::TokenStream;
18use proc_macro2::Span;
19use quote::{format_ident, quote};
20use syn::{parse_macro_input, Data, DeriveInput, Expr, Fields, Lit, Meta, Type};
21
22// T23 (2026-06-08): the macro-reception sub-prism. Realises
23// `@code/rust/macro.shim_type` per
24// `mirror/shards/code/rust/macro.mirror`. The function-like
25// proc-macro `declaration!{}` reads a substrate `type` declaration
26// and emits the Rust struct/enum that realises it. Round-trip OID
27// witness: emission is a deterministic function of the substrate
28// input (the `oid_function(shim_type)` law). Per the spec's §12
29// implementation cascade, this tick lands `type` only; `prism`,
30// `action`, `grammar` are next cascade ticks.
31mod declaration;
32
33/// `declaration!{}` — the @code/rust/macro reception entry point.
34///
35/// Realises the four shims declared at
36/// `mirror/shards/code/metalogue.mirror` (universal ground; the
37/// 34th-instance reframe lifted the contract from per-species
38/// `@code/X/macro` to `@code/metalogue`) and bound to
39/// `code/rust.ast` at `mirror/shards/code/rust/macro.mirror`. Reads
40/// a substrate declaration (passed as token-tree input) and emits
41/// the Rust item that realises it.
42///
43/// # Input grammar (T23 + T24 + T25 + T26)
44///
45/// ```ignore
46/// // T23 — `type` declarations.
47/// // Record shape:
48/// declaration!{ type Foo { a: u32, b: u64 } }
49/// // Sum shape (closed sum of unit variants):
50/// declaration!{ type Color = red | green | blue }
51/// // Sum shape with parametric variants:
52/// declaration!{ type Result = ok(u32) | err(u32) }
53///
54/// // T24 — `prism` declarations.
55/// // Bare path:
56/// declaration!{ prism @parse }
57/// // With five-op block:
58/// declaration!{
59///     prism @kernel {
60///         focus kernel  project kernel  split kernel
61///         shift kernel  settle kernel
62///     }
63/// }
64///
65/// // T25 — `action` declarations.
66/// // Substrate `action name(args) -> ret { \ }` — emits `pub fn`.
67/// // Rust-altitude call site: `{ }` (empty body) per the
68/// // substrate-pull seam; the shim emits `todo!()`.
69/// declaration!{ action increment(x: u32) -> u32 { } }
70///
71/// // T26 — `grammar` declarations.
72/// // Substrate `grammar @path(ext1, ext2, ...) { body }` — emits a
73/// // Rust unit struct with #[derive(prismqueer::DerivePrism)] +
74/// // #[oid("@path")]. Same floor as `prism`; the path IS the
75/// // runtime address. Extension list and body parsed-but-not-encoded.
76/// declaration!{ grammar @mirror_spec("spec") { focus project } }
77/// ```
78///
79/// # Four laws (per spec §5)
80///
81/// 1. Type-soundness: emitted Rust is well-typed when the substrate
82///    declaration is well-typed.
83/// 2. Round-trip identity: `syn::parse2::<Item>(emission)` succeeds
84///    — emission is itself a valid Rust declaration.
85/// 3. OID functionality: same substrate input → byte-identical Rust
86///    emission → same OID. For `prism` declarations, the runtime
87///    `Addressable::oid(&Path)` equals `Oid::hash("@path".as_bytes())`
88///    — the substrate path IS the runtime address.
89/// 4. Substrate-pull preservation: emission references only
90///    primitive Rust types and constructs already at the
91///    `@code/rust` altitude (`pub struct`, `pub enum`,
92///    `#[derive(prismqueer::DerivePrism)]`, primitive integer
93///    types).
94///
95/// `action` (T25) and `grammar` (T26) declarations are supported;
96/// the four-shim cascade is now total at the Rust altitude.
97#[proc_macro]
98pub fn declaration(input: TokenStream) -> TokenStream {
99    declaration::expand(input.into()).into()
100}
101
102/// Which optic annotation a field carries.
103#[derive(Clone, Copy)]
104enum FieldKind {
105    Lens,
106    Prism,
107    Traversal,
108    Iso,
109}
110
111/// A field with its optic annotation.
112struct AnnotatedField {
113    ident: syn::Ident,
114    ty: Type,
115    kind: FieldKind,
116}
117
118/// Derive `Addressable`, `Display`, optic accessors, and `optic_fields()`.
119///
120/// # Struct-level attributes
121///
122/// - `#[oid("@name")]` — required. The `@name` must start with `@`.
123///
124/// # Field-level attributes
125///
126/// - `#[lens]` — total, bidirectional access. Compile error on `Option<T>`.
127/// - `#[prism]` — partial access. Typically on `Option<T>` or `Result<T, E>`.
128/// - `#[traversal]` — multiple targets. Typically on `Vec<T>`.
129/// - `#[iso]` — round-trip lossless conversion.
130/// - `#[prism(inner)]` — delegation marker (recognized, no code generated yet).
131///
132/// # Example
133///
134/// ```ignore
135/// #[derive(Prism)]
136/// #[oid("@claims")]
137/// struct ClaimProcessor {
138///     #[lens]
139///     adjuster_id: u64,
140///     #[prism]
141///     override_reason: Option<String>,
142///     #[traversal]
143///     history: Vec<Event>,
144/// }
145/// ```
146#[proc_macro_derive(Prism, attributes(oid, prism, lens, traversal, iso))]
147pub fn derive_prism(input: TokenStream) -> TokenStream {
148    let input = parse_macro_input!(input as DeriveInput);
149    let name = &input.ident;
150    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
151
152    // Extract #[oid("@something")] from struct attributes
153    let oid_name = extract_oid_name(&input, "Prism");
154
155    // Extract annotated fields from struct body
156    let annotated = extract_annotated_fields(&input);
157
158    // Validate annotations
159    validate_annotations(&annotated);
160
161    // Generate accessor structs for each annotated field
162    let accessor_structs = generate_accessors(name, &annotated);
163
164    // Generate optic_fields() metadata
165    let optic_fields_impl =
166        generate_optic_fields(name, &annotated, &impl_generics, &ty_generics, where_clause);
167
168    let expanded = quote! {
169        impl #impl_generics prismqueer::Addressable for #name #ty_generics #where_clause {
170            fn oid(&self) -> prismqueer::Oid {
171                prismqueer::Oid::hash(#oid_name.as_bytes())
172            }
173        }
174
175        impl #impl_generics ::std::fmt::Display for #name #ty_generics #where_clause {
176            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
177                write!(f, "{}", #oid_name)
178            }
179        }
180
181        #accessor_structs
182
183        #optic_fields_impl
184    };
185
186    TokenStream::from(expanded)
187}
188
189/// Derive `Composable`, `LambdaImpl`, `Addressable`, and `Display` for a named lambda.
190///
191/// # Struct-level attributes
192///
193/// - `#[oid("@name")]` — required. The `@name` must start with `@`.
194///
195/// # Generated impls
196///
197/// - `Addressable` — Oid from the `@name` string
198/// - `Display` — prints the `@name`
199/// - `LambdaImpl` — default identity body (Abs(oid, Bind(oid)))
200/// - `Composable` — `.then()` composition producing `Composed<T>`
201///
202/// # Example
203///
204/// ```ignore
205/// #[derive(Lambda)]
206/// #[oid("@parse")]
207/// pub struct Parse;
208/// ```
209#[proc_macro_derive(Lambda, attributes(oid))]
210pub fn derive_lambda(input: TokenStream) -> TokenStream {
211    let input = parse_macro_input!(input as DeriveInput);
212    let name = &input.ident;
213    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
214
215    let oid_name = extract_oid_name(&input, "Lambda");
216
217    let expanded = quote! {
218        impl #impl_generics prismqueer::Addressable for #name #ty_generics #where_clause {
219            fn oid(&self) -> prismqueer::Oid {
220                prismqueer::Oid::hash(#oid_name.as_bytes())
221            }
222        }
223
224        impl #impl_generics ::std::fmt::Display for #name #ty_generics #where_clause {
225            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
226                write!(f, "{}", #oid_name)
227            }
228        }
229
230        impl<__LambdaT: Clone + PartialEq> ::std::convert::From<#name> for prismqueer::lambda::Lambda<__LambdaT> {
231            fn from(_: #name) -> prismqueer::lambda::Lambda<__LambdaT> {
232                let oid = prismqueer::Oid::hash(#oid_name.as_bytes());
233                prismqueer::lambda::Lambda::abs(oid.clone(), prismqueer::lambda::Lambda::bind(oid))
234            }
235        }
236
237        impl<__LambdaT: Clone + PartialEq> prismqueer::lambda::Composable<__LambdaT> for #name #ty_generics #where_clause {}
238    };
239
240    TokenStream::from(expanded)
241}
242
243fn extract_oid_name(input: &DeriveInput, derive_name: &str) -> String {
244    for attr in &input.attrs {
245        if attr.path().is_ident("oid") {
246            if let Meta::List(meta_list) = &attr.meta {
247                let tokens = meta_list.tokens.clone();
248                if let Ok(expr) = syn::parse2::<Expr>(tokens) {
249                    if let Expr::Lit(lit) = expr {
250                        if let Lit::Str(s) = &lit.lit {
251                            let val = s.value();
252                            if !val.starts_with('@') {
253                                panic!("#[oid(\"...\")] value must start with '@', got: {:?}", val);
254                            }
255                            return val;
256                        }
257                    }
258                }
259            }
260        }
261    }
262    panic!(
263        "#[derive({})] requires #[oid(\"@name\")] attribute",
264        derive_name
265    );
266}
267
268fn extract_annotated_fields(input: &DeriveInput) -> Vec<AnnotatedField> {
269    let fields = match &input.data {
270        Data::Struct(data) => match &data.fields {
271            Fields::Named(named) => &named.named,
272            _ => return Vec::new(),
273        },
274        _ => return Vec::new(),
275    };
276
277    let mut annotated = Vec::new();
278
279    for field in fields {
280        let ident = match &field.ident {
281            Some(id) => id.clone(),
282            None => continue,
283        };
284
285        let mut kind = None;
286
287        for attr in &field.attrs {
288            if attr.path().is_ident("lens") {
289                kind = Some(FieldKind::Lens);
290            } else if attr.path().is_ident("prism") {
291                // Distinguish #[prism] (optic) from #[prism(inner)] (delegation)
292                match &attr.meta {
293                    Meta::Path(_) => {
294                        kind = Some(FieldKind::Prism);
295                    }
296                    Meta::List(_) => {
297                        // #[prism(inner)] — delegation marker, skip
298                    }
299                    _ => {}
300                }
301            } else if attr.path().is_ident("traversal") {
302                kind = Some(FieldKind::Traversal);
303            } else if attr.path().is_ident("iso") {
304                kind = Some(FieldKind::Iso);
305            }
306        }
307
308        if let Some(k) = kind {
309            annotated.push(AnnotatedField {
310                ident,
311                ty: field.ty.clone(),
312                kind: k,
313            });
314        }
315    }
316
317    annotated
318}
319
320/// Validate optic annotations against field types.
321fn validate_annotations(fields: &[AnnotatedField]) {
322    for field in fields {
323        match field.kind {
324            FieldKind::Lens => {
325                // #[lens] on Option<T> is a compile error
326                if is_option_type(&field.ty) {
327                    panic!(
328                        "#[lens] on field `{}`: Lens is total, but Option<T> is partial. Use #[prism] instead.",
329                        field.ident
330                    );
331                }
332            }
333            _ => {}
334        }
335    }
336}
337
338/// Check if a type is `Option<T>`.
339fn is_option_type(ty: &Type) -> bool {
340    if let Type::Path(type_path) = ty {
341        if let Some(segment) = type_path.path.segments.last() {
342            return segment.ident == "Option";
343        }
344    }
345    false
346}
347
348/// Check if a type is `Vec<T>`.
349fn is_vec_type(ty: &Type) -> bool {
350    if let Type::Path(type_path) = ty {
351        if let Some(segment) = type_path.path.segments.last() {
352            return segment.ident == "Vec";
353        }
354    }
355    false
356}
357
358/// Extract the inner type from `Option<T>` or `Vec<T>`.
359fn extract_inner_type(ty: &Type) -> Option<&Type> {
360    if let Type::Path(type_path) = ty {
361        if let Some(segment) = type_path.path.segments.last() {
362            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
363                if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
364                    return Some(inner);
365                }
366            }
367        }
368    }
369    None
370}
371
372/// Convert a snake_case field name to PascalCase for the accessor struct name.
373fn to_pascal_case(s: &str) -> String {
374    s.split('_')
375        .filter(|part| !part.is_empty())
376        .map(|part| {
377            let mut chars = part.chars();
378            match chars.next() {
379                Some(c) => c.to_uppercase().to_string() + chars.as_str(),
380                None => String::new(),
381            }
382        })
383        .collect()
384}
385
386fn generate_accessors(
387    struct_name: &syn::Ident,
388    fields: &[AnnotatedField],
389) -> proc_macro2::TokenStream {
390    let mut tokens = proc_macro2::TokenStream::new();
391
392    for field in fields {
393        let field_ident = &field.ident;
394        let field_name = field.ident.to_string();
395        let pascal = to_pascal_case(&field_name);
396
397        match field.kind {
398            FieldKind::Lens => {
399                let accessor_name = format_ident!("{}Lens", pascal, span = Span::call_site());
400                let field_ty = &field.ty;
401
402                tokens.extend(quote! {
403                    /// Lens accessor for field `#field_name`. Total, bidirectional.
404                    pub struct #accessor_name;
405
406                    impl #accessor_name {
407                        /// Get a reference to the field value.
408                        pub fn view(source: &#struct_name) -> &#field_ty {
409                            &source.#field_ident
410                        }
411
412                        /// Set the field value.
413                        pub fn set(source: &mut #struct_name, value: #field_ty) {
414                            source.#field_ident = value;
415                        }
416                    }
417
418                    impl prismqueer::Named<#accessor_name> {
419                        /// Create a named lens for this field.
420                        pub fn lens() -> prismqueer::Named<#accessor_name> {
421                            prismqueer::Named(#field_name, #accessor_name)
422                        }
423                    }
424                });
425            }
426            FieldKind::Prism => {
427                let accessor_name = format_ident!("{}Prism", pascal, span = Span::call_site());
428                let inner_ty = extract_inner_type(&field.ty);
429
430                if let Some(inner) = inner_ty {
431                    if is_option_type(&field.ty) {
432                        tokens.extend(quote! {
433                            /// Prism accessor for field `#field_name`. Partial access.
434                            pub struct #accessor_name;
435
436                            impl #accessor_name {
437                                /// Extract the value if present.
438                                pub fn extract(source: &#struct_name) -> Option<&#inner> {
439                                    source.#field_ident.as_ref()
440                                }
441
442                                /// Set the value (wraps in Some).
443                                pub fn review(source: &mut #struct_name, value: #inner) {
444                                    source.#field_ident = Some(value);
445                                }
446                            }
447
448                            impl prismqueer::Named<#accessor_name> {
449                                /// Create a named prism for this field.
450                                pub fn prism() -> prismqueer::Named<#accessor_name> {
451                                    prismqueer::Named(#field_name, #accessor_name)
452                                }
453                            }
454                        });
455                    } else {
456                        // Non-Option prism: direct access with Option wrapping
457                        let field_ty = &field.ty;
458                        tokens.extend(quote! {
459                            /// Prism accessor for field `#field_name`. Partial access.
460                            pub struct #accessor_name;
461
462                            impl #accessor_name {
463                                /// Extract the value (always Some for non-Option types).
464                                pub fn extract(source: &#struct_name) -> Option<&#field_ty> {
465                                    Some(&source.#field_ident)
466                                }
467
468                                /// Set the value.
469                                pub fn review(source: &mut #struct_name, value: #field_ty) {
470                                    source.#field_ident = value;
471                                }
472                            }
473                        });
474                    }
475                } else {
476                    // Fallback for non-generic types
477                    let field_ty = &field.ty;
478                    tokens.extend(quote! {
479                        pub struct #accessor_name;
480
481                        impl #accessor_name {
482                            pub fn extract(source: &#struct_name) -> Option<&#field_ty> {
483                                Some(&source.#field_ident)
484                            }
485
486                            pub fn review(source: &mut #struct_name, value: #field_ty) {
487                                source.#field_ident = value;
488                            }
489                        }
490                    });
491                }
492            }
493            FieldKind::Traversal => {
494                let accessor_name = format_ident!("{}Traversal", pascal, span = Span::call_site());
495                let field_ty = &field.ty;
496
497                if is_vec_type(&field.ty) {
498                    if let Some(inner) = extract_inner_type(&field.ty) {
499                        tokens.extend(quote! {
500                            /// Traversal accessor for field `#field_name`. Multiple targets.
501                            pub struct #accessor_name;
502
503                            impl #accessor_name {
504                                /// Get a slice of all elements.
505                                pub fn traverse(source: &#struct_name) -> &[#inner] {
506                                    &source.#field_ident
507                                }
508
509                                /// Get a mutable reference to the vec.
510                                pub fn traverse_mut(source: &mut #struct_name) -> &mut Vec<#inner> {
511                                    &mut source.#field_ident
512                                }
513                            }
514
515                            impl prismqueer::Named<#accessor_name> {
516                                /// Create a named traversal for this field.
517                                pub fn traversal() -> prismqueer::Named<#accessor_name> {
518                                    prismqueer::Named(#field_name, #accessor_name)
519                                }
520                            }
521                        });
522                    }
523                } else {
524                    // Non-Vec traversal: just expose the field
525                    tokens.extend(quote! {
526                        pub struct #accessor_name;
527
528                        impl #accessor_name {
529                            pub fn traverse(source: &#struct_name) -> &#field_ty {
530                                &source.#field_ident
531                            }
532
533                            pub fn traverse_mut(source: &mut #struct_name) -> &mut #field_ty {
534                                &mut source.#field_ident
535                            }
536                        }
537                    });
538                }
539            }
540            FieldKind::Iso => {
541                let accessor_name = format_ident!("{}Iso", pascal, span = Span::call_site());
542                let field_ty = &field.ty;
543
544                tokens.extend(quote! {
545                    /// Iso accessor for field `#field_name`. Round-trip lossless.
546                    pub struct #accessor_name;
547
548                    impl #accessor_name {
549                        /// Forward: get the field value.
550                        pub fn forward(source: &#struct_name) -> &#field_ty {
551                            &source.#field_ident
552                        }
553
554                        /// Backward: set the field value.
555                        pub fn backward(source: &mut #struct_name, value: #field_ty) {
556                            source.#field_ident = value;
557                        }
558                    }
559                });
560            }
561        }
562    }
563
564    tokens
565}
566
567fn generate_optic_fields(
568    struct_name: &syn::Ident,
569    fields: &[AnnotatedField],
570    impl_generics: &syn::ImplGenerics,
571    ty_generics: &syn::TypeGenerics,
572    where_clause: Option<&syn::WhereClause>,
573) -> proc_macro2::TokenStream {
574    if fields.is_empty() {
575        return quote! {};
576    }
577
578    let entries: Vec<proc_macro2::TokenStream> = fields
579        .iter()
580        .map(|f| {
581            let field_name = f.ident.to_string();
582            let kind = match f.kind {
583                FieldKind::Iso => quote! { prismqueer::OpticKind::Iso },
584                FieldKind::Lens => quote! { prismqueer::OpticKind::Lens },
585                FieldKind::Prism => quote! { prismqueer::OpticKind::Prism },
586                FieldKind::Traversal => quote! { prismqueer::OpticKind::Traversal },
587            };
588            quote! {
589                prismqueer::FieldOptic {
590                    name: #field_name,
591                    kind: #kind,
592                }
593            }
594        })
595        .collect();
596
597    let count = entries.len();
598
599    quote! {
600        impl #impl_generics #struct_name #ty_generics #where_clause {
601            /// Returns metadata about all optic-annotated fields.
602            pub fn optic_fields() -> &'static [prismqueer::FieldOptic] {
603                static FIELDS: [prismqueer::FieldOptic; #count] = [
604                    #(#entries),*
605                ];
606                &FIELDS
607            }
608        }
609    }
610}