brush_parser/
ast.rs

1//! Defines the Abstract Syntax Tree (ast) for shell programs. Includes types and utilities
2//! for manipulating the AST.
3
4use std::fmt::{Display, Write};
5
6use crate::{TokenLocation, tokenizer};
7
8const DISPLAY_INDENT: &str = "    ";
9
10/// Provides the source location for the syntax item
11pub trait SourceLocation {
12    /// The location of the syntax item, when known
13    fn location(&self) -> Option<TokenLocation>;
14}
15
16pub(crate) fn maybe_location(
17    start: Option<&TokenLocation>,
18    end: Option<&TokenLocation>,
19) -> Option<TokenLocation> {
20    if let (Some(s), Some(e)) = (start, end) {
21        Some(TokenLocation::within(s, e))
22    } else {
23        None
24    }
25}
26
27/// Represents a complete shell program.
28#[derive(Clone, Debug)]
29#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
30#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
31pub struct Program {
32    /// A sequence of complete shell commands.
33    #[cfg_attr(test, serde(rename = "cmds"))]
34    pub complete_commands: Vec<CompleteCommand>,
35}
36
37impl SourceLocation for Program {
38    fn location(&self) -> Option<TokenLocation> {
39        let start = self
40            .complete_commands
41            .first()
42            .and_then(SourceLocation::location);
43        let end = self
44            .complete_commands
45            .last()
46            .and_then(SourceLocation::location);
47        maybe_location(start.as_ref(), end.as_ref())
48    }
49}
50
51impl Display for Program {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        for complete_command in &self.complete_commands {
54            write!(f, "{complete_command}")?;
55        }
56        Ok(())
57    }
58}
59
60/// Represents a complete shell command.
61pub type CompleteCommand = CompoundList;
62
63/// Represents a complete shell command item.
64pub type CompleteCommandItem = CompoundListItem;
65
66// TODO: decide if we want to trace this location or consider it a whitespace separator
67/// Indicates whether the preceding command is executed synchronously or asynchronously.
68#[derive(Clone, Debug)]
69#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
70#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
71pub enum SeparatorOperator {
72    /// The preceding command is executed asynchronously.
73    Async,
74    /// The preceding command is executed synchronously.
75    Sequence,
76}
77
78impl Display for SeparatorOperator {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            Self::Async => write!(f, "&"),
82            Self::Sequence => write!(f, ";"),
83        }
84    }
85}
86
87/// Represents a sequence of command pipelines connected by boolean operators.
88#[derive(Clone, Debug)]
89#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
90#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
91#[cfg_attr(test, serde(rename = "AndOr"))]
92pub struct AndOrList {
93    /// The first command pipeline.
94    pub first: Pipeline,
95    /// Any additional command pipelines, in sequence order.
96    #[cfg_attr(test, serde(skip_serializing_if = "Vec::is_empty"))]
97    pub additional: Vec<AndOr>,
98}
99
100impl SourceLocation for AndOrList {
101    fn location(&self) -> Option<TokenLocation> {
102        let start = self.first.location();
103        let last = self.additional.last();
104        let end = last.and_then(SourceLocation::location);
105
106        match (start, end) {
107            (Some(s), Some(e)) => Some(TokenLocation::within(&s, &e)),
108            (start, _) => start,
109        }
110    }
111}
112
113impl Display for AndOrList {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "{}", self.first)?;
116        for item in &self.additional {
117            write!(f, "{item}")?;
118        }
119
120        Ok(())
121    }
122}
123
124/// Represents a boolean operator used to connect command pipelines in an [`AndOrList`]
125#[derive(PartialEq, Eq)]
126pub enum PipelineOperator {
127    /// The command pipelines are connected by a boolean AND operator.
128    And,
129    /// The command pipelines are connected by a boolean OR operator.
130    Or,
131}
132
133impl PartialEq<AndOr> for PipelineOperator {
134    fn eq(&self, other: &AndOr) -> bool {
135        matches!(
136            (self, other),
137            (Self::And, AndOr::And(_)) | (Self::Or, AndOr::Or(_))
138        )
139    }
140}
141
142// We cannot losslessly convert into `AndOr`, hence we can only do `Into`.
143#[expect(clippy::from_over_into)]
144impl Into<PipelineOperator> for AndOr {
145    fn into(self) -> PipelineOperator {
146        match self {
147            Self::And(_) => PipelineOperator::And,
148            Self::Or(_) => PipelineOperator::Or,
149        }
150    }
151}
152
153/// An iterator over the pipelines in an [`AndOrList`].
154pub struct AndOrListIter<'a> {
155    first: Option<&'a Pipeline>,
156    additional_iter: std::slice::Iter<'a, AndOr>,
157}
158
159impl<'a> Iterator for AndOrListIter<'a> {
160    type Item = (PipelineOperator, &'a Pipeline);
161
162    fn next(&mut self) -> Option<Self::Item> {
163        if let Some(first) = self.first.take() {
164            Some((PipelineOperator::And, first))
165        } else {
166            self.additional_iter.next().map(|and_or| match and_or {
167                AndOr::And(pipeline) => (PipelineOperator::And, pipeline),
168                AndOr::Or(pipeline) => (PipelineOperator::Or, pipeline),
169            })
170        }
171    }
172}
173
174impl<'a> IntoIterator for &'a AndOrList {
175    type Item = (PipelineOperator, &'a Pipeline);
176    type IntoIter = AndOrListIter<'a>;
177
178    fn into_iter(self) -> Self::IntoIter {
179        AndOrListIter {
180            first: Some(&self.first),
181            additional_iter: self.additional.iter(),
182        }
183    }
184}
185
186impl<'a> From<(PipelineOperator, &'a Pipeline)> for AndOr {
187    fn from(value: (PipelineOperator, &'a Pipeline)) -> Self {
188        match value.0 {
189            PipelineOperator::Or => Self::Or(value.1.to_owned()),
190            PipelineOperator::And => Self::And(value.1.to_owned()),
191        }
192    }
193}
194
195impl AndOrList {
196    /// Returns an iterator over the pipelines in this `AndOrList`.
197    pub fn iter(&self) -> AndOrListIter<'_> {
198        self.into_iter()
199    }
200}
201
202/// Represents a boolean operator used to connect command pipelines, along with the
203/// succeeding pipeline.
204#[derive(Clone, Debug)]
205#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
206#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
207pub enum AndOr {
208    /// Boolean AND operator; the embedded pipeline is only to be executed if the
209    /// preceding command has succeeded.
210    And(Pipeline),
211    /// Boolean OR operator; the embedded pipeline is only to be executed if the
212    /// preceding command has not succeeded.
213    Or(Pipeline),
214}
215
216// TODO: add a loc
217impl SourceLocation for AndOr {
218    fn location(&self) -> Option<TokenLocation> {
219        match self {
220            Self::And(p) => p.location(),
221            Self::Or(p) => p.location(),
222        }
223    }
224}
225
226impl Display for AndOr {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        match self {
229            Self::And(pipeline) => write!(f, " && {pipeline}"),
230            Self::Or(pipeline) => write!(f, " || {pipeline}"),
231        }
232    }
233}
234
235/// The type of timing requested for a pipeline.
236#[derive(Clone, Debug)]
237#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
238#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
239pub enum PipelineTimed {
240    /// The pipeline should be timed with bash-like output.
241    Timed(TokenLocation),
242    /// The pipeline should be timed with POSIX-like output.
243    TimedWithPosixOutput(TokenLocation),
244}
245
246impl SourceLocation for PipelineTimed {
247    fn location(&self) -> Option<TokenLocation> {
248        match self {
249            Self::Timed(t) => Some(t.to_owned()),
250            Self::TimedWithPosixOutput(t) => Some(t.to_owned()),
251        }
252    }
253}
254
255impl PipelineTimed {
256    /// Returns true if the pipeline should be timed with POSIX-like output.
257    pub const fn is_posix_output(&self) -> bool {
258        matches!(self, Self::TimedWithPosixOutput(_))
259    }
260}
261
262/// A pipeline of commands, where each command's output is passed as standard input
263/// to the command that follows it.
264#[derive(Clone, Debug)]
265#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
266#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
267pub struct Pipeline {
268    /// Indicates whether the pipeline's execution should be timed with reported
269    /// timings in output.
270    #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
271    pub timed: Option<PipelineTimed>,
272    /// Indicates whether the result of the overall pipeline should be the logical
273    /// negation of the result of the pipeline.
274    #[cfg_attr(test, serde(skip_serializing_if = "<&bool as std::ops::Not>::not"))]
275    pub bang: bool,
276    /// The sequence of commands in the pipeline.
277    pub seq: Vec<Command>,
278}
279
280impl SourceLocation for Pipeline {
281    fn location(&self) -> Option<TokenLocation> {
282        let start = self
283            .timed
284            .as_ref()
285            .and_then(SourceLocation::location)
286            .or_else(|| self.seq.first().and_then(SourceLocation::location));
287        let end = self.seq.last().and_then(SourceLocation::location);
288
289        maybe_location(start.as_ref(), end.as_ref())
290    }
291}
292
293impl Display for Pipeline {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        if self.bang {
296            write!(f, "!")?;
297        }
298        for (i, command) in self.seq.iter().enumerate() {
299            if i > 0 {
300                write!(f, " |")?;
301            }
302            write!(f, "{command}")?;
303        }
304
305        Ok(())
306    }
307}
308
309/// Represents a shell command.
310#[derive(Clone, Debug)]
311#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
312#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
313pub enum Command {
314    /// A simple command, directly invoking an external command, a built-in command,
315    /// a shell function, or similar.
316    Simple(SimpleCommand),
317    /// A compound command, composed of multiple commands.
318    Compound(CompoundCommand, Option<RedirectList>),
319    /// A command whose side effect is to define a shell function.
320    Function(FunctionDefinition),
321    /// A command that evaluates an extended test expression.
322    ExtendedTest(ExtendedTestExprCommand),
323}
324
325impl SourceLocation for Command {
326    fn location(&self) -> Option<TokenLocation> {
327        match self {
328            Self::Simple(s) => s.location(),
329            Self::Compound(c, r) => {
330                match (c.location(), r.as_ref().and_then(SourceLocation::location)) {
331                    (Some(s), Some(e)) => Some(TokenLocation::within(&s, &e)),
332                    (s, _) => s,
333                }
334            }
335            Self::Function(f) => f.location(),
336            Self::ExtendedTest(e) => e.location(),
337        }
338    }
339}
340
341impl Display for Command {
342    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343        match self {
344            Self::Simple(simple_command) => write!(f, "{simple_command}"),
345            Self::Compound(compound_command, redirect_list) => {
346                write!(f, "{compound_command}")?;
347                if let Some(redirect_list) = redirect_list {
348                    write!(f, "{redirect_list}")?;
349                }
350                Ok(())
351            }
352            Self::Function(function_definition) => write!(f, "{function_definition}"),
353            Self::ExtendedTest(extended_test_expr) => {
354                write!(f, "[[ {extended_test_expr} ]]")
355            }
356        }
357    }
358}
359
360/// Represents a compound command, potentially made up of multiple nested commands.
361#[derive(Clone, Debug)]
362#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
363#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
364pub enum CompoundCommand {
365    /// An arithmetic command, evaluating an arithmetic expression.
366    Arithmetic(ArithmeticCommand),
367    /// An arithmetic for clause, which loops until an arithmetic condition is reached.
368    ArithmeticForClause(ArithmeticForClauseCommand),
369    /// A brace group, which groups commands together.
370    BraceGroup(BraceGroupCommand),
371    /// A subshell, which executes commands in a subshell.
372    Subshell(SubshellCommand),
373    /// A for clause, which loops over a set of values.
374    ForClause(ForClauseCommand),
375    /// A case clause, which selects a command based on a value and a set of
376    /// pattern-based filters.
377    CaseClause(CaseClauseCommand),
378    /// An if clause, which conditionally executes a command.
379    IfClause(IfClauseCommand),
380    /// A while clause, which loops while a condition is met.
381    WhileClause(WhileOrUntilClauseCommand),
382    /// An until clause, which loops until a condition is met.
383    UntilClause(WhileOrUntilClauseCommand),
384}
385
386// TODO: complete the list
387impl SourceLocation for CompoundCommand {
388    fn location(&self) -> Option<TokenLocation> {
389        match self {
390            Self::Arithmetic(a) => a.location(),
391            Self::ArithmeticForClause(a) => a.location(),
392            Self::BraceGroup(b) => b.location(),
393            Self::Subshell(s) => s.location(),
394            Self::ForClause(f) => f.location(),
395            Self::CaseClause(c) => c.location(),
396            Self::IfClause(i) => i.location(),
397            Self::WhileClause(w) => w.location(),
398            Self::UntilClause(u) => u.location(),
399        }
400    }
401}
402
403impl Display for CompoundCommand {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        match self {
406            Self::Arithmetic(arithmetic_command) => write!(f, "{arithmetic_command}"),
407            Self::ArithmeticForClause(arithmetic_for_clause_command) => {
408                write!(f, "{arithmetic_for_clause_command}")
409            }
410            Self::BraceGroup(brace_group_command) => {
411                write!(f, "{brace_group_command}")
412            }
413            Self::Subshell(subshell_command) => write!(f, "{subshell_command}"),
414            Self::ForClause(for_clause_command) => write!(f, "{for_clause_command}"),
415            Self::CaseClause(case_clause_command) => {
416                write!(f, "{case_clause_command}")
417            }
418            Self::IfClause(if_clause_command) => write!(f, "{if_clause_command}"),
419            Self::WhileClause(while_or_until_clause_command) => {
420                write!(f, "while {while_or_until_clause_command}")
421            }
422            Self::UntilClause(while_or_until_clause_command) => {
423                write!(f, "until {while_or_until_clause_command}")
424            }
425        }
426    }
427}
428
429/// An arithmetic command, evaluating an arithmetic expression.
430#[derive(Clone, Debug)]
431#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
432#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
433pub struct ArithmeticCommand {
434    /// The raw, unparsed and unexpanded arithmetic expression.
435    pub expr: UnexpandedArithmeticExpr,
436    /// Location of the command
437    pub loc: TokenLocation,
438}
439
440impl SourceLocation for ArithmeticCommand {
441    fn location(&self) -> Option<TokenLocation> {
442        Some(self.loc.clone())
443    }
444}
445
446impl Display for ArithmeticCommand {
447    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448        write!(f, "(({}))", self.expr)
449    }
450}
451
452/// A subshell, which executes commands in a subshell.
453#[derive(Clone, Debug)]
454#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
455#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
456pub struct SubshellCommand {
457    /// Command list in the subshell
458    pub list: CompoundList,
459    /// Location of the subshell
460    pub loc: TokenLocation,
461}
462
463impl SourceLocation for SubshellCommand {
464    fn location(&self) -> Option<TokenLocation> {
465        Some(self.loc.clone())
466    }
467}
468
469impl Display for SubshellCommand {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        write!(f, "( ")?;
472        write!(f, "{}", self.list)?;
473        write!(f, " )")
474    }
475}
476
477/// A for clause, which loops over a set of values.
478#[derive(Clone, Debug)]
479#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
480#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
481pub struct ForClauseCommand {
482    /// The name of the iterator variable.
483    pub variable_name: String,
484    /// The values being iterated over.
485    pub values: Option<Vec<Word>>,
486    /// The command to run for each iteration of the loop.
487    pub body: DoGroupCommand,
488    /// Location of the for loop
489    pub loc: TokenLocation,
490}
491
492impl SourceLocation for ForClauseCommand {
493    fn location(&self) -> Option<TokenLocation> {
494        Some(self.loc.clone())
495    }
496}
497
498impl Display for ForClauseCommand {
499    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500        write!(f, "for {} in ", self.variable_name)?;
501
502        if let Some(values) = &self.values {
503            for (i, value) in values.iter().enumerate() {
504                if i > 0 {
505                    write!(f, " ")?;
506                }
507
508                write!(f, "{value}")?;
509            }
510        }
511
512        writeln!(f, ";")?;
513
514        write!(f, "{}", self.body)
515    }
516}
517
518/// An arithmetic for clause, which loops until an arithmetic condition is reached.
519#[derive(Clone, Debug)]
520#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
521#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
522pub struct ArithmeticForClauseCommand {
523    /// Optionally, the initializer expression evaluated before the first iteration of the loop.
524    pub initializer: Option<UnexpandedArithmeticExpr>,
525    /// Optionally, the expression evaluated as the exit condition of the loop.
526    pub condition: Option<UnexpandedArithmeticExpr>,
527    /// Optionally, the expression evaluated after each iteration of the loop.
528    pub updater: Option<UnexpandedArithmeticExpr>,
529    /// The command to run for each iteration of the loop.
530    pub body: DoGroupCommand,
531    /// Location of the clause
532    pub loc: TokenLocation,
533}
534
535impl SourceLocation for ArithmeticForClauseCommand {
536    fn location(&self) -> Option<TokenLocation> {
537        Some(self.loc.clone())
538    }
539}
540
541impl Display for ArithmeticForClauseCommand {
542    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
543        write!(f, "for ((")?;
544
545        if let Some(initializer) = &self.initializer {
546            write!(f, "{initializer}")?;
547        }
548
549        write!(f, "; ")?;
550
551        if let Some(condition) = &self.condition {
552            write!(f, "{condition}")?;
553        }
554
555        write!(f, "; ")?;
556
557        if let Some(updater) = &self.updater {
558            write!(f, "{updater}")?;
559        }
560
561        writeln!(f, "))")?;
562
563        write!(f, "{}", self.body)
564    }
565}
566
567/// A case clause, which selects a command based on a value and a set of
568/// pattern-based filters.
569#[derive(Clone, Debug)]
570#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
571#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
572pub struct CaseClauseCommand {
573    /// The value being matched on.
574    pub value: Word,
575    /// The individual case branches.
576    pub cases: Vec<CaseItem>,
577}
578
579impl SourceLocation for CaseClauseCommand {
580    fn location(&self) -> Option<TokenLocation> {
581        let start = self.value.location();
582        let end = self.cases.last().and_then(SourceLocation::location);
583
584        maybe_location(start.as_ref(), end.as_ref())
585    }
586}
587
588impl Display for CaseClauseCommand {
589    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
590        write!(f, "case {} in", self.value)?;
591        for case in &self.cases {
592            write!(indenter::indented(f).with_str(DISPLAY_INDENT), "{case}")?;
593        }
594        writeln!(f)?;
595        write!(f, "esac")
596    }
597}
598
599/// A sequence of commands.
600#[derive(Clone, Debug)]
601#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
602#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
603#[cfg_attr(test, serde(rename = "List"))]
604pub struct CompoundList(pub Vec<CompoundListItem>);
605
606// TODO: doublecheck
607impl SourceLocation for CompoundList {
608    fn location(&self) -> Option<TokenLocation> {
609        let start = self.0.first().and_then(SourceLocation::location);
610        let end = self.0.last().and_then(SourceLocation::location);
611
612        if let (Some(s), Some(e)) = (start, end) {
613            Some(TokenLocation::within(&s, &e))
614        } else {
615            None
616        }
617    }
618}
619
620impl Display for CompoundList {
621    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
622        for (i, item) in self.0.iter().enumerate() {
623            if i > 0 {
624                writeln!(f)?;
625            }
626
627            // Write the and-or list.
628            write!(f, "{}", item.0)?;
629
630            // Write the separator... unless we're on the list item and it's a ';'.
631            if i == self.0.len() - 1 && matches!(item.1, SeparatorOperator::Sequence) {
632                // Skip
633            } else {
634                write!(f, "{}", item.1)?;
635            }
636        }
637
638        Ok(())
639    }
640}
641
642/// An element of a compound command list.
643#[derive(Clone, Debug)]
644#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
645#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
646#[cfg_attr(test, serde(rename = "Item"))]
647pub struct CompoundListItem(pub AndOrList, pub SeparatorOperator);
648
649impl SourceLocation for CompoundListItem {
650    fn location(&self) -> Option<TokenLocation> {
651        self.0.location()
652    }
653}
654
655impl Display for CompoundListItem {
656    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657        write!(f, "{}", self.0)?;
658        write!(f, "{}", self.1)?;
659        Ok(())
660    }
661}
662
663/// An if clause, which conditionally executes a command.
664#[derive(Clone, Debug)]
665#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
666#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
667pub struct IfClauseCommand {
668    /// The command whose execution result is inspected.
669    pub condition: CompoundList,
670    /// The command to execute if the condition is true.
671    pub then: CompoundList,
672    /// Optionally, `else` clauses that will be evaluated if the condition is false.
673    #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
674    pub elses: Option<Vec<ElseClause>>,
675    /// Location of the if clause
676    pub loc: TokenLocation,
677}
678
679impl SourceLocation for IfClauseCommand {
680    fn location(&self) -> Option<TokenLocation> {
681        Some(self.loc.clone())
682    }
683}
684
685impl Display for IfClauseCommand {
686    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
687        writeln!(f, "if {}; then", self.condition)?;
688        write!(
689            indenter::indented(f).with_str(DISPLAY_INDENT),
690            "{}",
691            self.then
692        )?;
693        if let Some(elses) = &self.elses {
694            for else_clause in elses {
695                write!(f, "{else_clause}")?;
696            }
697        }
698
699        writeln!(f)?;
700        write!(f, "fi")?;
701
702        Ok(())
703    }
704}
705
706/// Represents the `else` clause of a conditional command.
707#[derive(Clone, Debug)]
708#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
709#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
710pub struct ElseClause {
711    /// If present, the condition that must be met for this `else` clause to be executed.
712    #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
713    pub condition: Option<CompoundList>,
714    /// The commands to execute if this `else` clause is selected.
715    pub body: CompoundList,
716}
717
718impl Display for ElseClause {
719    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
720        writeln!(f)?;
721        if let Some(condition) = &self.condition {
722            writeln!(f, "elif {condition}; then")?;
723        } else {
724            writeln!(f, "else")?;
725        }
726
727        write!(
728            indenter::indented(f).with_str(DISPLAY_INDENT),
729            "{}",
730            self.body
731        )
732    }
733}
734
735/// An individual matching case item in a case clause.
736#[derive(Clone, Debug)]
737#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
738#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
739pub struct CaseItem {
740    /// The patterns that select this case branch.
741    pub patterns: Vec<Word>,
742    /// The commands to execute if this case branch is selected.
743    pub cmd: Option<CompoundList>,
744    /// When the case branch is selected, the action to take after the command is executed.
745    pub post_action: CaseItemPostAction,
746    /// Location of the item
747    pub loc: Option<TokenLocation>,
748}
749
750impl SourceLocation for CaseItem {
751    fn location(&self) -> Option<TokenLocation> {
752        self.loc.clone()
753    }
754}
755
756impl Display for CaseItem {
757    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
758        writeln!(f)?;
759        for (i, pattern) in self.patterns.iter().enumerate() {
760            if i > 0 {
761                write!(f, "|")?;
762            }
763            write!(f, "{pattern}")?;
764        }
765        writeln!(f, ")")?;
766
767        if let Some(cmd) = &self.cmd {
768            write!(indenter::indented(f).with_str(DISPLAY_INDENT), "{cmd}")?;
769        }
770        writeln!(f)?;
771        write!(f, "{}", self.post_action)
772    }
773}
774
775/// Describes the action to take after executing the body command of a case clause.
776#[derive(Clone, Debug)]
777#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
778#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
779pub enum CaseItemPostAction {
780    /// The containing case should be exited.
781    ExitCase,
782    /// If one is present, the command body of the succeeding case item should be
783    /// executed (without evaluating its pattern).
784    UnconditionallyExecuteNextCaseItem,
785    /// The case should continue evaluating the remaining case items, as if this
786    /// item had not been executed.
787    ContinueEvaluatingCases,
788}
789
790impl Display for CaseItemPostAction {
791    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
792        match self {
793            Self::ExitCase => write!(f, ";;"),
794            Self::UnconditionallyExecuteNextCaseItem => write!(f, ";&"),
795            Self::ContinueEvaluatingCases => write!(f, ";;&"),
796        }
797    }
798}
799
800/// A while or until clause, whose looping is controlled by a condition.
801#[derive(Clone, Debug)]
802#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
803#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
804pub struct WhileOrUntilClauseCommand(pub CompoundList, pub DoGroupCommand, pub TokenLocation);
805
806impl SourceLocation for WhileOrUntilClauseCommand {
807    fn location(&self) -> Option<TokenLocation> {
808        Some(self.2.clone())
809    }
810}
811
812impl Display for WhileOrUntilClauseCommand {
813    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
814        write!(f, "{}; {}", self.0, self.1)
815    }
816}
817
818/// Encapsulates the definition of a shell function.
819#[derive(Clone, Debug)]
820#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
821#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
822pub struct FunctionDefinition {
823    /// The name of the function.
824    pub fname: Word,
825    /// The body of the function.
826    pub body: FunctionBody,
827    /// The source of the function definition.
828    pub source: String,
829}
830
831impl SourceLocation for FunctionDefinition {
832    fn location(&self) -> Option<TokenLocation> {
833        let start = self.fname.location();
834        let end = self.body.location();
835
836        if let (Some(s), Some(e)) = (start, end) {
837            Some(TokenLocation::within(&s, &e))
838        } else {
839            None
840        }
841    }
842}
843
844impl Display for FunctionDefinition {
845    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846        writeln!(f, "{} () ", self.fname.value)?;
847        write!(f, "{}", self.body)?;
848        Ok(())
849    }
850}
851
852/// Encapsulates the body of a function definition.
853#[derive(Clone, Debug)]
854#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
855#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
856pub struct FunctionBody(pub CompoundCommand, pub Option<RedirectList>);
857
858impl SourceLocation for FunctionBody {
859    fn location(&self) -> Option<TokenLocation> {
860        let start = self.0.location();
861
862        let end = self.1.as_ref().and_then(SourceLocation::location);
863
864        if let (Some(s), Some(e)) = (start, end) {
865            Some(TokenLocation::within(&s, &e))
866        } else {
867            None
868        }
869    }
870}
871
872impl Display for FunctionBody {
873    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
874        write!(f, "{}", self.0)?;
875        if let Some(redirect_list) = &self.1 {
876            write!(f, "{redirect_list}")?;
877        }
878
879        Ok(())
880    }
881}
882
883/// A brace group, which groups commands together.
884#[derive(Clone, Debug)]
885#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
886#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
887pub struct BraceGroupCommand {
888    /// List of commands
889    pub list: CompoundList,
890    /// Location of the group
891    pub loc: TokenLocation,
892}
893
894impl SourceLocation for BraceGroupCommand {
895    fn location(&self) -> Option<TokenLocation> {
896        Some(self.loc.clone())
897    }
898}
899
900impl Display for BraceGroupCommand {
901    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
902        writeln!(f, "{{ ")?;
903        write!(
904            indenter::indented(f).with_str(DISPLAY_INDENT),
905            "{}",
906            self.list
907        )?;
908        writeln!(f)?;
909        write!(f, "}}")?;
910
911        Ok(())
912    }
913}
914
915/// A do group, which groups commands together.
916#[derive(Clone, Debug)]
917#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
918#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
919pub struct DoGroupCommand {
920    /// List of commands
921    pub list: CompoundList,
922    /// Location of the group
923    pub loc: TokenLocation,
924}
925
926impl Display for DoGroupCommand {
927    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
928        writeln!(f, "do")?;
929        write!(
930            indenter::indented(f).with_str(DISPLAY_INDENT),
931            "{}",
932            self.list
933        )?;
934        writeln!(f)?;
935        write!(f, "done")
936    }
937}
938
939/// Represents the invocation of a simple command.
940#[derive(Clone, Debug)]
941#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
942#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
943#[cfg_attr(test, serde(rename = "Simple"))]
944pub struct SimpleCommand {
945    /// Optionally, a prefix to the command.
946    #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
947    pub prefix: Option<CommandPrefix>,
948    /// The name of the command to execute.
949    #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
950    #[cfg_attr(test, serde(rename = "w"))]
951    pub word_or_name: Option<Word>,
952    /// Optionally, a suffix to the command.
953    #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
954    pub suffix: Option<CommandSuffix>,
955}
956
957impl SourceLocation for SimpleCommand {
958    fn location(&self) -> Option<TokenLocation> {
959        let mid = &self
960            .word_or_name
961            .as_ref()
962            .and_then(SourceLocation::location);
963        let start = self.prefix.as_ref().and_then(SourceLocation::location);
964        let end = self.suffix.as_ref().and_then(SourceLocation::location);
965
966        maybe_location(
967            start.as_ref().or(mid.as_ref()),
968            end.as_ref().or(mid.as_ref()),
969        )
970    }
971}
972
973impl Display for SimpleCommand {
974    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
975        let mut wrote_something = false;
976
977        if let Some(prefix) = &self.prefix {
978            if wrote_something {
979                write!(f, " ")?;
980            }
981
982            write!(f, "{prefix}")?;
983            wrote_something = true;
984        }
985
986        if let Some(word_or_name) = &self.word_or_name {
987            if wrote_something {
988                write!(f, " ")?;
989            }
990
991            write!(f, "{word_or_name}")?;
992            wrote_something = true;
993        }
994
995        if let Some(suffix) = &self.suffix {
996            if wrote_something {
997                write!(f, " ")?;
998            }
999
1000            write!(f, "{suffix}")?;
1001        }
1002
1003        Ok(())
1004    }
1005}
1006
1007/// Represents a prefix to a simple command.
1008#[derive(Clone, Debug, Default)]
1009#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1010#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1011#[cfg_attr(test, serde(rename = "Prefix"))]
1012pub struct CommandPrefix(pub Vec<CommandPrefixOrSuffixItem>);
1013
1014impl SourceLocation for CommandPrefix {
1015    fn location(&self) -> Option<TokenLocation> {
1016        let start = self.0.first().and_then(SourceLocation::location);
1017        let end = self.0.last().and_then(SourceLocation::location);
1018
1019        maybe_location(start.as_ref(), end.as_ref())
1020    }
1021}
1022
1023impl Display for CommandPrefix {
1024    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1025        for (i, item) in self.0.iter().enumerate() {
1026            if i > 0 {
1027                write!(f, " ")?;
1028            }
1029
1030            write!(f, "{item}")?;
1031        }
1032        Ok(())
1033    }
1034}
1035
1036/// Represents a suffix to a simple command; a word argument, declaration, or I/O redirection.
1037#[derive(Clone, Default, Debug)]
1038#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1039#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1040#[cfg_attr(test, serde(rename = "Suffix"))]
1041pub struct CommandSuffix(pub Vec<CommandPrefixOrSuffixItem>);
1042
1043impl SourceLocation for CommandSuffix {
1044    fn location(&self) -> Option<TokenLocation> {
1045        let start = self.0.first().and_then(SourceLocation::location);
1046        let end = self.0.last().and_then(SourceLocation::location);
1047
1048        maybe_location(start.as_ref(), end.as_ref())
1049    }
1050}
1051
1052impl Display for CommandSuffix {
1053    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1054        for (i, item) in self.0.iter().enumerate() {
1055            if i > 0 {
1056                write!(f, " ")?;
1057            }
1058
1059            write!(f, "{item}")?;
1060        }
1061        Ok(())
1062    }
1063}
1064
1065/// Represents the I/O direction of a process substitution.
1066#[derive(Clone, Debug)]
1067#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1068#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1069pub enum ProcessSubstitutionKind {
1070    /// The process is read from.
1071    Read,
1072    /// The process is written to.
1073    Write,
1074}
1075
1076impl Display for ProcessSubstitutionKind {
1077    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1078        match self {
1079            Self::Read => write!(f, "<"),
1080            Self::Write => write!(f, ">"),
1081        }
1082    }
1083}
1084
1085/// A prefix or suffix for a simple command.
1086#[derive(Clone, Debug)]
1087#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1088#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1089pub enum CommandPrefixOrSuffixItem {
1090    /// An I/O redirection.
1091    IoRedirect(IoRedirect),
1092    /// A word.
1093    Word(Word),
1094    /// An assignment/declaration word.
1095    #[cfg_attr(test, serde(rename = "Assign"))]
1096    AssignmentWord(Assignment, Word),
1097    /// A process substitution.
1098    ProcessSubstitution(ProcessSubstitutionKind, SubshellCommand),
1099}
1100
1101// TODO: complete
1102impl SourceLocation for CommandPrefixOrSuffixItem {
1103    fn location(&self) -> Option<TokenLocation> {
1104        match self {
1105            Self::Word(w) => w.location(),
1106            _ => None,
1107        }
1108    }
1109}
1110
1111impl Display for CommandPrefixOrSuffixItem {
1112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1113        match self {
1114            Self::IoRedirect(io_redirect) => write!(f, "{io_redirect}"),
1115            Self::Word(word) => write!(f, "{word}"),
1116            Self::AssignmentWord(_assignment, word) => write!(f, "{word}"),
1117            Self::ProcessSubstitution(kind, subshell_command) => {
1118                write!(f, "{kind}({subshell_command})")
1119            }
1120        }
1121    }
1122}
1123
1124/// Encapsulates an assignment declaration.
1125#[derive(Clone, Debug)]
1126#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1127#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1128#[cfg_attr(test, serde(rename = "Assign"))]
1129pub struct Assignment {
1130    /// Name being assigned to.
1131    pub name: AssignmentName,
1132    /// Value being assigned.
1133    pub value: AssignmentValue,
1134    /// Whether or not to append to the preexisting value associated with the named variable.
1135    #[cfg_attr(test, serde(skip_serializing_if = "<&bool as std::ops::Not>::not"))]
1136    pub append: bool,
1137    /// Location of the assignment
1138    pub loc: TokenLocation,
1139}
1140
1141impl SourceLocation for Assignment {
1142    fn location(&self) -> Option<TokenLocation> {
1143        Some(self.loc.clone())
1144    }
1145}
1146
1147impl Display for Assignment {
1148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1149        write!(f, "{}", self.name)?;
1150        if self.append {
1151            write!(f, "+")?;
1152        }
1153        write!(f, "={}", self.value)
1154    }
1155}
1156
1157/// The target of an assignment.
1158#[derive(Clone, Debug)]
1159#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1160#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1161pub enum AssignmentName {
1162    /// A named variable.
1163    #[cfg_attr(test, serde(rename = "Var"))]
1164    VariableName(String),
1165    /// An element in a named array.
1166    ArrayElementName(String, String),
1167}
1168
1169impl Display for AssignmentName {
1170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1171        match self {
1172            Self::VariableName(name) => write!(f, "{name}"),
1173            Self::ArrayElementName(name, index) => {
1174                write!(f, "{name}[{index}]")
1175            }
1176        }
1177    }
1178}
1179
1180/// A value being assigned to a variable.
1181#[derive(Clone, Debug)]
1182#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1183#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1184pub enum AssignmentValue {
1185    /// A scalar (word) value.
1186    Scalar(Word),
1187    /// An array of elements.
1188    Array(Vec<(Option<Word>, Word)>),
1189}
1190
1191// TODO: complete
1192impl SourceLocation for AssignmentValue {
1193    fn location(&self) -> Option<TokenLocation> {
1194        None
1195    }
1196}
1197
1198impl Display for AssignmentValue {
1199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1200        match self {
1201            Self::Scalar(word) => write!(f, "{word}"),
1202            Self::Array(words) => {
1203                write!(f, "(")?;
1204                for (i, value) in words.iter().enumerate() {
1205                    if i > 0 {
1206                        write!(f, " ")?;
1207                    }
1208                    match value {
1209                        (Some(key), value) => write!(f, "[{key}]={value}")?,
1210                        (None, value) => write!(f, "{value}")?,
1211                    }
1212                }
1213                write!(f, ")")
1214            }
1215        }
1216    }
1217}
1218
1219/// A list of I/O redirections to be applied to a command.
1220#[derive(Clone, Debug)]
1221#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1222#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1223pub struct RedirectList(pub Vec<IoRedirect>);
1224
1225// TODO: complete
1226impl SourceLocation for RedirectList {
1227    fn location(&self) -> Option<TokenLocation> {
1228        None
1229    }
1230}
1231
1232impl Display for RedirectList {
1233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1234        for item in &self.0 {
1235            write!(f, "{item}")?;
1236        }
1237        Ok(())
1238    }
1239}
1240
1241/// A file descriptor number.
1242pub type IoFd = i32;
1243
1244/// An I/O redirection.
1245#[derive(Clone, Debug)]
1246#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1247#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1248pub enum IoRedirect {
1249    /// Redirection to a file.
1250    File(Option<IoFd>, IoFileRedirectKind, IoFileRedirectTarget),
1251    /// Redirection from a here-document.
1252    HereDocument(Option<IoFd>, IoHereDocument),
1253    /// Redirection from a here-string.
1254    HereString(Option<IoFd>, Word),
1255    /// Redirection of both standard output and standard error (with optional append).
1256    OutputAndError(Word, bool),
1257}
1258
1259// TODO: complete
1260impl SourceLocation for IoRedirect {
1261    fn location(&self) -> Option<TokenLocation> {
1262        None
1263    }
1264}
1265
1266impl Display for IoRedirect {
1267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1268        match self {
1269            Self::File(fd_num, kind, target) => {
1270                if let Some(fd_num) = fd_num {
1271                    write!(f, "{fd_num}")?;
1272                }
1273
1274                write!(f, "{kind} {target}")?;
1275            }
1276            Self::OutputAndError(target, append) => {
1277                write!(f, "&>")?;
1278                if *append {
1279                    write!(f, ">")?;
1280                }
1281                write!(f, " {target}")?;
1282            }
1283            Self::HereDocument(
1284                fd_num,
1285                IoHereDocument {
1286                    remove_tabs,
1287                    here_end,
1288                    doc,
1289                    ..
1290                },
1291            ) => {
1292                if let Some(fd_num) = fd_num {
1293                    write!(f, "{fd_num}")?;
1294                }
1295
1296                write!(f, "<<")?;
1297                if *remove_tabs {
1298                    write!(f, "-")?;
1299                }
1300
1301                writeln!(f, "{here_end}")?;
1302
1303                write!(f, "{doc}")?;
1304                writeln!(f, "{here_end}")?;
1305            }
1306            Self::HereString(fd_num, s) => {
1307                if let Some(fd_num) = fd_num {
1308                    write!(f, "{fd_num}")?;
1309                }
1310
1311                write!(f, "<<< {s}")?;
1312            }
1313        }
1314
1315        Ok(())
1316    }
1317}
1318
1319/// Kind of file I/O redirection.
1320#[derive(Clone, Debug)]
1321#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1322#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1323pub enum IoFileRedirectKind {
1324    /// Read (`<`).
1325    Read,
1326    /// Write (`>`).
1327    Write,
1328    /// Append (`>>`).
1329    Append,
1330    /// Read and write (`<>`).
1331    ReadAndWrite,
1332    /// Clobber (`>|`).
1333    Clobber,
1334    /// Duplicate input (`<&`).
1335    DuplicateInput,
1336    /// Duplicate output (`>&`).
1337    DuplicateOutput,
1338}
1339
1340impl Display for IoFileRedirectKind {
1341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1342        match self {
1343            Self::Read => write!(f, "<"),
1344            Self::Write => write!(f, ">"),
1345            Self::Append => write!(f, ">>"),
1346            Self::ReadAndWrite => write!(f, "<>"),
1347            Self::Clobber => write!(f, ">|"),
1348            Self::DuplicateInput => write!(f, "<&"),
1349            Self::DuplicateOutput => write!(f, ">&"),
1350        }
1351    }
1352}
1353
1354/// Target for an I/O file redirection.
1355#[derive(Clone, Debug)]
1356#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1357#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1358pub enum IoFileRedirectTarget {
1359    /// Path to a file.
1360    Filename(Word),
1361    /// File descriptor number.
1362    Fd(IoFd),
1363    /// Process substitution: substitution with the results of executing the given
1364    /// command in a subshell.
1365    ProcessSubstitution(ProcessSubstitutionKind, SubshellCommand),
1366    /// Item to duplicate in a word redirection. After expansion, this could be a
1367    /// filename, a file descriptor, or a file descriptor and a "-" to indicate
1368    /// requested closure.
1369    Duplicate(Word),
1370}
1371
1372impl Display for IoFileRedirectTarget {
1373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1374        match self {
1375            Self::Filename(word) => write!(f, "{word}"),
1376            Self::Fd(fd) => write!(f, "{fd}"),
1377            Self::ProcessSubstitution(kind, subshell_command) => {
1378                write!(f, "{kind}{subshell_command}")
1379            }
1380            Self::Duplicate(word) => write!(f, "{word}"),
1381        }
1382    }
1383}
1384
1385/// Represents an I/O here document.
1386#[derive(Clone, Debug)]
1387#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1388#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1389pub struct IoHereDocument {
1390    /// Whether to remove leading tabs from the here document.
1391    #[cfg_attr(test, serde(skip_serializing_if = "<&bool as std::ops::Not>::not"))]
1392    pub remove_tabs: bool,
1393    /// Whether to basic-expand the contents of the here document.
1394    #[cfg_attr(test, serde(skip_serializing_if = "<&bool as std::ops::Not>::not"))]
1395    pub requires_expansion: bool,
1396    /// The delimiter marking the end of the here document.
1397    pub here_end: Word,
1398    /// The contents of the here document.
1399    pub doc: Word,
1400}
1401
1402// TODO: complete
1403impl SourceLocation for IoHereDocument {
1404    fn location(&self) -> Option<TokenLocation> {
1405        None
1406    }
1407}
1408
1409/// A (non-extended) test expression.
1410#[derive(Clone, Debug)]
1411#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1412pub enum TestExpr {
1413    /// Always evaluates to false.
1414    False,
1415    /// A literal string.
1416    Literal(String),
1417    /// Logical AND operation on two nested expressions.
1418    And(Box<Self>, Box<Self>),
1419    /// Logical OR operation on two nested expressions.
1420    Or(Box<Self>, Box<Self>),
1421    /// Logical NOT operation on a nested expression.
1422    Not(Box<Self>),
1423    /// A parenthesized expression.
1424    Parenthesized(Box<Self>),
1425    /// A unary test operation.
1426    UnaryTest(UnaryPredicate, String),
1427    /// A binary test operation.
1428    BinaryTest(BinaryPredicate, String, String),
1429}
1430
1431// TODO: complete
1432impl SourceLocation for TestExpr {
1433    fn location(&self) -> Option<TokenLocation> {
1434        None
1435    }
1436}
1437
1438impl Display for TestExpr {
1439    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1440        match self {
1441            Self::False => Ok(()),
1442            Self::Literal(s) => write!(f, "{s}"),
1443            Self::And(left, right) => write!(f, "{left} -a {right}"),
1444            Self::Or(left, right) => write!(f, "{left} -o {right}"),
1445            Self::Not(expr) => write!(f, "! {expr}"),
1446            Self::Parenthesized(expr) => write!(f, "( {expr} )"),
1447            Self::UnaryTest(pred, word) => write!(f, "{pred} {word}"),
1448            Self::BinaryTest(left, op, right) => write!(f, "{left} {op} {right}"),
1449        }
1450    }
1451}
1452
1453/// An extended test expression.
1454#[derive(Clone, Debug)]
1455#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1456#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1457pub enum ExtendedTestExpr {
1458    /// Logical AND operation on two nested expressions.
1459    And(Box<Self>, Box<Self>),
1460    /// Logical OR operation on two nested expressions.
1461    Or(Box<Self>, Box<Self>),
1462    /// Logical NOT operation on a nested expression.
1463    Not(Box<Self>),
1464    /// A parenthesized expression.
1465    Parenthesized(Box<Self>),
1466    /// A unary test operation.
1467    UnaryTest(UnaryPredicate, Word),
1468    /// A binary test operation.
1469    BinaryTest(BinaryPredicate, Word, Word),
1470}
1471
1472impl Display for ExtendedTestExpr {
1473    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1474        match self {
1475            Self::And(left, right) => {
1476                write!(f, "{left} && {right}")
1477            }
1478            Self::Or(left, right) => {
1479                write!(f, "{left} || {right}")
1480            }
1481            Self::Not(expr) => {
1482                write!(f, "! {expr}")
1483            }
1484            Self::Parenthesized(expr) => {
1485                write!(f, "( {expr} )")
1486            }
1487            Self::UnaryTest(pred, word) => {
1488                write!(f, "{pred} {word}")
1489            }
1490            Self::BinaryTest(pred, left, right) => {
1491                write!(f, "{left} {pred} {right}")
1492            }
1493        }
1494    }
1495}
1496
1497/// An extended test expression command.
1498#[derive(Clone, Debug)]
1499#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1500#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1501pub struct ExtendedTestExprCommand {
1502    /// The extended test expression
1503    pub expr: ExtendedTestExpr,
1504    /// Location of the expression
1505    pub loc: TokenLocation,
1506}
1507
1508impl SourceLocation for ExtendedTestExprCommand {
1509    fn location(&self) -> Option<TokenLocation> {
1510        Some(self.loc.clone())
1511    }
1512}
1513
1514impl Display for ExtendedTestExprCommand {
1515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1516        self.expr.fmt(f)
1517    }
1518}
1519
1520/// A unary predicate usable in an extended test expression.
1521#[derive(Clone, Debug)]
1522#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1523#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1524pub enum UnaryPredicate {
1525    /// Computes if the operand is a path to an existing file.
1526    FileExists,
1527    /// Computes if the operand is a path to an existing block device file.
1528    FileExistsAndIsBlockSpecialFile,
1529    /// Computes if the operand is a path to an existing character device file.
1530    FileExistsAndIsCharSpecialFile,
1531    /// Computes if the operand is a path to an existing directory.
1532    FileExistsAndIsDir,
1533    /// Computes if the operand is a path to an existing regular file.
1534    FileExistsAndIsRegularFile,
1535    /// Computes if the operand is a path to an existing file with the setgid bit set.
1536    FileExistsAndIsSetgid,
1537    /// Computes if the operand is a path to an existing symbolic link.
1538    FileExistsAndIsSymlink,
1539    /// Computes if the operand is a path to an existing file with the sticky bit set.
1540    FileExistsAndHasStickyBit,
1541    /// Computes if the operand is a path to an existing FIFO file.
1542    FileExistsAndIsFifo,
1543    /// Computes if the operand is a path to an existing file that is readable.
1544    FileExistsAndIsReadable,
1545    /// Computes if the operand is a path to an existing file with a non-zero length.
1546    FileExistsAndIsNotZeroLength,
1547    /// Computes if the operand is a file descriptor that is an open terminal.
1548    FdIsOpenTerminal,
1549    /// Computes if the operand is a path to an existing file with the setuid bit set.
1550    FileExistsAndIsSetuid,
1551    /// Computes if the operand is a path to an existing file that is writable.
1552    FileExistsAndIsWritable,
1553    /// Computes if the operand is a path to an existing file that is executable.
1554    FileExistsAndIsExecutable,
1555    /// Computes if the operand is a path to an existing file owned by the current context's
1556    /// effective group ID.
1557    FileExistsAndOwnedByEffectiveGroupId,
1558    /// Computes if the operand is a path to an existing file that has been modified since last
1559    /// being read.
1560    FileExistsAndModifiedSinceLastRead,
1561    /// Computes if the operand is a path to an existing file owned by the current context's
1562    /// effective user ID.
1563    FileExistsAndOwnedByEffectiveUserId,
1564    /// Computes if the operand is a path to an existing socket file.
1565    FileExistsAndIsSocket,
1566    /// Computes if the operand is a 'set -o' option that is enabled.
1567    ShellOptionEnabled,
1568    /// Computes if the operand names a shell variable that is set and assigned a value.
1569    ShellVariableIsSetAndAssigned,
1570    /// Computes if the operand names a shell variable that is set and of nameref type.
1571    ShellVariableIsSetAndNameRef,
1572    /// Computes if the operand is a string with zero length.
1573    StringHasZeroLength,
1574    /// Computes if the operand is a string with non-zero length.
1575    StringHasNonZeroLength,
1576}
1577
1578impl Display for UnaryPredicate {
1579    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1580        match self {
1581            Self::FileExists => write!(f, "-e"),
1582            Self::FileExistsAndIsBlockSpecialFile => write!(f, "-b"),
1583            Self::FileExistsAndIsCharSpecialFile => write!(f, "-c"),
1584            Self::FileExistsAndIsDir => write!(f, "-d"),
1585            Self::FileExistsAndIsRegularFile => write!(f, "-f"),
1586            Self::FileExistsAndIsSetgid => write!(f, "-g"),
1587            Self::FileExistsAndIsSymlink => write!(f, "-h"),
1588            Self::FileExistsAndHasStickyBit => write!(f, "-k"),
1589            Self::FileExistsAndIsFifo => write!(f, "-p"),
1590            Self::FileExistsAndIsReadable => write!(f, "-r"),
1591            Self::FileExistsAndIsNotZeroLength => write!(f, "-s"),
1592            Self::FdIsOpenTerminal => write!(f, "-t"),
1593            Self::FileExistsAndIsSetuid => write!(f, "-u"),
1594            Self::FileExistsAndIsWritable => write!(f, "-w"),
1595            Self::FileExistsAndIsExecutable => write!(f, "-x"),
1596            Self::FileExistsAndOwnedByEffectiveGroupId => write!(f, "-G"),
1597            Self::FileExistsAndModifiedSinceLastRead => write!(f, "-N"),
1598            Self::FileExistsAndOwnedByEffectiveUserId => write!(f, "-O"),
1599            Self::FileExistsAndIsSocket => write!(f, "-S"),
1600            Self::ShellOptionEnabled => write!(f, "-o"),
1601            Self::ShellVariableIsSetAndAssigned => write!(f, "-v"),
1602            Self::ShellVariableIsSetAndNameRef => write!(f, "-R"),
1603            Self::StringHasZeroLength => write!(f, "-z"),
1604            Self::StringHasNonZeroLength => write!(f, "-n"),
1605        }
1606    }
1607}
1608
1609/// A binary predicate usable in an extended test expression.
1610#[derive(Clone, Debug)]
1611#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1612#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1613pub enum BinaryPredicate {
1614    /// Computes if two files refer to the same device and inode numbers.
1615    FilesReferToSameDeviceAndInodeNumbers,
1616    /// Computes if the left file is newer than the right, or exists when the right does not.
1617    LeftFileIsNewerOrExistsWhenRightDoesNot,
1618    /// Computes if the left file is older than the right, or does not exist when the right does.
1619    LeftFileIsOlderOrDoesNotExistWhenRightDoes,
1620    /// Computes if a string exactly matches a pattern.
1621    StringExactlyMatchesPattern,
1622    /// Computes if a string does not exactly match a pattern.
1623    StringDoesNotExactlyMatchPattern,
1624    /// Computes if a string matches a regular expression.
1625    StringMatchesRegex,
1626    /// Computes if a string exactly matches another string.
1627    StringExactlyMatchesString,
1628    /// Computes if a string does not exactly match another string.
1629    StringDoesNotExactlyMatchString,
1630    /// Computes if a string contains a substring.
1631    StringContainsSubstring,
1632    /// Computes if the left value sorts before the right.
1633    LeftSortsBeforeRight,
1634    /// Computes if the left value sorts after the right.
1635    LeftSortsAfterRight,
1636    /// Computes if two values are equal via arithmetic comparison.
1637    ArithmeticEqualTo,
1638    /// Computes if two values are not equal via arithmetic comparison.
1639    ArithmeticNotEqualTo,
1640    /// Computes if the left value is less than the right via arithmetic comparison.
1641    ArithmeticLessThan,
1642    /// Computes if the left value is less than or equal to the right via arithmetic comparison.
1643    ArithmeticLessThanOrEqualTo,
1644    /// Computes if the left value is greater than the right via arithmetic comparison.
1645    ArithmeticGreaterThan,
1646    /// Computes if the left value is greater than or equal to the right via arithmetic comparison.
1647    ArithmeticGreaterThanOrEqualTo,
1648}
1649
1650impl Display for BinaryPredicate {
1651    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1652        match self {
1653            Self::FilesReferToSameDeviceAndInodeNumbers => write!(f, "-ef"),
1654            Self::LeftFileIsNewerOrExistsWhenRightDoesNot => write!(f, "-nt"),
1655            Self::LeftFileIsOlderOrDoesNotExistWhenRightDoes => write!(f, "-ot"),
1656            Self::StringExactlyMatchesPattern => write!(f, "=="),
1657            Self::StringDoesNotExactlyMatchPattern => write!(f, "!="),
1658            Self::StringMatchesRegex => write!(f, "=~"),
1659            Self::StringContainsSubstring => write!(f, "=~"),
1660            Self::StringExactlyMatchesString => write!(f, "=="),
1661            Self::StringDoesNotExactlyMatchString => write!(f, "!="),
1662            Self::LeftSortsBeforeRight => write!(f, "<"),
1663            Self::LeftSortsAfterRight => write!(f, ">"),
1664            Self::ArithmeticEqualTo => write!(f, "-eq"),
1665            Self::ArithmeticNotEqualTo => write!(f, "-ne"),
1666            Self::ArithmeticLessThan => write!(f, "-lt"),
1667            Self::ArithmeticLessThanOrEqualTo => write!(f, "-le"),
1668            Self::ArithmeticGreaterThan => write!(f, "-gt"),
1669            Self::ArithmeticGreaterThanOrEqualTo => write!(f, "-ge"),
1670        }
1671    }
1672}
1673
1674/// Represents a shell word.
1675#[derive(Clone, Debug)]
1676#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1677#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1678#[cfg_attr(test, serde(rename = "W"))]
1679pub struct Word {
1680    /// Raw text of the word.
1681    #[cfg_attr(test, serde(rename = "v"))]
1682    pub value: String,
1683    /// Location of the word
1684    pub loc: Option<TokenLocation>,
1685}
1686
1687impl SourceLocation for Word {
1688    fn location(&self) -> Option<TokenLocation> {
1689        self.loc.clone()
1690    }
1691}
1692
1693impl Display for Word {
1694    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1695        write!(f, "{}", self.value)
1696    }
1697}
1698
1699impl From<&tokenizer::Token> for Word {
1700    fn from(t: &tokenizer::Token) -> Self {
1701        match t {
1702            tokenizer::Token::Word(value, loc) => Self {
1703                value: value.clone(),
1704                loc: Some(loc.clone()),
1705            },
1706            tokenizer::Token::Operator(value, loc) => Self {
1707                value: value.clone(),
1708                loc: Some(loc.clone()),
1709            },
1710        }
1711    }
1712}
1713
1714impl From<String> for Word {
1715    fn from(s: String) -> Self {
1716        Self {
1717            value: s,
1718            loc: None,
1719        }
1720    }
1721}
1722
1723impl Word {
1724    /// Constructs a new `Word` from a given string.
1725    pub fn new(s: &str) -> Self {
1726        Self {
1727            value: s.to_owned(),
1728            loc: None,
1729        }
1730    }
1731
1732    /// Constructs a new `Word` from a given string and location.
1733    pub fn with_location(s: &str, loc: &TokenLocation) -> Self {
1734        Self {
1735            value: s.to_owned(),
1736            loc: Some(loc.to_owned()),
1737        }
1738    }
1739
1740    /// Returns the raw text of the word, consuming the `Word`.
1741    pub fn flatten(&self) -> String {
1742        self.value.clone()
1743    }
1744}
1745
1746/// Encapsulates an unparsed arithmetic expression.
1747#[derive(Clone, Debug)]
1748#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1749#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1750pub struct UnexpandedArithmeticExpr {
1751    /// The raw text of the expression.
1752    pub value: String,
1753}
1754
1755impl Display for UnexpandedArithmeticExpr {
1756    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1757        write!(f, "{}", self.value)
1758    }
1759}
1760
1761/// An arithmetic expression.
1762#[derive(Clone, Debug)]
1763#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1764pub enum ArithmeticExpr {
1765    /// A literal integer value.
1766    Literal(i64),
1767    /// A dereference of a variable or array element.
1768    Reference(ArithmeticTarget),
1769    /// A unary operation on an the result of a given nested expression.
1770    UnaryOp(UnaryOperator, Box<Self>),
1771    /// A binary operation on two nested expressions.
1772    BinaryOp(BinaryOperator, Box<Self>, Box<Self>),
1773    /// A ternary conditional expression.
1774    Conditional(Box<Self>, Box<Self>, Box<Self>),
1775    /// An assignment operation.
1776    Assignment(ArithmeticTarget, Box<Self>),
1777    /// A binary assignment operation.
1778    BinaryAssignment(BinaryOperator, ArithmeticTarget, Box<Self>),
1779    /// A unary assignment operation.
1780    UnaryAssignment(UnaryAssignmentOperator, ArithmeticTarget),
1781}
1782
1783// TODO: complete and add loc for literal
1784impl SourceLocation for ArithmeticExpr {
1785    fn location(&self) -> Option<TokenLocation> {
1786        None
1787    }
1788}
1789
1790#[cfg(feature = "fuzz-testing")]
1791impl<'a> arbitrary::Arbitrary<'a> for ArithmeticExpr {
1792    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
1793        let variant = u.choose(&[
1794            "Literal",
1795            "Reference",
1796            "UnaryOp",
1797            "BinaryOp",
1798            "Conditional",
1799            "Assignment",
1800            "BinaryAssignment",
1801            "UnaryAssignment",
1802        ])?;
1803
1804        match *variant {
1805            "Literal" => Ok(Self::Literal(i64::arbitrary(u)?)),
1806            "Reference" => Ok(Self::Reference(ArithmeticTarget::arbitrary(u)?)),
1807            "UnaryOp" => Ok(Self::UnaryOp(
1808                UnaryOperator::arbitrary(u)?,
1809                Box::new(Self::arbitrary(u)?),
1810            )),
1811            "BinaryOp" => Ok(Self::BinaryOp(
1812                BinaryOperator::arbitrary(u)?,
1813                Box::new(Self::arbitrary(u)?),
1814                Box::new(Self::arbitrary(u)?),
1815            )),
1816            "Conditional" => Ok(Self::Conditional(
1817                Box::new(Self::arbitrary(u)?),
1818                Box::new(Self::arbitrary(u)?),
1819                Box::new(Self::arbitrary(u)?),
1820            )),
1821            "Assignment" => Ok(Self::Assignment(
1822                ArithmeticTarget::arbitrary(u)?,
1823                Box::new(Self::arbitrary(u)?),
1824            )),
1825            "BinaryAssignment" => Ok(Self::BinaryAssignment(
1826                BinaryOperator::arbitrary(u)?,
1827                ArithmeticTarget::arbitrary(u)?,
1828                Box::new(Self::arbitrary(u)?),
1829            )),
1830            "UnaryAssignment" => Ok(Self::UnaryAssignment(
1831                UnaryAssignmentOperator::arbitrary(u)?,
1832                ArithmeticTarget::arbitrary(u)?,
1833            )),
1834            _ => unreachable!(),
1835        }
1836    }
1837}
1838
1839impl Display for ArithmeticExpr {
1840    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1841        match self {
1842            Self::Literal(literal) => write!(f, "{literal}"),
1843            Self::Reference(target) => write!(f, "{target}"),
1844            Self::UnaryOp(op, operand) => write!(f, "{op}{operand}"),
1845            Self::BinaryOp(op, left, right) => {
1846                if matches!(op, BinaryOperator::Comma) {
1847                    write!(f, "{left}{op} {right}")
1848                } else {
1849                    write!(f, "{left} {op} {right}")
1850                }
1851            }
1852            Self::Conditional(condition, if_branch, else_branch) => {
1853                write!(f, "{condition} ? {if_branch} : {else_branch}")
1854            }
1855            Self::Assignment(target, value) => write!(f, "{target} = {value}"),
1856            Self::BinaryAssignment(op, target, operand) => {
1857                write!(f, "{target} {op}= {operand}")
1858            }
1859            Self::UnaryAssignment(op, target) => match op {
1860                UnaryAssignmentOperator::PrefixIncrement
1861                | UnaryAssignmentOperator::PrefixDecrement => write!(f, "{op}{target}"),
1862                UnaryAssignmentOperator::PostfixIncrement
1863                | UnaryAssignmentOperator::PostfixDecrement => write!(f, "{target}{op}"),
1864            },
1865        }
1866    }
1867}
1868
1869/// A binary arithmetic operator.
1870#[derive(Clone, Copy, Debug)]
1871#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1872#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1873pub enum BinaryOperator {
1874    /// Exponentiation (e.g., `x ** y`).
1875    Power,
1876    /// Multiplication (e.g., `x * y`).
1877    Multiply,
1878    /// Division (e.g., `x / y`).
1879    Divide,
1880    /// Modulo (e.g., `x % y`).
1881    Modulo,
1882    /// Comma (e.g., `x, y`).
1883    Comma,
1884    /// Addition (e.g., `x + y`).
1885    Add,
1886    /// Subtraction (e.g., `x - y`).
1887    Subtract,
1888    /// Bitwise left shift (e.g., `x << y`).
1889    ShiftLeft,
1890    /// Bitwise right shift (e.g., `x >> y`).
1891    ShiftRight,
1892    /// Less than (e.g., `x < y`).
1893    LessThan,
1894    /// Less than or equal to (e.g., `x <= y`).
1895    LessThanOrEqualTo,
1896    /// Greater than (e.g., `x > y`).
1897    GreaterThan,
1898    /// Greater than or equal to (e.g., `x >= y`).
1899    GreaterThanOrEqualTo,
1900    /// Equals (e.g., `x == y`).
1901    Equals,
1902    /// Not equals (e.g., `x != y`).
1903    NotEquals,
1904    /// Bitwise AND (e.g., `x & y`).
1905    BitwiseAnd,
1906    /// Bitwise exclusive OR (xor) (e.g., `x ^ y`).
1907    BitwiseXor,
1908    /// Bitwise OR (e.g., `x | y`).
1909    BitwiseOr,
1910    /// Logical AND (e.g., `x && y`).
1911    LogicalAnd,
1912    /// Logical OR (e.g., `x || y`).
1913    LogicalOr,
1914}
1915
1916impl Display for BinaryOperator {
1917    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1918        match self {
1919            Self::Power => write!(f, "**"),
1920            Self::Multiply => write!(f, "*"),
1921            Self::Divide => write!(f, "/"),
1922            Self::Modulo => write!(f, "%"),
1923            Self::Comma => write!(f, ","),
1924            Self::Add => write!(f, "+"),
1925            Self::Subtract => write!(f, "-"),
1926            Self::ShiftLeft => write!(f, "<<"),
1927            Self::ShiftRight => write!(f, ">>"),
1928            Self::LessThan => write!(f, "<"),
1929            Self::LessThanOrEqualTo => write!(f, "<="),
1930            Self::GreaterThan => write!(f, ">"),
1931            Self::GreaterThanOrEqualTo => write!(f, ">="),
1932            Self::Equals => write!(f, "=="),
1933            Self::NotEquals => write!(f, "!="),
1934            Self::BitwiseAnd => write!(f, "&"),
1935            Self::BitwiseXor => write!(f, "^"),
1936            Self::BitwiseOr => write!(f, "|"),
1937            Self::LogicalAnd => write!(f, "&&"),
1938            Self::LogicalOr => write!(f, "||"),
1939        }
1940    }
1941}
1942
1943/// A unary arithmetic operator.
1944#[derive(Clone, Copy, Debug)]
1945#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1946#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1947pub enum UnaryOperator {
1948    /// Unary plus (e.g., `+x`).
1949    UnaryPlus,
1950    /// Unary minus (e.g., `-x`).
1951    UnaryMinus,
1952    /// Bitwise not (e.g., `~x`).
1953    BitwiseNot,
1954    /// Logical not (e.g., `!x`).
1955    LogicalNot,
1956}
1957
1958impl Display for UnaryOperator {
1959    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1960        match self {
1961            Self::UnaryPlus => write!(f, "+"),
1962            Self::UnaryMinus => write!(f, "-"),
1963            Self::BitwiseNot => write!(f, "~"),
1964            Self::LogicalNot => write!(f, "!"),
1965        }
1966    }
1967}
1968
1969/// A unary arithmetic assignment operator.
1970#[derive(Clone, Copy, Debug)]
1971#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1972#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1973pub enum UnaryAssignmentOperator {
1974    /// Prefix increment (e.g., `++x`).
1975    PrefixIncrement,
1976    /// Prefix increment (e.g., `--x`).
1977    PrefixDecrement,
1978    /// Postfix increment (e.g., `x++`).
1979    PostfixIncrement,
1980    /// Postfix decrement (e.g., `x--`).
1981    PostfixDecrement,
1982}
1983
1984impl Display for UnaryAssignmentOperator {
1985    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1986        match self {
1987            Self::PrefixIncrement => write!(f, "++"),
1988            Self::PrefixDecrement => write!(f, "--"),
1989            Self::PostfixIncrement => write!(f, "++"),
1990            Self::PostfixDecrement => write!(f, "--"),
1991        }
1992    }
1993}
1994
1995/// Identifies the target of an arithmetic assignment expression.
1996#[derive(Clone, Debug)]
1997#[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))]
1998#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
1999pub enum ArithmeticTarget {
2000    /// A named variable.
2001    Variable(String),
2002    /// An element in an array.
2003    ArrayElement(String, Box<ArithmeticExpr>),
2004}
2005
2006// TODO: complete and add loc
2007impl SourceLocation for ArithmeticTarget {
2008    fn location(&self) -> Option<TokenLocation> {
2009        None
2010    }
2011}
2012
2013impl Display for ArithmeticTarget {
2014    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2015        match self {
2016            Self::Variable(name) => write!(f, "{name}"),
2017            Self::ArrayElement(name, index) => write!(f, "{name}[{index}]"),
2018        }
2019    }
2020}