use crate::ast::{Declaration, Evo, Gen, Rule, Statement, System, Trait, TypeExpr};
use crate::typechecker::Type;
use super::{to_pascal_case, CodegenOptions, TypeMapper};
fn to_camel_case(s: &str) -> String {
let pascal = to_pascal_case(s);
let mut chars = pascal.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_lowercase().chain(chars).collect(),
}
}
#[derive(Debug, Clone, Default)]
pub struct TypeScriptCodegen {
options: CodegenOptions,
}
impl TypeScriptCodegen {
pub fn new() -> Self {
Self::default()
}
pub fn with_options(options: CodegenOptions) -> Self {
Self { options }
}
pub fn generate(decl: &Declaration) -> String {
Self::new().generate_declaration(decl)
}
pub fn generate_all(decls: &[Declaration]) -> String {
let generator = Self::new();
decls
.iter()
.map(|d| generator.generate_declaration(d))
.collect::<Vec<_>>()
.join("\n\n")
}
fn generate_declaration(&self, decl: &Declaration) -> String {
match decl {
Declaration::Gene(gene) => self.generate_gene(gene),
Declaration::Trait(trait_decl) => self.generate_trait(trait_decl),
Declaration::Constraint(constraint) => self.generate_constraint(constraint),
Declaration::System(system) => self.generate_system(system),
Declaration::Evolution(evolution) => self.generate_evolution(evolution),
Declaration::Function(func) => self.generate_function(func),
Declaration::Const(c) => self.generate_const(c),
Declaration::SexVar(v) => self.generate_sex_var(v),
}
}
fn generate_const(&self, c: &crate::ast::ConstDecl) -> String {
let name = c.name.to_uppercase().replace('.', "_");
let type_str = c
.type_ann
.as_ref()
.map(Self::map_type_expr)
.unwrap_or_else(|| "number".to_string());
let value = "0 /* TODO: const expression */".to_string();
format!("export const {}: {} = {};", name, type_str, value)
}
fn generate_sex_var(&self, v: &crate::ast::VarDecl) -> String {
let name = v.name.to_uppercase().replace('.', "_");
let type_str = v
.type_ann
.as_ref()
.map(Self::map_type_expr)
.unwrap_or_else(|| "number".to_string());
let value = "0 /* TODO: sex var expression */".to_string();
format!("export let {}: {} = {};", name, type_str, value)
}
fn generate_function(&self, func: &crate::ast::FunctionDecl) -> String {
let mut output = String::new();
if !func.exegesis.is_empty() {
output.push_str(&self.format_jsdoc(&func.exegesis));
}
output.push_str(&format!("export function {}(", func.name));
let params: Vec<String> = func
.params
.iter()
.map(|p| format!("{}: {}", p.name, Self::map_type_expr(&p.type_ann)))
.collect();
output.push_str(¶ms.join(", "));
output.push(')');
if let Some(ret_ty) = &func.return_type {
output.push_str(": ");
output.push_str(&Self::map_type_expr(ret_ty));
}
output.push_str(" {\n");
output.push_str(" // TODO: implement\n");
output.push_str("}\n");
output
}
fn generate_gene(&self, gene: &Gen) -> String {
let interface_name = to_pascal_case(&gene.name);
let fields = self.extract_fields(&gene.statements);
let mut output = String::new();
output.push_str(&self.format_jsdoc(&gene.exegesis));
let export = if self.is_public() { "export " } else { "" };
output.push_str(&format!("{export}interface {interface_name} {{\n"));
for (field_name, field_type) in &fields {
let ts_field = to_camel_case(field_name);
output.push_str(&format!(" {ts_field}: {field_type};\n"));
}
output.push_str("}\n");
output
}
fn generate_trait(&self, trait_decl: &Trait) -> String {
let interface_name = to_pascal_case(&trait_decl.name);
let extends: Vec<String> = trait_decl
.statements
.iter()
.filter_map(|stmt| {
if let Statement::Uses { reference, .. } = stmt {
Some(to_pascal_case(reference))
} else {
None
}
})
.collect();
let methods = self.extract_methods(&trait_decl.statements);
let mut output = String::new();
output.push_str(&self.format_jsdoc(&trait_decl.exegesis));
let export = if self.is_public() { "export " } else { "" };
if extends.is_empty() {
output.push_str(&format!("{export}interface {interface_name} {{\n"));
} else {
output.push_str(&format!(
"{export}interface {interface_name} extends {} {{\n",
extends.join(", ")
));
}
for (method_name, return_type) in &methods {
let ts_method = to_camel_case(method_name);
output.push_str(&format!(" /** Get the {} state. */\n", method_name));
output.push_str(&format!(" {ts_method}(): {return_type};\n"));
}
output.push_str("}\n");
output
}
fn generate_constraint(&self, constraint: &Rule) -> String {
let mut output = String::new();
output.push_str(&self.format_jsdoc(&constraint.exegesis));
let export = if self.is_public() { "export " } else { "" };
output.push_str(&format!(
"{export}function validate{}<T>(value: T): boolean {{\n",
to_pascal_case(&constraint.name)
));
for stmt in &constraint.statements {
match stmt {
Statement::Matches {
subject, target, ..
} => {
output.push_str(&format!(" // {subject} matches {target}\n"));
}
Statement::Never {
subject, action, ..
} => {
output.push_str(&format!(" // {subject} never {action}\n"));
}
_ => {}
}
}
output.push_str(" // Implement validation logic based on the constraint rules above\n");
output.push_str(" return true;\n");
output.push_str("}\n");
output
}
fn generate_system(&self, system: &System) -> String {
let namespace_name = to_pascal_case(&system.name);
let mut output = String::new();
output.push_str(&self.format_jsdoc(&system.exegesis));
let export = if self.is_public() { "export " } else { "" };
output.push_str(&format!("{export}namespace {namespace_name} {{\n"));
output.push_str(&format!(" export const VERSION = '{}';\n", system.version));
if !system.requirements.is_empty() {
output.push_str("\n // Requirements:\n");
for req in &system.requirements {
output.push_str(&format!(
" // - {} {} {}\n",
req.name, req.constraint, req.version
));
}
}
output.push_str("}\n");
output
}
fn generate_evolution(&self, evolution: &Evo) -> String {
let type_name = to_pascal_case(&evolution.name);
let mut output = String::new();
output.push_str("/**\n");
output.push_str(&format!(
" * Evolution: {} -> {}\n",
evolution.parent_version, evolution.version
));
output.push_str(" *\n");
for line in evolution.exegesis.lines() {
output.push_str(&format!(" * {}\n", line.trim()));
}
if !evolution.additions.is_empty() {
output.push_str(" *\n");
output.push_str(" * Additions:\n");
for addition in &evolution.additions {
match addition {
Statement::Has {
subject, property, ..
} => {
output.push_str(&format!(" * - {} has {}\n", subject, property));
}
Statement::Is { subject, state, .. } => {
output.push_str(&format!(" * - {} is {}\n", subject, state));
}
_ => {}
}
}
}
output.push_str(" */\n");
let export = if self.is_public() { "export " } else { "" };
output.push_str(&format!(
"{export}type {type_name}Evolution = '{}';\n",
evolution.version
));
output
}
fn extract_fields(&self, statements: &[Statement]) -> Vec<(String, String)> {
statements
.iter()
.filter_map(|stmt| {
if let Statement::Has { property, .. } = stmt {
Some((property.clone(), "unknown".to_string()))
} else {
None
}
})
.collect()
}
fn extract_methods(&self, statements: &[Statement]) -> Vec<(String, String)> {
statements
.iter()
.filter_map(|stmt| {
if let Statement::Is { state, .. } = stmt {
Some((state.clone(), "boolean".to_string()))
} else {
None
}
})
.collect()
}
fn format_jsdoc(&self, exegesis: &str) -> String {
let mut output = String::new();
output.push_str("/**\n");
for line in exegesis.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
output.push_str(" *\n");
} else {
output.push_str(&format!(" * {}\n", trimmed));
}
}
output.push_str(" */\n");
output
}
fn is_public(&self) -> bool {
matches!(
self.options.visibility,
super::Visibility::Public | super::Visibility::Crate
)
}
}
impl TypeMapper for TypeScriptCodegen {
fn map_type(ty: &Type) -> String {
match ty {
Type::Void => "void".to_string(),
Type::Bool => "boolean".to_string(),
Type::Int8 | Type::Int16 | Type::Int32 | Type::Int64 => "number".to_string(),
Type::UInt8 | Type::UInt16 | Type::UInt32 | Type::UInt64 => "number".to_string(),
Type::Float32 | Type::Float64 => "number".to_string(),
Type::String => "string".to_string(),
Type::Function {
params,
return_type,
} => {
let param_types: Vec<_> = params
.iter()
.enumerate()
.map(|(i, t)| format!("arg{}: {}", i, Self::map_type(t)))
.collect();
let ret = Self::map_type(return_type);
format!("({}) => {}", param_types.join(", "), ret)
}
Type::Tuple(types) => {
let mapped: Vec<_> = types.iter().map(Self::map_type).collect();
format!("[{}]", mapped.join(", "))
}
Type::Generic { name, args } => {
let mapped_args: Vec<_> = args.iter().map(Self::map_type).collect();
match name.as_str() {
"List" => {
if args.len() == 1 {
format!("{}[]", mapped_args[0])
} else {
"unknown[]".to_string()
}
}
"Map" => {
if args.len() == 2 {
format!("Map<{}, {}>", mapped_args[0], mapped_args[1])
} else {
"Map<unknown, unknown>".to_string()
}
}
"Option" => {
if args.len() == 1 {
format!("{} | undefined", mapped_args[0])
} else {
"unknown | undefined".to_string()
}
}
"Result" => {
if args.len() == 2 {
format!(
"{{ ok: true; value: {} }} | {{ ok: false; error: {} }}",
mapped_args[0], mapped_args[1]
)
} else {
"{ ok: boolean; value?: unknown; error?: unknown }".to_string()
}
}
_ => {
if args.is_empty() {
to_pascal_case(name)
} else {
format!("{}<{}>", to_pascal_case(name), mapped_args.join(", "))
}
}
}
}
Type::Var(id) => format!("T{}", id),
Type::Any => "any".to_string(),
Type::Unknown => "unknown".to_string(),
Type::Never => "never".to_string(),
Type::Error => "never".to_string(),
}
}
fn map_type_expr(ty: &TypeExpr) -> String {
match ty {
TypeExpr::Named(name) => match name.as_str() {
"Int8" | "Int16" | "Int32" | "Int64" => "number".to_string(),
"UInt8" | "UInt16" | "UInt32" | "UInt64" => "number".to_string(),
"Float32" | "Float64" => "number".to_string(),
"String" => "string".to_string(),
"Bool" => "boolean".to_string(),
"Void" => "void".to_string(),
"Any" => "any".to_string(),
_ => to_pascal_case(name),
},
TypeExpr::Generic { name, args } => {
let mapped_args: Vec<_> = args.iter().map(Self::map_type_expr).collect();
match name.as_str() {
"List" => {
if args.len() == 1 {
format!("{}[]", mapped_args[0])
} else {
"unknown[]".to_string()
}
}
"Map" => {
if args.len() == 2 {
format!("Map<{}, {}>", mapped_args[0], mapped_args[1])
} else {
"Map<unknown, unknown>".to_string()
}
}
"Option" => {
if args.len() == 1 {
format!("{} | undefined", mapped_args[0])
} else {
"unknown | undefined".to_string()
}
}
"Result" => {
if args.len() == 2 {
format!(
"{{ ok: true; value: {} }} | {{ ok: false; error: {} }}",
mapped_args[0], mapped_args[1]
)
} else {
"{ ok: boolean; value?: unknown; error?: unknown }".to_string()
}
}
_ => format!("{}<{}>", to_pascal_case(name), mapped_args.join(", ")),
}
}
TypeExpr::Function {
params,
return_type,
} => {
let param_types: Vec<_> = params
.iter()
.enumerate()
.map(|(i, t)| format!("arg{}: {}", i, Self::map_type_expr(t)))
.collect();
let ret = Self::map_type_expr(return_type);
format!("({}) => {}", param_types.join(", "), ret)
}
TypeExpr::Tuple(types) => {
let mapped: Vec<_> = types.iter().map(Self::map_type_expr).collect();
format!("[{}]", mapped.join(", "))
}
TypeExpr::Never => "never".to_string(),
TypeExpr::Enum { variants } => {
variants
.iter()
.map(|v| format!("\"{}\"", v.name))
.collect::<Vec<_>>()
.join(" | ")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Span, Visibility};
#[test]
fn test_generate_gene_interface() {
let gene = Gen {
visibility: Visibility::default(),
name: "container.exists".to_string(),
extends: None,
statements: vec![
Statement::Has {
subject: "container".to_string(),
property: "id".to_string(),
span: Span::default(),
},
Statement::Has {
subject: "container".to_string(),
property: "image".to_string(),
span: Span::default(),
},
],
exegesis: "A container is the fundamental unit.".to_string(),
span: Span::default(),
};
let code = TypeScriptCodegen::generate(&Declaration::Gene(gene));
assert!(code.contains("export interface ContainerExists"));
assert!(code.contains("id: unknown;"));
assert!(code.contains("image: unknown;"));
}
#[test]
fn test_generate_trait_interface() {
let trait_decl = Trait {
visibility: Visibility::default(),
name: "container.lifecycle".to_string(),
statements: vec![
Statement::Uses {
reference: "container.exists".to_string(),
span: Span::default(),
},
Statement::Is {
subject: "container".to_string(),
state: "created".to_string(),
span: Span::default(),
},
],
exegesis: "Container lifecycle management.".to_string(),
span: Span::default(),
};
let code = TypeScriptCodegen::generate(&Declaration::Trait(trait_decl));
assert!(code.contains("export interface ContainerLifecycle extends ContainerExists"));
assert!(code.contains("created(): boolean;"));
}
#[test]
fn test_map_type() {
assert_eq!(TypeScriptCodegen::map_type(&Type::Int32), "number");
assert_eq!(TypeScriptCodegen::map_type(&Type::String), "string");
assert_eq!(TypeScriptCodegen::map_type(&Type::Bool), "boolean");
assert_eq!(
TypeScriptCodegen::map_type(&Type::Generic {
name: "Option".to_string(),
args: vec![Type::String]
}),
"string | undefined"
);
assert_eq!(
TypeScriptCodegen::map_type(&Type::Generic {
name: "List".to_string(),
args: vec![Type::Int32]
}),
"number[]"
);
assert_eq!(
TypeScriptCodegen::map_type(&Type::Generic {
name: "Map".to_string(),
args: vec![Type::String, Type::Int64]
}),
"Map<string, number>"
);
}
#[test]
fn test_map_type_expr() {
assert_eq!(
TypeScriptCodegen::map_type_expr(&TypeExpr::Named("Int32".to_string())),
"number"
);
assert_eq!(
TypeScriptCodegen::map_type_expr(&TypeExpr::Generic {
name: "List".to_string(),
args: vec![TypeExpr::Named("String".to_string())]
}),
"string[]"
);
assert_eq!(
TypeScriptCodegen::map_type_expr(&TypeExpr::Generic {
name: "Result".to_string(),
args: vec![
TypeExpr::Named("String".to_string()),
TypeExpr::Named("String".to_string())
]
}),
"{ ok: true; value: string } | { ok: false; error: string }"
);
}
#[test]
fn test_generate_constraint_type_guard() {
let constraint = Rule {
visibility: Visibility::default(),
name: "container.integrity".to_string(),
statements: vec![Statement::Matches {
subject: "state".to_string(),
target: "declared_state".to_string(),
span: Span::default(),
}],
exegesis: "Container integrity constraints.".to_string(),
span: Span::default(),
};
let code = TypeScriptCodegen::generate(&Declaration::Constraint(constraint));
assert!(code.contains("export function validateContainerIntegrity"));
assert!(code.contains("value: T"));
assert!(code.contains("boolean"));
}
#[test]
fn test_generate_system_namespace() {
let system = System {
visibility: Visibility::default(),
name: "container.runtime".to_string(),
version: "1.0.0".to_string(),
requirements: vec![],
statements: vec![],
exegesis: "Container runtime system.".to_string(),
span: Span::default(),
};
let code = TypeScriptCodegen::generate(&Declaration::System(system));
assert!(code.contains("export namespace ContainerRuntime"));
assert!(code.contains("VERSION = '1.0.0'"));
}
#[test]
fn test_to_camel_case() {
assert_eq!(to_camel_case("container.exists"), "containerExists");
assert_eq!(
to_camel_case("identity.cryptographic"),
"identityCryptographic"
);
assert_eq!(to_camel_case("Simple"), "simple");
}
}