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:
2026-05-18 00:01:28 -04:00
parent a1d1d6928f
commit 54f060c273
2 changed files with 225 additions and 17 deletions

View File

@@ -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! {