Skip to main content

intent_codegen/
lib.rs

1//! Skeleton code generator for IntentLang specifications.
2//!
3//! Generates typed stubs in Rust, TypeScript, Python, Go, Java, C#, or Swift
4//! from a parsed `.intent` AST. Entities become structs/classes/dataclasses/records,
5//! actions become function signatures with contract documentation.
6
7pub mod csharp;
8pub mod go;
9pub mod java;
10pub mod openapi;
11pub mod python;
12pub mod rust;
13pub mod rust_tests;
14pub mod swift;
15pub mod test_harness;
16mod types;
17pub mod typescript;
18
19#[cfg(test)]
20mod codegen_tests;
21#[cfg(test)]
22mod openapi_tests;
23
24use intent_parser::ast::{
25    self, ArithOp, CallArg, CmpOp, EnsuresItem, ExprKind, Literal, PropValue, QuantifierKind,
26};
27
28/// Target language for code generation.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Language {
31    Rust,
32    TypeScript,
33    Python,
34    Go,
35    Java,
36    CSharp,
37    Swift,
38}
39
40/// Generate skeleton code from a parsed intent file.
41pub fn generate(file: &ast::File, lang: Language) -> String {
42    match lang {
43        Language::Rust => rust::generate(file),
44        Language::TypeScript => typescript::generate(file),
45        Language::Python => python::generate(file),
46        Language::Go => go::generate(file),
47        Language::Java => java::generate(file),
48        Language::CSharp => csharp::generate(file),
49        Language::Swift => swift::generate(file),
50    }
51}
52
53/// Convert a PascalCase name to snake_case.
54pub fn to_snake_case(s: &str) -> String {
55    let mut result = String::new();
56    for (i, ch) in s.chars().enumerate() {
57        if ch.is_uppercase() && i > 0 {
58            result.push('_');
59        }
60        result.push(ch.to_ascii_lowercase());
61    }
62    result
63}
64
65/// Convert a PascalCase name to camelCase.
66pub fn to_camel_case(s: &str) -> String {
67    let snake = to_snake_case(s);
68    let mut result = String::new();
69    let mut capitalize_next = false;
70    for (i, ch) in snake.chars().enumerate() {
71        if ch == '_' {
72            capitalize_next = true;
73        } else if capitalize_next {
74            result.push(ch.to_ascii_uppercase());
75            capitalize_next = false;
76        } else if i == 0 {
77            result.push(ch.to_ascii_lowercase());
78        } else {
79            result.push(ch);
80        }
81    }
82    result
83}
84
85/// Format an expression as a human-readable comment string.
86pub fn format_expr(expr: &ast::Expr) -> String {
87    match &expr.kind {
88        ExprKind::Implies(l, r) => format!("{} => {}", format_expr(l), format_expr(r)),
89        ExprKind::Or(l, r) => format!("{} || {}", format_expr(l), format_expr(r)),
90        ExprKind::And(l, r) => format!("{} && {}", format_expr(l), format_expr(r)),
91        ExprKind::Not(e) => format!("!{}", format_expr(e)),
92        ExprKind::Compare { left, op, right } => {
93            let op_str = match op {
94                CmpOp::Eq => "==",
95                CmpOp::Ne => "!=",
96                CmpOp::Lt => "<",
97                CmpOp::Gt => ">",
98                CmpOp::Le => "<=",
99                CmpOp::Ge => ">=",
100            };
101            format!("{} {} {}", format_expr(left), op_str, format_expr(right))
102        }
103        ExprKind::Arithmetic { left, op, right } => {
104            let op_str = match op {
105                ArithOp::Add => "+",
106                ArithOp::Sub => "-",
107            };
108            format!("{} {} {}", format_expr(left), op_str, format_expr(right))
109        }
110        ExprKind::Old(e) => format!("old({})", format_expr(e)),
111        ExprKind::Quantifier {
112            kind,
113            binding,
114            ty,
115            body,
116        } => {
117            let kw = match kind {
118                QuantifierKind::Forall => "forall",
119                QuantifierKind::Exists => "exists",
120            };
121            format!("{kw} {binding}: {ty} => {}", format_expr(body))
122        }
123        ExprKind::Call { name, args } => {
124            let args_str = format_call_args(args);
125            format!("{name}({args_str})")
126        }
127        ExprKind::FieldAccess { root, fields } => {
128            format!("{}.{}", format_expr(root), fields.join("."))
129        }
130        ExprKind::List(items) => {
131            let inner: Vec<_> = items.iter().map(format_expr).collect();
132            format!("[{}]", inner.join(", "))
133        }
134        ExprKind::Ident(name) => name.clone(),
135        ExprKind::Literal(lit) => format_literal(lit),
136    }
137}
138
139fn format_call_args(args: &[CallArg]) -> String {
140    args.iter()
141        .map(|a| match a {
142            CallArg::Named { key, value, .. } => format!("{key}: {}", format_expr(value)),
143            CallArg::Positional(e) => format_expr(e),
144        })
145        .collect::<Vec<_>>()
146        .join(", ")
147}
148
149fn format_literal(lit: &Literal) -> String {
150    match lit {
151        Literal::Null => "null".to_string(),
152        Literal::Bool(b) => b.to_string(),
153        Literal::Int(n) => n.to_string(),
154        Literal::Decimal(s) => s.clone(),
155        Literal::String(s) => format!("\"{s}\""),
156    }
157}
158
159/// Format an ensures item as a comment string.
160pub fn format_ensures_item(item: &EnsuresItem) -> String {
161    match item {
162        EnsuresItem::Expr(e) => format_expr(e),
163        EnsuresItem::When(w) => {
164            format!(
165                "when {} => {}",
166                format_expr(&w.condition),
167                format_expr(&w.consequence)
168            )
169        }
170    }
171}
172
173/// Format a property value as a human-readable string.
174pub fn format_prop_value(val: &PropValue) -> String {
175    match val {
176        PropValue::Literal(lit) => format_literal(lit),
177        PropValue::Ident(name) => name.clone(),
178        PropValue::List(items) => {
179            let inner: Vec<_> = items.iter().map(format_prop_value).collect();
180            format!("[{}]", inner.join(", "))
181        }
182        PropValue::Object(entries) => {
183            let inner: Vec<_> = entries
184                .iter()
185                .map(|(k, v)| format!("{k}: {}", format_prop_value(v)))
186                .collect();
187            format!("{{{}}}", inner.join(", "))
188        }
189    }
190}
191
192/// Render doc block lines as a joined string.
193pub fn doc_text(doc: &ast::DocBlock) -> String {
194    doc.lines.join("\n")
195}
196
197/// File extension for a language.
198pub fn file_extension(lang: Language) -> &'static str {
199    match lang {
200        Language::Rust => "rs",
201        Language::TypeScript => "ts",
202        Language::Python => "py",
203        Language::Go => "go",
204        Language::Java => "java",
205        Language::CSharp => "cs",
206        Language::Swift => "swift",
207    }
208}
209
210/// Output file name for a module in the target language.
211pub fn output_filename(module_name: &str, lang: Language) -> String {
212    match lang {
213        Language::Rust | Language::Python | Language::Go => {
214            format!("{}.{}", to_snake_case(module_name), file_extension(lang))
215        }
216        Language::TypeScript => {
217            format!("{}.{}", to_camel_case(module_name), file_extension(lang))
218        }
219        Language::Java | Language::CSharp | Language::Swift => {
220            format!("{}.{}", module_name, file_extension(lang))
221        }
222    }
223}