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}