mizan: IR inline-substitution + reachability tree-shake + serde rename_all
Three substrate moves required by the Blazr-session port that surfaced
real cross-backend divergences:
1. Inline-substitution for primitive aliases and string enums. Named
types whose body is `Alias(Primitive(_))` or `Enum(_)` collapse into
their inline TypeShape at every `Ref` use site, and don't emit as
their own `type "X" { ... }` entry. Matches Python's Pydantic Literal
and `Foo = str` alias inlining — codegen consumers see the primitive
directly rather than chasing a single-hop indirection.
2. Reachability tree-shake on the type registry. `#[derive(Mizan)]` now
auto-registers every Mizan type into the TYPES slice; the emitter
then transitively walks Refs from function inputs/outputs and emits
only the reachable subset. Original-named entries from derive
register only when something refs them; canonical-renamed entries
from the function macro are reachable by definition. Mirrors
Python's `_collect_named_types`.
3. `#[serde(rename_all = "...")]` + `#[serde(rename = "...")]`
propagation in `#[derive(Mizan)]` for enums. IR enum variants now
match the on-wire JSON casing (lowercase / snake_case / kebab-case /
etc.), not the Rust variant idents. Supports all serde casings.
AFI codegen + wire parity stays green after these changes (the AFI
fixture's enum-free + Pydantic-shape types are unchanged by the three
substrate extensions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,101 @@
|
||||
//! `#[derive(Mizan)]` — emit `MizanType` impl + linkme registration.
|
||||
|
||||
use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{Data, DataEnum, DataStruct, DeriveInput, Fields};
|
||||
use syn::{
|
||||
parse::Parser, punctuated::Punctuated, Data, DataEnum, DataStruct, DeriveInput, Fields, Lit,
|
||||
Meta, Token,
|
||||
};
|
||||
|
||||
use crate::shape::type_shape_expr;
|
||||
|
||||
/// Expand `#[derive(Mizan)]`. Emits only the `MizanType` trait impl —
|
||||
/// registration into the IR `TYPES` slice happens at the function macro
|
||||
/// (which owns the canonical-named type entries `<camelName>Input` /
|
||||
/// `<camelName>Output`) and at sub-type discovery inside `Vec<T>` outputs.
|
||||
/// This keeps the type registry tree-shaken: only types actually reachable
|
||||
/// from a registered function appear in the emitted IR.
|
||||
/// Apply a `#[serde(rename_all = "...")]` casing transform to a Rust
|
||||
/// variant identifier so the IR's enum variant matches what serde emits
|
||||
/// on the wire. Supported casings mirror serde's set.
|
||||
fn apply_rename_all(rule: &str, ident: &str) -> String {
|
||||
match rule {
|
||||
"lowercase" => ident.to_lowercase(),
|
||||
"UPPERCASE" => ident.to_uppercase(),
|
||||
"PascalCase" => ident.to_upper_camel_case(),
|
||||
"camelCase" => ident.to_lower_camel_case(),
|
||||
"snake_case" => ident.to_snake_case(),
|
||||
"SCREAMING_SNAKE_CASE" => ident.to_shouty_snake_case(),
|
||||
"kebab-case" => ident.to_kebab_case(),
|
||||
_ => ident.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk the enum's outer attributes for `#[serde(rename_all = "...")]`.
|
||||
fn serde_rename_all(attrs: &[syn::Attribute]) -> Option<String> {
|
||||
for attr in attrs {
|
||||
if !attr.path().is_ident("serde") {
|
||||
continue;
|
||||
}
|
||||
let list = match &attr.meta {
|
||||
Meta::List(l) => l,
|
||||
_ => continue,
|
||||
};
|
||||
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||
let metas = match parser.parse2(list.tokens.clone()) {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for meta in metas {
|
||||
if let Meta::NameValue(nv) = meta {
|
||||
if nv.path.is_ident("rename_all") {
|
||||
if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = nv.value {
|
||||
return Some(s.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Walk a variant's attributes for an explicit `#[serde(rename = "...")]`
|
||||
/// override. Variant-level rename overrides the enum-level rename_all.
|
||||
fn serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
|
||||
for attr in attrs {
|
||||
if !attr.path().is_ident("serde") {
|
||||
continue;
|
||||
}
|
||||
let list = match &attr.meta {
|
||||
Meta::List(l) => l,
|
||||
_ => continue,
|
||||
};
|
||||
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||
let metas = match parser.parse2(list.tokens.clone()) {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for meta in metas {
|
||||
if let Meta::NameValue(nv) = meta {
|
||||
if nv.path.is_ident("rename") {
|
||||
if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = nv.value {
|
||||
return Some(s.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Expand `#[derive(Mizan)]`. Emits the `MizanType` impl AND a linkme
|
||||
/// TypeEntry registration. Every Mizan-shaped type lands in the IR;
|
||||
/// the emitter's inline-substitution pass collapses primitive-aliases
|
||||
/// and enums at use sites so the IR stays tight.
|
||||
pub fn expand(input: DeriveInput) -> TokenStream {
|
||||
let ident = input.ident.clone();
|
||||
let type_name = ident.to_string();
|
||||
|
||||
let rename_all = serde_rename_all(&input.attrs);
|
||||
|
||||
let named_type_body = match &input.data {
|
||||
Data::Struct(s) => emit_struct(s),
|
||||
Data::Enum(e) => emit_enum(e),
|
||||
Data::Enum(e) => emit_enum(e, rename_all.as_deref()),
|
||||
Data::Union(_) => {
|
||||
return syn::Error::new_spanned(
|
||||
&input,
|
||||
@@ -28,11 +105,22 @@ pub fn expand(input: DeriveInput) -> TokenStream {
|
||||
}
|
||||
};
|
||||
|
||||
let register_static =
|
||||
quote::format_ident!("__MIZAN_TYPE_REGISTER_{}", ident.to_string().to_shouty_snake_case());
|
||||
|
||||
quote! {
|
||||
impl ::mizan_core::MizanType for #ident {
|
||||
const TYPE_NAME: &'static str = #type_name;
|
||||
fn shape() -> ::mizan_core::NamedType { #named_type_body }
|
||||
}
|
||||
|
||||
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||
#[allow(non_upper_case_globals)]
|
||||
static #register_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||
name: #type_name,
|
||||
shape_fn: <#ident as ::mizan_core::MizanType>::shape,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +169,7 @@ fn emit_struct(s: &DataStruct) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_enum(e: &DataEnum) -> TokenStream {
|
||||
fn emit_enum(e: &DataEnum, rename_all: Option<&str>) -> TokenStream {
|
||||
let mut variants: Vec<TokenStream> = Vec::new();
|
||||
for variant in &e.variants {
|
||||
if !matches!(variant.fields, Fields::Unit) {
|
||||
@@ -91,7 +179,16 @@ fn emit_enum(e: &DataEnum) -> TokenStream {
|
||||
)
|
||||
.to_compile_error();
|
||||
}
|
||||
let name = variant.ident.to_string();
|
||||
let raw = variant.ident.to_string();
|
||||
// Variant-level `#[serde(rename = "...")]` wins; otherwise apply
|
||||
// the enum-level `#[serde(rename_all = "...")]` rule.
|
||||
let name = if let Some(explicit) = serde_rename(&variant.attrs) {
|
||||
explicit
|
||||
} else if let Some(rule) = rename_all {
|
||||
apply_rename_all(rule, &raw)
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
variants.push(quote! { #name });
|
||||
}
|
||||
quote! {
|
||||
|
||||
Reference in New Issue
Block a user