amalgam_codegen/
nickel.rs

1//! Nickel code generator with improved formatting
2
3use 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    /// Format a documentation string properly
37    /// Uses triple quotes for multiline, regular quotes for single line
38    fn format_doc(&self, doc: &str) -> String {
39        if doc.contains('\n') || doc.len() > 80 {
40            // Use triple quotes for multiline or long docs
41            format!("m%\"\n{}\n\"%", doc.trim())
42        } else {
43            // Use regular quotes for short docs
44            format!("\"{}\"", doc.replace('"', "\\\""))
45        }
46    }
47
48    fn type_to_nickel(
49        &mut self,
50        ty: &Type,
51        module: &amalgam_core::ir::Module,
52        indent_level: usize,
53    ) -> Result<String, CodegenError> {
54        match ty {
55            Type::String => Ok("String".to_string()),
56            Type::Number => Ok("Number".to_string()),
57            Type::Integer => Ok("Number".to_string()), // Nickel uses Number for all numerics
58            Type::Bool => Ok("Bool".to_string()),
59            Type::Null => Ok("Null".to_string()),
60            Type::Any => Ok("Dyn".to_string()),
61
62            Type::Array(elem) => {
63                let elem_type = self.type_to_nickel(elem, module, indent_level)?;
64                Ok(format!("Array {}", elem_type))
65            }
66
67            Type::Map { value, .. } => {
68                let value_type = self.type_to_nickel(value, module, indent_level)?;
69                Ok(format!("{{ _ : {} }}", value_type))
70            }
71
72            Type::Optional(inner) => {
73                let inner_type = self.type_to_nickel(inner, module, indent_level)?;
74                Ok(format!("{} | Null", inner_type))
75            }
76
77            Type::Record { fields, open } => {
78                if fields.is_empty() && *open {
79                    return Ok("{ .. }".to_string());
80                }
81
82                let mut result = String::from("{\n");
83
84                // Sort fields for consistent output
85                let mut sorted_fields: Vec<_> = fields.iter().collect();
86                sorted_fields.sort_by_key(|(name, _)| *name);
87
88                for (name, field) in sorted_fields {
89                    let field_str = self.field_to_nickel(name, field, module, indent_level + 1)?;
90                    result.push_str(&field_str);
91                    result.push_str(",\n");
92                }
93
94                if *open {
95                    result.push_str(&format!("{}.. | Dyn,\n", self.indent(indent_level + 1)));
96                }
97
98                result.push_str(&self.indent(indent_level));
99                result.push('}');
100                Ok(result)
101            }
102
103            Type::Union(types) => {
104                let type_strs: Result<Vec<_>, _> = types
105                    .iter()
106                    .map(|t| self.type_to_nickel(t, module, indent_level))
107                    .collect();
108                Ok(type_strs?.join(" | "))
109            }
110
111            Type::TaggedUnion {
112                tag_field,
113                variants,
114            } => {
115                let mut contracts = Vec::new();
116                for (tag, variant_type) in variants {
117                    let variant_str = self.type_to_nickel(variant_type, module, indent_level)?;
118                    contracts.push(format!("({} == \"{}\" && {})", tag_field, tag, variant_str));
119                }
120                Ok(contracts.join(" | "))
121            }
122
123            Type::Reference(name) => {
124                // Use the resolver to get the proper reference
125                let context = ResolutionContext {
126                    current_group: None, // Could extract from module.name if needed
127                    current_version: None,
128                    current_kind: None,
129                };
130                Ok(self.resolver.resolve(name, module, &context))
131            }
132
133            Type::Contract { base, predicate } => {
134                let base_type = self.type_to_nickel(base, module, indent_level)?;
135                Ok(format!("{} | Contract({})", base_type, predicate))
136            }
137        }
138    }
139
140    fn field_to_nickel(
141        &mut self,
142        name: &str,
143        field: &Field,
144        module: &amalgam_core::ir::Module,
145        indent_level: usize,
146    ) -> Result<String, CodegenError> {
147        let indent = self.indent(indent_level);
148        let type_str = self.type_to_nickel(&field.ty, module, indent_level)?;
149
150        let mut parts = Vec::new();
151
152        // Field name
153        parts.push(format!("{}{}", indent, name));
154
155        // In Nickel, a field with a default value is implicitly optional
156        // So we only add 'optional' if there's no default value
157        if !field.required && field.default.is_none() {
158            parts.push("optional".to_string());
159        }
160
161        // Type
162        parts.push(type_str);
163
164        // Documentation (must come BEFORE default in Nickel)
165        if let Some(desc) = &field.description {
166            parts.push(format!("doc {}", self.format_doc(desc)));
167        }
168
169        // Default value (must come AFTER doc in Nickel)
170        if let Some(default) = &field.default {
171            let default_str = format_json_value(default, indent_level);
172            parts.push(format!("default = {}", default_str));
173        }
174
175        Ok(parts.join(" | "))
176    }
177}
178
179/// Format a JSON value for Nickel
180fn format_json_value(value: &serde_json::Value, indent_level: usize) -> String {
181    match value {
182        serde_json::Value::Null => "null".to_string(),
183        serde_json::Value::Bool(b) => b.to_string(),
184        serde_json::Value::Number(n) => n.to_string(),
185        serde_json::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
186        serde_json::Value::Array(arr) => {
187            let items: Vec<String> = arr
188                .iter()
189                .map(|v| format_json_value(v, indent_level))
190                .collect();
191            format!("[{}]", items.join(", "))
192        }
193        serde_json::Value::Object(obj) => {
194            if obj.is_empty() {
195                "{}".to_string()
196            } else {
197                let indent = " ".repeat((indent_level + 1) * 2);
198                let mut items = Vec::new();
199                for (k, v) in obj {
200                    items.push(format!(
201                        "{}{} = {}",
202                        indent,
203                        k,
204                        format_json_value(v, indent_level + 1)
205                    ));
206                }
207                format!(
208                    "{{\n{}\n{}}}",
209                    items.join(",\n"),
210                    " ".repeat(indent_level * 2)
211                )
212            }
213        }
214    }
215}
216
217impl Default for NickelCodegen {
218    fn default() -> Self {
219        Self::new()
220    }
221}
222
223impl Codegen for NickelCodegen {
224    fn generate(&mut self, ir: &IR) -> Result<String, CodegenError> {
225        let mut output = String::new();
226
227        for module in &ir.modules {
228            // Module header comment
229            writeln!(output, "# Module: {}", module.name)
230                .map_err(|e| CodegenError::Generation(e.to_string()))?;
231            writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
232
233            // Generate imports if any
234            if !module.imports.is_empty() {
235                for import in &module.imports {
236                    // Convert import path based on package mode
237                    let import_path = self.package_mode.convert_import(&import.path);
238
239                    // Generate import statement
240                    // If the path is a package name (no slashes), use package import syntax
241                    let import_statement =
242                        if !import_path.contains('/') && import_path.starts_with('"') {
243                            // Package import: import "package_name"
244                            format!(
245                                "let {} = import {} in",
246                                import
247                                    .alias
248                                    .as_ref()
249                                    .unwrap_or(&import.path.replace('/', "_")),
250                                import_path
251                            )
252                        } else {
253                            // Regular file import
254                            format!(
255                                "let {} = import \"{}\" in",
256                                import
257                                    .alias
258                                    .as_ref()
259                                    .unwrap_or(&import.path.replace('/', "_")),
260                                import_path
261                            )
262                        };
263
264                    writeln!(output, "{}", import_statement)
265                        .map_err(|e| CodegenError::Generation(e.to_string()))?;
266                }
267                writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
268            }
269
270            // Generate type definitions with proper formatting
271            writeln!(output, "{{")?;
272
273            for (idx, type_def) in module.types.iter().enumerate() {
274                // Add type documentation as a comment if present
275                if let Some(doc) = &type_def.documentation {
276                    for line in doc.lines() {
277                        writeln!(output, "{}# {}", self.indent(1), line)
278                            .map_err(|e| CodegenError::Generation(e.to_string()))?;
279                    }
280                }
281
282                // Generate the type with proper indentation
283                let type_str = self.type_to_nickel(&type_def.ty, module, 1)?;
284
285                // Check if type is a record that needs special formatting
286                if matches!(type_def.ty, Type::Record { .. }) {
287                    // For records, put the opening brace on the same line
288                    write!(output, "  {} = ", type_def.name)?;
289                    writeln!(output, "{},", type_str)?;
290                } else {
291                    writeln!(output, "  {} = {},", type_def.name, type_str)?;
292                }
293
294                // Add spacing between types for readability
295                if idx < module.types.len() - 1 {
296                    writeln!(output)?;
297                }
298            }
299
300            // Generate constants with proper formatting
301            if !module.constants.is_empty() {
302                writeln!(output)?; // Add spacing before constants
303
304                for constant in &module.constants {
305                    if let Some(doc) = &constant.documentation {
306                        writeln!(output, "  # {}", doc)
307                            .map_err(|e| CodegenError::Generation(e.to_string()))?;
308                    }
309
310                    let value_str = format_json_value(&constant.value, 1);
311                    writeln!(output, "  {} = {},", constant.name, value_str)
312                        .map_err(|e| CodegenError::Generation(e.to_string()))?;
313                }
314            }
315
316            writeln!(output, "}}")?;
317        }
318
319        Ok(output)
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use amalgam_core::ir::{Metadata, Module};
327    use std::collections::BTreeMap;
328
329    fn create_test_module() -> Module {
330        Module {
331            name: "test".to_string(),
332            imports: Vec::new(),
333            types: Vec::new(),
334            constants: Vec::new(),
335            metadata: Metadata {
336                source_language: None,
337                source_file: None,
338                version: None,
339                generated_at: None,
340                custom: BTreeMap::new(),
341            },
342        }
343    }
344
345    #[test]
346    fn test_simple_type_generation() {
347        let mut codegen = NickelCodegen::new();
348        let module = create_test_module();
349
350        assert_eq!(
351            codegen.type_to_nickel(&Type::String, &module, 0).unwrap(),
352            "String"
353        );
354        assert_eq!(
355            codegen.type_to_nickel(&Type::Number, &module, 0).unwrap(),
356            "Number"
357        );
358        assert_eq!(
359            codegen.type_to_nickel(&Type::Bool, &module, 0).unwrap(),
360            "Bool"
361        );
362        assert_eq!(
363            codegen.type_to_nickel(&Type::Any, &module, 0).unwrap(),
364            "Dyn"
365        );
366    }
367
368    #[test]
369    fn test_array_generation() {
370        let mut codegen = NickelCodegen::new();
371        let module = create_test_module();
372        let array_type = Type::Array(Box::new(Type::String));
373        assert_eq!(
374            codegen.type_to_nickel(&array_type, &module, 0).unwrap(),
375            "Array String"
376        );
377    }
378
379    #[test]
380    fn test_optional_generation() {
381        let mut codegen = NickelCodegen::new();
382        let module = create_test_module();
383        let optional_type = Type::Optional(Box::new(Type::String));
384        assert_eq!(
385            codegen.type_to_nickel(&optional_type, &module, 0).unwrap(),
386            "String | Null"
387        );
388    }
389
390    #[test]
391    fn test_doc_formatting() {
392        let codegen = NickelCodegen::new();
393
394        // Short doc uses regular quotes
395        assert_eq!(codegen.format_doc("Short doc"), "\"Short doc\"");
396
397        // Multiline doc uses triple quotes
398        let multiline = "This is a\nmultiline doc";
399        assert_eq!(
400            codegen.format_doc(multiline),
401            "m%\"\nThis is a\nmultiline doc\n\"%"
402        );
403
404        // Escapes quotes in short docs
405        assert_eq!(
406            codegen.format_doc("Doc with \"quotes\""),
407            "\"Doc with \\\"quotes\\\"\""
408        );
409    }
410}