amalgam_codegen/
nickel.rs

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