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,474 @@
//! Rust target — emits a complete Cargo crate consuming the
//! `mizan-rust` kernel. Output shape lives at `templates/rust/*.j2`.
use std::path::PathBuf;
use askama::Template;
use indexmap::IndexMap;
use crate::config::{Config, RustKernelSpec};
use crate::emit::CodegenTarget;
use crate::emit::EmittedFile;
use crate::emit::casing::{pascal_case, rust_ident, rust_type_ident, snake_case};
use crate::ir::{IsContext, JsonSchema, MizanContext, MizanFunction, MizanIR};
pub struct RustCrate;
impl CodegenTarget for RustCrate {
fn name(&self) -> &'static str { "rust" }
fn emit(&self, ir: &MizanIR, config: &Config) -> Vec<EmittedFile> {
let crate_name = config
.rust_crate_name
.clone()
.unwrap_or_else(|| "mizan_client".to_string());
let kernel_dep = format_kernel_dep(config.rust_kernel.as_ref());
let mut out: Vec<EmittedFile> = Vec::new();
out.push(EmittedFile::new(
"Cargo.toml",
CargoTemplate { crate_name: &crate_name, kernel_dep: &kernel_dep }
.render().expect("Cargo.toml renders"),
));
out.push(EmittedFile::new("src/types.rs", emit_types_rs(&ir.components.schemas)));
let mut context_modules: Vec<String> = Vec::new();
for (ctx_name, ctx_meta) in &ir.contexts {
let module_name = snake_case(ctx_name);
out.push(EmittedFile::new(
PathBuf::from("src/contexts").join(format!("{module_name}.rs")),
emit_context_file(ctx_name, ctx_meta, &ir.functions),
));
context_modules.push(module_name);
}
if !context_modules.is_empty() {
out.push(EmittedFile::new("src/contexts/mod.rs", emit_mod_file(&context_modules)));
}
let mut mutation_modules: Vec<String> = Vec::new();
let mut function_modules: Vec<String> = Vec::new();
for fn_meta in &ir.functions {
if !matches!(fn_meta.is_context, IsContext::No) || fn_meta.is_form { continue; }
let is_mutation = !fn_meta.affects.is_empty();
let kind = if is_mutation { "mutations" } else { "functions" };
let module_name = snake_case(&fn_meta.camel_name);
out.push(EmittedFile::new(
PathBuf::from(format!("src/{kind}")).join(format!("{module_name}.rs")),
emit_call_file(fn_meta),
));
if is_mutation {
mutation_modules.push(module_name);
} else {
function_modules.push(module_name);
}
}
if !mutation_modules.is_empty() {
out.push(EmittedFile::new("src/mutations/mod.rs", emit_mod_file(&mutation_modules)));
}
if !function_modules.is_empty() {
out.push(EmittedFile::new("src/functions/mod.rs", emit_mod_file(&function_modules)));
}
out.push(EmittedFile::new(
"src/lib.rs",
LibTemplate {
has_contexts: !context_modules.is_empty(),
has_mutations: !mutation_modules.is_empty(),
has_functions: !function_modules.is_empty(),
}.render().expect("lib.rs renders"),
));
out
}
}
#[derive(Template)]
#[template(path = "rust/Cargo.toml.j2", escape = "none")]
struct CargoTemplate<'a> {
crate_name: &'a str,
kernel_dep: &'a str,
}
#[derive(Template)]
#[template(path = "rust/lib.rs.j2", escape = "none")]
struct LibTemplate {
has_contexts: bool,
has_mutations: bool,
has_functions: bool,
}
#[derive(Template)]
#[template(path = "rust/mod.rs.j2", escape = "none")]
struct ModTemplate {
modules: Vec<String>,
}
#[derive(Template)]
#[template(path = "rust/context.rs.j2", escape = "none")]
struct ContextTemplate<'a> {
pascal: String,
snake: String,
ctx_name: &'a str,
type_imports: Vec<String>,
data_fields: Vec<StructField>,
params: Vec<StructField>,
}
#[derive(Template)]
#[template(path = "rust/call.rs.j2", escape = "none")]
struct CallTemplate<'a> {
snake: String,
name: &'a str,
return_type: String,
type_imports: Vec<String>,
input_param: String,
args_value: &'static str,
}
#[derive(Template)]
#[template(path = "rust/types.rs.j2", escape = "none")]
struct TypesTemplate {
schemas_block: String,
hoisted_enums_block: String,
}
struct StructField {
raw_name: String,
ident: String,
ty: String,
has_rename: bool,
}
fn dedupe_preserving_order(items: impl IntoIterator<Item = String>) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
items.into_iter().filter(|s| seen.insert(s.clone())).collect()
}
// ─── Cargo.toml ────────────────────────────────────────────────────────────
fn format_kernel_dep(spec: Option<&RustKernelSpec>) -> String {
match spec {
Some(RustKernelSpec::Path { path }) => format!("{{ path = {} }}", json_str(path)),
Some(RustKernelSpec::Git { git, tag, rev, branch }) => {
let mut parts = vec![format!("git = {}", json_str(git))];
if let Some(t) = tag { parts.push(format!("tag = {}", json_str(t))); }
if let Some(r) = rev { parts.push(format!("rev = {}", json_str(r))); }
if let Some(b) = branch { parts.push(format!("branch = {}", json_str(b))); }
format!("{{ {} }}", parts.join(", "))
}
Some(RustKernelSpec::Version { version }) => format!("{{ version = {} }}", json_str(version)),
None => "{ version = \"0.1\" }".to_string(),
}
}
fn json_str(s: &str) -> String {
serde_json::to_string(s).expect("string literal serializes")
}
// ─── mod.rs ────────────────────────────────────────────────────────────────
fn emit_mod_file(module_names: &[String]) -> String {
let mut sorted = module_names.to_vec();
sorted.sort();
ModTemplate { modules: sorted }.render().expect("mod.rs renders")
}
// ─── Context file ──────────────────────────────────────────────────────────
fn emit_context_file(
ctx_name: &str,
ctx_meta: &MizanContext,
all_functions: &[MizanFunction],
) -> String {
let pascal = pascal_case(ctx_name);
let snake = snake_case(ctx_name);
let ctx_fns: Vec<&MizanFunction> = all_functions
.iter()
.filter(|f| f.is_context.as_str() == Some(ctx_name))
.collect();
let type_imports = dedupe_preserving_order(
ctx_fns.iter().map(|f| rust_type_ident(&f.output_type)),
);
let data_fields: Vec<StructField> = ctx_fns.iter()
.map(|f| {
let ident = rust_ident(&f.name);
StructField {
has_rename: ident != f.name,
raw_name: f.name.clone(),
ident,
ty: rust_type_ident(&f.output_type),
}
})
.collect();
let params: Vec<StructField> = ctx_meta.params.iter()
.map(|(p_name, p_meta)| {
let ident = rust_ident(p_name);
let base = param_rust_type(&p_meta.ty);
let ty = if p_meta.required { base.to_string() } else { format!("Option<{base}>") };
StructField {
has_rename: ident != *p_name,
raw_name: p_name.clone(),
ident,
ty,
}
})
.collect();
ContextTemplate {
pascal,
snake,
ctx_name,
type_imports,
data_fields,
params,
}.render().expect("context.rs renders")
}
fn param_rust_type(json_ty: &str) -> &'static str {
match json_ty {
"integer" => "i64",
"number" => "f64",
"boolean" => "bool",
_ => "String",
}
}
// ─── Call file ─────────────────────────────────────────────────────────────
fn emit_call_file(fn_meta: &MizanFunction) -> String {
let output_type = rust_type_ident(&fn_meta.output_type);
let return_type = if fn_meta.output_nullable {
format!("Option<{output_type}>")
} else {
output_type.clone()
};
let input_type = fn_meta.input_type.as_deref().map(rust_type_ident);
let mut used_seed: Vec<String> = vec![output_type.clone()];
if let Some(t) = &input_type { used_seed.push(t.clone()); }
let type_imports = dedupe_preserving_order(used_seed);
let (input_param, args_value) = if fn_meta.has_input {
let it = input_type.as_deref().unwrap_or("");
(
format!(", args: &{it}"),
"serde_json::to_value(args).unwrap_or(Value::Object(Default::default()))",
)
} else {
(
String::new(),
"Value::Object(Default::default())",
)
};
CallTemplate {
snake: snake_case(&fn_meta.name),
name: &fn_meta.name,
return_type,
type_imports,
input_param,
args_value,
}.render().expect("call.rs renders")
}
// ─── types.rs ──────────────────────────────────────────────────────────────
struct EnumCtx {
hoisted: Vec<(String, Vec<serde_json::Value>)>,
depth: usize,
enum_name: Option<String>,
}
fn emit_types_rs(schemas: &IndexMap<String, JsonSchema>) -> String {
let mut ctx = EnumCtx { hoisted: Vec::new(), depth: 0, enum_name: None };
let schemas_block = schemas.iter()
.map(|(raw_name, schema)| {
let name = rust_type_ident(raw_name);
if let Some(values) = &schema.r#enum {
if schema.ty.as_deref() == Some("string") {
return emit_string_enum(&name, values);
}
}
if schema.ty.as_deref() == Some("array") {
return emit_transparent_array(&name, schema, &mut ctx);
}
if schema.ty.as_deref() == Some("object") {
if let Some(props) = &schema.properties {
return emit_struct(&name, schema, props, &mut ctx);
}
}
emit_type_alias(&name, schema, &mut ctx)
})
.collect::<Vec<_>>()
.join("\n");
let hoisted_enums_block = ctx.hoisted.iter()
.map(|(n, v)| emit_string_enum(n, v))
.collect::<Vec<_>>()
.join("\n");
TypesTemplate { schemas_block, hoisted_enums_block }
.render().expect("types.rs renders")
}
fn emit_string_enum(name: &str, variants: &[serde_json::Value]) -> String {
let body = variants.iter()
.filter_map(|v| v.as_str())
.map(|v| {
let ident = pascal_case(v);
let rename = if ident == v {
String::new()
} else {
format!(" #[serde(rename = {})]\n", json_str(v))
};
format!("{rename} {ident},")
})
.collect::<Vec<_>>()
.join("\n");
format!(
"#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum {name} {{\n{body}\n}}\n",
name = rust_type_ident(name),
)
}
fn emit_transparent_array(name: &str, schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
ctx.depth = 1;
ctx.enum_name = None;
let inner = rust_type_from_schema(schema.items.as_deref().unwrap_or(&JsonSchema::default()), ctx);
format!(
"#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(transparent)]\npub struct {name}(pub Vec<{inner}>);\n",
)
}
fn emit_struct(
name: &str,
schema: &JsonSchema,
properties: &IndexMap<String, JsonSchema>,
ctx: &mut EnumCtx,
) -> String {
let required: std::collections::HashSet<&str> =
schema.required.iter().map(String::as_str).collect();
let fields = properties.iter()
.map(|(field_raw, field_schema)| {
let field_name = rust_ident(field_raw);
ctx.depth = 1;
ctx.enum_name = Some(format!("{name}_{}", pascal_case(field_raw)));
let mut ty = rust_type_from_schema(field_schema, ctx);
let is_required = required.contains(field_raw.as_str())
|| field_schema.default.is_some();
if !is_required && !ty.starts_with("Option<") {
ty = format!("Option<{ty}>");
}
let rename = if field_name == *field_raw {
String::new()
} else {
format!(" #[serde(rename = \"{field_raw}\")]\n")
};
format!("{rename} pub {field_name}: {ty},")
})
.collect::<Vec<_>>()
.join("\n");
format!(
"#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {name} {{\n{fields}\n}}\n",
)
}
fn emit_type_alias(name: &str, schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
ctx.depth = 0;
ctx.enum_name = Some(name.to_string());
let ty = rust_type_from_schema(schema, ctx);
format!("pub type {name} = {ty};\n")
}
fn rust_type_from_schema(schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
if let Some(r) = schema.ref_name() {
return rust_type_ident(r);
}
if let Some(any_of) = &schema.any_of {
let has_null = any_of.iter().any(|s| s.ty.as_deref() == Some("null"));
let non_null: Vec<&JsonSchema> = any_of
.iter()
.filter(|s| s.ty.as_deref() != Some("null"))
.collect();
if has_null && non_null.len() == 1 {
ctx.enum_name = None;
return format!("Option<{}>", rust_type_from_schema(non_null[0], ctx));
}
}
let nullable = schema.nullable;
let inner = inner_rust_type(schema, ctx);
if nullable {
format!("Option<{inner}>")
} else {
inner
}
}
fn inner_rust_type(schema: &JsonSchema, ctx: &mut EnumCtx) -> String {
if let Some(values) = &schema.r#enum {
if schema.ty.as_deref() == Some("string") {
let enum_name = ctx
.enum_name
.clone()
.unwrap_or_else(|| format!("Enum_{}", ctx.depth));
ctx.hoisted.push((enum_name.clone(), values.clone()));
return enum_name;
}
}
match schema.ty.as_deref() {
Some("integer") => "i64".to_string(),
Some("number") => "f64".to_string(),
Some("boolean") => "bool".to_string(),
Some("string") => "String".to_string(),
Some("array") => {
ctx.depth += 1;
ctx.enum_name = None;
let inner = rust_type_from_schema(schema.items.as_deref().unwrap_or(&JsonSchema::default()), ctx);
format!("Vec<{inner}>")
}
Some("object") => "serde_json::Value".to_string(),
_ => "serde_json::Value".to_string(),
}
}