1use crate::package_mode::PackageMode;
4use crate::resolver::{ResolutionContext, TypeResolver};
5use crate::{Codegen, CodegenError};
6use amalgam_core::{
7 types::{Field, Type},
8 IR,
9};
10use std::fmt::Write;
11
12pub struct NickelCodegen {
13 indent_size: usize,
14 resolver: TypeResolver,
15 package_mode: PackageMode,
16}
17
18impl NickelCodegen {
19 pub fn new() -> Self {
20 Self {
21 indent_size: 2,
22 resolver: TypeResolver::new(),
23 package_mode: PackageMode::default(),
24 }
25 }
26
27 pub fn with_package_mode(mut self, mode: PackageMode) -> Self {
28 self.package_mode = mode;
29 self
30 }
31
32 fn indent(&self, level: usize) -> String {
33 " ".repeat(level * self.indent_size)
34 }
35
36 fn escape_field_name(&self, name: &str) -> String {
38 if name.starts_with('$') || self.is_reserved_keyword(name) {
40 format!("\"{}\"", name)
41 } else {
42 name.to_string()
43 }
44 }
45
46 fn is_reserved_keyword(&self, name: &str) -> bool {
48 matches!(
49 name,
50 "and"
51 | "or"
52 | "not"
53 | "if"
54 | "then"
55 | "else"
56 | "let"
57 | "in"
58 | "fun"
59 | "import"
60 | "match"
61 | "rec"
62 | "null"
63 | "true"
64 | "false"
65 | "switch"
66 | "default"
67 | "forall"
68 | "doc"
69 | "optional"
70 | "priority"
71 | "force"
72 | "merge"
73 )
74 }
75
76 fn format_doc(&self, doc: &str) -> String {
79 if doc.contains('\n') || doc.len() > 80 {
80 format!("m%\"\n{}\n\"%", doc.trim())
82 } else {
83 format!("\"{}\"", doc.replace('"', "\\\""))
85 }
86 }
87
88 fn type_to_nickel(
89 &mut self,
90 ty: &Type,
91 module: &amalgam_core::ir::Module,
92 indent_level: usize,
93 ) -> Result<String, CodegenError> {
94 match ty {
95 Type::String => Ok("String".to_string()),
96 Type::Number => Ok("Number".to_string()),
97 Type::Integer => Ok("Number".to_string()), Type::Bool => Ok("Bool".to_string()),
99 Type::Null => Ok("Null".to_string()),
100 Type::Any => Ok("Dyn".to_string()),
101
102 Type::Array(elem) => {
103 let elem_type = self.type_to_nickel(elem, module, indent_level)?;
104 Ok(format!("Array {}", elem_type))
105 }
106
107 Type::Map { value, .. } => {
108 let value_type = self.type_to_nickel(value, module, indent_level)?;
109 Ok(format!("{{ _ : {} }}", value_type))
110 }
111
112 Type::Optional(inner) => {
113 let inner_type = self.type_to_nickel(inner, module, indent_level)?;
114 Ok(format!("{} | Null", inner_type))
115 }
116
117 Type::Record { fields, open } => {
118 if fields.is_empty() && *open {
119 return Ok("{ .. }".to_string());
120 }
121
122 let mut result = String::from("{\n");
123
124 let mut sorted_fields: Vec<_> = fields.iter().collect();
126 sorted_fields.sort_by_key(|(name, _)| *name);
127
128 for (name, field) in sorted_fields {
129 let field_str = self.field_to_nickel(name, field, module, indent_level + 1)?;
130 result.push_str(&field_str);
131 result.push_str(",\n");
132 }
133
134 if *open {
135 result.push_str(&format!("{}.. | Dyn,\n", self.indent(indent_level + 1)));
136 }
137
138 result.push_str(&self.indent(indent_level));
139 result.push('}');
140 Ok(result)
141 }
142
143 Type::Union(types) => {
144 let type_strs: Result<Vec<_>, _> = types
145 .iter()
146 .map(|t| self.type_to_nickel(t, module, indent_level))
147 .collect();
148 Ok(type_strs?.join(" | "))
149 }
150
151 Type::TaggedUnion {
152 tag_field,
153 variants,
154 } => {
155 let mut contracts = Vec::new();
156 for (tag, variant_type) in variants {
157 let variant_str = self.type_to_nickel(variant_type, module, indent_level)?;
158 contracts.push(format!("({} == \"{}\" && {})", tag_field, tag, variant_str));
159 }
160 Ok(contracts.join(" | "))
161 }
162
163 Type::Reference(name) => {
164 let context = ResolutionContext {
166 current_group: None, current_version: None,
168 current_kind: None,
169 };
170 Ok(self.resolver.resolve(name, module, &context))
171 }
172
173 Type::Contract { base, predicate } => {
174 let base_type = self.type_to_nickel(base, module, indent_level)?;
175 Ok(format!("{} | Contract({})", base_type, predicate))
176 }
177 }
178 }
179
180 fn field_to_nickel(
181 &mut self,
182 name: &str,
183 field: &Field,
184 module: &amalgam_core::ir::Module,
185 indent_level: usize,
186 ) -> Result<String, CodegenError> {
187 let indent = self.indent(indent_level);
188 let type_str = self.type_to_nickel(&field.ty, module, indent_level)?;
189
190 let mut parts = Vec::new();
191
192 let field_name = self.escape_field_name(name);
194 parts.push(format!("{}{}", indent, field_name));
195
196 if field.default.is_none() {
200 parts.push("optional".to_string());
201 }
202
203 parts.push(type_str);
205
206 if let Some(desc) = &field.description {
208 parts.push(format!("doc {}", self.format_doc(desc)));
209 }
210
211 if let Some(default) = &field.default {
213 let default_str = format_json_value_impl(default, indent_level, self);
214 parts.push(format!("default = {}", default_str));
215 }
216
217 Ok(parts.join(" | "))
218 }
219}
220
221fn format_json_value_impl(
223 value: &serde_json::Value,
224 indent_level: usize,
225 codegen: &NickelCodegen,
226) -> String {
227 match value {
228 serde_json::Value::Null => "null".to_string(),
229 serde_json::Value::Bool(b) => b.to_string(),
230 serde_json::Value::Number(n) => n.to_string(),
231 serde_json::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
232 serde_json::Value::Array(arr) => {
233 let items: Vec<String> = arr
234 .iter()
235 .map(|v| format_json_value_impl(v, indent_level, codegen))
236 .collect();
237 format!("[{}]", items.join(", "))
238 }
239 serde_json::Value::Object(obj) => {
240 if obj.is_empty() {
241 "{}".to_string()
242 } else {
243 let indent = " ".repeat((indent_level + 1) * 2);
244 let mut items = Vec::new();
245 for (k, v) in obj {
246 let escaped_key = codegen.escape_field_name(k);
247 items.push(format!(
248 "{}{} = {}",
249 indent,
250 escaped_key,
251 format_json_value_impl(v, indent_level + 1, codegen)
252 ));
253 }
254 format!(
255 "{{\n{}\n{}}}",
256 items.join(",\n"),
257 " ".repeat(indent_level * 2)
258 )
259 }
260 }
261 }
262}
263
264impl Default for NickelCodegen {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270impl Codegen for NickelCodegen {
271 fn generate(&mut self, ir: &IR) -> Result<String, CodegenError> {
272 let mut output = String::new();
273
274 for module in &ir.modules {
275 writeln!(output, "# Module: {}", module.name)
277 .map_err(|e| CodegenError::Generation(e.to_string()))?;
278 writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
279
280 if !module.imports.is_empty() {
282 for import in &module.imports {
283 let import_path = self.package_mode.convert_import(&import.path);
285
286 let import_statement =
289 if !import_path.contains('/') && import_path.starts_with('"') {
290 format!(
292 "let {} = import {} in",
293 import
294 .alias
295 .as_ref()
296 .unwrap_or(&import.path.replace('/', "_")),
297 import_path
298 )
299 } else {
300 format!(
302 "let {} = import \"{}\" in",
303 import
304 .alias
305 .as_ref()
306 .unwrap_or(&import.path.replace('/', "_")),
307 import_path
308 )
309 };
310
311 writeln!(output, "{}", import_statement)
312 .map_err(|e| CodegenError::Generation(e.to_string()))?;
313 }
314 writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
315 }
316
317 writeln!(output, "{{")?;
319
320 for (idx, type_def) in module.types.iter().enumerate() {
321 if let Some(doc) = &type_def.documentation {
323 for line in doc.lines() {
324 writeln!(output, "{}# {}", self.indent(1), line)
325 .map_err(|e| CodegenError::Generation(e.to_string()))?;
326 }
327 }
328
329 let type_str = self.type_to_nickel(&type_def.ty, module, 1)?;
331
332 if matches!(type_def.ty, Type::Record { .. }) {
334 write!(output, " {} = ", type_def.name)?;
336 writeln!(output, "{},", type_str)?;
337 } else {
338 writeln!(output, " {} = {},", type_def.name, type_str)?;
339 }
340
341 if idx < module.types.len() - 1 {
343 writeln!(output)?;
344 }
345 }
346
347 if !module.constants.is_empty() {
349 writeln!(output)?; for constant in &module.constants {
352 if let Some(doc) = &constant.documentation {
353 writeln!(output, " # {}", doc)
354 .map_err(|e| CodegenError::Generation(e.to_string()))?;
355 }
356
357 let value_str = format_json_value_impl(&constant.value, 1, self);
358 writeln!(output, " {} = {},", constant.name, value_str)
359 .map_err(|e| CodegenError::Generation(e.to_string()))?;
360 }
361 }
362
363 writeln!(output, "}}")?;
364 }
365
366 Ok(output)
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use amalgam_core::ir::{Metadata, Module};
374 use std::collections::BTreeMap;
375
376 fn create_test_module() -> Module {
377 Module {
378 name: "test".to_string(),
379 imports: Vec::new(),
380 types: Vec::new(),
381 constants: Vec::new(),
382 metadata: Metadata {
383 source_language: None,
384 source_file: None,
385 version: None,
386 generated_at: None,
387 custom: BTreeMap::new(),
388 },
389 }
390 }
391
392 #[test]
393 fn test_simple_type_generation() {
394 let mut codegen = NickelCodegen::new();
395 let module = create_test_module();
396
397 assert_eq!(
398 codegen.type_to_nickel(&Type::String, &module, 0).unwrap(),
399 "String"
400 );
401 assert_eq!(
402 codegen.type_to_nickel(&Type::Number, &module, 0).unwrap(),
403 "Number"
404 );
405 assert_eq!(
406 codegen.type_to_nickel(&Type::Bool, &module, 0).unwrap(),
407 "Bool"
408 );
409 assert_eq!(
410 codegen.type_to_nickel(&Type::Any, &module, 0).unwrap(),
411 "Dyn"
412 );
413 }
414
415 #[test]
416 fn test_array_generation() {
417 let mut codegen = NickelCodegen::new();
418 let module = create_test_module();
419 let array_type = Type::Array(Box::new(Type::String));
420 assert_eq!(
421 codegen.type_to_nickel(&array_type, &module, 0).unwrap(),
422 "Array String"
423 );
424 }
425
426 #[test]
427 fn test_optional_generation() {
428 let mut codegen = NickelCodegen::new();
429 let module = create_test_module();
430 let optional_type = Type::Optional(Box::new(Type::String));
431 assert_eq!(
432 codegen.type_to_nickel(&optional_type, &module, 0).unwrap(),
433 "String | Null"
434 );
435 }
436
437 #[test]
438 fn test_doc_formatting() {
439 let codegen = NickelCodegen::new();
440
441 assert_eq!(codegen.format_doc("Short doc"), "\"Short doc\"");
443
444 let multiline = "This is a\nmultiline doc";
446 assert_eq!(
447 codegen.format_doc(multiline),
448 "m%\"\nThis is a\nmultiline doc\n\"%"
449 );
450
451 assert_eq!(
453 codegen.format_doc("Doc with \"quotes\""),
454 "\"Doc with \\\"quotes\\\"\""
455 );
456 }
457}