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>
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>
Adds first-class Rust-backed Mizan to sit alongside mizan-django and
mizan-fastapi. A Rust dev writes:
#[derive(Mizan, Serialize, Deserialize)]
pub struct ProfileOutput { pub user_id: i64, pub name: String }
#[mizan::context("user")]
pub struct UserCtx;
#[mizan::client(context = UserCtx)]
pub async fn user_profile(_req: &RequestHandle<'_>, user_id: i64) -> ProfileOutput { ... }
…and gets byte-identical KDL to the Python emitters, served over the
same wire protocol the React / Rust / Vue / Svelte kernels speak.
New crates:
- cores/mizan-rust/ (Cargo: mizan-core) — IR types, KDL emitter, traits, registry,
runtime (compute_invalidation / compute_merges
ported from mizan-fastapi), graph_check with
structural type-matching
- cores/mizan-rust-macros/ (Cargo: mizan-macros) — #[derive(Mizan)], #[mizan::context],
#[mizan::client] proc macros
- backends/mizan-rust-axum/ (Cargo: mizan-axum) — axum HTTP adapter: /session/, /call/, /ctx/:name/
- tests/afi/rust_app/ — AFI fixture port + server / export-ir binaries
Substrate-shape moves required by cross-language equivalence:
- IR canonicalization: functions / contexts / context-members / shared-by
now sort alphabetically in both Python and Rust emitters. The IR is a
contract; linkme doesn't preserve declaration order, so canonical sort
is the only stable mapping. afi_ir.kdl + per-target baselines regenerated.
- MizanType::TYPE_NAME is a const (with a default type_name() reader) so
it's usable in linkme TypeEntry static initializers.
- Tree-shaken type registry: #[derive(Mizan)] only emits the trait impl;
the #[mizan::client] macro registers canonical-named entries from
fn signatures, including Vec<T> element types for ref resolution.
- Merge resolution is structural (NamedType shape comparison) rather than
by name — matches the Python types_match_for_merge semantics.
Three-way forcing functions:
- tests/afi/test_codegen_parity.py — Django ≡ FastAPI ≡ Rust on KDL bytes (3 pass)
- tests/rust/run_wire_parity.py — 12/12 probes against FastAPI + Rust (EXIT=0)
Incidental fixes surfaced by the new tests:
- Stale `from .registry import validate_registry` import removed from
mizan-django/setup/discovery.py (referenced a function that no longer
exists; was masking codegen-parity).
- BASE_DIR added to tests/afi/django_app/project/settings.py.
- /session/ endpoint added to mizan-fastapi for protocol-shaped readiness
probe parity (wire-parity harness now polls /api/mizan/session/ on both
backends rather than FastAPI's /openapi.json).
- Root .gitignore picks up Rust target/ across the tree so new crates
don't need per-crate gitignore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>