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.

#![allow(unused)]
fn main() {
use formalang::compile_to_ir;
use formalang::ir::{
    IrModule, IrStruct, IrEnum, IrEnumVariant, IrField, IrVisitor,
    StructId, EnumId, ResolvedType, walk_module
};
use formalang::ast::PrimitiveType;

struct TypeScriptGenerator<'a> {
    module: &'a IrModule,
    output: String,
}

impl<'a> TypeScriptGenerator<'a> {
    fn new(module: &'a IrModule) -> Self {
        Self {
            module,
            output: String::new(),
        }
    }

    fn resolve_type(&self, ty: &ResolvedType) -> String {
        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::Path => "string".to_string(),
                PrimitiveType::Regex => "RegExp".to_string(),
                PrimitiveType::Never => "never".to_string(),
            },
            ResolvedType::Struct(id) => {
                self.module.get_struct(*id).unwrap().name.clone()
            }
            ResolvedType::Trait(id) => {
                self.module.get_trait(*id).unwrap().name.clone()
            }
            ResolvedType::Enum(id) => {
                self.module.get_enum(*id).unwrap().name.clone()
            }
            ResolvedType::Array(inner) => {
                format!("{}[]", self.resolve_type(inner))
            }
            ResolvedType::Optional(inner) => {
                format!("{} | null", self.resolve_type(inner))
            }
            ResolvedType::Tuple(fields) => {
                let fields_str: Vec<_> = fields
                    .iter()
                    .map(|(name, ty)| format!("{}: {}", name, self.resolve_type(ty)))
                    .collect();
                format!("{{ {} }}", fields_str.join("; "))
            }
            ResolvedType::Generic { base, args } => {
                let base_name = match base {
                    GenericBase::Struct(id) => self.module.get_struct(*id).unwrap().name.clone(),
                    GenericBase::Enum(id) => self.module.get_enum(*id).unwrap().name.clone(),
                    GenericBase::Trait(id) => self.module.get_trait(*id).unwrap().name.clone(),
                };
                let args_str: Vec<_> = args.iter().map(|a| self.resolve_type(a)).collect();
                format!("{}<{}>", base_name, args_str.join(", "))
            }
            ResolvedType::Dictionary { key_ty, value_ty } => {
                format!(
                    "Record<{}, {}>",
                    self.resolve_type(key_ty),
                    self.resolve_type(value_ty)
                )
            }
            ResolvedType::Closure { param_tys, return_ty } => {
                let params: Vec<_> = 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<_> = type_args.iter().map(|a| self.resolve_type(a)).collect();
                    format!("{}<{}>", name, args.join(", "))
                }
            }
            ResolvedType::TypeParam(name) => name.clone(),
        }
    }

    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<'a> IrVisitor for TypeScriptGenerator<'a> {
    fn visit_struct(&mut self, _id: StructId, s: &IrStruct) {
        // Skip private structs
        if !s.visibility.is_public() {
            return;
        }

        // Generic parameters
        let generics = if s.generic_params.is_empty() {
            String::new()
        } else {
            let params: Vec<_> = s.generic_params
                .iter()
                .map(|p| p.name.clone())
                .collect();
            format!("<{}>", params.join(", "))
        };

        // Extends clause for traits
        let extends = if s.traits.is_empty() {
            String::new()
        } else {
            let traits: Vec<_> = s.traits
                .iter()
                .map(|id| self.module.get_trait(*id).name.clone())
                .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 !e.visibility.is_public() {
            return;
        }

        // Generate discriminated union
        self.output.push_str(&format!(
            "export type {} =\n",
            e.name
        ));

        for (i, variant) in e.variants.iter().enumerate() {
            let sep = if i == e.variants.len() - 1 { ";" } else { " |" };

            if variant.fields.is_empty() {
                self.output.push_str(&format!(
                    "  | {{ type: \"{}\" }}{}\n",
                    variant.name, sep
                ));
            } else {
                let fields: Vec<_> = 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("; "), sep
                ));
            }
        }

        self.output.push('\n');
    }
}

fn generate_typescript(source: &str) -> Result<String, Vec<formalang::CompilerError>> {
    let module = compile_to_ir(source)?;
    let mut gen = TypeScriptGenerator::new(&module);
    walk_module(&mut gen, &module);
    Ok(gen.output)
}

// Usage
let source = r#"
pub trait Named {
    name: String
}

pub struct User: Named {
    name: String,
    age: I32,
    email: String?
}

pub enum Status {
    active,
    pending(reason: String),
    inactive
}
"#;

let typescript = generate_typescript(source).unwrap();
println!("{}", typescript);

// Output:
// export interface Named {
//   name: string;
// }
//
// export interface User extends Named {
//   name: string;
//   age: number;
//   email?: string | null;
// }
//
// export type Status =
//   | { type: "active" } |
//   | { type: "pending"; reason: string } |
//   | { type: "inactive" };
}