use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{braced, parenthesized, FnArg, Ident, LitStr, Result, Token, Type};
enum SubstrateDecl {
Type(SubstrateTypeDecl),
Prism(SubstratePrismDecl),
Action(SubstrateActionDecl),
Grammar(SubstrateGrammarDecl),
}
impl Parse for SubstrateDecl {
fn parse(input: ParseStream) -> Result<Self> {
if input.peek(Token![type]) {
Ok(SubstrateDecl::Type(input.parse()?))
} else if input.peek(Ident::peek_any) {
let fork = input.fork();
let ident: Ident = fork.call(Ident::parse_any)?;
match ident.to_string().as_str() {
"prism" => Ok(SubstrateDecl::Prism(input.parse()?)),
"action" => Ok(SubstrateDecl::Action(input.parse()?)),
"grammar" => Ok(SubstrateDecl::Grammar(input.parse()?)),
other => Err(syn::Error::new(
ident.span(),
format!(
"unexpected substrate declaration kind `{other}`; expected one of \
`type`, `prism`, `action`, `grammar` per \
mirror/shards/code/metalogue.mirror's `declaration_kind`."
),
)),
}
} else {
Err(input
.error("expected a substrate declaration: `type`, `prism`, `action`, or `grammar`"))
}
}
}
struct SubstrateActionDecl {
name: Ident,
args: Punctuated<FnArg, Token![,]>,
ret: Type,
}
impl Parse for SubstrateActionDecl {
fn parse(input: ParseStream) -> Result<Self> {
let _action_kw: Ident = input.call(Ident::parse_any)?;
let name: Ident = input.call(Ident::parse_any)?;
let args_content;
parenthesized!(args_content in input);
let args = Punctuated::<FnArg, Token![,]>::parse_terminated(&args_content)?;
input.parse::<Token![->]>()?;
let ret: Type = input.parse()?;
let body_content;
braced!(body_content in input);
while !body_content.is_empty() {
let _tok: proc_macro2::TokenTree = body_content.parse()?;
}
Ok(SubstrateActionDecl { name, args, ret })
}
}
struct SubstratePrismDecl {
path: String,
struct_name: Ident,
}
impl Parse for SubstratePrismDecl {
fn parse(input: ParseStream) -> Result<Self> {
let _prism_kw: Ident = input.call(Ident::parse_any)?;
input.parse::<Token![@]>()?;
let mut segments: Vec<String> = Vec::new();
loop {
let seg: Ident = input.call(Ident::parse_any)?;
segments.push(seg.to_string());
if input.peek(Token![/]) {
input.parse::<Token![/]>()?;
continue;
}
break;
}
let path = format!("@{}", segments.join("/"));
let final_seg = segments.last().expect("path has at least one segment");
let struct_name = format_ident!("{}", to_pascal_case(final_seg));
if input.peek(syn::token::Brace) {
let content;
braced!(content in input);
while !content.is_empty() {
let _tok: proc_macro2::TokenTree = content.parse()?;
}
}
Ok(SubstratePrismDecl { path, struct_name })
}
}
struct SubstrateGrammarDecl {
path: String,
struct_name: Ident,
#[allow(dead_code)]
extensions: Vec<String>,
}
impl Parse for SubstrateGrammarDecl {
fn parse(input: ParseStream) -> Result<Self> {
let _grammar_kw: Ident = input.call(Ident::parse_any)?;
input.parse::<Token![@]>()?;
let mut segments: Vec<String> = Vec::new();
loop {
let seg: Ident = input.call(Ident::parse_any)?;
segments.push(seg.to_string());
if input.peek(Token![/]) {
input.parse::<Token![/]>()?;
continue;
}
break;
}
let path = format!("@{}", segments.join("/"));
let final_seg = segments.last().expect("path has at least one segment");
let struct_name = format_ident!("{}", to_pascal_case(final_seg));
let mut extensions: Vec<String> = Vec::new();
if input.peek(syn::token::Paren) {
let content;
parenthesized!(content in input);
let lits = Punctuated::<LitStr, Token![,]>::parse_terminated(&content)?;
extensions = lits.iter().map(|l| l.value()).collect();
}
if input.peek(syn::token::Brace) {
let content;
braced!(content in input);
while !content.is_empty() {
let _tok: proc_macro2::TokenTree = content.parse()?;
}
}
Ok(SubstrateGrammarDecl {
path,
struct_name,
extensions,
})
}
}
enum SubstrateTypeDecl {
Record {
name: Ident,
fields: Vec<(Ident, Ident)>,
},
Sum {
name: Ident,
variants: Vec<SumVariant>,
},
}
struct SumVariant {
name: Ident,
args: Vec<Ident>,
}
impl Parse for SubstrateTypeDecl {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<Token![type]>()?;
let name: Ident = input.call(Ident::parse_any)?;
let lookahead = input.lookahead1();
if lookahead.peek(syn::token::Brace) {
let content;
braced!(content in input);
let mut fields = Vec::new();
while !content.is_empty() {
let fname: Ident = content.call(Ident::parse_any)?;
content.parse::<Token![:]>()?;
let fty: Ident = content.call(Ident::parse_any)?;
fields.push((fname, fty));
if content.is_empty() {
break;
}
content.parse::<Token![,]>()?;
}
Ok(SubstrateTypeDecl::Record { name, fields })
} else if lookahead.peek(Token![=]) {
input.parse::<Token![=]>()?;
let mut variants = Vec::new();
loop {
let vname: Ident = input.call(Ident::parse_any)?;
let mut args = Vec::new();
if input.peek(syn::token::Paren) {
let content;
parenthesized!(content in input);
while !content.is_empty() {
let arg_ty: Ident = content.call(Ident::parse_any)?;
args.push(arg_ty);
if content.is_empty() {
break;
}
content.parse::<Token![,]>()?;
}
}
variants.push(SumVariant { name: vname, args });
if input.is_empty() {
break;
}
input.parse::<Token![|]>()?;
}
Ok(SubstrateTypeDecl::Sum { name, variants })
} else {
Err(lookahead.error())
}
}
}
pub(crate) fn expand(input: TokenStream) -> TokenStream {
let decl = match syn::parse2::<SubstrateDecl>(input) {
Ok(d) => d,
Err(e) => return e.to_compile_error(),
};
match decl {
SubstrateDecl::Type(t) => emit_type(&t),
SubstrateDecl::Prism(p) => emit_prism(&p),
SubstrateDecl::Action(a) => emit_action(&a),
SubstrateDecl::Grammar(g) => emit_grammar(&g),
}
}
fn emit_action(decl: &SubstrateActionDecl) -> TokenStream {
let name = &decl.name;
let args = &decl.args;
let ret = &decl.ret;
quote! {
#[allow(unused_variables)]
pub fn #name(#args) -> #ret { todo!() }
}
}
fn emit_prism(decl: &SubstratePrismDecl) -> TokenStream {
let struct_name = &decl.struct_name;
let oid_lit = LitStr::new(&decl.path, proc_macro2::Span::call_site());
quote! {
#[derive(prismqueer::DerivePrism)]
#[oid(#oid_lit)]
pub struct #struct_name;
}
}
fn emit_grammar(decl: &SubstrateGrammarDecl) -> TokenStream {
let struct_name = &decl.struct_name;
let oid_lit = LitStr::new(&decl.path, proc_macro2::Span::call_site());
quote! {
#[derive(prismqueer::DerivePrism)]
#[oid(#oid_lit)]
pub struct #struct_name;
}
}
fn emit_type(decl: &SubstrateTypeDecl) -> TokenStream {
match decl {
SubstrateTypeDecl::Record { name, fields } => {
let struct_name = format_ident!("{}", to_pascal_case(&name.to_string()));
let field_tokens: Vec<TokenStream> = fields
.iter()
.map(|(fname, fty)| {
quote! { pub #fname: #fty, }
})
.collect();
quote! {
pub struct #struct_name {
#(#field_tokens)*
}
}
}
SubstrateTypeDecl::Sum { name, variants } => {
let enum_name = format_ident!("{}", to_pascal_case(&name.to_string()));
let variant_tokens: Vec<TokenStream> = variants
.iter()
.map(|v| {
let vname = format_ident!("{}", to_pascal_case(&v.name.to_string()));
if v.args.is_empty() {
quote! { #vname, }
} else {
let args = &v.args;
quote! { #vname(#(#args),*), }
}
})
.collect();
quote! {
pub enum #enum_name {
#(#variant_tokens)*
}
}
}
}
}
fn to_pascal_case(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut next_upper = true;
for c in s.chars() {
if c == '_' {
next_upper = true;
} else if next_upper {
out.extend(c.to_uppercase());
next_upper = false;
} else {
out.push(c);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
#[test]
fn bare_prism_emits_unit_struct_with_derive_and_oid() {
let input = quote! { prism @parse };
let emitted = expand(input);
let parsed: syn::Result<syn::Item> = syn::parse2(emitted);
let item = parsed.expect("emission must be a valid Rust item");
let s = match item {
syn::Item::Struct(s) => s,
_ => panic!("prism-shaped substrate must emit a struct item"),
};
assert_eq!(s.ident.to_string(), "Parse");
assert!(matches!(s.fields, syn::Fields::Unit));
let has_derive = s.attrs.iter().any(|a| a.path().is_ident("derive"));
let has_oid = s.attrs.iter().any(|a| a.path().is_ident("oid"));
assert!(has_derive, "emission must carry a #[derive(...)] attribute");
assert!(has_oid, "emission must carry a #[oid(\"@path\")] attribute");
}
#[test]
fn prism_with_five_op_block_emits_same_unit_struct() {
let input = quote! {
prism @kernel {
focus kernel
project kernel
split kernel
shift kernel
settle kernel
}
};
let emitted = expand(input);
let parsed: syn::Result<syn::Item> = syn::parse2(emitted);
let item = parsed.expect("emission must be a valid Rust item");
let s = match item {
syn::Item::Struct(s) => s,
_ => panic!("prism-shaped substrate must emit a struct item"),
};
assert_eq!(s.ident.to_string(), "Kernel");
assert!(matches!(s.fields, syn::Fields::Unit));
}
#[test]
fn distinct_prism_paths_produce_distinct_emissions() {
let a = expand(quote! { prism @observer }).to_string();
let b = expand(quote! { prism @actor }).to_string();
assert_ne!(
a, b,
"distinct substrate paths must produce distinct emissions"
);
assert!(a.contains("\"@observer\""));
assert!(b.contains("\"@actor\""));
}
#[test]
fn record_emission_is_deterministic() {
let input = quote! { type point { x: u32, y: u32 } };
let a = expand(input.clone());
let b = expand(input);
assert_eq!(a.to_string(), b.to_string());
}
#[test]
fn sum_emission_is_deterministic() {
let input = quote! { type color = red | green | blue };
let a = expand(input.clone());
let b = expand(input);
assert_eq!(a.to_string(), b.to_string());
}
#[test]
fn record_emission_round_trips_through_syn() {
let input = quote! { type point { x: u32, y: u32 } };
let emitted = expand(input);
let parsed: syn::Result<syn::Item> = syn::parse2(emitted);
let item = parsed.expect("emission must be a valid Rust item");
let s = match item {
syn::Item::Struct(s) => s,
_ => panic!("record-shaped substrate type must emit a struct item"),
};
assert_eq!(s.ident.to_string(), "Point");
match s.fields {
syn::Fields::Named(named) => {
let names: Vec<String> = named
.named
.iter()
.map(|f| f.ident.as_ref().unwrap().to_string())
.collect();
assert_eq!(names, vec!["x", "y"]);
}
_ => panic!("record must emit named-fields struct"),
}
}
#[test]
fn sum_emission_round_trips_through_syn() {
let input = quote! { type color = red | green | blue };
let emitted = expand(input);
let parsed: syn::Result<syn::Item> = syn::parse2(emitted);
let item = parsed.expect("emission must be a valid Rust item");
let e = match item {
syn::Item::Enum(e) => e,
_ => panic!("sum-shaped substrate type must emit an enum item"),
};
assert_eq!(e.ident.to_string(), "Color");
let names: Vec<String> = e.variants.iter().map(|v| v.ident.to_string()).collect();
assert_eq!(names, vec!["Red", "Green", "Blue"]);
}
#[test]
fn parametric_sum_emits_tuple_variants() {
let input = quote! { type maybe_int = some(u32) | none };
let emitted = expand(input);
let parsed: syn::Result<syn::Item> = syn::parse2(emitted);
let item = parsed.expect("emission must be a valid Rust item");
let e = match item {
syn::Item::Enum(e) => e,
_ => panic!("sum-shaped substrate type must emit an enum item"),
};
assert_eq!(e.ident.to_string(), "MaybeInt");
let some = e.variants.iter().find(|v| v.ident == "Some").unwrap();
assert!(matches!(some.fields, syn::Fields::Unnamed(_)));
let none = e.variants.iter().find(|v| v.ident == "None").unwrap();
assert!(matches!(none.fields, syn::Fields::Unit));
}
#[test]
fn snake_to_pascal_is_lossless() {
assert_eq!(to_pascal_case("point"), "Point");
assert_eq!(to_pascal_case("circle_of_fifths"), "CircleOfFifths");
assert_eq!(to_pascal_case("imperfect"), "Imperfect");
}
#[test]
fn bare_grammar_emits_unit_struct_with_derive_and_oid() {
let input = quote! { grammar @parse_spec { } };
let emitted = expand(input);
let parsed: syn::Result<syn::Item> = syn::parse2(emitted);
let item = parsed.expect("emission must be a valid Rust item");
let s = match item {
syn::Item::Struct(s) => s,
_ => panic!("grammar-shaped substrate must emit a struct item"),
};
assert_eq!(s.ident.to_string(), "ParseSpec");
assert!(matches!(s.fields, syn::Fields::Unit));
let has_derive = s.attrs.iter().any(|a| a.path().is_ident("derive"));
let has_oid = s.attrs.iter().any(|a| a.path().is_ident("oid"));
assert!(has_derive, "emission must carry a #[derive(...)] attribute");
assert!(has_oid, "emission must carry a #[oid(\"@path\")] attribute");
}
#[test]
fn grammar_with_single_extension_emits_same_unit_struct() {
let input = quote! {
grammar @mirror_spec("spec") {
project in
project out
}
};
let emitted = expand(input);
let parsed: syn::Result<syn::Item> = syn::parse2(emitted);
let item = parsed.expect("emission must be a valid Rust item");
let s = match item {
syn::Item::Struct(s) => s,
_ => panic!("grammar-shaped substrate must emit a struct item"),
};
assert_eq!(s.ident.to_string(), "MirrorSpec");
assert!(matches!(s.fields, syn::Fields::Unit));
}
#[test]
fn grammar_with_multi_extension_emits_same_unit_struct() {
let input = quote! {
grammar @mirror_grammar("mirror", "spec", "meta") {
focus prism
focus grammar
}
};
let emitted = expand(input);
let parsed: syn::Result<syn::Item> = syn::parse2(emitted);
let item = parsed.expect("emission must be a valid Rust item");
let s = match item {
syn::Item::Struct(s) => s,
_ => panic!("grammar-shaped substrate must emit a struct item"),
};
assert_eq!(s.ident.to_string(), "MirrorGrammar");
assert!(matches!(s.fields, syn::Fields::Unit));
}
#[test]
fn distinct_grammar_paths_produce_distinct_emissions() {
let a = expand(quote! { grammar @first("a") { } }).to_string();
let b = expand(quote! { grammar @second("b") { } }).to_string();
assert_ne!(
a, b,
"distinct substrate paths must produce distinct emissions"
);
assert!(a.contains("\"@first\""));
assert!(b.contains("\"@second\""));
}
#[test]
fn grammar_emission_is_deterministic() {
let input = quote! { grammar @repeat("x") { focus a } };
let a = expand(input.clone());
let b = expand(input);
assert_eq!(a.to_string(), b.to_string());
}
}