//! KDL emitter — byte-equivalent to `cores/mizan-python/src/mizan_core/ir.py`. //! //! The Python emitter is the spec; this is the second implementation under //! the same contract. Any divergence is a bug here, not a contract change. use crate::ir::{DefaultValue, NamedType, Primitive, StructField, TypeShape}; use crate::registry::{CONTEXTS, FUNCTIONS, TYPES}; use crate::traits::FunctionSpec; use std::collections::BTreeMap; const INDENT: &str = " "; /// Escape a string for KDL — same escape set as the Python emitter. fn kdl_string(s: &str) -> String { let mut out = String::with_capacity(s.len() + 2); out.push('"'); for c in s.chars() { match c { '\\' => out.push_str("\\\\"), '"' => out.push_str("\\\""), '\n' => out.push_str("\\n"), '\r' => out.push_str("\\r"), '\t' => out.push_str("\\t"), other => out.push(other), } } out.push('"'); out } fn kdl_bool(b: bool) -> &'static str { if b { "#true" } else { "#false" } } fn kdl_default(v: &DefaultValue) -> String { match v { DefaultValue::Null => "#null".into(), DefaultValue::Boolean(b) => kdl_bool(*b).into(), DefaultValue::Integer(i) => i.to_string(), DefaultValue::Number(f) => { // Match Python's `repr(float)` for whole-number-equal-but-float // values: e.g. 1.0 → "1.0", not "1". if f.fract() == 0.0 && f.is_finite() { format!("{f:.1}") } else { f.to_string() } } DefaultValue::String(s) => kdl_string(s), } } /// Convert snake_case to camelCase. Matches Python's `_snake_to_camel`. pub fn snake_to_camel(name: &str) -> String { let normalized = name.replace('.', "_").replace('-', "_"); let mut parts = normalized.split('_'); let mut out = String::new(); if let Some(first) = parts.next() { out.push_str(first); } for part in parts { if part.is_empty() { continue; } let mut chars = part.chars(); if let Some(c) = chars.next() { out.extend(c.to_uppercase()); out.push_str(chars.as_str()); } } out } struct Emitter { lines: Vec, } impl Emitter { fn new() -> Self { Self { lines: Vec::new() } } fn prefix(&self, indent: usize) -> String { INDENT.repeat(indent) } fn leaf(&mut self, indent: usize, parts: &[&str]) { let mut line = self.prefix(indent); line.push_str(&parts.join(" ")); self.lines.push(line); } fn open(&mut self, indent: usize, parts: &[&str]) { let mut line = self.prefix(indent); line.push_str(&parts.join(" ")); line.push_str(" {"); self.lines.push(line); } fn close(&mut self, indent: usize) { let mut line = self.prefix(indent); line.push('}'); self.lines.push(line); } fn blank(&mut self) { self.lines.push(String::new()); } fn emit_type_child(&mut self, indent: usize, shape: &TypeShape) { match shape { TypeShape::Primitive(p) => { let name = kdl_string(p.name()); self.leaf(indent, &["primitive", &name]); } TypeShape::Ref(name) => { let n = kdl_string(name); self.leaf(indent, &["ref", &n]); } TypeShape::List(inner) => { self.open(indent, &["list"]); self.emit_type_child(indent + 1, inner); self.close(indent); } TypeShape::Optional(inner) => { self.open(indent, &["optional"]); self.emit_type_child(indent + 1, inner); self.close(indent); } TypeShape::Enum(variants) => { let mut parts: Vec = vec!["enum".into()]; for v in variants { parts.push(kdl_string(v)); } let line: Vec<&str> = parts.iter().map(String::as_str).collect(); self.leaf(indent, &line); } TypeShape::Union(branches) => { self.open(indent, &["union"]); for b in branches { self.emit_type_child(indent + 1, b); } self.close(indent); } } } fn emit_named_type(&mut self, indent: usize, name: &str, body: &NamedType) { let name_lit = kdl_string(name); self.open(indent, &["type", &name_lit]); match body { NamedType::Struct(fields) => { self.open(indent + 1, &["struct"]); for field in fields { self.emit_struct_field(indent + 2, field); } self.close(indent + 1); } NamedType::Alias(inner) => { self.open(indent + 1, &["alias"]); self.emit_type_child(indent + 2, inner); self.close(indent + 1); } NamedType::Enum(variants) => { let mut parts: Vec = vec!["enum".into()]; for v in variants { parts.push(kdl_string(v)); } let line: Vec<&str> = parts.iter().map(String::as_str).collect(); self.leaf(indent + 1, &line); } } self.close(indent); } fn emit_struct_field(&mut self, indent: usize, field: &StructField) { let name = kdl_string(field.name); let mut header: Vec = vec!["field".into(), name]; if !field.required { header.push(format!("required={}", kdl_bool(false))); if let Some(default) = &field.default { header.push(format!("default={}", kdl_default(default))); } } let line_parts: Vec<&str> = header.iter().map(String::as_str).collect(); self.open(indent, &line_parts); self.emit_type_child(indent + 1, &field.shape); self.close(indent); } fn emit_function(&mut self, indent: usize, fn_spec: &dyn FunctionSpec) { let name = kdl_string(fn_spec.name()); self.open(indent, &["function", &name]); let camel = kdl_string(fn_spec.camel_name()); self.leaf(indent + 1, &["camel", &camel]); self.leaf(indent + 1, &["has-input", kdl_bool(fn_spec.has_input())]); if let Some(input_type) = fn_spec.input_type() { let lit = kdl_string(input_type); self.leaf(indent + 1, &["input", &lit]); } let output_lit = kdl_string(fn_spec.output_type()); self.leaf(indent + 1, &["output", &output_lit]); if fn_spec.output_nullable() { self.leaf(indent + 1, &["output-nullable", kdl_bool(true)]); } let transport_lit = kdl_string(fn_spec.transport().name()); self.leaf(indent + 1, &["transport", &transport_lit]); if let Some(ctx) = fn_spec.context() { let lit = kdl_string(ctx); self.leaf(indent + 1, &["context", &lit]); } for affect in fn_spec.affects() { // Mirror Python's behavior: only context-typed affects make it // into the KDL `affects` leaf. Function-typed affects are // reserved for a future IR extension. if let crate::ir::AffectTarget::Context(name) = affect { let lit = kdl_string(name); self.leaf(indent + 1, &["affects", &lit]); } } for merge in fn_spec.merge() { let lit = kdl_string(merge); self.leaf(indent + 1, &["merge", &lit]); } if fn_spec.is_form() { self.leaf(indent + 1, &["is-form", kdl_bool(true)]); if let Some(form_name) = fn_spec.form_name() { let lit = kdl_string(form_name); self.leaf(indent + 1, &["form-name", &lit]); } if let Some(form_role) = fn_spec.form_role() { let lit = kdl_string(form_role); self.leaf(indent + 1, &["form-role", &lit]); } } self.close(indent); } fn emit_context(&mut self, indent: usize, ctx_name: &str, members: &[&'static dyn FunctionSpec]) { let name_lit = kdl_string(ctx_name); self.open(indent, &["context", &name_lit]); // Function membership in registration order. for fn_spec in members { let lit = kdl_string(fn_spec.name()); self.leaf(indent + 1, &["function", &lit]); } // Param info — collect across every member, then emit alphabetized // by param name to match Python. struct ParamSlot { primitive: Primitive, shared_by: Vec<&'static str>, } let mut params: BTreeMap<&'static str, ParamSlot> = BTreeMap::new(); for fn_spec in members { for p in fn_spec.input_params() { let slot = params.entry(p.name).or_insert(ParamSlot { primitive: p.primitive, shared_by: Vec::new(), }); slot.primitive = p.primitive; slot.shared_by.push(fn_spec.name()); } } let member_count = members.len(); for (param_name, slot) in params.iter() { let name_lit = kdl_string(param_name); self.open(indent + 1, &["param", &name_lit]); let type_lit = kdl_string(slot.primitive.name()); self.leaf(indent + 2, &["type", &type_lit]); let required = slot.shared_by.len() == member_count; self.leaf(indent + 2, &["required", kdl_bool(required)]); for sharer in &slot.shared_by { let lit = kdl_string(sharer); self.leaf(indent + 2, &["shared-by", &lit]); } self.close(indent + 1); } self.close(indent); } fn into_string(mut self) -> String { // Trim trailing blanks, then add a single terminating newline. while matches!(self.lines.last(), Some(s) if s.is_empty()) { self.lines.pop(); } let mut out = self.lines.join("\n"); out.push('\n'); out } } /// Collected typed registries view used by `build_ir`. pub(crate) struct IrSnapshot { pub types: BTreeMap<&'static str, NamedType>, pub functions: Vec<&'static dyn FunctionSpec>, pub contexts: Vec<(&'static str, Vec<&'static dyn FunctionSpec>)>, } impl IrSnapshot { pub(crate) fn collect() -> Self { // Types: alphabetized for byte-equivalence with Python's `sorted(named_types)`. let mut types: BTreeMap<&'static str, NamedType> = BTreeMap::new(); for entry in TYPES { types.insert(entry.name, (entry.shape_fn)()); } // Functions: alphabetical by wire name (canonical IR ordering, // matches the Python emitter's `sorted(functions)`). Skip `private`. let mut functions: Vec<&'static dyn FunctionSpec> = FUNCTIONS .iter() .copied() .filter(|f| !f.private()) .collect(); functions.sort_by_key(|f| f.name()); // Contexts: alphabetical by name (canonical IR ordering), each with // its members sorted alphabetically too. let mut context_names: Vec<&'static str> = CONTEXTS.iter().map(|c| c.name).collect(); context_names.sort(); let mut contexts: Vec<(&'static str, Vec<&'static dyn FunctionSpec>)> = Vec::new(); for name in context_names { let mut members: Vec<&'static dyn FunctionSpec> = functions .iter() .copied() .filter(|f| f.context() == Some(name)) .collect(); members.sort_by_key(|f| f.name()); if !members.is_empty() { contexts.push((name, members)); } } Self { types, functions, contexts, } } } /// Build the Mizan IR for every registered type/function/context. Returns KDL. pub fn build_ir() -> String { crate::graph_check::verify_invariants(); let snap = IrSnapshot::collect(); let mut em = Emitter::new(); // Type definitions let types_emitted = !snap.types.is_empty(); for (name, body) in &snap.types { em.emit_named_type(0, name, body); } if types_emitted { em.blank(); } // Functions let fns_emitted = !snap.functions.is_empty(); for fn_spec in &snap.functions { em.emit_function(0, *fn_spec); } if fns_emitted { em.blank(); } // Contexts let ctxs_emitted = !snap.contexts.is_empty(); for (ctx_name, members) in &snap.contexts { em.emit_context(0, ctx_name, members); } if ctxs_emitted { em.blank(); } // Future: channels — once channel registry lands on the Rust side. em.into_string() }