Skip to main content

frost_parser/
ast.rs

1use compact_str::CompactString;
2use frost_lexer::Span;
3
4#[derive(Debug, Clone, PartialEq, Default)]
5pub struct Program {
6    pub commands: Vec<CompleteCommand>,
7    /// Syntax errors the parser recovered from, in source order.
8    ///
9    /// ## Why this exists, and why it is not `Result`
10    ///
11    /// `Parser::expect` recovers from a token mismatch by skipping and
12    /// carrying on, which is right for an interactive shell — it lets a
13    /// half-typed line still produce a usable AST. What was wrong is that the
14    /// recovery was SILENT: the `ParseError` type existed and was never once
15    /// constructed, so a syntax error and a correct parse were indistinguishable
16    /// to every caller.
17    ///
18    /// That is not a cosmetic gap. `while true` with no `do` recovered into
19    /// `While { condition: [true], body: [] }` — a syntactically-valid infinite
20    /// loop with an empty body — and the executor ran it forever. zsh 5.9, bash
21    /// and dash all reject that input outright. It was found by
22    /// `tools/hang-sweep.tlisp`, which looks for non-termination as a property
23    /// distinct from wrongness.
24    ///
25    /// So recovery is kept and the evidence is recorded alongside it: a
26    /// non-empty vec means the program is not what the author wrote, and a
27    /// non-interactive caller refuses to run it (POSIX puts a shell script's
28    /// syntax-error exit at 2, which is what `frost_exec::execute` returns).
29    pub syntax_errors: Vec<String>,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct CompleteCommand {
34    pub list: List,
35    pub is_async: bool,
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub struct List {
40    pub first: Pipeline,
41    pub rest: Vec<(ListOp, Pipeline)>,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ListOp {
46    And,
47    Or,
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct Pipeline {
52    pub bang: bool,
53    pub commands: Vec<Command>,
54    pub pipe_stderr: Vec<bool>,
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub enum Command {
59    Simple(SimpleCommand),
60    Subshell(Subshell),
61    BraceGroup(BraceGroup),
62    If(Box<IfClause>),
63    For(Box<ForClause>),
64    While(Box<WhileClause>),
65    Until(Box<UntilClause>),
66    Case(Box<CaseClause>),
67    Select(Box<SelectClause>),
68    FunctionDef(Box<FunctionDef>),
69    Coproc(Box<Coproc>),
70    Time(Box<TimeClause>),
71    /// `(( expr ))` — arithmetic evaluation command.
72    ArithCmd(CompactString),
73    /// `[[ expr ]]` — conditional expression.
74    Cond(Box<CondExpr>),
75    /// `for (( init; cond; step )) { body }` — C-style for loop.
76    CFor(Box<CForClause>),
77    /// `repeat N { body }` — repeat loop.
78    Repeat(Box<RepeatClause>),
79    /// `{ try } always { always }` — try-always block.
80    TryAlways(Box<TryAlwaysClause>),
81}
82
83/// Conditional expression inside `[[ … ]]`.
84#[derive(Debug, Clone, PartialEq)]
85pub enum CondExpr {
86    Unary(CondOp, Word),
87    Binary(Word, CondOp, Word),
88    Not(Box<CondExpr>),
89    And(Box<CondExpr>, Box<CondExpr>),
90    Or(Box<CondExpr>, Box<CondExpr>),
91}
92
93/// Operators for `[[ … ]]` conditions.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum CondOp {
96    // File tests
97    FileExists,        // -e, -a
98    IsFile,            // -f
99    IsDir,             // -d
100    IsSymlink,         // -L, -h
101    IsReadable,        // -r
102    IsWritable,        // -w
103    IsExecutable,      // -x
104    IsNonEmpty,        // -s
105    IsBlockDev,        // -b
106    IsCharDev,         // -c
107    IsFifo,            // -p
108    IsSocket,          // -S
109    IsSetuid,          // -u
110    IsSetgid,          // -g
111    IsSticky,          // -k
112    OwnedByUser,       // -O
113    OwnedByGroup,      // -G
114    ModifiedSinceRead, // -N
115    IsTty,             // -t
116    OptionSet,         // -o
117    VarIsSet,          // -v
118    // String tests
119    StrEmpty,    // -z
120    StrNonEmpty, // -n
121    StrEq,       // == or =
122    StrNeq,      // !=
123    StrLt,       // <
124    StrGt,       // >
125    StrMatch,    // =~
126    // Integer tests
127    IntEq,
128    IntNe,
129    IntLt,
130    IntLe,
131    IntGt,
132    IntGe,
133    // File comparisons
134    NewerThan, // -nt
135    OlderThan, // -ot
136    SameFile,  // -ef
137}
138
139/// C-style for loop: `for (( init; cond; step )) { body }`.
140#[derive(Debug, Clone, PartialEq)]
141pub struct CForClause {
142    pub init: CompactString,
143    pub condition: CompactString,
144    pub step: CompactString,
145    pub body: Vec<CompleteCommand>,
146    pub redirects: Vec<Redirect>,
147}
148
149/// `repeat N { body }` loop.
150#[derive(Debug, Clone, PartialEq)]
151pub struct RepeatClause {
152    pub count: Word,
153    pub body: Vec<CompleteCommand>,
154    pub redirects: Vec<Redirect>,
155}
156
157/// `{ try } always { always }` block.
158#[derive(Debug, Clone, PartialEq)]
159pub struct TryAlwaysClause {
160    pub try_body: Vec<CompleteCommand>,
161    pub always_body: Vec<CompleteCommand>,
162}
163
164#[derive(Debug, Clone, PartialEq)]
165pub struct SimpleCommand {
166    pub assignments: Vec<Assignment>,
167    pub words: Vec<Word>,
168    pub redirects: Vec<Redirect>,
169}
170
171#[derive(Debug, Clone, PartialEq)]
172pub struct Word {
173    pub parts: Vec<WordPart>,
174    pub span: Span,
175}
176
177#[derive(Debug, Clone, PartialEq)]
178pub enum WordPart {
179    Literal(CompactString),
180    SingleQuoted(CompactString),
181    DoubleQuoted(Vec<WordPart>),
182    /// `$'…'` — ANSI-C quoting. Carries the RAW body with escapes still
183    /// encoded; `frost_expand::expand_ansi_c` decodes them.
184    ///
185    /// A distinct variant rather than a pre-decoded `SingleQuoted` because
186    /// the two differ where it matters: this one is the only quoted form
187    /// whose *value* is a function of its text, and keeping the decode on
188    /// the expansion side keeps the parser purely syntactic. It behaves like
189    /// `SingleQuoted` everywhere else — one field, no expansion inside, no
190    /// splitting or globbing after.
191    AnsiCQuoted(CompactString),
192    DollarVar(CompactString),
193    DollarBrace {
194        param: CompactString,
195        operator: Option<CompactString>,
196        arg: Option<Box<Word>>,
197    },
198    /// Structured parameter expansion — rich `${...}` forms.
199    ParamExp(Box<ParamExpansion>),
200    CommandSub(Box<Program>),
201    ArithSub(CompactString),
202    Glob(GlobKind),
203    Tilde(CompactString),
204    /// Brace expansion: `{a,b,c}` or `{1..10}`.
205    BraceExp(BraceExpansion),
206    /// Process substitution: `<(cmd)` or `>(cmd)`.
207    ProcessSub {
208        kind: ProcessSubKind,
209        body: Box<Program>,
210    },
211    /// Extended glob: `*(pat)`, `+(pat)`, `?(pat)`, `@(pat)`, `!(pat)`.
212    ExtGlob {
213        op: ExtGlobOp,
214        pattern: CompactString,
215    },
216}
217
218// ── Structured parameter expansion ────────────────────────────────
219
220/// Rich parameter expansion: `${(flags)#name[subscript]modifier}`.
221///
222/// Follows the 14-field model from mvdan/sh:
223/// flags → length → nested → name → subscript → modifier.
224#[derive(Debug, Clone, PartialEq)]
225pub struct ParamExpansion {
226    /// `${(flags)name}` — processing flags like `(L)`, `(U)`, `(f)`, etc.
227    pub flags: Vec<ParamFlag>,
228    /// `${#name}` — length operator.
229    pub length: bool,
230    /// `${+name}` — is-set test.
231    pub is_set_test: bool,
232    /// The parameter name (or special: `?`, `$`, `#`, `*`, `@`, digits).
233    pub name: CompactString,
234    /// `${${inner}}` — nested expansion (indirect).
235    pub nested: Option<Box<ParamExpansion>>,
236    /// `${name[subscript]}` — array subscript.
237    pub subscript: Option<Subscript>,
238    /// The operation to apply: default, trim, replace, case, substring, etc.
239    pub modifier: Option<ParamModifier>,
240}
241
242/// Array subscript expressions.
243#[derive(Debug, Clone, PartialEq)]
244pub enum Subscript {
245    /// Numeric index (possibly negative): `${arr[3]}`, `${arr[-1]}`.
246    Index(CompactString),
247    /// `${arr[@]}` — all elements as separate words.
248    All,
249    /// `${arr[*]}` — all elements joined.
250    Star,
251    /// Pattern search: `${arr[(r)pat]}` or `${arr[(i)idx]}`.
252    Pattern {
253        reverse: bool,
254        pattern: CompactString,
255    },
256}
257
258/// Modifier operations on parameter values.
259#[derive(Debug, Clone, PartialEq)]
260pub enum ParamModifier {
261    /// `${name:-word}` or `${name-word}` — use default.
262    Default { colon: bool, word: Box<Word> },
263    /// `${name:=word}` or `${name=word}` — assign default.
264    Assign { colon: bool, word: Box<Word> },
265    /// `${name:+word}` or `${name+word}` — use alternative.
266    Alternative { colon: bool, word: Box<Word> },
267    /// `${name:?word}` or `${name?word}` — error if unset.
268    Error { colon: bool, word: Box<Word> },
269    /// `${name#pat}` or `${name##pat}` — remove prefix.
270    TrimPrefix { longest: bool, pattern: Box<Word> },
271    /// `${name%pat}` or `${name%%pat}` — remove suffix.
272    TrimSuffix { longest: bool, pattern: Box<Word> },
273    /// `${name/pat/rep}` or `${name//pat/rep}` — substitution.
274    Substitute {
275        anchor: SubAnchor,
276        pattern: Box<Word>,
277        replacement: Option<Box<Word>>,
278    },
279    /// `${name:offset}` or `${name:offset:length}` — substring.
280    Substring {
281        offset: Box<Word>,
282        length: Option<Box<Word>>,
283    },
284    /// `${name^pat}`, `${name^^pat}`, `${name,pat}`, `${name,,pat}`.
285    Case(CaseOp),
286}
287
288/// Anchor mode for `${name/pat/rep}`.
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub enum SubAnchor {
291    /// `${name/pat/rep}` — first match.
292    First,
293    /// `${name//pat/rep}` — all matches.
294    All,
295    /// `${name/#pat/rep}` — anchored at start.
296    Start,
297    /// `${name/%pat/rep}` — anchored at end.
298    End,
299}
300
301/// Case transformation in `${name^}`, `${name^^}`, etc.
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum CaseOp {
304    /// `${name^}` — uppercase first char.
305    UpperFirst,
306    /// `${name^^}` — uppercase all.
307    UpperAll,
308    /// `${name,}` — lowercase first char.
309    LowerFirst,
310    /// `${name,,}` — lowercase all.
311    LowerAll,
312}
313
314/// Flags in `${(flags)name}`.
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum ParamFlag {
317    Lower,          // (L)
318    Upper,          // (U)
319    Capitalize,     // (C)
320    Split,          // (f) — split on newlines
321    Join,           // (F) — join with newlines
322    SplitSep(char), // (s:sep:)
323    JoinSep(char),  // (j:sep:)
324    SortAsc,        // (o)
325    SortDesc,       // (O)
326    Unique,         // (u)
327    Keys,           // (k)
328    Values,         // (v)
329    TypeFlag,       // (t) — type of variable
330    Prompt,         // (P) — prompt expansion
331    Quote,          // (q)
332    Unquote,        // (Q)
333    Expand,         // (e) — perform expansion
334    Words,          // (z) — split into words
335    Visible,        // (V) — make special chars visible
336}
337
338// ── Brace expansion ────────────────────────────────────────────────
339
340/// Brace expansion: `{a,b,c}` or `{1..10..2}`.
341#[derive(Debug, Clone, PartialEq)]
342pub enum BraceExpansion {
343    /// `{a,b,c}` — list of alternatives.
344    List(Vec<CompactString>),
345    /// `{start..end[..step]}` — range (numeric or char).
346    Range {
347        start: CompactString,
348        end: CompactString,
349        step: Option<CompactString>,
350    },
351}
352
353/// Process substitution direction.
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355pub enum ProcessSubKind {
356    /// `<(cmd)` — read from process.
357    Input,
358    /// `>(cmd)` — write to process.
359    Output,
360}
361
362/// Extended glob operators.
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub enum ExtGlobOp {
365    /// `*(pat)` — zero or more.
366    Star,
367    /// `+(pat)` — one or more.
368    Plus,
369    /// `?(pat)` — zero or one.
370    Question,
371    /// `@(pat)` — exactly one.
372    At,
373    /// `!(pat)` — anything except.
374    Not,
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
378pub enum GlobKind {
379    Star,
380    Question,
381    At,
382}
383
384#[derive(Debug, Clone, PartialEq)]
385pub struct Redirect {
386    pub fd: Option<u32>,
387    pub op: RedirectOp,
388    pub target: Word,
389    pub span: Span,
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393pub enum RedirectOp {
394    Less,
395    Greater,
396    DoubleGreater,
397    GreaterPipe,
398    GreaterBang,
399    AmpGreater,
400    AmpDoubleGreater,
401    DoubleLess,
402    TripleLess,
403    DoubleLessDash,
404    LessGreater,
405    FdDup,
406}
407
408#[derive(Debug, Clone, PartialEq)]
409pub struct Assignment {
410    pub name: CompactString,
411    /// Optional subscript for `name[sub]=value`.
412    pub subscript: Option<CompactString>,
413    pub op: AssignOp,
414    pub value: Option<Word>,
415    /// Array literal: `name=(word1 word2 ...)`.
416    pub array_value: Option<Vec<Word>>,
417    pub span: Span,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum AssignOp {
422    Assign,
423    Append,
424}
425
426#[derive(Debug, Clone, PartialEq)]
427pub struct Subshell {
428    pub body: Vec<CompleteCommand>,
429    pub redirects: Vec<Redirect>,
430}
431
432#[derive(Debug, Clone, PartialEq)]
433pub struct BraceGroup {
434    pub body: Vec<CompleteCommand>,
435    pub redirects: Vec<Redirect>,
436}
437
438#[derive(Debug, Clone, PartialEq)]
439pub struct IfClause {
440    pub condition: Vec<CompleteCommand>,
441    pub then_body: Vec<CompleteCommand>,
442    pub elifs: Vec<(Vec<CompleteCommand>, Vec<CompleteCommand>)>,
443    pub else_body: Option<Vec<CompleteCommand>>,
444    pub redirects: Vec<Redirect>,
445}
446
447#[derive(Debug, Clone, PartialEq)]
448pub struct ForClause {
449    pub var: CompactString,
450    pub words: Option<Vec<Word>>,
451    pub body: Vec<CompleteCommand>,
452    pub redirects: Vec<Redirect>,
453}
454
455#[derive(Debug, Clone, PartialEq)]
456pub struct WhileClause {
457    pub condition: Vec<CompleteCommand>,
458    pub body: Vec<CompleteCommand>,
459    pub redirects: Vec<Redirect>,
460}
461
462#[derive(Debug, Clone, PartialEq)]
463pub struct UntilClause {
464    pub condition: Vec<CompleteCommand>,
465    pub body: Vec<CompleteCommand>,
466    pub redirects: Vec<Redirect>,
467}
468
469#[derive(Debug, Clone, PartialEq)]
470pub struct CaseClause {
471    pub word: Word,
472    pub items: Vec<CaseItem>,
473    pub redirects: Vec<Redirect>,
474}
475
476#[derive(Debug, Clone, PartialEq)]
477pub struct CaseItem {
478    pub patterns: Vec<Word>,
479    pub body: Vec<CompleteCommand>,
480    pub terminator: CaseTerminator,
481}
482
483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484pub enum CaseTerminator {
485    DoubleSemi,
486    SemiAnd,
487    SemiPipe,
488}
489
490#[derive(Debug, Clone, PartialEq)]
491pub struct SelectClause {
492    pub var: CompactString,
493    pub words: Option<Vec<Word>>,
494    pub body: Vec<CompleteCommand>,
495    pub redirects: Vec<Redirect>,
496}
497
498#[derive(Debug, Clone, PartialEq)]
499pub struct FunctionDef {
500    pub name: CompactString,
501    pub body: Command,
502    pub redirects: Vec<Redirect>,
503}
504
505#[derive(Debug, Clone, PartialEq)]
506pub struct Coproc {
507    pub name: Option<CompactString>,
508    pub command: Command,
509}
510
511#[derive(Debug, Clone, PartialEq)]
512pub struct TimeClause {
513    pub pipeline: Pipeline,
514}