Files
mizan/protocol/mizan-codegen/src/emit/rust.rs
Ryth Azhur ae684a36cb Restore approved state (tree of 4effcc7 "Added LICENSE")
Roll the working tree back to the last approved shape, before the post-LICENSE span that false-greened the AFI parity matrix with symbol-presence probes and smuggled an unauthorized SQLAlchemy dependency into FastAPI's Shapes binding.

Forward commit, not a history rewrite — the six commits since 4effcc7 stay in the log as the record of what happened.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:59:53 -04:00

445 lines
14 KiB
Rust

//! 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, MizanContext, MizanFunction, MizanIR, NamedType, Primitive,
StructField as IrStructField, TypeShape,
};
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.types)));
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<RustField>,
params: Vec<RustField>,
}
#[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,
}
/// Renderer-side view of a single Rust struct field. Distinct from
/// `ir::StructField` (the IR shape) because the renderer carries
/// already-rendered identifiers and rename flags.
struct RustField {
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<RustField> = ctx_fns.iter()
.map(|f| {
let ident = rust_ident(&f.name);
RustField {
has_rename: ident != f.name,
raw_name: f.name.clone(),
ident,
ty: rust_type_ident(&f.output_type),
}
})
.collect();
let params: Vec<RustField> = 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}>") };
RustField {
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(p: Primitive) -> &'static str {
match p {
Primitive::Integer => "i64",
Primitive::Number => "f64",
Primitive::Boolean => "bool",
Primitive::String => "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 ──────────────────────────────────────────────────────────────
/// Per-types-file context tracking enum names hoisted out of inline
/// `field { enum "a" "b" }` declarations into Rust top-level enum types.
struct EnumCtx {
hoisted: Vec<(String, Vec<String>)>,
enum_name: Option<String>,
}
fn emit_types_rs(types: &IndexMap<String, NamedType>) -> String {
let mut ctx = EnumCtx { hoisted: Vec::new(), enum_name: None };
let schemas_block = types.iter()
.map(|(raw_name, ty)| {
let name = rust_type_ident(raw_name);
match ty {
NamedType::Struct(fields) => emit_struct_decl(&name, fields, &mut ctx),
NamedType::List(inner) => emit_transparent_array(&name, inner, &mut ctx),
NamedType::Enum(variants) => emit_string_enum(&name, variants),
NamedType::Alias(inner) => emit_type_alias(&name, inner, &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: &[String]) -> String {
let body = variants.iter()
.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, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
ctx.enum_name = None;
let inner_ty = rust_type_from_shape(inner, ctx);
format!(
"#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(transparent)]\npub struct {name}(pub Vec<{inner_ty}>);\n",
)
}
fn emit_struct_decl(
name: &str,
fields: &[IrStructField],
ctx: &mut EnumCtx,
) -> String {
let fields_body = fields.iter()
.map(|f| {
let field_name = rust_ident(&f.name);
ctx.enum_name = Some(format!("{name}_{}", pascal_case(&f.name)));
let mut ty = rust_type_from_shape(&f.shape, ctx);
let is_required = f.required || f.default.is_some();
if !is_required && !ty.starts_with("Option<") {
ty = format!("Option<{ty}>");
}
let rename = if field_name == f.name {
String::new()
} else {
format!(" #[serde(rename = \"{raw}\")]\n", raw = f.name)
};
format!("{rename} pub {field_name}: {ty},")
})
.collect::<Vec<_>>()
.join("\n");
format!(
"#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {name} {{\n{fields_body}\n}}\n",
)
}
fn emit_type_alias(name: &str, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
ctx.enum_name = Some(name.to_string());
let ty = rust_type_from_shape(inner, ctx);
format!("pub type {name} = {ty};\n")
}
fn rust_type_from_shape(shape: &TypeShape, ctx: &mut EnumCtx) -> String {
match shape {
TypeShape::Ref(name) => rust_type_ident(name),
TypeShape::Primitive(Primitive::Integer) => "i64".to_string(),
TypeShape::Primitive(Primitive::Number) => "f64".to_string(),
TypeShape::Primitive(Primitive::Boolean) => "bool".to_string(),
TypeShape::Primitive(Primitive::String) => "String".to_string(),
TypeShape::List(inner) => {
ctx.enum_name = None;
format!("Vec<{}>", rust_type_from_shape(inner, ctx))
}
TypeShape::Optional(inner) => {
ctx.enum_name = None;
format!("Option<{}>", rust_type_from_shape(inner, ctx))
}
TypeShape::Enum(variants) => {
// Inline enums hoist out into top-level Rust enum types so the
// generated struct field can reference them by name.
let enum_name = ctx
.enum_name
.clone()
.unwrap_or_else(|| "Enum_inline".to_string());
ctx.hoisted.push((enum_name.clone(), variants.clone()));
enum_name
}
TypeShape::Union(_branches) => {
// Rust serde doesn't have a clean way to deserialize an untagged
// multi-arm union without losing type info; fall back to a JSON
// Value so the consumer can match on the runtime variant.
"serde_json::Value".to_string()
}
}
}