1use crate::{Codegen, CodegenError};
4use crate::resolver::{TypeResolver, ResolutionContext};
5use amalgam_core::{
6 types::{Field, Type},
7 IR,
8};
9use std::fmt::Write;
10
11pub struct NickelCodegen {
12 indent_size: usize,
13 resolver: TypeResolver,
14}
15
16impl NickelCodegen {
17 pub fn new() -> Self {
18 Self {
19 indent_size: 2,
20 resolver: TypeResolver::new(),
21 }
22 }
23
24 fn indent(&self, level: usize) -> String {
25 " ".repeat(level * self.indent_size)
26 }
27
28 fn format_doc(&self, doc: &str) -> String {
31 if doc.contains('\n') || doc.len() > 80 {
32 format!("m%\"\n{}\n\"%", doc.trim())
34 } else {
35 format!("\"{}\"", doc.replace('"', "\\\""))
37 }
38 }
39
40 fn type_to_nickel(&mut self, ty: &Type, module: &amalgam_core::ir::Module, indent_level: usize) -> Result<String, CodegenError> {
41 match ty {
42 Type::String => Ok("String".to_string()),
43 Type::Number => Ok("Number".to_string()),
44 Type::Integer => Ok("Number".to_string()), Type::Bool => Ok("Bool".to_string()),
46 Type::Null => Ok("Null".to_string()),
47 Type::Any => Ok("Dyn".to_string()),
48
49 Type::Array(elem) => {
50 let elem_type = self.type_to_nickel(elem, module, indent_level)?;
51 Ok(format!("Array {}", elem_type))
52 }
53
54 Type::Map { value, .. } => {
55 let value_type = self.type_to_nickel(value, module, indent_level)?;
56 Ok(format!("{{ _ : {} }}", value_type))
57 }
58
59 Type::Optional(inner) => {
60 let inner_type = self.type_to_nickel(inner, module, indent_level)?;
61 Ok(format!("{} | Null", inner_type))
62 }
63
64 Type::Record { fields, open } => {
65 if fields.is_empty() && *open {
66 return Ok("{ .. }".to_string());
67 }
68
69 let mut result = String::from("{\n");
70
71 let mut sorted_fields: Vec<_> = fields.iter().collect();
73 sorted_fields.sort_by_key(|(name, _)| *name);
74
75 for (name, field) in sorted_fields {
76 let field_str = self.field_to_nickel(name, field, module, indent_level + 1)?;
77 result.push_str(&field_str);
78 result.push_str(",\n");
79 }
80
81 if *open {
82 result.push_str(&format!("{}.. | Dyn,\n", self.indent(indent_level + 1)));
83 }
84
85 result.push_str(&self.indent(indent_level));
86 result.push('}');
87 Ok(result)
88 }
89
90 Type::Union(types) => {
91 let type_strs: Result<Vec<_>, _> = types
92 .iter()
93 .map(|t| self.type_to_nickel(t, module, indent_level))
94 .collect();
95 Ok(type_strs?.join(" | "))
96 }
97
98 Type::TaggedUnion {
99 tag_field,
100 variants,
101 } => {
102 let mut contracts = Vec::new();
103 for (tag, variant_type) in variants {
104 let variant_str = self.type_to_nickel(variant_type, module, indent_level)?;
105 contracts.push(format!("({} == \"{}\" && {})", tag_field, tag, variant_str));
106 }
107 Ok(contracts.join(" | "))
108 }
109
110 Type::Reference(name) => {
111 let context = ResolutionContext {
113 current_group: None, current_version: None,
115 current_kind: None,
116 };
117 Ok(self.resolver.resolve(name, module, &context))
118 },
119
120 Type::Contract { base, predicate } => {
121 let base_type = self.type_to_nickel(base, module, indent_level)?;
122 Ok(format!("{} | Contract({})", base_type, predicate))
123 }
124 }
125 }
126
127 fn field_to_nickel(
128 &mut self,
129 name: &str,
130 field: &Field,
131 module: &amalgam_core::ir::Module,
132 indent_level: usize,
133 ) -> Result<String, CodegenError> {
134 let indent = self.indent(indent_level);
135 let type_str = self.type_to_nickel(&field.ty, module, indent_level)?;
136
137 let mut parts = Vec::new();
138
139 parts.push(format!("{}{}", indent, name));
141
142 if !field.required {
144 parts.push("optional".to_string());
145 }
146
147 parts.push(type_str);
149
150 if let Some(default) = &field.default {
152 let default_str = format_json_value(default, indent_level);
153 parts.push(format!("default = {}", default_str));
154 }
155
156 if let Some(desc) = &field.description {
158 parts.push(format!("doc {}", self.format_doc(desc)));
159 }
160
161 Ok(parts.join(" | "))
162 }
163}
164
165fn format_json_value(value: &serde_json::Value, indent_level: usize) -> String {
167 match value {
168 serde_json::Value::Null => "null".to_string(),
169 serde_json::Value::Bool(b) => b.to_string(),
170 serde_json::Value::Number(n) => n.to_string(),
171 serde_json::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
172 serde_json::Value::Array(arr) => {
173 let items: Vec<String> = arr
174 .iter()
175 .map(|v| format_json_value(v, indent_level))
176 .collect();
177 format!("[{}]", items.join(", "))
178 }
179 serde_json::Value::Object(obj) => {
180 if obj.is_empty() {
181 "{}".to_string()
182 } else {
183 let indent = " ".repeat((indent_level + 1) * 2);
184 let mut items = Vec::new();
185 for (k, v) in obj {
186 items.push(format!(
187 "{}{} = {}",
188 indent,
189 k,
190 format_json_value(v, indent_level + 1)
191 ));
192 }
193 format!(
194 "{{\n{}\n{}}}",
195 items.join(",\n"),
196 " ".repeat(indent_level * 2)
197 )
198 }
199 }
200 }
201}
202
203impl Default for NickelCodegen {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209impl Codegen for NickelCodegen {
210 fn generate(&mut self, ir: &IR) -> Result<String, CodegenError> {
211 let mut output = String::new();
212
213 for module in &ir.modules {
214 writeln!(output, "# Module: {}", module.name)
216 .map_err(|e| CodegenError::Generation(e.to_string()))?;
217 writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
218
219 if !module.imports.is_empty() {
221 for import in &module.imports {
222 writeln!(
223 output,
224 "let {} = import \"{}\" in",
225 import.alias.as_ref().unwrap_or(&import.path),
226 import.path
227 )
228 .map_err(|e| CodegenError::Generation(e.to_string()))?;
229 }
230 writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
231 }
232
233 writeln!(output, "{{")?;
235
236 for (idx, type_def) in module.types.iter().enumerate() {
237 if let Some(doc) = &type_def.documentation {
239 for line in doc.lines() {
240 writeln!(output, "{}# {}", self.indent(1), line)
241 .map_err(|e| CodegenError::Generation(e.to_string()))?;
242 }
243 }
244
245 let type_str = self.type_to_nickel(&type_def.ty, module, 1)?;
247
248 if matches!(type_def.ty, Type::Record { .. }) {
250 write!(output, " {} = ", type_def.name)?;
252 writeln!(output, "{},", type_str)?;
253 } else {
254 writeln!(output, " {} = {},", type_def.name, type_str)?;
255 }
256
257 if idx < module.types.len() - 1 {
259 writeln!(output)?;
260 }
261 }
262
263 if !module.constants.is_empty() {
265 writeln!(output)?; for constant in &module.constants {
268 if let Some(doc) = &constant.documentation {
269 writeln!(output, " # {}", doc)
270 .map_err(|e| CodegenError::Generation(e.to_string()))?;
271 }
272
273 let value_str = format_json_value(&constant.value, 1);
274 writeln!(output, " {} = {},", constant.name, value_str)
275 .map_err(|e| CodegenError::Generation(e.to_string()))?;
276 }
277 }
278
279 writeln!(output, "}}")?;
280 }
281
282 Ok(output)
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_simple_type_generation() {
292 let codegen = NickelCodegen::new();
293
294 assert_eq!(codegen.type_to_nickel(&Type::String, 0).unwrap(), "String");
295 assert_eq!(codegen.type_to_nickel(&Type::Number, 0).unwrap(), "Number");
296 assert_eq!(codegen.type_to_nickel(&Type::Bool, 0).unwrap(), "Bool");
297 assert_eq!(codegen.type_to_nickel(&Type::Any, 0).unwrap(), "Dyn");
298 }
299
300 #[test]
301 fn test_array_generation() {
302 let codegen = NickelCodegen::new();
303 let array_type = Type::Array(Box::new(Type::String));
304 assert_eq!(
305 codegen.type_to_nickel(&array_type, 0).unwrap(),
306 "Array String"
307 );
308 }
309
310 #[test]
311 fn test_optional_generation() {
312 let codegen = NickelCodegen::new();
313 let optional_type = Type::Optional(Box::new(Type::String));
314 assert_eq!(
315 codegen.type_to_nickel(&optional_type, 0).unwrap(),
316 "String | Null"
317 );
318 }
319
320 #[test]
321 fn test_doc_formatting() {
322 let codegen = NickelCodegen::new();
323
324 assert_eq!(codegen.format_doc("Short doc"), "\"Short doc\"");
326
327 let multiline = "This is a\nmultiline doc";
329 assert_eq!(
330 codegen.format_doc(multiline),
331 "m%\"\nThis is a\nmultiline doc\n\"%"
332 );
333
334 assert_eq!(
336 codegen.format_doc("Doc with \"quotes\""),
337 "\"Doc with \\\"quotes\\\"\""
338 );
339 }
340}