mizan-axum + macros: state threading, array/map lowering, merge shape semantics
Three substrate extensions surfaced by the Blazr session port: 1. **App-state threading.** mizan-axum::router() is now generic over a user-supplied state type and threads `Arc<dyn Any + Send + Sync>` into every dispatch via RequestHandle. Handlers downcast to their concrete AppState. The stateless AFI fixture uses `router_stateless()` (matches the prior signature). RequestHandle gains a `from_dyn()` constructor to wrap already-erased trait-object references. 2. **`[T; N]` and `BTreeMap<K, V>` lowering in #[derive(Mizan)].** Fixed arrays emit as `List<T>` (matches Python `tuple[float,...]` → JSON array). String-keyed maps emit as `List<V>` — closest approximation until KDL grows a `dict` shape. Also: vec-element registrations get a per-function scope suffix so two handlers returning `Vec<Same>` don't collide at the static-name layer. 3. **`types_match` for merge: upsert-into-list semantics.** Now matches Python `types_match_for_merge`: direct (T == T), upsert (slot is `Alias(List(T))`, value is T), and list-replace (both sides list). The AFI fixture only exercised the direct path; the Blazr port's `morph_set_value` returning a single `MorphLayer` into a context with `Vec<MorphLayer>` slot is what surfaced the gap. AFI codegen + wire parity stays 12/12 green after these substrate changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,12 +56,68 @@ pub fn type_shape_expr(ty: &Type) -> TokenStream {
|
||||
::mizan_core::TypeShape::List(::std::boxed::Box::new(#inner_shape))
|
||||
};
|
||||
}
|
||||
if let Some(elem) = unwrap_array(ty) {
|
||||
// `[T; N]` lowers to `list { T }` on the wire — JSON arrays don't
|
||||
// carry length, so the IR contract is the same as `Vec<T>`.
|
||||
let inner_shape = type_shape_expr(&elem);
|
||||
return quote! {
|
||||
::mizan_core::TypeShape::List(::std::boxed::Box::new(#inner_shape))
|
||||
};
|
||||
}
|
||||
if let Some(elem) = unwrap_btreemap_value(ty) {
|
||||
// `BTreeMap<K, V>` on the wire is a JSON object keyed by `K`'s
|
||||
// string form. The Mizan IR doesn't model dynamic-keyed maps as a
|
||||
// distinct shape — closest equivalent is a list of value entries.
|
||||
let inner_shape = type_shape_expr(&elem);
|
||||
return quote! {
|
||||
::mizan_core::TypeShape::List(::std::boxed::Box::new(#inner_shape))
|
||||
};
|
||||
}
|
||||
if let Some(p) = primitive_of(ty) {
|
||||
return quote! { ::mizan_core::TypeShape::Primitive(#p) };
|
||||
}
|
||||
// Fallback: assume a user-defined struct/enum implementing MizanType.
|
||||
// The Ref name comes from `<T as MizanType>::type_name()` at runtime.
|
||||
quote! { ::mizan_core::TypeShape::Ref(<#ty as ::mizan_core::MizanType>::type_name()) }
|
||||
// The Ref name comes from `<T as MizanType>::TYPE_NAME` (associated const).
|
||||
quote! { ::mizan_core::TypeShape::Ref(<#ty as ::mizan_core::MizanType>::TYPE_NAME) }
|
||||
}
|
||||
|
||||
/// If `ty` is `[T; N]`, return `T`. Otherwise None.
|
||||
pub fn unwrap_array(ty: &Type) -> Option<Type> {
|
||||
if let Type::Array(a) = ty {
|
||||
Some((*a.elem).clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// If `ty` is `BTreeMap<K, V>` or `HashMap<K, V>`, return `V` (the value).
|
||||
/// String-keyed maps land on the wire as JSON objects; the IR carries the
|
||||
/// value shape as a list element since KDL doesn't model dynamic-keyed maps
|
||||
/// distinctly yet.
|
||||
pub fn unwrap_btreemap_value(ty: &Type) -> Option<Type> {
|
||||
let path = match ty {
|
||||
Type::Path(TypePath { qself: None, path }) => path,
|
||||
_ => return None,
|
||||
};
|
||||
let last = path.segments.last()?;
|
||||
let name = last.ident.to_string();
|
||||
if name != "BTreeMap" && name != "HashMap" {
|
||||
return None;
|
||||
}
|
||||
let args = match &last.arguments {
|
||||
PathArguments::AngleBracketed(a) => a,
|
||||
_ => return None,
|
||||
};
|
||||
// BTreeMap<K, V> — second type argument is V.
|
||||
let mut type_args = args.args.iter().filter_map(|a| {
|
||||
if let GenericArgument::Type(t) = a {
|
||||
Some(t.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
type_args.next()?; // skip K
|
||||
type_args.next()
|
||||
}
|
||||
|
||||
/// Emit a `Primitive` const-expression for `ty`, or `None` if `ty` isn't a
|
||||
|
||||
Reference in New Issue
Block a user