Skip to main content

sigil_stitch/
code_node.rs

1//! Tree-based intermediate representation for code generation.
2//!
3//! `CodeNode` is the internal IR used by [`CodeBlock`](crate::code_block::CodeBlock).
4//! Each node is self-contained — type references, names, and nested blocks are
5//! stored inline rather than in a separate argument vector. This enables natural
6//! tree traversal for import collection, structural transformation, and rendering.
7
8use crate::code_block::{Arg, CodeBlock, FormatPart};
9use crate::type_name::TypeName;
10
11/// A single node in the code generation tree.
12///
13/// Each variant is self-contained: a type reference is `CodeNode::TypeRef(TypeName)`,
14/// not a separate format tag plus a positional argument.
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16#[non_exhaustive]
17pub enum CodeNode {
18    /// Literal text (no interpolation).
19    Literal(String),
20    /// A type reference with import tracking (was `%T` + `Arg::TypeName`).
21    TypeRef(TypeName),
22    /// A name identifier (was `%N` + `Arg::Name`).
23    NameRef(String),
24    /// A string literal value, rendered with language-specific quoting
25    /// (was `%S` + `Arg::StringLit`).
26    StringLit(String),
27    /// An inline literal string (was `%L` + `Arg::Literal`).
28    InlineLiteral(String),
29    /// A nested code block (was `%L` + `Arg::Code`).
30    Nested(CodeBlock),
31    /// A comment line. Rendered as `{prefix} {text}{suffix}` using the
32    /// language's comment syntax.
33    Comment(String),
34    /// Soft line break point (`%W`). In direct mode emits a space; in pretty
35    /// mode becomes `BoxDoc::softline()`.
36    SoftBreak,
37    /// Increase indent level (`%>`).
38    Indent,
39    /// Decrease indent level (`%<`).
40    Dedent,
41    /// Statement begin marker (`%[`). Triggers `ensure_indent()`.
42    StatementBegin,
43    /// Statement end marker (`%]`). Emits `;` if the language uses semicolons.
44    StatementEnd,
45    /// Hard newline.
46    Newline,
47    /// Block open delimiter, resolved at render time via `lang.block_syntax().block_open`.
48    BlockOpen,
49    /// Block open with an overridden delimiter string.
50    BlockOpenOverride(String),
51    /// Terminal block close delimiter, resolved via `lang.block_syntax().block_close`.
52    BlockClose,
53    /// Transitional block close delimiter (e.g. `} else`), resolved via
54    /// `lang.block_syntax().block_close` + `" "`.
55    BlockCloseTransition,
56    /// A sequence of nodes (for grouping, e.g. a statement or control flow block).
57    Sequence(Vec<CodeNode>),
58}
59
60/// Convert legacy `(FormatPart, Arg)` parallel vectors into `Vec<CodeNode>`.
61///
62/// Used by `CodeBlockBuilder::add()` which still calls `parse_format()` to get
63/// `Vec<FormatPart>`, then zips with args into self-contained nodes.
64pub(crate) fn parts_args_to_nodes(parts: &[FormatPart], args: &[Arg]) -> Vec<CodeNode> {
65    let mut nodes = Vec::with_capacity(parts.len());
66    let mut arg_index = 0;
67
68    for part in parts {
69        let node = match part {
70            FormatPart::Literal(text) => CodeNode::Literal(text.clone()),
71            FormatPart::Type => {
72                let arg = &args[arg_index];
73                arg_index += 1;
74                match arg {
75                    Arg::TypeName(tn) => CodeNode::TypeRef(tn.clone()),
76                    _ => CodeNode::Literal(String::new()),
77                }
78            }
79            FormatPart::Name => {
80                let arg = &args[arg_index];
81                arg_index += 1;
82                match arg {
83                    Arg::Name(n) => CodeNode::NameRef(n.clone()),
84                    _ => CodeNode::Literal(String::new()),
85                }
86            }
87            FormatPart::StringLit => {
88                let arg = &args[arg_index];
89                arg_index += 1;
90                match arg {
91                    Arg::StringLit(s) => CodeNode::StringLit(s.clone()),
92                    _ => CodeNode::Literal(String::new()),
93                }
94            }
95            FormatPart::Literal_ => {
96                let arg = &args[arg_index];
97                arg_index += 1;
98                match arg {
99                    Arg::Literal(s) => CodeNode::InlineLiteral(s.clone()),
100                    Arg::Code(block) => CodeNode::Nested(block.clone()),
101                    _ => CodeNode::Literal(String::new()),
102                }
103            }
104            FormatPart::Wrap => CodeNode::SoftBreak,
105            FormatPart::Indent => CodeNode::Indent,
106            FormatPart::Dedent => CodeNode::Dedent,
107            FormatPart::StatementBegin => CodeNode::StatementBegin,
108            FormatPart::StatementEnd => CodeNode::StatementEnd,
109            FormatPart::Newline => CodeNode::Newline,
110            FormatPart::BlockOpen => CodeNode::BlockOpen,
111            FormatPart::BlockOpenOverride(s) => CodeNode::BlockOpenOverride(s.clone()),
112            FormatPart::BlockClose => CodeNode::BlockClose,
113            FormatPart::BlockCloseTransition => CodeNode::BlockCloseTransition,
114        };
115        nodes.push(node);
116    }
117
118    nodes
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::code_block::CodeBlock;
125    use crate::type_name::TypeName;
126
127    #[test]
128    fn test_literal_conversion() {
129        let parts = vec![FormatPart::Literal("hello".to_string())];
130        let args = vec![];
131        let nodes = parts_args_to_nodes(&parts, &args);
132        assert_eq!(nodes.len(), 1);
133        assert!(matches!(&nodes[0], CodeNode::Literal(s) if s == "hello"));
134    }
135
136    #[test]
137    fn test_type_ref_conversion() {
138        let tn = TypeName::primitive("string");
139        let parts = vec![FormatPart::Literal("x: ".to_string()), FormatPart::Type];
140        let args = vec![Arg::TypeName(tn)];
141        let nodes = parts_args_to_nodes(&parts, &args);
142        assert_eq!(nodes.len(), 2);
143        assert!(matches!(&nodes[0], CodeNode::Literal(s) if s == "x: "));
144        assert!(matches!(&nodes[1], CodeNode::TypeRef(_)));
145    }
146
147    #[test]
148    fn test_nested_block_conversion() {
149        let inner = CodeBlock::of("inner()", ()).unwrap();
150        let parts = vec![FormatPart::Literal_];
151        let args = vec![Arg::Code(inner)];
152        let nodes = parts_args_to_nodes(&parts, &args);
153        assert_eq!(nodes.len(), 1);
154        assert!(matches!(&nodes[0], CodeNode::Nested(_)));
155    }
156
157    #[test]
158    fn test_structural_nodes() {
159        let parts = vec![
160            FormatPart::Indent,
161            FormatPart::StatementBegin,
162            FormatPart::Literal("x".to_string()),
163            FormatPart::StatementEnd,
164            FormatPart::Newline,
165            FormatPart::Dedent,
166        ];
167        let nodes = parts_args_to_nodes(&parts, &[]);
168        assert_eq!(nodes.len(), 6);
169        assert!(matches!(nodes[0], CodeNode::Indent));
170        assert!(matches!(nodes[1], CodeNode::StatementBegin));
171        assert!(matches!(nodes[3], CodeNode::StatementEnd));
172        assert!(matches!(nodes[4], CodeNode::Newline));
173        assert!(matches!(nodes[5], CodeNode::Dedent));
174    }
175
176    #[test]
177    fn test_soft_break_conversion() {
178        let parts = vec![
179            FormatPart::Literal("a".to_string()),
180            FormatPart::Wrap,
181            FormatPart::Literal("b".to_string()),
182        ];
183        let nodes = parts_args_to_nodes(&parts, &[]);
184        assert_eq!(nodes.len(), 3);
185        assert!(matches!(nodes[1], CodeNode::SoftBreak));
186    }
187
188    #[test]
189    fn test_block_open_close_conversion() {
190        let parts = vec![
191            FormatPart::BlockOpen,
192            FormatPart::BlockClose,
193            FormatPart::BlockOpenOverride("where".to_string()),
194            FormatPart::BlockCloseTransition,
195        ];
196        let nodes = parts_args_to_nodes(&parts, &[]);
197        assert_eq!(nodes.len(), 4);
198        assert!(matches!(nodes[0], CodeNode::BlockOpen));
199        assert!(matches!(nodes[1], CodeNode::BlockClose));
200        assert!(matches!(&nodes[2], CodeNode::BlockOpenOverride(s) if s == "where"));
201        assert!(matches!(nodes[3], CodeNode::BlockCloseTransition));
202    }
203
204    #[test]
205    fn test_mixed_args_conversion() {
206        let tn = TypeName::primitive("number");
207        let parts = vec![
208            FormatPart::Literal("let ".to_string()),
209            FormatPart::Name,
210            FormatPart::Literal(": ".to_string()),
211            FormatPart::Type,
212            FormatPart::Literal(" = ".to_string()),
213            FormatPart::StringLit,
214        ];
215        let args = vec![
216            Arg::Name("x".to_string()),
217            Arg::TypeName(tn),
218            Arg::StringLit("hello".to_string()),
219        ];
220        let nodes = parts_args_to_nodes(&parts, &args);
221        assert_eq!(nodes.len(), 6);
222        assert!(matches!(&nodes[1], CodeNode::NameRef(s) if s == "x"));
223        assert!(matches!(&nodes[3], CodeNode::TypeRef(_)));
224        assert!(matches!(&nodes[5], CodeNode::StringLit(s) if s == "hello"));
225    }
226}