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}