Mizan codegen substrate: Rust kernel + Rust codegen binary, JS generator deleted

The Mizan codegen substrate moves off JavaScript template-literal emission
onto a compiled Rust binary that consumes the same OpenAPI + x-mizan-* IR
the JS substrate consumed. Three structural wins fall out of one move:

1. Moat closes. The codegen logic (how `affects` becomes auto-invalidation,
   how named contexts collapse onto bundled fetches, how the registry-to-
   Provider mapping is shaped) ships compiled instead of as source bytes
   in every consumer's node_modules.

2. Pattern F (lines.push append-walls) becomes structurally unauthorable.
   The emit substrate is askama templates in templates/<target>/*.j2 —
   actual target-language files with {{ ... }} substitution markers,
   syntax-highlighted natively, type-checked against the render context
   structs at compile time. The Rust emit modules build typed render
   contexts and call .render(); no string-builder surface exists.

3. OpenAPI `default`-bearing fields now emit as non-optional in TS / Python
   / Rust — the server always populates them, so consumer code reads them
   without nullable checks. Surfaced by Blazr's typecheck on regeneration.

Layout:
  frontends/mizan-rust/        — Rust port of @mizan/base; #[cfg(feature="pyo3")]
                                 exposes PyMizanClient for the Python target.
  protocol/mizan-codegen/      — codegen binary source + askama templates.
  protocol/mizan-generate/     — npm-package shim. bin/launcher.mjs dispatches
                                 to the platform-appropriate prebuilt binary.
                                 Old generator/ JS tree deleted.
  tests/rust/                  — wire-parity drivers. drive_kernel exercises
                                 raw client.call() / fetch_context(); drive_emitted
                                 exercises the typed crate the codegen emits.
  tests/afi/afi_codegen_app.py — codegen entrypoint module (imports + registers).
  backends/mizan-fastapi/.../schema.py — adds outputNullable so the Rust
                                 codegen can wrap T | None responses in Option<T>.

Verification:
  - 20 mizan-codegen tests green (IR deserialization, byte-equivalent
    parity vs JS baseline for stage1/rust/python/react/vue/svelte,
    structural test for channels).
  - tests/rust/run_wire_parity.py — 12/12 probes green via the Rust binary
    driving the FastAPI fixture end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 18:26:32 -04:00
parent c15c6f3e14
commit 43bcf3f26f
114 changed files with 11090 additions and 2342 deletions

View File

@@ -0,0 +1,137 @@
//! Casing transforms — port of `protocol/mizan-generate/generator/lib/casing.mjs`.
//!
//! The Mizan IR uses snake_case names (`user_id`, `update_profile`). Per-target
//! identifier conventions vary: TypeScript wants `pascalCase`/`camelCase`,
//! Rust wants `snake_case` (with `r#`-escaping for keywords). These helpers
//! pin the conversion so emit-targets share one vocabulary.
fn split_parts(s: &str) -> Vec<&str> {
s.split(|c: char| c == '.' || c == '-' || c == '_')
.filter(|p| !p.is_empty())
.collect()
}
fn uppercase_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}
fn lowercase_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_lowercase().chain(chars).collect(),
None => String::new(),
}
}
pub fn pascal_case(s: &str) -> String {
split_parts(s).into_iter().map(uppercase_first).collect()
}
pub fn camel_case(s: &str) -> String {
let pascal = pascal_case(s);
lowercase_first(&pascal)
}
/// Insert underscores at lowercase/digit-to-uppercase boundaries, unify with
/// the existing `.`/`-`/`_` separators, then lowercase + join.
pub fn snake_case(s: &str) -> String {
let mut with_boundaries = String::with_capacity(s.len() + 4);
let mut prev: Option<char> = None;
for c in s.chars() {
if let Some(p) = prev {
if (p.is_ascii_lowercase() || p.is_ascii_digit()) && c.is_ascii_uppercase() {
with_boundaries.push('_');
}
}
with_boundaries.push(c);
prev = Some(c);
}
split_parts(&with_boundaries)
.into_iter()
.map(|p| p.to_ascii_lowercase())
.collect::<Vec<_>>()
.join("_")
}
/// Rust reserved words that can be escaped via `r#` (excludes `crate`, `self`,
/// `Self`, `super`, `extern`, which can't be raw-escaped on stable).
const RUST_RAW_KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "else", "enum", "false", "fn", "for",
"if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
"ref", "return", "static", "struct", "trait", "true", "type", "unsafe",
"use", "where", "while", "async", "await", "dyn", "abstract", "become",
"box", "do", "final", "macro", "override", "priv", "typeof", "unsized",
"virtual", "yield", "try", "union",
];
const RUST_HARD_RESERVED: &[&str] = &["crate", "self", "Self", "super", "extern"];
pub fn rust_ident(name: &str) -> String {
let snake = snake_case(name);
if RUST_HARD_RESERVED.contains(&snake.as_str()) {
format!("{snake}_")
} else if RUST_RAW_KEYWORDS.contains(&snake.as_str()) {
format!("r#{snake}")
} else {
snake
}
}
pub fn rust_type_ident(name: &str) -> String {
let pascal = pascal_case(name);
if RUST_HARD_RESERVED.contains(&pascal.as_str()) {
format!("{pascal}_")
} else if RUST_RAW_KEYWORDS.contains(&pascal.as_str()) {
format!("r#{pascal}")
} else {
pascal
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pascal_case_matches_js_codegen() {
assert_eq!(pascal_case("user_profile"), "UserProfile");
assert_eq!(pascal_case("find-user"), "FindUser");
assert_eq!(pascal_case("api.v1.users"), "ApiV1Users");
assert_eq!(pascal_case(""), "");
}
#[test]
fn camel_case_matches_js_codegen() {
assert_eq!(camel_case("user_profile"), "userProfile");
assert_eq!(camel_case("UpdateProfile"), "updateProfile");
}
#[test]
fn snake_case_inserts_pascal_boundaries() {
assert_eq!(snake_case("UserProfile"), "user_profile");
assert_eq!(snake_case("camelCase"), "camel_case");
assert_eq!(snake_case("already_snake"), "already_snake");
assert_eq!(snake_case("HTTPResponse"), "httpresponse"); // matches JS behavior
}
#[test]
fn rust_ident_escapes_keywords() {
assert_eq!(rust_ident("type"), "r#type");
assert_eq!(rust_ident("normal"), "normal");
assert_eq!(rust_ident("self"), "self_");
}
}