1use intent_parser::ast;
4
5use crate::types::map_type;
6use crate::{Language, doc_text, format_ensures_item, format_expr, to_snake_case};
7
8const PYTHON_KEYWORDS: &[&str] = &[
10 "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue",
11 "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import",
12 "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while",
13 "with", "yield",
14];
15
16fn safe_ident(name: &str) -> String {
18 let snake = to_snake_case(name);
19 if PYTHON_KEYWORDS.contains(&snake.as_str()) {
20 format!("{snake}_")
21 } else {
22 snake
23 }
24}
25
26pub fn generate(file: &ast::File) -> String {
28 let lang = Language::Python;
29 let mut out = String::new();
30
31 out.push_str(&format!("# Generated from {}.intent\n", file.module.name));
33 if let Some(doc) = &file.doc {
34 out.push_str(&format!("\"\"\"{}\"\"\"", doc_text(doc)));
35 out.push('\n');
36 }
37 out.push('\n');
38
39 out.push_str(&generate_imports(file));
41 out.push('\n');
42
43 for item in &file.items {
44 match item {
45 ast::TopLevelItem::Entity(e) => generate_entity(&mut out, e, &lang),
46 ast::TopLevelItem::Action(a) => generate_action(&mut out, a, &lang),
47 ast::TopLevelItem::Invariant(inv) => generate_invariant(&mut out, inv),
48 ast::TopLevelItem::EdgeCases(ec) => generate_edge_cases(&mut out, ec),
49 ast::TopLevelItem::Test(_) => {}
50 }
51 }
52
53 out
54}
55
56fn generate_imports(file: &ast::File) -> String {
57 let mut out = String::from("from __future__ import annotations\n\n");
58 let source = collect_type_names(file);
59
60 out.push_str("from dataclasses import dataclass\n");
61
62 if source.contains("Decimal") {
63 out.push_str("from decimal import Decimal\n");
64 }
65 if source.contains("DateTime") {
66 out.push_str("from datetime import datetime\n");
67 }
68 if source.contains("UUID") {
69 out.push_str("import uuid\n");
70 }
71
72 let has_union = file.items.iter().any(|item| {
74 if let ast::TopLevelItem::Entity(e) = item {
75 e.fields
76 .iter()
77 .any(|f| matches!(f.ty.ty, ast::TypeKind::Union(_)))
78 } else {
79 false
80 }
81 });
82 if has_union {
83 out.push_str("from typing import Literal\n");
84 }
85
86 out.push('\n');
87 out
88}
89
90fn collect_type_names(file: &ast::File) -> String {
91 let mut names = String::new();
92 for item in &file.items {
93 match item {
94 ast::TopLevelItem::Entity(e) => {
95 for f in &e.fields {
96 collect_type_name(&f.ty, &mut names);
97 }
98 }
99 ast::TopLevelItem::Action(a) => {
100 for p in &a.params {
101 collect_type_name(&p.ty, &mut names);
102 }
103 }
104 _ => {}
105 }
106 }
107 names
108}
109
110fn collect_type_name(ty: &ast::TypeExpr, out: &mut String) {
111 match &ty.ty {
112 ast::TypeKind::Simple(n) => {
113 out.push_str(n);
114 out.push(' ');
115 }
116 ast::TypeKind::Parameterized { name, .. } => {
117 out.push_str(name);
118 out.push(' ');
119 }
120 ast::TypeKind::List(inner) | ast::TypeKind::Set(inner) => collect_type_name(inner, out),
121 ast::TypeKind::Map(k, v) => {
122 collect_type_name(k, out);
123 collect_type_name(v, out);
124 }
125 ast::TypeKind::Union(_) => {} }
127}
128
129fn generate_entity(out: &mut String, entity: &ast::EntityDecl, lang: &Language) {
130 out.push_str("@dataclass\n");
131 out.push_str(&format!("class {}:\n", entity.name));
132
133 if let Some(doc) = &entity.doc {
135 out.push_str(&format!(" \"\"\"{}\"\"\"\n\n", doc_text(doc)));
136 }
137
138 for field in &entity.fields {
139 let ty = map_type(&field.ty, lang);
140 out.push_str(&format!(" {}: {}\n", safe_ident(&field.name), ty));
141 }
142
143 out.push('\n');
144 out.push('\n');
145}
146
147fn generate_action(out: &mut String, action: &ast::ActionDecl, lang: &Language) {
148 let fn_name = to_snake_case(&action.name);
149 let params: Vec<String> = action
150 .params
151 .iter()
152 .map(|p| {
153 let ty = map_type(&p.ty, lang);
154 format!("{}: {ty}", safe_ident(&p.name))
155 })
156 .collect();
157
158 out.push_str(&format!("def {fn_name}({}) -> None:\n", params.join(", ")));
159
160 let mut doc_lines = Vec::new();
162 if let Some(doc) = &action.doc {
163 doc_lines.push(doc_text(doc));
164 doc_lines.push(String::new());
165 }
166
167 if let Some(req) = &action.requires {
168 doc_lines.push("Requires:".to_string());
169 for cond in &req.conditions {
170 doc_lines.push(format!(" - {}", format_expr(cond)));
171 }
172 doc_lines.push(String::new());
173 }
174
175 if let Some(ens) = &action.ensures {
176 doc_lines.push("Ensures:".to_string());
177 for item in &ens.items {
178 doc_lines.push(format!(" - {}", format_ensures_item(item)));
179 }
180 doc_lines.push(String::new());
181 }
182
183 if let Some(props) = &action.properties {
184 doc_lines.push("Properties:".to_string());
185 for entry in &props.entries {
186 doc_lines.push(format!(
187 " - {}: {}",
188 entry.key,
189 crate::format_prop_value(&entry.value)
190 ));
191 }
192 doc_lines.push(String::new());
193 }
194
195 if !doc_lines.is_empty() {
196 out.push_str(" \"\"\"\n");
197 for line in &doc_lines {
198 if line.is_empty() {
199 out.push('\n');
200 } else {
201 out.push_str(&format!(" {line}\n"));
202 }
203 }
204 out.push_str(" \"\"\"\n");
205 }
206
207 out.push_str(&format!(
208 " raise NotImplementedError(\"TODO: implement {fn_name}\")\n"
209 ));
210 out.push('\n');
211 out.push('\n');
212}
213
214fn generate_invariant(out: &mut String, inv: &ast::InvariantDecl) {
215 out.push_str(&format!("# Invariant: {}\n", inv.name));
216 if let Some(doc) = &inv.doc {
217 for line in doc_text(doc).lines() {
218 out.push_str(&format!("# {line}\n"));
219 }
220 }
221 out.push_str(&format!("# {}\n\n", format_expr(&inv.body)));
222}
223
224fn generate_edge_cases(out: &mut String, ec: &ast::EdgeCasesDecl) {
225 out.push_str("# Edge cases:\n");
226 for rule in &ec.rules {
227 out.push_str(&format!(
228 "# when {} => {}()\n",
229 format_expr(&rule.condition),
230 rule.action.name,
231 ));
232 }
233 out.push('\n');
234}