Skip to main content

rable/
ast.rs

1/// Source span representing a byte range in the original input.
2#[derive(Debug, Clone, PartialEq, Eq)]
3pub struct Span {
4    pub start: usize,
5    pub end: usize,
6}
7
8impl Span {
9    /// Creates a new span with the given byte offsets.
10    pub const fn new(start: usize, end: usize) -> Self {
11        Self { start, end }
12    }
13
14    /// Creates an empty span (used for synthetic nodes).
15    pub const fn empty() -> Self {
16        Self { start: 0, end: 0 }
17    }
18
19    /// Returns true if this span has no extent (synthetic or unset).
20    pub const fn is_empty(&self) -> bool {
21        self.start >= self.end
22    }
23}
24
25/// A spanned AST node combining a [`NodeKind`] with its source [`Span`].
26#[derive(Debug, Clone, PartialEq)]
27pub struct Node {
28    pub kind: NodeKind,
29    pub span: Span,
30}
31
32impl Node {
33    /// Creates a new node with the given kind and span.
34    pub const fn new(kind: NodeKind, span: Span) -> Self {
35        Self { kind, span }
36    }
37
38    /// Creates a node with an empty span (for synthetic or temporary nodes).
39    pub const fn empty(kind: NodeKind) -> Self {
40        Self {
41            kind,
42            span: Span::empty(),
43        }
44    }
45
46    /// Extracts the source text for this node from the original source string.
47    ///
48    /// Spans use character indices (matching `Token.pos` semantics).
49    /// Returns an empty string for synthetic nodes or invalid spans.
50    pub fn source_text<'a>(&self, source: &'a str) -> &'a str {
51        if self.span.is_empty() {
52            return "";
53        }
54        // Convert char indices to byte offsets
55        let byte_start = source.char_indices().nth(self.span.start).map(|(i, _)| i);
56        let byte_end = source
57            .char_indices()
58            .nth(self.span.end)
59            .map_or(source.len(), |(i, _)| i);
60        match byte_start {
61            Some(s) if byte_end <= source.len() => &source[s..byte_end],
62            _ => "",
63        }
64    }
65}
66
67/// AST node representing all bash constructs.
68///
69/// This enum mirrors Parable's AST node classes exactly, ensuring
70/// S-expression output compatibility.
71#[derive(Debug, Clone, PartialEq)]
72#[allow(clippy::use_self)]
73pub enum NodeKind {
74    /// A word token, possibly containing expansion parts.
75    Word {
76        value: String,
77        parts: Vec<Node>,
78        spans: Vec<crate::lexer::word_builder::WordSpan>,
79    },
80
81    /// A literal text segment within a word's parts list.
82    WordLiteral { value: String },
83
84    /// A simple command: assignments, words, and redirects.
85    Command {
86        assignments: Vec<Node>,
87        words: Vec<Node>,
88        redirects: Vec<Node>,
89    },
90
91    /// A pipeline of commands separated by `|` or `|&`.
92    Pipeline {
93        commands: Vec<Node>,
94        separators: Vec<PipeSep>,
95    },
96
97    /// A list of commands with operators (`;`, `&&`, `||`, `&`, `\n`).
98    List { items: Vec<ListItem> },
99
100    // -- Compound commands --
101    /// `if condition; then body; [elif ...; then ...;] [else ...;] fi`
102    If {
103        condition: Box<Node>,
104        then_body: Box<Node>,
105        else_body: Option<Box<Node>>,
106        redirects: Vec<Node>,
107    },
108
109    /// `while condition; do body; done`
110    While {
111        condition: Box<Node>,
112        body: Box<Node>,
113        redirects: Vec<Node>,
114    },
115
116    /// `until condition; do body; done`
117    Until {
118        condition: Box<Node>,
119        body: Box<Node>,
120        redirects: Vec<Node>,
121    },
122
123    /// `for var [in words]; do body; done`
124    For {
125        var: String,
126        words: Option<Vec<Node>>,
127        body: Box<Node>,
128        redirects: Vec<Node>,
129    },
130
131    /// C-style for loop: `for (( init; cond; incr )); do body; done`
132    ForArith {
133        init: String,
134        cond: String,
135        incr: String,
136        body: Box<Node>,
137        redirects: Vec<Node>,
138    },
139
140    /// `select var [in words]; do body; done`
141    Select {
142        var: String,
143        words: Option<Vec<Node>>,
144        body: Box<Node>,
145        redirects: Vec<Node>,
146    },
147
148    /// `case word in pattern) body;; ... esac`
149    Case {
150        word: Box<Node>,
151        patterns: Vec<CasePattern>,
152        redirects: Vec<Node>,
153    },
154
155    /// A function definition: `name() { body; }` or `function name { body; }`
156    Function { name: String, body: Box<Node> },
157
158    /// A subshell: `( commands )`
159    Subshell {
160        body: Box<Node>,
161        redirects: Vec<Node>,
162    },
163
164    /// A brace group: `{ commands; }`
165    BraceGroup {
166        body: Box<Node>,
167        redirects: Vec<Node>,
168    },
169
170    /// A coprocess: `coproc [name] command`
171    Coproc {
172        name: Option<String>,
173        command: Box<Node>,
174    },
175
176    // -- Redirections --
177    /// I/O redirection: `[fd]op target`
178    Redirect {
179        op: String,
180        target: Box<Node>,
181        fd: i32,
182    },
183
184    /// Here-document: `<<[-]DELIM\ncontent\nDELIM`
185    HereDoc {
186        delimiter: String,
187        content: String,
188        strip_tabs: bool,
189        quoted: bool,
190        fd: i32,
191        complete: bool,
192    },
193
194    // -- Expansions --
195    /// Parameter expansion: `$var` or `${var[op arg]}`
196    ParamExpansion {
197        param: String,
198        op: Option<String>,
199        arg: Option<String>,
200    },
201
202    /// Parameter length: `${#var}`
203    ParamLength { param: String },
204
205    /// Indirect expansion: `${!var[op arg]}`
206    ParamIndirect {
207        param: String,
208        op: Option<String>,
209        arg: Option<String>,
210    },
211
212    /// Command substitution: `$(cmd)` or `` `cmd` ``
213    CommandSubstitution { command: Box<Node>, brace: bool },
214
215    /// Process substitution: `<(cmd)` or `>(cmd)`
216    ProcessSubstitution {
217        direction: String,
218        command: Box<Node>,
219    },
220
221    /// ANSI-C quoting: `$'...'`
222    AnsiCQuote { content: String },
223
224    /// Locale string: `$"..."`
225    LocaleString { content: String },
226
227    /// Arithmetic expansion: `$(( expr ))`
228    ArithmeticExpansion { expression: Option<Box<Node>> },
229
230    /// Arithmetic command: `(( expr ))`
231    ArithmeticCommand {
232        expression: Option<Box<Node>>,
233        redirects: Vec<Node>,
234        raw_content: String,
235    },
236
237    // -- Arithmetic expression nodes --
238    /// A numeric literal in arithmetic context.
239    ArithNumber { value: String },
240
241    /// A variable reference in arithmetic context.
242    ArithVar { name: String },
243
244    /// A binary operation in arithmetic context.
245    ArithBinaryOp {
246        op: String,
247        left: Box<Node>,
248        right: Box<Node>,
249    },
250
251    /// A unary operation in arithmetic context.
252    ArithUnaryOp { op: String, operand: Box<Node> },
253
254    /// Pre-increment `++var`.
255    ArithPreIncr { operand: Box<Node> },
256
257    /// Post-increment `var++`.
258    ArithPostIncr { operand: Box<Node> },
259
260    /// Pre-decrement `--var`.
261    ArithPreDecr { operand: Box<Node> },
262
263    /// Post-decrement `var--`.
264    ArithPostDecr { operand: Box<Node> },
265
266    /// Assignment in arithmetic context.
267    ArithAssign {
268        op: String,
269        target: Box<Node>,
270        value: Box<Node>,
271    },
272
273    /// Ternary `cond ? true : false`.
274    ArithTernary {
275        condition: Box<Node>,
276        if_true: Option<Box<Node>>,
277        if_false: Option<Box<Node>>,
278    },
279
280    /// Comma operator in arithmetic context.
281    ArithComma { left: Box<Node>, right: Box<Node> },
282
283    /// Array subscript in arithmetic context.
284    ArithSubscript { array: String, index: Box<Node> },
285
286    /// Empty arithmetic expression.
287    ArithEmpty,
288
289    /// An escaped character in arithmetic context.
290    ArithEscape { ch: String },
291
292    /// Deprecated `$[expr]` arithmetic.
293    ArithDeprecated { expression: String },
294
295    /// Concatenation in arithmetic context (e.g., `0x$var`).
296    ArithConcat { parts: Vec<Node> },
297
298    // -- Conditional expression nodes (`[[ ]]`) --
299    /// `[[ expr ]]`
300    ConditionalExpr {
301        body: Box<Node>,
302        redirects: Vec<Node>,
303    },
304
305    /// Unary test: `-f file`, `-z string`, etc.
306    UnaryTest { op: String, operand: Box<Node> },
307
308    /// Binary test: `a == b`, `a -nt b`, etc.
309    BinaryTest {
310        op: String,
311        left: Box<Node>,
312        right: Box<Node>,
313    },
314
315    /// `[[ a && b ]]`
316    CondAnd { left: Box<Node>, right: Box<Node> },
317
318    /// `[[ a || b ]]`
319    CondOr { left: Box<Node>, right: Box<Node> },
320
321    /// `[[ ! expr ]]`
322    CondNot { operand: Box<Node> },
323
324    /// `[[ ( expr ) ]]`
325    CondParen { inner: Box<Node> },
326
327    /// A term (word) in a conditional expression.
328    CondTerm {
329        value: String,
330        spans: Vec<crate::lexer::word_builder::WordSpan>,
331    },
332
333    // -- Other --
334    /// Pipeline negation with `!`.
335    Negation { pipeline: Box<Node> },
336
337    /// `time [-p] pipeline`
338    Time { pipeline: Box<Node>, posix: bool },
339
340    /// Array literal: `(a b c)`.
341    Array { elements: Vec<Node> },
342
343    /// An empty node.
344    Empty,
345
346    /// A comment: `# text`.
347    Comment { text: String },
348}
349
350/// Operator between commands in a list.
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum ListOperator {
353    /// `&&`
354    And,
355    /// `||`
356    Or,
357    /// `;` or `\n`
358    Semi,
359    /// `&`
360    Background,
361}
362
363/// Separator between commands in a pipeline.
364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
365pub enum PipeSep {
366    /// `|` — pipe stdout only.
367    Pipe,
368    /// `|&` — pipe both stdout and stderr.
369    PipeBoth,
370}
371
372/// An item in a command list: a command with an optional trailing operator.
373#[derive(Debug, Clone, PartialEq)]
374pub struct ListItem {
375    pub command: Node,
376    pub operator: Option<ListOperator>,
377}
378
379/// A single case pattern clause within a `case` statement.
380#[derive(Debug, Clone, PartialEq)]
381pub struct CasePattern {
382    pub patterns: Vec<Node>,
383    pub body: Option<Node>,
384    pub terminator: String,
385}
386
387impl CasePattern {
388    pub const fn new(patterns: Vec<Node>, body: Option<Node>, terminator: String) -> Self {
389        Self {
390            patterns,
391            body,
392            terminator,
393        }
394    }
395}