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, Specifier};
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    /// A verbatim string literal, rendered with minimal escaping that preserves
28    /// interpolation sigils (was `%V` + `Arg::VerbatimStr`).
29    VerbatimStr(String),
30    /// An inline literal string (was `%L` + `Arg::Literal`).
31    InlineLiteral(String),
32    /// A nested code block (was `%L` + `Arg::Code`).
33    Nested(CodeBlock),
34    /// A comment line. Rendered as `{prefix} {text}{suffix}` using the
35    /// language's comment syntax.
36    Comment(String),
37    /// An attribute / annotation line. Rendered with the language's annotation
38    /// prefix and suffix (Rust: `#[text]`, Java/Python: `@text`, C++: `[[text]]`).
39    Attribute(String),
40    /// Soft line break point (`%W`). In direct mode emits a space; in pretty
41    /// mode becomes `BoxDoc::softline()`.
42    SoftBreak,
43    /// Increase indent level (`%>`).
44    Indent,
45    /// Decrease indent level (`%<`).
46    Dedent,
47    /// Statement begin marker (`%[`). Triggers `ensure_indent()`.
48    StatementBegin,
49    /// Statement end marker (`%]`). Emits `;` if the language uses semicolons.
50    StatementEnd,
51    /// Hard newline.
52    Newline,
53    /// Block open delimiter. Carries the control-flow condition text (e.g.,
54    /// `"if x > 0"`, `"for i in range(10)"`). At render time the renderer calls
55    /// `lang.block_open_for(condition)` — if it returns `Some(s)`, emit `s`;
56    /// otherwise fall back to `lang.block_syntax().block_open`.
57    /// Empty string means no condition (e.g., a bare `{ }` block).
58    BlockOpen(String),
59    /// Terminal block close delimiter. Carries the condition from the matching
60    /// `begin_control_flow` call. At render time the renderer calls
61    /// `lang.block_close_for(condition)` — if it returns `Some(s)`, emit `s`;
62    /// otherwise fall back to `lang.block_syntax().block_close`.
63    /// Emits: the closer only (no semicolon, no newline — those come from
64    /// `StatementEnd` and `Newline` nodes that follow).
65    BlockClose(String),
66    /// Non-terminal block close before a branch keyword (`else`, `elif`, `catch`).
67    /// Like `BlockClose` but emits closer + space (not newline) so the branch
68    /// keyword continues on the same line (e.g., `} else {`).
69    /// Suppressed when `block_syntax().close_on_transition` is `false`
70    /// (Lua, Bash — where `else`/`elif` sit between opener and closer).
71    BranchClose(String),
72    /// A sequence of nodes (for grouping, e.g. a statement or control flow block).
73    Sequence(Vec<CodeNode>),
74}
75
76/// Convert legacy `(FormatPart, Arg)` parallel vectors into `Vec<CodeNode>`.
77///
78/// Used by `CodeBlockBuilder::add()` which still calls `parse_format()` to get
79/// `Vec<FormatPart>`, then zips with args into self-contained nodes.
80pub(crate) fn parts_args_to_nodes(parts: &[FormatPart], args: &[Arg]) -> Vec<CodeNode> {
81    let mut nodes = Vec::with_capacity(parts.len());
82    let mut arg_index = 0;
83
84    for part in parts {
85        let node = match part {
86            FormatPart::Literal(text) => CodeNode::Literal(text.clone()),
87            FormatPart::Arg(spec) => {
88                let arg = &args[arg_index];
89                arg_index += 1;
90                match (spec, arg) {
91                    (Specifier::Type, Arg::TypeName(tn)) => CodeNode::TypeRef(tn.clone()),
92                    (Specifier::Name, Arg::Name(n)) => CodeNode::NameRef(n.clone()),
93                    (Specifier::StringLit, Arg::StringLit(s)) => CodeNode::StringLit(s.clone()),
94                    (Specifier::VerbatimStr, Arg::VerbatimStr(s)) => {
95                        CodeNode::VerbatimStr(s.clone())
96                    }
97                    (Specifier::Literal, Arg::Literal(s)) => CodeNode::InlineLiteral(s.clone()),
98                    (Specifier::Literal, Arg::Code(block)) => CodeNode::Nested(block.clone()),
99                    (Specifier::Comment, Arg::Comment(s)) => CodeNode::Comment(s.clone()),
100                    _ => CodeNode::Literal(String::new()),
101                }
102            }
103            FormatPart::Wrap => CodeNode::SoftBreak,
104            FormatPart::Indent => CodeNode::Indent,
105            FormatPart::Dedent => CodeNode::Dedent,
106            FormatPart::StatementBegin => CodeNode::StatementBegin,
107            FormatPart::StatementEnd => CodeNode::StatementEnd,
108            FormatPart::Newline => CodeNode::Newline,
109            FormatPart::BlockOpen(s) => CodeNode::BlockOpen(s.clone()),
110            FormatPart::BlockClose(s) => CodeNode::BlockClose(s.clone()),
111            FormatPart::BranchClose(s) => CodeNode::BranchClose(s.clone()),
112        };
113        nodes.push(node);
114    }
115
116    nodes
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::code_block::CodeBlock;
123    use crate::type_name::TypeName;
124
125    #[test]
126    fn test_literal_conversion() {
127        let parts = vec![FormatPart::Literal("hello".to_string())];
128        let args = vec![];
129        let nodes = parts_args_to_nodes(&parts, &args);
130        assert_eq!(nodes.len(), 1);
131        assert!(matches!(&nodes[0], CodeNode::Literal(s) if s == "hello"));
132    }
133
134    #[test]
135    fn test_type_ref_conversion() {
136        let tn = TypeName::primitive("string");
137        let parts = vec![
138            FormatPart::Literal("x: ".to_string()),
139            FormatPart::Arg(Specifier::Type),
140        ];
141        let args = vec![Arg::TypeName(tn)];
142        let nodes = parts_args_to_nodes(&parts, &args);
143        assert_eq!(nodes.len(), 2);
144        assert!(matches!(&nodes[0], CodeNode::Literal(s) if s == "x: "));
145        assert!(matches!(&nodes[1], CodeNode::TypeRef(_)));
146    }
147
148    #[test]
149    fn test_nested_block_conversion() {
150        let inner = CodeBlock::of("inner()", ()).unwrap();
151        let parts = vec![FormatPart::Arg(Specifier::Literal)];
152        let args = vec![Arg::Code(inner)];
153        let nodes = parts_args_to_nodes(&parts, &args);
154        assert_eq!(nodes.len(), 1);
155        assert!(matches!(&nodes[0], CodeNode::Nested(_)));
156    }
157
158    #[test]
159    fn test_structural_nodes() {
160        let parts = vec![
161            FormatPart::Indent,
162            FormatPart::StatementBegin,
163            FormatPart::Literal("x".to_string()),
164            FormatPart::StatementEnd,
165            FormatPart::Newline,
166            FormatPart::Dedent,
167        ];
168        let nodes = parts_args_to_nodes(&parts, &[]);
169        assert_eq!(nodes.len(), 6);
170        assert!(matches!(nodes[0], CodeNode::Indent));
171        assert!(matches!(nodes[1], CodeNode::StatementBegin));
172        assert!(matches!(nodes[3], CodeNode::StatementEnd));
173        assert!(matches!(nodes[4], CodeNode::Newline));
174        assert!(matches!(nodes[5], CodeNode::Dedent));
175    }
176
177    #[test]
178    fn test_soft_break_conversion() {
179        let parts = vec![
180            FormatPart::Literal("a".to_string()),
181            FormatPart::Wrap,
182            FormatPart::Literal("b".to_string()),
183        ];
184        let nodes = parts_args_to_nodes(&parts, &[]);
185        assert_eq!(nodes.len(), 3);
186        assert!(matches!(nodes[1], CodeNode::SoftBreak));
187    }
188
189    #[test]
190    fn test_block_open_close_conversion() {
191        let parts = vec![
192            FormatPart::BlockOpen("if x".to_string()),
193            FormatPart::BlockClose("if x".to_string()),
194            FormatPart::BranchClose("if x".to_string()),
195        ];
196        let nodes = parts_args_to_nodes(&parts, &[]);
197        assert_eq!(nodes.len(), 3);
198        assert!(matches!(&nodes[0], CodeNode::BlockOpen(s) if s == "if x"));
199        assert!(matches!(&nodes[1], CodeNode::BlockClose(s) if s == "if x"));
200        assert!(matches!(&nodes[2], CodeNode::BranchClose(s) if s == "if x"));
201    }
202
203    #[test]
204    fn test_mixed_args_conversion() {
205        let tn = TypeName::primitive("number");
206        let parts = vec![
207            FormatPart::Literal("let ".to_string()),
208            FormatPart::Arg(Specifier::Name),
209            FormatPart::Literal(": ".to_string()),
210            FormatPart::Arg(Specifier::Type),
211            FormatPart::Literal(" = ".to_string()),
212            FormatPart::Arg(Specifier::StringLit),
213        ];
214        let args = vec![
215            Arg::Name("x".to_string()),
216            Arg::TypeName(tn),
217            Arg::StringLit("hello".to_string()),
218        ];
219        let nodes = parts_args_to_nodes(&parts, &args);
220        assert_eq!(nodes.len(), 6);
221        assert!(matches!(&nodes[1], CodeNode::NameRef(s) if s == "x"));
222        assert!(matches!(&nodes[3], CodeNode::TypeRef(_)));
223        assert!(matches!(&nodes[5], CodeNode::StringLit(s) if s == "hello"));
224    }
225}