1use intent_parser::ast;
8
9use crate::types::map_type;
10use crate::{Language, doc_text, format_ensures_item, format_expr, to_snake_case};
11
12const 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
93fn 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
103fn 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
112fn to_pascal_case(s: &str) -> String {
114 to_snake_case(s)
115 .split('_')
116 .map(capitalize)
117 .collect::<String>()
118}
119
120pub fn generate(file: &ast::File) -> String {
122 let lang = Language::CSharp;
123 let mut out = String::new();
124
125 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 out.push_str("#nullable enable\n\n");
140
141 out.push_str(&format!("namespace {};\n\n", file.module.name));
143
144 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 for item in &file.items {
166 if let ast::TopLevelItem::Entity(e) = item {
167 generate_entity(&mut out, e, &lang);
168 }
169 }
170
171 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
206fn 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 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 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 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 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 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 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 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 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}