Building a Code Generator
A complete TypeScript interface generator demonstrating how to walk
IrModule with a IrVisitor, resolve types via
ResolvedType, and emit target-language source.
The program below compiles and runs against the current crate.
use formalang::ast::PrimitiveType; use formalang::compile_to_ir; use formalang::ir::{ walk_module, EnumId, GenericBase, IrEnum, IrField, IrModule, IrStruct, IrTrait, IrVisitor, ResolvedType, StructId, TraitId, }; struct TypeScriptGenerator<'a> { module: &'a IrModule, output: String, } impl<'a> TypeScriptGenerator<'a> { fn new(module: &'a IrModule) -> Self { Self { module, output: String::new(), } } fn struct_name(&self, id: StructId) -> String { self.module .get_struct(id) .map_or_else(|| "unknown".to_string(), |s| s.name.clone()) } fn enum_name(&self, id: EnumId) -> String { self.module .get_enum(id) .map_or_else(|| "unknown".to_string(), |e| e.name.clone()) } fn trait_name(&self, id: TraitId) -> String { self.module .get_trait(id) .map_or_else(|| "unknown".to_string(), |t| t.name.clone()) } fn resolve_type(&self, ty: &ResolvedType) -> String { // The built-in carriers first: they are `Generic` over a // prelude definition. if let Some(inner) = self.module.array_element_ty(ty) { return format!("{}[]", self.resolve_type(inner)); } if let Some(inner) = self.module.optional_inner_ty(ty) { return format!("{} | null", self.resolve_type(inner)); } if let Some((key, value)) = self.module.dictionary_kv_ty(ty) { return format!( "Record<{}, {}>", self.resolve_type(key), self.resolve_type(value) ); } match ty { ResolvedType::Primitive(p) => match p { PrimitiveType::String => "string".to_string(), PrimitiveType::I32 | PrimitiveType::I64 | PrimitiveType::F32 | PrimitiveType::F64 => "number".to_string(), PrimitiveType::Boolean => "boolean".to_string(), PrimitiveType::Never => "never".to_string(), _ => "unknown".to_string(), }, ResolvedType::Struct(id) => self.struct_name(*id), ResolvedType::Trait(id) => self.trait_name(*id), ResolvedType::Enum(id) => self.enum_name(*id), ResolvedType::Tuple(fields) => { let fields: Vec<String> = fields .iter() .map(|(name, ty)| format!("{}: {}", name, self.resolve_type(ty))) .collect(); format!("{{ {} }}", fields.join("; ")) } ResolvedType::Generic { base, args } => { let base_name = match base { GenericBase::Struct(id) => self.struct_name(*id), GenericBase::Enum(id) => self.enum_name(*id), GenericBase::Trait(id) => self.trait_name(*id), }; let args: Vec<String> = args.iter().map(|a| self.resolve_type(a)).collect(); format!("{}<{}>", base_name, args.join(", ")) } ResolvedType::Closure { param_tys, return_ty, } => { let params: Vec<String> = param_tys .iter() .enumerate() .map(|(i, (_, t))| format!("a{}: {}", i, self.resolve_type(t))) .collect(); format!("({}) => {}", params.join(", "), self.resolve_type(return_ty)) } ResolvedType::External { name, type_args, .. } => { if type_args.is_empty() { name.clone() } else { let args: Vec<String> = type_args.iter().map(|a| self.resolve_type(a)).collect(); format!("{}<{}>", name, args.join(", ")) } } ResolvedType::TypeParam(name) => name.clone(), ResolvedType::Error => "never".to_string(), } } fn emit_field(&mut self, field: &IrField) { let ts_type = self.resolve_type(&field.ty); let optional = if field.optional { "?" } else { "" }; self.output .push_str(&format!(" {}{}: {};\n", field.name, optional, ts_type)); } } impl IrVisitor for TypeScriptGenerator<'_> { fn visit_trait(&mut self, _id: TraitId, t: &IrTrait) { if !t.visibility.is_public() { return; } self.output .push_str(&format!("export interface {} {{\n", t.name)); for field in &t.fields { self.emit_field(field); } self.output.push_str("}\n\n"); } fn visit_struct(&mut self, id: StructId, s: &IrStruct) { // Skip the prelude carriers and private structs if self.module.is_prelude_struct(id) || !s.visibility.is_public() { return; } let generics = if s.generic_params.is_empty() { String::new() } else { let params: Vec<String> = s.generic_params.iter().map(|p| p.name.clone()).collect(); format!("<{}>", params.join(", ")) }; // `impl Named for User` puts `Named` in `traits` let extends = if s.traits.is_empty() { String::new() } else { let traits: Vec<String> = s .traits .iter() .map(|t| self.trait_name(t.trait_id)) .collect(); format!(" extends {}", traits.join(", ")) }; self.output.push_str(&format!( "export interface {}{}{} {{\n", s.name, generics, extends )); for field in &s.fields { self.emit_field(field); } self.output.push_str("}\n\n"); } fn visit_enum(&mut self, id: EnumId, e: &IrEnum) { if self.module.is_prelude_enum(id) || !e.visibility.is_public() { return; } self.output.push_str(&format!("export type {} =\n", e.name)); for (i, variant) in e.variants.iter().enumerate() { let end = if i + 1 == e.variants.len() { ";" } else { "" }; if variant.fields.is_empty() { self.output .push_str(&format!(" | {{ type: \"{}\" }}{}\n", variant.name, end)); } else { let fields: Vec<String> = variant .fields .iter() .map(|f| format!("{}: {}", f.name, self.resolve_type(&f.ty))) .collect(); self.output.push_str(&format!( " | {{ type: \"{}\"; {} }}{}\n", variant.name, fields.join("; "), end )); } } self.output.push('\n'); } } fn generate_typescript(source: &str) -> Result<String, Vec<formalang::CompilerError>> { let module = compile_to_ir(source)?; let mut generator = TypeScriptGenerator::new(&module); walk_module(&mut generator, &module); Ok(generator.output) } fn main() { // Usage let source = r#" pub trait Named { name: String } pub struct User { name: String, age: I32, email: String? } impl Named for User {} pub enum Status { active, pending(reason: String), inactive } "#; let typescript = generate_typescript(source).unwrap(); print!("{}", typescript); }
Output. The visitor visits the structs before the traits:
export interface User extends Named {
name: string;
age: number;
email?: string | null;
}
export interface Named {
name: string;
}
export type Status =
| { type: "active" }
| { type: "pending"; reason: string }
| { type: "inactive" };