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    /// Escape field names that are reserved keywords or start with special characters
37    fn escape_field_name(&self, name: &str) -> String {
38        // Fields starting with $ need to be quoted
39        if name.starts_with('$') || self.is_reserved_keyword(name) {
40            format!("\"{}\"", name)
41        } else {
42            name.to_string()
43        }
44    }
45
46    /// Check if a field name is a Nickel reserved keyword
47    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    /// Format a documentation string properly
77    /// Uses triple quotes for multiline, regular quotes for single line
78    fn format_doc(&self, doc: &str) -> String {
79        if doc.contains('\n') || doc.len() > 80 {
80            // Use triple quotes for multiline or long docs
81            format!("m%\"\n{}\n\"%", doc.trim())
82        } else {
83            // Use regular quotes for short docs
84            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()), // Nickel uses Number for all numerics
98            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                // Sort fields for consistent output
125                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                // Use the resolver to get the proper reference
165                let context = ResolutionContext {
166                    current_group: None, // Could extract from module.name if needed
167                    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        // Field name - escape reserved keywords and fields starting with $
193        let field_name = self.escape_field_name(name);
194        parts.push(format!("{}{}", indent, field_name));
195
196        // In Nickel, a field with a default value is implicitly optional
197        // For Kubernetes types, we make most fields optional to enable gradual construction
198        // Only skip 'optional' if there's an explicit default value
199        if field.default.is_none() {
200            parts.push("optional".to_string());
201        }
202
203        // Type
204        parts.push(type_str);
205
206        // Documentation (must come BEFORE default in Nickel)
207        if let Some(desc) = &field.description {
208            parts.push(format!("doc {}", self.format_doc(desc)));
209        }
210
211        // Default value (must come AFTER doc in Nickel)
212        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
221/// Format a JSON value for Nickel with proper field name escaping
222fn 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            // Module header comment
276            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            // Generate imports if any
281            if !module.imports.is_empty() {
282                for import in &module.imports {
283                    // Convert import path based on package mode
284                    let import_path = self.package_mode.convert_import(&import.path);
285
286                    // Generate import statement
287                    // If the path is a package name (no slashes), use package import syntax
288                    let import_statement =
289                        if !import_path.contains('/') && import_path.starts_with('"') {
290                            // Package import: import "package_name"
291                            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                            // Regular file import
301                            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            // Generate type definitions with proper formatting
318            writeln!(output, "{{")?;
319
320            for (idx, type_def) in module.types.iter().enumerate() {
321                // Add type documentation as a comment if present
322                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                // Generate the type with proper indentation
330                let type_str = self.type_to_nickel(&type_def.ty, module, 1)?;
331
332                // Check if type is a record that needs special formatting
333                if matches!(type_def.ty, Type::Record { .. }) {
334                    // For records, put the opening brace on the same line
335                    write!(output, "  {} = ", type_def.name)?;
336                    writeln!(output, "{},", type_str)?;
337                } else {
338                    writeln!(output, "  {} = {},", type_def.name, type_str)?;
339                }
340
341                // Add spacing between types for readability
342                if idx < module.types.len() - 1 {
343                    writeln!(output)?;
344                }
345            }
346
347            // Generate constants with proper formatting
348            if !module.constants.is_empty() {
349                writeln!(output)?; // Add spacing before constants
350
351                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        // Short doc uses regular quotes
442        assert_eq!(codegen.format_doc("Short doc"), "\"Short doc\"");
443
444        // Multiline doc uses triple quotes
445        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        // Escapes quotes in short docs
452        assert_eq!(
453            codegen.format_doc("Doc with \"quotes\""),
454            "\"Doc with \\\"quotes\\\"\""
455        );
456    }
457}