amalgam_codegen/
nickel.rs

1//! Nickel code generator with improved formatting
2
3use 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    /// 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(&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()), // Nickel uses Number for all numerics
45            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                // Sort fields for consistent output
72                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                // Use the resolver to get the proper reference
112                let context = ResolutionContext {
113                    current_group: None,  // Could extract from module.name if needed
114                    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        // Field name
140        parts.push(format!("{}{}", indent, name));
141
142        // Optional modifier (put before type for readability)
143        if !field.required {
144            parts.push("optional".to_string());
145        }
146
147        // Type
148        parts.push(type_str);
149
150        // Default value (before doc)
151        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        // Documentation (always last for better readability)
157        if let Some(desc) = &field.description {
158            parts.push(format!("doc {}", self.format_doc(desc)));
159        }
160
161        Ok(parts.join(" | "))
162    }
163}
164
165/// Format a JSON value for Nickel
166fn 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            // Module header comment
215            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            // Generate imports if any
220            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            // Generate type definitions with proper formatting
234            writeln!(output, "{{")?;
235
236            for (idx, type_def) in module.types.iter().enumerate() {
237                // Add type documentation as a comment if present
238                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                // Generate the type with proper indentation
246                let type_str = self.type_to_nickel(&type_def.ty, module, 1)?;
247
248                // Check if type is a record that needs special formatting
249                if matches!(type_def.ty, Type::Record { .. }) {
250                    // For records, put the opening brace on the same line
251                    write!(output, "  {} = ", type_def.name)?;
252                    writeln!(output, "{},", type_str)?;
253                } else {
254                    writeln!(output, "  {} = {},", type_def.name, type_str)?;
255                }
256
257                // Add spacing between types for readability
258                if idx < module.types.len() - 1 {
259                    writeln!(output)?;
260                }
261            }
262
263            // Generate constants with proper formatting
264            if !module.constants.is_empty() {
265                writeln!(output)?; // Add spacing before constants
266
267                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        // Short doc uses regular quotes
325        assert_eq!(codegen.format_doc("Short doc"), "\"Short doc\"");
326
327        // Multiline doc uses triple quotes
328        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        // Escapes quotes in short docs
335        assert_eq!(
336            codegen.format_doc("Doc with \"quotes\""),
337            "\"Doc with \\\"quotes\\\"\""
338        );
339    }
340}