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        // In Nickel, a field with a default value is implicitly optional
148        // So we only add 'optional' if there's no default value
149        if !field.required && field.default.is_none() {
150            parts.push("optional".to_string());
151        }
152
153        // Type
154        parts.push(type_str);
155
156        // Documentation (must come BEFORE default in Nickel)
157        if let Some(desc) = &field.description {
158            parts.push(format!("doc {}", self.format_doc(desc)));
159        }
160
161        // Default value (must come AFTER doc in Nickel)
162        if let Some(default) = &field.default {
163            let default_str = format_json_value(default, indent_level);
164            parts.push(format!("default = {}", default_str));
165        }
166
167        Ok(parts.join(" | "))
168    }
169}
170
171/// Format a JSON value for Nickel
172fn format_json_value(value: &serde_json::Value, indent_level: usize) -> String {
173    match value {
174        serde_json::Value::Null => "null".to_string(),
175        serde_json::Value::Bool(b) => b.to_string(),
176        serde_json::Value::Number(n) => n.to_string(),
177        serde_json::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
178        serde_json::Value::Array(arr) => {
179            let items: Vec<String> = arr
180                .iter()
181                .map(|v| format_json_value(v, indent_level))
182                .collect();
183            format!("[{}]", items.join(", "))
184        }
185        serde_json::Value::Object(obj) => {
186            if obj.is_empty() {
187                "{}".to_string()
188            } else {
189                let indent = " ".repeat((indent_level + 1) * 2);
190                let mut items = Vec::new();
191                for (k, v) in obj {
192                    items.push(format!(
193                        "{}{} = {}",
194                        indent,
195                        k,
196                        format_json_value(v, indent_level + 1)
197                    ));
198                }
199                format!(
200                    "{{\n{}\n{}}}",
201                    items.join(",\n"),
202                    " ".repeat(indent_level * 2)
203                )
204            }
205        }
206    }
207}
208
209impl Default for NickelCodegen {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215impl Codegen for NickelCodegen {
216    fn generate(&mut self, ir: &IR) -> Result<String, CodegenError> {
217        let mut output = String::new();
218
219        for module in &ir.modules {
220            // Module header comment
221            writeln!(output, "# Module: {}", module.name)
222                .map_err(|e| CodegenError::Generation(e.to_string()))?;
223            writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
224
225            // Generate imports if any
226            if !module.imports.is_empty() {
227                for import in &module.imports {
228                    writeln!(
229                        output,
230                        "let {} = import \"{}\" in",
231                        import.alias.as_ref().unwrap_or(&import.path),
232                        import.path
233                    )
234                    .map_err(|e| CodegenError::Generation(e.to_string()))?;
235                }
236                writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
237            }
238
239            // Generate type definitions with proper formatting
240            writeln!(output, "{{")?;
241
242            for (idx, type_def) in module.types.iter().enumerate() {
243                // Add type documentation as a comment if present
244                if let Some(doc) = &type_def.documentation {
245                    for line in doc.lines() {
246                        writeln!(output, "{}# {}", self.indent(1), line)
247                            .map_err(|e| CodegenError::Generation(e.to_string()))?;
248                    }
249                }
250
251                // Generate the type with proper indentation
252                let type_str = self.type_to_nickel(&type_def.ty, module, 1)?;
253
254                // Check if type is a record that needs special formatting
255                if matches!(type_def.ty, Type::Record { .. }) {
256                    // For records, put the opening brace on the same line
257                    write!(output, "  {} = ", type_def.name)?;
258                    writeln!(output, "{},", type_str)?;
259                } else {
260                    writeln!(output, "  {} = {},", type_def.name, type_str)?;
261                }
262
263                // Add spacing between types for readability
264                if idx < module.types.len() - 1 {
265                    writeln!(output)?;
266                }
267            }
268
269            // Generate constants with proper formatting
270            if !module.constants.is_empty() {
271                writeln!(output)?; // Add spacing before constants
272
273                for constant in &module.constants {
274                    if let Some(doc) = &constant.documentation {
275                        writeln!(output, "  # {}", doc)
276                            .map_err(|e| CodegenError::Generation(e.to_string()))?;
277                    }
278
279                    let value_str = format_json_value(&constant.value, 1);
280                    writeln!(output, "  {} = {},", constant.name, value_str)
281                        .map_err(|e| CodegenError::Generation(e.to_string()))?;
282                }
283            }
284
285            writeln!(output, "}}")?;
286        }
287
288        Ok(output)
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use amalgam_core::ir::{Metadata, Module};
296    use std::collections::BTreeMap;
297
298    fn create_test_module() -> Module {
299        Module {
300            name: "test".to_string(),
301            imports: Vec::new(),
302            types: Vec::new(),
303            constants: Vec::new(),
304            metadata: Metadata {
305                source_language: None,
306                source_file: None,
307                version: None,
308                generated_at: None,
309                custom: BTreeMap::new(),
310            },
311        }
312    }
313
314    #[test]
315    fn test_simple_type_generation() {
316        let mut codegen = NickelCodegen::new();
317        let module = create_test_module();
318
319        assert_eq!(
320            codegen.type_to_nickel(&Type::String, &module, 0).unwrap(),
321            "String"
322        );
323        assert_eq!(
324            codegen.type_to_nickel(&Type::Number, &module, 0).unwrap(),
325            "Number"
326        );
327        assert_eq!(
328            codegen.type_to_nickel(&Type::Bool, &module, 0).unwrap(),
329            "Bool"
330        );
331        assert_eq!(
332            codegen.type_to_nickel(&Type::Any, &module, 0).unwrap(),
333            "Dyn"
334        );
335    }
336
337    #[test]
338    fn test_array_generation() {
339        let mut codegen = NickelCodegen::new();
340        let module = create_test_module();
341        let array_type = Type::Array(Box::new(Type::String));
342        assert_eq!(
343            codegen.type_to_nickel(&array_type, &module, 0).unwrap(),
344            "Array String"
345        );
346    }
347
348    #[test]
349    fn test_optional_generation() {
350        let mut codegen = NickelCodegen::new();
351        let module = create_test_module();
352        let optional_type = Type::Optional(Box::new(Type::String));
353        assert_eq!(
354            codegen.type_to_nickel(&optional_type, &module, 0).unwrap(),
355            "String | Null"
356        );
357    }
358
359    #[test]
360    fn test_doc_formatting() {
361        let codegen = NickelCodegen::new();
362
363        // Short doc uses regular quotes
364        assert_eq!(codegen.format_doc("Short doc"), "\"Short doc\"");
365
366        // Multiline doc uses triple quotes
367        let multiline = "This is a\nmultiline doc";
368        assert_eq!(
369            codegen.format_doc(multiline),
370            "m%\"\nThis is a\nmultiline doc\n\"%"
371        );
372
373        // Escapes quotes in short docs
374        assert_eq!(
375            codegen.format_doc("Doc with \"quotes\""),
376            "\"Doc with \\\"quotes\\\"\""
377        );
378    }
379}