Skip to main content

intent_codegen/
csharp.rs

1//! C# skeleton code generator.
2//!
3//! Generates C# records for entities, enums for union types,
4//! and static methods in a static Actions class. Uses file-scoped
5//! namespaces and nullable reference types (C# 10+).
6
7use intent_parser::ast;
8
9use crate::types::map_type;
10use crate::{Language, doc_text, format_ensures_item, format_expr, to_snake_case};
11
12/// C# reserved keywords that cannot be used as identifiers.
13const CSHARP_KEYWORDS: &[&str] = &[
14    "abstract",
15    "as",
16    "base",
17    "bool",
18    "break",
19    "byte",
20    "case",
21    "catch",
22    "char",
23    "checked",
24    "class",
25    "const",
26    "continue",
27    "decimal",
28    "default",
29    "delegate",
30    "do",
31    "double",
32    "else",
33    "enum",
34    "event",
35    "explicit",
36    "extern",
37    "false",
38    "finally",
39    "fixed",
40    "float",
41    "for",
42    "foreach",
43    "goto",
44    "if",
45    "implicit",
46    "in",
47    "int",
48    "interface",
49    "internal",
50    "is",
51    "lock",
52    "long",
53    "namespace",
54    "new",
55    "null",
56    "object",
57    "operator",
58    "out",
59    "override",
60    "params",
61    "private",
62    "protected",
63    "public",
64    "readonly",
65    "ref",
66    "return",
67    "sbyte",
68    "sealed",
69    "short",
70    "sizeof",
71    "stackalloc",
72    "static",
73    "string",
74    "struct",
75    "switch",
76    "this",
77    "throw",
78    "true",
79    "try",
80    "typeof",
81    "uint",
82    "ulong",
83    "unchecked",
84    "unsafe",
85    "ushort",
86    "using",
87    "virtual",
88    "void",
89    "volatile",
90    "while",
91];
92
93/// Escape a C# identifier if it collides with a reserved keyword.
94fn safe_ident(name: &str) -> String {
95    let camel = crate::to_camel_case(name);
96    if CSHARP_KEYWORDS.contains(&camel.as_str()) {
97        format!("@{camel}")
98    } else {
99        camel
100    }
101}
102
103/// Capitalize the first character of a string.
104fn capitalize(s: &str) -> String {
105    let mut chars = s.chars();
106    match chars.next() {
107        None => String::new(),
108        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
109    }
110}
111
112/// Convert a name to PascalCase (C# convention for public members).
113fn to_pascal_case(s: &str) -> String {
114    to_snake_case(s)
115        .split('_')
116        .map(capitalize)
117        .collect::<String>()
118}
119
120/// Generate C# skeleton code from a parsed intent file.
121pub fn generate(file: &ast::File) -> String {
122    let lang = Language::CSharp;
123    let mut out = String::new();
124
125    // Header
126    out.push_str(&format!(
127        "// Generated from {}.intent. DO NOT EDIT.\n",
128        file.module.name
129    ));
130    if let Some(doc) = &file.doc {
131        out.push('\n');
132        for line in &doc.lines {
133            out.push_str(&format!("// {line}\n"));
134        }
135    }
136    out.push('\n');
137
138    // Nullable enable
139    out.push_str("#nullable enable\n\n");
140
141    // File-scoped namespace
142    out.push_str(&format!("namespace {};\n\n", file.module.name));
143
144    // Imports
145    let imports = generate_imports(file);
146    if !imports.is_empty() {
147        out.push_str(&imports);
148        out.push('\n');
149    }
150
151    let has_actions = file
152        .items
153        .iter()
154        .any(|item| matches!(item, ast::TopLevelItem::Action(_)));
155    let has_invariants = file
156        .items
157        .iter()
158        .any(|item| matches!(item, ast::TopLevelItem::Invariant(_)));
159    let has_edge_cases = file
160        .items
161        .iter()
162        .any(|item| matches!(item, ast::TopLevelItem::EdgeCases(_)));
163
164    // Entities and enums (top-level)
165    for item in &file.items {
166        if let ast::TopLevelItem::Entity(e) = item {
167            generate_entity(&mut out, e, &lang);
168        }
169    }
170
171    // Actions in a static class
172    if has_actions || has_invariants || has_edge_cases {
173        out.push_str(&format!(
174            "public static class {}Actions\n{{\n",
175            file.module.name
176        ));
177        for item in &file.items {
178            match item {
179                ast::TopLevelItem::Action(a) => generate_action(&mut out, a, &lang),
180                ast::TopLevelItem::Invariant(inv) => generate_invariant(&mut out, inv),
181                ast::TopLevelItem::EdgeCases(ec) => generate_edge_cases(&mut out, ec),
182                _ => {}
183            }
184        }
185        out.push_str("}\n");
186    }
187
188    out
189}
190
191fn generate_imports(file: &ast::File) -> String {
192    let source = collect_type_names(file);
193    let mut imports = Vec::new();
194
195    if source.contains("List<") || source.contains("Set<") || source.contains("Map<") {
196        imports.push("using System.Collections.Generic;");
197    }
198
199    if imports.is_empty() {
200        return String::new();
201    }
202
203    imports.join("\n") + "\n"
204}
205
206/// Collect all type names as a single string for import detection.
207fn collect_type_names(file: &ast::File) -> String {
208    let mut names = String::new();
209    for item in &file.items {
210        match item {
211            ast::TopLevelItem::Entity(e) => {
212                for f in &e.fields {
213                    collect_type_name(&f.ty, &mut names);
214                }
215            }
216            ast::TopLevelItem::Action(a) => {
217                for p in &a.params {
218                    collect_type_name(&p.ty, &mut names);
219                }
220            }
221            _ => {}
222        }
223    }
224    names
225}
226
227fn collect_type_name(ty: &ast::TypeExpr, out: &mut String) {
228    match &ty.ty {
229        ast::TypeKind::Simple(n) => {
230            out.push_str(n);
231            out.push(' ');
232        }
233        ast::TypeKind::Parameterized { name, .. } => {
234            out.push_str(name);
235            out.push(' ');
236        }
237        ast::TypeKind::List(inner) => {
238            out.push_str("List<");
239            collect_type_name(inner, out);
240        }
241        ast::TypeKind::Set(inner) => {
242            out.push_str("Set<");
243            collect_type_name(inner, out);
244        }
245        ast::TypeKind::Map(k, v) => {
246            out.push_str("Map<");
247            collect_type_name(k, out);
248            collect_type_name(v, out);
249        }
250        ast::TypeKind::Union(_) => {}
251    }
252}
253
254fn generate_entity(out: &mut String, entity: &ast::EntityDecl, lang: &Language) {
255    // Emit enum types for union-typed fields
256    for field in &entity.fields {
257        if let ast::TypeKind::Union(variants) = &field.ty.ty {
258            let enum_name = format!("{}{}", entity.name, capitalize(&field.name));
259            generate_union_enum(out, &enum_name, variants);
260        }
261    }
262
263    // XML doc comment
264    if let Some(doc) = &entity.doc {
265        out.push_str("/// <summary>\n");
266        for line in doc_text(doc).lines() {
267            out.push_str(&format!("/// {line}\n"));
268        }
269        out.push_str("/// </summary>\n");
270    }
271
272    // Record declaration
273    let params: Vec<String> = entity
274        .fields
275        .iter()
276        .map(|f| {
277            let ty = if let ast::TypeKind::Union(_) = &f.ty.ty {
278                let enum_name = format!("{}{}", entity.name, capitalize(&f.name));
279                if f.ty.optional {
280                    format!("{enum_name}?")
281                } else {
282                    enum_name
283                }
284            } else {
285                map_type(&f.ty, lang)
286            };
287            format!("{ty} {}", to_pascal_case(&f.name))
288        })
289        .collect();
290
291    out.push_str(&format!("public record {}(\n", entity.name));
292    for (i, param) in params.iter().enumerate() {
293        let comma = if i < params.len() - 1 { "," } else { "" };
294        out.push_str(&format!("    {param}{comma}\n"));
295    }
296    out.push_str(");\n\n");
297}
298
299fn generate_union_enum(out: &mut String, name: &str, variants: &[ast::TypeKind]) {
300    let names: Vec<&str> = variants
301        .iter()
302        .filter_map(|v| match v {
303            ast::TypeKind::Simple(n) => Some(n.as_str()),
304            _ => None,
305        })
306        .collect();
307
308    out.push_str(&format!("public enum {name}\n{{\n"));
309    for (i, n) in names.iter().enumerate() {
310        let comma = if i < names.len() - 1 { "," } else { "" };
311        out.push_str(&format!("    {n}{comma}\n"));
312    }
313    out.push_str("}\n\n");
314}
315
316fn generate_action(out: &mut String, action: &ast::ActionDecl, lang: &Language) {
317    let fn_name = to_pascal_case(&action.name);
318
319    // XML doc comment
320    out.push_str("    /// <summary>\n");
321    if let Some(doc) = &action.doc {
322        for line in doc_text(doc).lines() {
323            out.push_str(&format!("    /// {line}\n"));
324        }
325    }
326    out.push_str("    /// </summary>\n");
327
328    // Requires
329    if let Some(req) = &action.requires {
330        out.push_str("    /// <remarks>\n    /// Requires:\n");
331        for cond in &req.conditions {
332            out.push_str(&format!("    /// - {}\n", format_expr(cond)));
333        }
334    }
335
336    // Ensures
337    if let Some(ens) = &action.ensures {
338        if action.requires.is_none() {
339            out.push_str("    /// <remarks>\n");
340        }
341        out.push_str("    /// Ensures:\n");
342        for item in &ens.items {
343            out.push_str(&format!("    /// - {}\n", format_ensures_item(item)));
344        }
345    }
346
347    if action.requires.is_some() || action.ensures.is_some() {
348        out.push_str("    /// </remarks>\n");
349    }
350
351    // Properties
352    if let Some(props) = &action.properties {
353        out.push_str("    /// <remarks>\n    /// Properties:\n");
354        for entry in &props.entries {
355            out.push_str(&format!(
356                "    /// - {}: {}\n",
357                entry.key,
358                crate::format_prop_value(&entry.value)
359            ));
360        }
361        out.push_str("    /// </remarks>\n");
362    }
363
364    // Method signature
365    let params: Vec<String> = action
366        .params
367        .iter()
368        .map(|p| {
369            let ty = map_type(&p.ty, lang);
370            format!("{ty} {}", safe_ident(&p.name))
371        })
372        .collect();
373
374    out.push_str(&format!(
375        "    public static void {fn_name}({})\n",
376        params.join(", ")
377    ));
378    out.push_str("    {\n");
379    out.push_str(&format!(
380        "        throw new NotImplementedException(\"TODO: implement {fn_name}\");\n"
381    ));
382    out.push_str("    }\n\n");
383}
384
385fn generate_invariant(out: &mut String, inv: &ast::InvariantDecl) {
386    out.push_str(&format!("    // Invariant: {}\n", inv.name));
387    if let Some(doc) = &inv.doc {
388        for line in doc_text(doc).lines() {
389            out.push_str(&format!("    // {line}\n"));
390        }
391    }
392    out.push_str(&format!("    // {}\n\n", format_expr(&inv.body)));
393}
394
395fn generate_edge_cases(out: &mut String, ec: &ast::EdgeCasesDecl) {
396    out.push_str("    // Edge cases:\n");
397    for rule in &ec.rules {
398        out.push_str(&format!(
399            "    // when {} => {}()\n",
400            format_expr(&rule.condition),
401            rule.action.name,
402        ));
403    }
404    out.push('\n');
405}