Skip to main content

dekopon_shell/
ast.rs

1//! Abstract syntax produced by [`crate::parser`] and walked by the evaluator.
2//!
3//! The shape covers exactly the grammar this sandbox keeps. Constructs that were deliberately
4//! dropped (backgrounding, subshells, process substitution, `eval`, brace groups) have no
5//! representation here at all, so no evaluator path can accidentally implement one.
6
7/// A parsed script or block.
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct Program {
10    /// Statements in source order.
11    pub statements: Vec<Statement>,
12}
13
14/// One top-level or block-level statement.
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub enum Statement {
17    /// An `&&`/`||` list of pipelines.
18    List(AndOrList),
19    /// `if ...; then ...; elif ...; else ...; fi`.
20    If(IfStatement),
21    /// `for NAME in WORDS...; do ...; done`.
22    For(ForLoop),
23    /// `while LIST; do ...; done` or `until LIST; do ...; done`.
24    While(WhileLoop),
25    /// `case WORD in PATTERN) ...;; esac`.
26    Case(CaseStatement),
27    /// `name() { ... }`.
28    Function(FunctionDefinition),
29}
30
31/// A pipeline chain joined by short-circuiting `&&` and `||`.
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct AndOrList {
34    /// The unconditional first pipeline.
35    pub first: Pipeline,
36    /// Conditionally executed continuations.
37    pub rest: Vec<(AndOr, Pipeline)>,
38}
39
40/// Short-circuit operator.
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum AndOr {
43    /// `&&`: run when the previous status was zero.
44    And,
45    /// `||`: run when the previous status was non-zero.
46    Or,
47}
48
49/// One or more commands joined by `|`.
50///
51/// A pipe hands the single structured value produced by the left command to the right command as
52/// its implicit input. This is jq-style value piping, not byte-stream piping.
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct Pipeline {
55    /// Commands in left-to-right order; never empty.
56    pub commands: Vec<SimpleCommand>,
57    /// `true` when a leading `!` inverts the pipeline's exit status.
58    pub negated: bool,
59}
60
61/// One command: optional assignment prefixes, argv words, and an optional buffer redirect.
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct SimpleCommand {
64    /// `NAME=value` prefixes. With no argv words these are plain assignments.
65    pub assignments: Vec<Assignment>,
66    /// Command word followed by arguments.
67    pub words: Vec<Word>,
68    /// `>` or `>>` into a named in-memory buffer.
69    pub redirect: Option<Redirect>,
70    /// `<<DELIM` body, supplying this command's input in place of anything piped into it.
71    pub here_doc: Option<Word>,
72}
73
74/// A `NAME=value` assignment.
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct Assignment {
77    /// Variable name.
78    pub name: String,
79    /// Right-hand side.
80    pub value: Word,
81}
82
83/// A write into the named in-memory buffer store.
84///
85/// These are not files. The buffer store lives for exactly one script execution and is unreachable
86/// from any real path; `cat <name>` is the only reader.
87#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct Redirect {
89    /// `true` for `>>`, `false` for `>`.
90    pub append: bool,
91    /// Buffer name word.
92    pub target: Word,
93}
94
95/// `if`/`elif`/`else`.
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct IfStatement {
98    /// `if` and each `elif` condition paired with its body.
99    pub branches: Vec<(AndOrList, Program)>,
100    /// Optional `else` body.
101    pub otherwise: Option<Program>,
102}
103
104/// `for NAME in WORDS...; do ...; done`.
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct ForLoop {
107    /// Loop variable.
108    pub variable: String,
109    /// Words expanded once before the loop begins.
110    pub words: Vec<Word>,
111    /// Loop body.
112    pub body: Program,
113}
114
115/// `while LIST; do ...; done` and its inverted `until` form.
116#[derive(Clone, Debug, Eq, PartialEq)]
117pub struct WhileLoop {
118    /// Condition re-evaluated before each iteration.
119    pub condition: AndOrList,
120    /// Loop body.
121    pub body: Program,
122    /// `true` for `until`, which iterates while the condition's status is non-zero.
123    pub until: bool,
124}
125
126/// `case WORD in PATTERN) ...;; esac`.
127#[derive(Clone, Debug, Eq, PartialEq)]
128pub struct CaseStatement {
129    /// The word each clause's patterns are matched against.
130    pub subject: Word,
131    /// Clauses in source order; the first whose pattern matches runs, and no other.
132    pub clauses: Vec<CaseClause>,
133}
134
135/// One `PATTERN|PATTERN) LIST ;;` clause.
136#[derive(Clone, Debug, Eq, PartialEq)]
137pub struct CaseClause {
138    /// Alternatives, any one of which selects this clause.
139    pub patterns: Vec<CasePattern>,
140    /// Body run when a pattern matches.
141    pub body: Program,
142}
143
144/// One `case` alternative.
145///
146/// Bash matches these as filename-style patterns. This shell matches literal text instead, for the
147/// same reason `builtins`' `grep` and `sed` take literal patterns: a partial wildcard is
148/// the pattern a literal matcher answers wrongly and silently, so it is rejected by name rather
149/// than quietly mismatched. A bare `*` is kept, because it is the default branch rather than a
150/// wildcard in any meaningful sense.
151#[derive(Clone, Debug, Eq, PartialEq)]
152pub enum CasePattern {
153    /// A bare `*`: the catch-all branch, which matches every subject.
154    Any,
155    /// A constant pattern, already checked for pattern syntax when it was parsed.
156    Literal(Word),
157    /// A pattern built from expansions, checked for pattern syntax when it is expanded.
158    ///
159    /// It cannot be checked earlier, because its text does not exist until the script runs — the
160    /// same reason `grep "$pattern"` is checked at run time rather than at parse time.
161    Expanded(Word),
162}
163
164/// `name() { ... }`.
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct FunctionDefinition {
167    /// Function name.
168    pub name: String,
169    /// Function body.
170    pub body: Program,
171}
172
173/// One argv word built from concatenated parts.
174#[derive(Clone, Debug, Default, Eq, PartialEq)]
175pub struct Word {
176    /// Parts in source order.
177    pub parts: Vec<WordPart>,
178}
179
180impl Word {
181    /// Returns the word's text when it is a single unquoted literal.
182    #[must_use]
183    pub fn as_literal(&self) -> Option<&str> {
184        match self.parts.as_slice() {
185            [WordPart::Literal(text)] => Some(text),
186            _ => None,
187        }
188    }
189
190    /// Reports whether the whole word is exactly one command substitution.
191    ///
192    /// `x=$(cmd)` preserves the command's structured value instead of coercing it to text. This is
193    /// a deliberate, documented deviation from bash, where `$()` is always textual; it is what lets
194    /// `ip=$(curl ...)` be followed by `echo ${ip[origin]}`.
195    #[must_use]
196    pub fn is_bare_command_substitution(&self) -> bool {
197        matches!(self.parts.as_slice(), [WordPart::CommandSubstitution(_)])
198    }
199}
200
201/// One component of a word.
202#[derive(Clone, Debug, Eq, PartialEq)]
203pub enum WordPart {
204    /// Unquoted literal text. `*`, `?`, `[`, `{`, and `~` are ordinary characters here.
205    Literal(String),
206    /// Single-quoted text; fully literal, bash-exact.
207    SingleQuoted(String),
208    /// Double-quoted text; interpolates parameters, `$(...)`, and `$(( ... ))`.
209    DoubleQuoted(Vec<WordPart>),
210    /// An unquoted parameter reference. A JSON array expands element-by-element into argv words.
211    Parameter(Parameter),
212    /// `$( ... )`.
213    CommandSubstitution(Program),
214    /// `$(( ... ))`.
215    Arithmetic(ArithExpr),
216}
217
218/// A parameter reference.
219#[derive(Clone, Debug, Eq, PartialEq)]
220pub enum Parameter {
221    /// `$NAME`, `${NAME}`, `${NAME[index]}`.
222    Named {
223        /// Variable name.
224        name: String,
225        /// Index words applied left to right; array offsets and object keys are backed by real JSON.
226        indices: Vec<Word>,
227    },
228    /// `$1` .. `${N}`.
229    Positional(usize),
230    /// `$@`, which splits one word per parameter even inside double quotes.
231    AllPositional,
232    /// `$*`, which is always exactly one space-joined word.
233    AllPositionalJoined,
234    /// `$#`.
235    PositionalCount,
236    /// `$?`.
237    LastStatus,
238}
239
240/// An arithmetic expression inside `$(( ... ))`.
241#[derive(Clone, Debug, PartialEq)]
242pub enum ArithExpr {
243    /// Integer literal.
244    Integer(i64),
245    /// Floating-point literal.
246    Float(f64),
247    /// A bare variable name; its value is coerced to a number.
248    Variable(String),
249    /// Prefix `-` or `!`.
250    Unary(ArithUnaryOp, Box<ArithExpr>),
251    /// An infix operator.
252    Binary(ArithBinaryOp, Box<ArithExpr>, Box<ArithExpr>),
253}
254
255impl Eq for ArithExpr {}
256
257/// Prefix arithmetic operator.
258#[derive(Clone, Copy, Debug, Eq, PartialEq)]
259pub enum ArithUnaryOp {
260    /// Arithmetic negation.
261    Negate,
262    /// Logical negation, yielding `1` or `0`.
263    Not,
264}
265
266/// Infix arithmetic operator.
267#[derive(Clone, Copy, Debug, Eq, PartialEq)]
268pub enum ArithBinaryOp {
269    /// `+`.
270    Add,
271    /// `-`.
272    Subtract,
273    /// `*`.
274    Multiply,
275    /// `/`.
276    Divide,
277    /// `%`.
278    Remainder,
279    /// `<`.
280    Less,
281    /// `<=`.
282    LessOrEqual,
283    /// `>`.
284    Greater,
285    /// `>=`.
286    GreaterOrEqual,
287    /// `==`.
288    Equal,
289    /// `!=`.
290    NotEqual,
291    /// `&&`.
292    And,
293    /// `||`.
294    Or,
295}