Skip to main content

oxdock_parser/
parser.rs

1use crate::ast::{Arg, Guard, GuardExpr, IoBinding, IoStream, PlatformGuard, Step, StepKind};
2use crate::commands::parse_duration;
3use crate::lexer::{self, RawToken, Rule};
4use anyhow::{Result, anyhow, bail};
5use pest::iterators::Pair;
6use std::collections::VecDeque;
7
8#[derive(Clone)]
9struct ScopeFrame {
10    line_no: usize,
11    had_command: bool,
12}
13
14#[derive(Clone)]
15struct PendingIoBlock {
16    line_no: usize,
17    bindings: Vec<IoBinding>,
18    guards: Option<GuardExpr>,
19}
20
21#[derive(Clone)]
22struct IoScopeFrame {
23    line_no: usize,
24    had_command: bool,
25    bindings: Vec<IoBinding>,
26    guards: Option<GuardExpr>,
27    /// Step index where this block's first command will land. Used to mark
28    /// scope boundaries so WITH_IO block bodies scope LET/ENV/WORKDIR like
29    /// every other braced block (only pipes leak).
30    first_step: usize,
31}
32
33#[derive(Clone, Copy, Debug)]
34enum BlockKind {
35    Guard,
36    Io,
37}
38
39#[derive(Default)]
40struct IoBindingSet {
41    stdin: Option<IoBinding>,
42    stdout: Option<IoBinding>,
43    stderr: Option<IoBinding>,
44}
45
46impl IoBindingSet {
47    fn insert(&mut self, binding: IoBinding) {
48        match binding.stream {
49            IoStream::Stdin => self.stdin = Some(binding),
50            IoStream::Stdout => self.stdout = Some(binding),
51            IoStream::Stderr => self.stderr = Some(binding),
52        }
53    }
54
55    fn into_vec(self) -> Vec<IoBinding> {
56        let mut out = Vec::new();
57        if let Some(binding) = self.stdin {
58            out.push(binding);
59        }
60        if let Some(binding) = self.stdout {
61            out.push(binding);
62        }
63        if let Some(binding) = self.stderr {
64            out.push(binding);
65        }
66        out
67    }
68}
69
70pub struct ScriptParser<'a, F: Fn(&str, Vec<Arg>) -> Result<StepKind>> {
71    tokens: VecDeque<RawToken<'a>>,
72    steps: Vec<Step>,
73    guard_stack: Vec<Option<GuardExpr>>,
74    pending_guards: Option<GuardExpr>,
75    pending_inline_guards: Option<GuardExpr>,
76    pending_can_open_block: bool,
77    pending_scope_enters: usize,
78    scope_stack: Vec<ScopeFrame>,
79    pending_io_block: Option<PendingIoBlock>,
80    io_scope_stack: Vec<IoScopeFrame>,
81    block_stack: Vec<BlockKind>,
82    lower: F,
83}
84
85impl<'a, F: Fn(&str, Vec<Arg>) -> Result<StepKind>> ScriptParser<'a, F> {
86    pub fn new(input: &'a str, lower: F) -> Result<Self> {
87        let tokens = VecDeque::from(lexer::tokenize(input)?);
88        Ok(Self {
89            tokens,
90            steps: Vec::new(),
91            guard_stack: vec![None],
92            pending_guards: None,
93            pending_inline_guards: None,
94            pending_can_open_block: false,
95            pending_scope_enters: 0,
96            scope_stack: Vec::new(),
97            pending_io_block: None,
98            io_scope_stack: Vec::new(),
99            block_stack: Vec::new(),
100            lower,
101        })
102    }
103
104    pub fn parse(mut self) -> Result<Vec<Step>> {
105        while let Some(token) = self.tokens.pop_front() {
106            if self.pending_io_block.is_some()
107                && !matches!(
108                    token,
109                    RawToken::BlockStart { .. }
110                        | RawToken::Command { .. }
111                        | RawToken::Instruction { .. }
112                )
113            {
114                let pending = self.pending_io_block.take().unwrap();
115                bail!(
116                    "line {}: WITH_IO block must be followed by '{{'",
117                    pending.line_no
118                );
119            }
120            match token {
121                RawToken::Guard { pair, line_end } => {
122                    let groups = parse_guard_line(pair)?;
123                    self.handle_guard_token(line_end, groups)?
124                }
125                RawToken::BlockStart { line_no } => self.start_block(line_no)?,
126                RawToken::BlockEnd { line_no } => self.end_block(line_no)?,
127                RawToken::Command { pair, line_no } => {
128                    let kind = parse_structural_command_with_lower(pair, &self.lower)?;
129                    self.handle_command_token(line_no, kind)?
130                }
131                RawToken::Instruction { pair, line_no } => {
132                    let kind = self.lower_instruction(pair)?;
133                    self.handle_command_token(line_no, kind)?
134                }
135            }
136        }
137
138        if let Some(pending) = self.pending_io_block.take() {
139            bail!(
140                "line {}: WITH_IO block must be followed by '{{'",
141                pending.line_no
142            );
143        }
144
145        if self.guard_stack.len() != 1 {
146            bail!("unclosed guard block at end of script");
147        }
148        if self.pending_guards.is_some() {
149            bail!("guard declared on final lines without a following command");
150        }
151
152        if let Some(frame) = self.io_scope_stack.last() {
153            bail!(
154                "WITH_IO block starting on line {} was not closed",
155                frame.line_no
156            );
157        }
158
159        // Validate `INHERIT_ENV` directives: only allowed in the prelude (before
160        // any other commands) and at most one occurrence.
161        {
162            let mut seen_non_prelude = false;
163            let mut inherit_count = 0usize;
164            for step in &self.steps {
165                match &step.kind {
166                    StepKind::InheritEnv { .. } => {
167                        if seen_non_prelude {
168                            bail!("INHERIT_ENV must appear before any other commands");
169                        }
170                        if step.guard.is_some() || step.scope_enter > 0 || step.scope_exit > 0 {
171                            bail!("INHERIT_ENV cannot be guarded or nested inside blocks");
172                        }
173                        inherit_count += 1;
174                    }
175                    kind => {
176                        if contains_inherit_env(kind) {
177                            bail!("INHERIT_ENV cannot be nested inside other commands");
178                        }
179                        seen_non_prelude = true;
180                    }
181                }
182            }
183            if inherit_count > 1 {
184                bail!("only one INHERIT_ENV directive is allowed");
185            }
186        }
187
188        Ok(self.steps)
189    }
190
191    fn lower_instruction(&self, pair: Pair<Rule>) -> Result<StepKind> {
192        let (name, args) = extract_instruction(pair)?;
193        (self.lower)(&name, args)
194    }
195
196    fn handle_guard_token(&mut self, line_end: usize, expr: GuardExpr) -> Result<()> {
197        if let Some(RawToken::Command { line_no, .. }) = self.tokens.front()
198            && *line_no == line_end
199        {
200            self.pending_inline_guards = Some(expr);
201            self.pending_can_open_block = false;
202            return Ok(());
203        }
204        self.stash_pending_guard(expr);
205        self.pending_can_open_block = true;
206        Ok(())
207    }
208
209    fn handle_command_token(&mut self, line_no: usize, kind: StepKind) -> Result<()> {
210        let inline = self.pending_inline_guards.take();
211        self.handle_command(line_no, kind, inline)
212    }
213
214    fn stash_pending_guard(&mut self, guard: GuardExpr) {
215        self.pending_guards = Some(if let Some(existing) = self.pending_guards.take() {
216            GuardExpr::all(vec![existing, guard])
217        } else {
218            guard
219        });
220    }
221
222    fn start_guard_block_from_pending(&mut self, line_no: usize) -> Result<()> {
223        let guards = self
224            .pending_guards
225            .take()
226            .ok_or_else(|| anyhow!("line {}: '{{' without a pending guard", line_no))?;
227        if !self.pending_can_open_block {
228            bail!("line {}: '{{' must directly follow a guard", line_no);
229        }
230        self.pending_can_open_block = false;
231        self.enter_guard_block(guards, line_no)
232    }
233
234    fn enter_guard_block(&mut self, guard: GuardExpr, line_no: usize) -> Result<()> {
235        let composed = if let Some(pending) = self.pending_guards.take() {
236            GuardExpr::all(vec![pending, guard])
237        } else {
238            guard
239        };
240        let parent = self.guard_stack.last().cloned().unwrap_or(None);
241        let next = and_guard_exprs(parent, Some(composed));
242        self.guard_stack.push(next);
243        self.scope_stack.push(ScopeFrame {
244            line_no,
245            had_command: false,
246        });
247        self.pending_scope_enters += 1;
248        Ok(())
249    }
250
251    fn begin_io_block(
252        &mut self,
253        line_no: usize,
254        bindings: Vec<IoBinding>,
255        guards: Option<GuardExpr>,
256    ) -> Result<()> {
257        if self.pending_io_block.is_some() {
258            bail!(
259                "line {}: previous WITH_IO block is still waiting for '{{'",
260                line_no
261            );
262        }
263        self.pending_io_block = Some(PendingIoBlock {
264            line_no,
265            bindings,
266            guards,
267        });
268        Ok(())
269    }
270
271    fn start_block(&mut self, line_no: usize) -> Result<()> {
272        if let Some(pending) = self.pending_io_block.take() {
273            self.block_stack.push(BlockKind::Io);
274            self.io_scope_stack.push(IoScopeFrame {
275                line_no: pending.line_no,
276                had_command: false,
277                bindings: pending.bindings,
278                guards: pending.guards,
279                first_step: self.steps.len(),
280            });
281            Ok(())
282        } else {
283            self.start_guard_block_from_pending(line_no)?;
284            self.block_stack.push(BlockKind::Guard);
285            Ok(())
286        }
287    }
288
289    fn end_block(&mut self, line_no: usize) -> Result<()> {
290        let kind = self
291            .block_stack
292            .pop()
293            .ok_or_else(|| anyhow!("line {}: unexpected '}}'", line_no))?;
294        match kind {
295            BlockKind::Guard => self.end_guard_block(line_no),
296            BlockKind::Io => self.end_io_block(line_no),
297        }
298    }
299
300    fn end_guard_block(&mut self, line_no: usize) -> Result<()> {
301        if self.guard_stack.len() == 1 {
302            bail!("line {}: unexpected '}}'", line_no);
303        }
304        if self.pending_guards.is_some() {
305            bail!(
306                "line {}: guard declared immediately before '}}' without a command",
307                line_no
308            );
309        }
310        let frame = self
311            .scope_stack
312            .last()
313            .cloned()
314            .ok_or_else(|| anyhow!("line {}: scope stack underflow", line_no))?;
315        if !frame.had_command {
316            bail!(
317                "line {}: guard block starting on line {} must contain at least one command",
318                line_no,
319                frame.line_no
320            );
321        }
322        let step = self
323            .steps
324            .last_mut()
325            .ok_or_else(|| anyhow!("line {}: guard block closed without any commands", line_no))?;
326        step.scope_exit += 1;
327        self.scope_stack.pop();
328        self.guard_stack.pop();
329        Ok(())
330    }
331
332    fn end_io_block(&mut self, line_no: usize) -> Result<()> {
333        let frame = self
334            .io_scope_stack
335            .pop()
336            .ok_or_else(|| anyhow!("line {}: unexpected '}}'", line_no))?;
337        if !frame.had_command {
338            bail!(
339                "line {}: WITH_IO block starting on line {} must contain at least one command",
340                line_no,
341                frame.line_no
342            );
343        }
344        // WITH_IO block bodies are lexical scopes like guard blocks: mark
345        // scope boundaries so LET/ENV/WORKDIR/WORKSPACE revert on exit.
346        // Pipe registrations live in ExecIo and are unaffected (they leak).
347        if self.steps.len() > frame.first_step {
348            self.steps[frame.first_step].scope_enter += 1;
349            if let Some(last) = self.steps.last_mut() {
350                last.scope_exit += 1;
351            }
352        }
353        Ok(())
354    }
355
356    fn guard_context(&mut self, inline: Option<GuardExpr>) -> Option<GuardExpr> {
357        let mut context = self.guard_stack.last().cloned().unwrap_or(None);
358        if let Some(pending) = self.pending_guards.take() {
359            context = and_guard_exprs(context, Some(pending));
360            self.pending_can_open_block = false;
361        }
362        if let Some(inline_guard) = inline {
363            context = and_guard_exprs(context, Some(inline_guard));
364        }
365        context
366    }
367
368    fn handle_command(
369        &mut self,
370        line_no: usize,
371        kind: StepKind,
372        inline_guards: Option<GuardExpr>,
373    ) -> Result<()> {
374        if let StepKind::WithIoBlock { bindings } = kind {
375            let guards = self.guard_context(inline_guards);
376            self.begin_io_block(line_no, bindings, guards)?;
377            return Ok(());
378        }
379
380        let guards = self.guard_context(inline_guards);
381        let guards = self.apply_io_guards(guards);
382        let scope_enter = self.pending_scope_enters;
383        self.pending_scope_enters = 0;
384        for frame in self.scope_stack.iter_mut() {
385            frame.had_command = true;
386        }
387        for frame in self.io_scope_stack.iter_mut() {
388            frame.had_command = true;
389        }
390        let kind = self.apply_io_defaults(kind);
391        self.steps.push(Step {
392            guard: guards,
393            kind,
394            scope_enter,
395            scope_exit: 0,
396        });
397        Ok(())
398    }
399
400    fn apply_io_defaults(&self, kind: StepKind) -> StepKind {
401        let defaults = self.current_io_defaults();
402        if defaults.is_empty() {
403            return kind;
404        }
405        match kind {
406            StepKind::WithIo { bindings, cmd } => StepKind::WithIo {
407                bindings: merge_bindings(&defaults, &bindings),
408                cmd,
409            },
410            other => StepKind::WithIo {
411                bindings: defaults,
412                cmd: Box::new(other),
413            },
414        }
415    }
416
417    fn current_io_defaults(&self) -> Vec<IoBinding> {
418        if self.io_scope_stack.is_empty() {
419            return Vec::new();
420        }
421        let mut set = IoBindingSet::default();
422        for frame in &self.io_scope_stack {
423            for binding in &frame.bindings {
424                set.insert(binding.clone());
425            }
426        }
427        set.into_vec()
428    }
429
430    fn apply_io_guards(&self, guard: Option<GuardExpr>) -> Option<GuardExpr> {
431        self.io_scope_stack.iter().fold(guard, |acc, frame| {
432            and_guard_exprs(acc, frame.guards.clone())
433        })
434    }
435}
436
437pub fn parse_script(
438    input: &str,
439    lower: impl Fn(&str, Vec<Arg>) -> Result<StepKind>,
440) -> Result<Vec<Step>> {
441    ScriptParser::new(input, lower)?.parse()
442}
443
444pub fn parse_guard_expr_str(input: &str) -> Result<GuardExpr> {
445    use pest::Parser;
446    let pairs = lexer::LanguageParser::parse(Rule::guard_expr, input)
447        .map_err(|e| anyhow!("guard parse error: {e}"))?;
448    let pair = pairs
449        .into_iter()
450        .next()
451        .ok_or_else(|| anyhow!("empty guard"))?;
452    parse_guard_expr(pair)
453}
454
455fn and_guard_exprs(left: Option<GuardExpr>, right: Option<GuardExpr>) -> Option<GuardExpr> {
456    match (left, right) {
457        (None, None) => None,
458        (Some(expr), None) | (None, Some(expr)) => Some(expr),
459        (Some(lhs), Some(rhs)) => Some(GuardExpr::all(vec![lhs, rhs])),
460    }
461}
462
463fn merge_bindings(defaults: &[IoBinding], overrides: &[IoBinding]) -> Vec<IoBinding> {
464    let mut set = IoBindingSet::default();
465    for binding in defaults {
466        set.insert(binding.clone());
467    }
468    for binding in overrides {
469        set.insert(binding.clone());
470    }
471    set.into_vec()
472}
473
474fn contains_inherit_env(kind: &StepKind) -> bool {
475    match kind {
476        StepKind::InheritEnv { .. } => true,
477        StepKind::WithIo { cmd, .. } => contains_inherit_env(cmd),
478        _ => false,
479    }
480}
481
482fn parse_structural_command_with_lower(
483    pair: Pair<Rule>,
484    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
485) -> Result<StepKind> {
486    let kind = match pair.as_rule() {
487        Rule::inherit_env_command => {
488            let mut keys = Vec::new();
489            for inner in pair.into_inner() {
490                if inner.as_rule() == Rule::inherit_list {
491                    for key in inner.into_inner() {
492                        if key.as_rule() == Rule::env_key {
493                            keys.push(key.as_str().trim().to_string());
494                        }
495                    }
496                } else if inner.as_rule() == Rule::env_key {
497                    keys.push(inner.as_str().trim().to_string());
498                }
499            }
500            StepKind::InheritEnv { keys }
501        }
502        Rule::with_io_command => {
503            let mut bindings = Vec::new();
504            let mut cmd = None;
505            for inner in pair.into_inner() {
506                match inner.as_rule() {
507                    Rule::io_flags => {
508                        for flag in inner.into_inner() {
509                            if flag.as_rule() == Rule::io_binding {
510                                bindings.push(parse_io_binding(flag)?);
511                            }
512                        }
513                    }
514                    Rule::with_io_command => {
515                        cmd = Some(Box::new(parse_structural_command_with_lower(inner, lower)?));
516                    }
517                    Rule::inherit_env_command => {
518                        cmd = Some(Box::new(parse_structural_command_with_lower(inner, lower)?));
519                    }
520                    Rule::async_statement | Rule::async_statement_block => {
521                        cmd = Some(Box::new(parse_structural_command_with_lower(inner, lower)?));
522                    }
523                    Rule::timeout_statement | Rule::cancel_statement => {
524                        cmd = Some(Box::new(parse_structural_command_with_lower(inner, lower)?));
525                    }
526                    Rule::instruction | Rule::instruction_inner => {
527                        let (name, args) = extract_instruction(inner)?;
528                        cmd = Some(Box::new(lower(&name, args)?));
529                    }
530                    _ => {}
531                }
532            }
533            if let Some(cmd) = cmd {
534                StepKind::WithIo { bindings, cmd }
535            } else {
536                StepKind::WithIoBlock { bindings }
537            }
538        }
539        Rule::for_statement => parse_for_statement_from_pair(pair, lower)?,
540        Rule::let_statement => parse_let_statement_from_pair(pair)?,
541        Rule::let_async_statement => parse_let_async_statement_from_pair(pair, lower)?,
542        Rule::await_statement => parse_await_statement_from_pair(pair)?,
543        Rule::cancel_statement => parse_cancel_statement_from_pair(pair)?,
544        Rule::if_statement => parse_if_statement_from_pair(pair, lower)?,
545        Rule::async_statement => parse_async_statement_from_pair(pair, lower)?,
546        Rule::async_statement_block => parse_async_statement_block_from_pair(pair, lower)?,
547        Rule::timeout_statement => parse_timeout_statement_from_pair(pair, lower)?,
548        Rule::command_inner => {
549            // command_inner = { inherit_env_command | instruction }
550            // Unwrap to the inner rule
551            let inner = pair
552                .into_inner()
553                .next()
554                .ok_or_else(|| anyhow!("empty command_inner"))?;
555            parse_structural_command_with_lower(inner, lower)?
556        }
557        Rule::instruction | Rule::instruction_inner => {
558            let (name, args) = extract_instruction(pair)?;
559            lower(&name, args)?
560        }
561        _ => bail!("unexpected structural command rule: {:?}", pair.as_rule()),
562    };
563    Ok(kind)
564}
565
566fn extract_instruction(pair: Pair<Rule>) -> Result<(String, Vec<Arg>)> {
567    let mut name = None;
568    let mut args = Vec::new();
569    for inner in pair.into_inner() {
570        match inner.as_rule() {
571            Rule::command_name => {
572                name = Some(inner.as_str().to_string());
573            }
574            Rule::argument => {
575                args.push(parse_argument(inner)?);
576            }
577            _ => {}
578        }
579    }
580    let name = name.ok_or_else(|| anyhow!("instruction missing command name"))?;
581    Ok((name, args))
582}
583
584fn parse_for_statement_from_pair(
585    pair: Pair<Rule>,
586    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
587) -> Result<StepKind> {
588    let mut idents = Vec::new();
589    let mut in_expr = None;
590    let mut body_steps = Vec::new();
591    for inner in pair.into_inner() {
592        match inner.as_rule() {
593            Rule::dollar_ident => {
594                idents.push(parse_dollar_ident(inner));
595            }
596            Rule::expr => {
597                in_expr = Some(parse_expr(inner)?);
598            }
599            Rule::block => {
600                body_steps = parse_block_elements_with_lower(inner, lower)?;
601            }
602            _ => {}
603        }
604    }
605    let (key_var, var) = match idents.len() {
606        1 => (None, idents.into_iter().next().unwrap()),
607        2 => {
608            let mut iter = idents.into_iter();
609            (Some(iter.next().unwrap()), iter.next().unwrap())
610        }
611        _ => bail!("FOR requires at least one variable"),
612    };
613    Ok(StepKind::For {
614        key_var,
615        var,
616        in_expr: in_expr.ok_or_else(|| anyhow!("FOR requires an iterable expression"))?,
617        body: body_steps,
618    })
619}
620
621fn parse_let_statement_from_pair(pair: Pair<Rule>) -> Result<StepKind> {
622    let mut var = None;
623    let mut expr = None;
624    for inner in pair.into_inner() {
625        match inner.as_rule() {
626            Rule::dollar_ident => {
627                var = Some(parse_dollar_ident(inner));
628            }
629            Rule::expr => {
630                expr = Some(parse_expr(inner)?);
631            }
632            _ => {}
633        }
634    }
635    Ok(StepKind::Assign {
636        var: var.ok_or_else(|| anyhow!("LET requires a variable"))?,
637        expr: expr.ok_or_else(|| anyhow!("LET requires an expression"))?,
638    })
639}
640
641fn parse_let_async_statement_from_pair(
642    pair: Pair<Rule>,
643    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
644) -> Result<StepKind> {
645    let mut var = None;
646    let mut body = None;
647    for inner in pair.into_inner() {
648        match inner.as_rule() {
649            Rule::dollar_ident => {
650                var = Some(parse_dollar_ident(inner));
651            }
652            Rule::block => {
653                body = Some(parse_block_elements_with_lower(inner, lower)?);
654            }
655            Rule::command_inner => {
656                // command_inner = { inherit_env_command | async_statement | async_statement_block | instruction }
657                // Unwrap to the inner rule
658                let inner = inner
659                    .into_inner()
660                    .next()
661                    .ok_or_else(|| anyhow!("empty command_inner"))?;
662                let step_kind = parse_structural_command_with_lower(inner, lower)?;
663                body = Some(vec![Step {
664                    guard: None,
665                    kind: step_kind,
666                    scope_enter: 0,
667                    scope_exit: 0,
668                }]);
669            }
670            Rule::with_io_command => {
671                // LET $var = WITH_IO [flags] ASYNC <single command> binds a
672                // pipe-wired background task. The bindings apply inside the
673                // task thread — the same shape as a braced body holding one
674                // WITH_IO step, which the AssignAsync runtime path supports.
675                let kind = parse_structural_command_with_lower(inner, lower)?;
676                let StepKind::WithIo { bindings, cmd } = kind else {
677                    bail!(
678                        "LET $var = WITH_IO requires an ASYNC command (e.g. LET $t = WITH_IO [stdin=pipe:p] ASYNC WRITE \"f\")"
679                    );
680                };
681                let StepKind::AsyncBlock { body: async_body } = *cmd else {
682                    bail!(
683                        "LET $var = WITH_IO requires an ASYNC command (e.g. LET $t = WITH_IO [stdin=pipe:p] ASYNC WRITE \"f\")"
684                    );
685                };
686                if async_body.len() != 1 {
687                    bail!(
688                        "LET $var = WITH_IO [..] ASYNC accepts a single command; use LET $var = ASYNC {{ ... }} with WITH_IO inside the block for multi-step tasks"
689                    );
690                }
691                let step = async_body
692                    .into_iter()
693                    .next()
694                    .ok_or_else(|| anyhow!("LET $var = ASYNC requires a body"))?;
695                body = Some(vec![Step {
696                    guard: step.guard,
697                    kind: StepKind::WithIo {
698                        bindings,
699                        cmd: Box::new(step.kind),
700                    },
701                    scope_enter: step.scope_enter,
702                    scope_exit: step.scope_exit,
703                }]);
704            }
705            _ => {}
706        }
707    }
708    Ok(StepKind::AssignAsync {
709        var: var.ok_or_else(|| anyhow!("LET $var = ASYNC requires a variable"))?,
710        body: body.ok_or_else(|| anyhow!("LET $var = ASYNC requires a body"))?,
711    })
712}
713
714fn parse_await_statement_from_pair(pair: Pair<Rule>) -> Result<StepKind> {
715    let mut var = None;
716    for inner in pair.into_inner() {
717        if inner.as_rule() == Rule::ident {
718            var = Some(inner.as_str().to_string());
719        }
720    }
721    Ok(StepKind::Await {
722        var: var.ok_or_else(|| anyhow!("AWAIT requires a variable"))?,
723    })
724}
725
726fn parse_cancel_statement_from_pair(pair: Pair<Rule>) -> Result<StepKind> {
727    let mut var = None;
728    for inner in pair.into_inner() {
729        if inner.as_rule() == Rule::ident {
730            var = Some(inner.as_str().to_string());
731        }
732    }
733    Ok(StepKind::Cancel {
734        var: var.ok_or_else(|| anyhow!("CANCEL requires a variable"))?,
735    })
736}
737
738fn parse_timeout_statement_from_pair(
739    pair: Pair<Rule>,
740    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
741) -> Result<StepKind> {
742    let mut duration = None;
743    let mut body: Option<Vec<Step>> = None;
744    for inner in pair.into_inner() {
745        match inner.as_rule() {
746            Rule::timeout_duration => {
747                duration = Some(parse_duration(inner.as_str())?);
748            }
749            Rule::block => {
750                body = Some(parse_block_elements_with_lower(inner, lower)?);
751            }
752            Rule::await_statement => {
753                let kind = parse_await_statement_from_pair(inner)?;
754                body = Some(vec![Step {
755                    guard: None,
756                    kind,
757                    scope_enter: 0,
758                    scope_exit: 0,
759                }]);
760            }
761            Rule::cancel_statement => {
762                let kind = parse_cancel_statement_from_pair(inner)?;
763                body = Some(vec![Step {
764                    guard: None,
765                    kind,
766                    scope_enter: 0,
767                    scope_exit: 0,
768                }]);
769            }
770            Rule::with_io_command
771            | Rule::inherit_env_command
772            | Rule::async_statement
773            | Rule::async_statement_block
774            | Rule::timeout_statement => {
775                let kind = parse_structural_command_with_lower(inner, lower)?;
776                body = Some(vec![Step {
777                    guard: None,
778                    kind,
779                    scope_enter: 0,
780                    scope_exit: 0,
781                }]);
782            }
783            Rule::instruction | Rule::instruction_inner => {
784                let (name, args) = extract_instruction(inner)?;
785                let kind = lower(&name, args)?;
786                body = Some(vec![Step {
787                    guard: None,
788                    kind,
789                    scope_enter: 0,
790                    scope_exit: 0,
791                }]);
792            }
793            _ => {}
794        }
795    }
796    Ok(StepKind::Timeout {
797        duration: duration.ok_or_else(|| anyhow!("TIMEOUT requires a duration"))?,
798        body: body.ok_or_else(|| anyhow!("TIMEOUT requires a command or block"))?,
799    })
800}
801
802fn parse_if_statement_from_pair(
803    pair: Pair<Rule>,
804    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
805) -> Result<StepKind> {
806    let mut cond = None;
807    let mut then_body = Vec::new();
808    let mut else_ifs = Vec::new();
809    let mut else_body = None;
810
811    for inner in pair.into_inner() {
812        match inner.as_rule() {
813            Rule::expr => {
814                if cond.is_none() {
815                    cond = Some(parse_expr(inner)?);
816                }
817            }
818            Rule::block => {
819                if then_body.is_empty() {
820                    then_body = parse_block_elements_with_lower(inner, lower)?;
821                }
822            }
823            Rule::else_if_clause => {
824                let (eif_cond, eif_body) = parse_else_if_clause(inner, lower)?;
825                else_ifs.push((eif_cond, eif_body));
826            }
827            Rule::else_clause => {
828                else_body = Some(parse_else_clause(inner, lower)?);
829            }
830            _ => {}
831        }
832    }
833    Ok(StepKind::If {
834        cond: Box::new(cond.ok_or_else(|| anyhow!("IF requires a condition"))?),
835        then_body,
836        else_ifs,
837        else_body,
838    })
839}
840
841fn parse_else_if_clause(
842    pair: Pair<Rule>,
843    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
844) -> Result<(Box<Expr>, Vec<Step>)> {
845    let mut cond = None;
846    let mut body = Vec::new();
847    for inner in pair.into_inner() {
848        match inner.as_rule() {
849            Rule::expr => cond = Some(parse_expr(inner)?),
850            Rule::block => body = parse_block_elements_with_lower(inner, lower)?,
851            _ => {}
852        }
853    }
854    Ok((
855        Box::new(cond.ok_or_else(|| anyhow!("ELSE IF requires a condition"))?),
856        body,
857    ))
858}
859
860fn parse_else_clause(
861    pair: Pair<Rule>,
862    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
863) -> Result<Vec<Step>> {
864    for inner in pair.into_inner() {
865        if let Rule::block = inner.as_rule() {
866            return parse_block_elements_with_lower(inner, lower);
867        }
868    }
869    Ok(Vec::new())
870}
871
872fn parse_async_statement_from_pair(
873    pair: Pair<Rule>,
874    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
875) -> Result<StepKind> {
876    let mut inner_cmd = None;
877    let mut block_body = None;
878    for inner in pair.into_inner() {
879        match inner.as_rule() {
880            Rule::command => {
881                // command is _{} = silent, so its children aren't visible as pairs
882                // when nested inside compound-atomic async_statement.
883                // Parse the command text directly.
884                let cmd_text = inner.as_str();
885                let steps = parse_script(cmd_text, |name, args| lower(name, args))?;
886                if steps.len() == 1 {
887                    inner_cmd = Some(steps.into_iter().next().unwrap().kind);
888                } else {
889                    bail!("unexpected multiple steps in async inner command");
890                }
891            }
892            Rule::command_inner => {
893                // command_inner = { inherit_env_command | async_statement | async_statement_block | instruction }
894                let child = inner
895                    .into_inner()
896                    .next()
897                    .ok_or_else(|| anyhow!("empty command_inner"))?;
898                match child.as_rule() {
899                    Rule::inherit_env_command => {
900                        inner_cmd = Some(parse_structural_command_with_lower(child, lower)?);
901                    }
902                    Rule::async_statement | Rule::async_statement_block => {
903                        inner_cmd = Some(parse_structural_command_with_lower(child, lower)?);
904                    }
905                    Rule::timeout_statement | Rule::cancel_statement => {
906                        inner_cmd = Some(parse_structural_command_with_lower(child, lower)?);
907                    }
908                    Rule::instruction => {
909                        let (name, args) = extract_instruction(child)?;
910                        inner_cmd = Some(lower(&name, args)?);
911                    }
912                    other => bail!("unexpected command_inner child: {:?}", other),
913                }
914            }
915            Rule::instruction | Rule::instruction_inner => {
916                let (name, args) = extract_instruction(inner)?;
917                inner_cmd = Some(lower(&name, args)?);
918            }
919            Rule::block => {
920                block_body = Some(parse_block_elements_with_lower(inner, lower)?);
921            }
922            _ => {}
923        }
924    }
925    if let Some(body) = block_body {
926        for step in &body {
927            if matches!(&step.kind, StepKind::WithIo { .. }) {
928                bail!(
929                    "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)"
930                );
931            }
932        }
933        Ok(StepKind::AsyncBlock { body })
934    } else if let Some(cmd) = inner_cmd {
935        if matches!(&cmd, StepKind::WithIo { .. }) {
936            bail!(
937                "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)"
938            );
939        }
940        Ok(StepKind::AsyncBlock {
941            body: vec![Step {
942                guard: None,
943                kind: cmd,
944                scope_enter: 0,
945                scope_exit: 0,
946            }],
947        })
948    } else {
949        bail!("ASYNC requires either a command or a block");
950    }
951}
952
953fn parse_async_statement_block_from_pair(
954    pair: Pair<Rule>,
955    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
956) -> Result<StepKind> {
957    let mut block_body = None;
958    for inner in pair.into_inner() {
959        if inner.as_rule() == Rule::block {
960            block_body = Some(parse_block_elements_with_lower(inner, lower)?);
961        }
962    }
963    let body = block_body.ok_or_else(|| anyhow!("async_statement_block requires a block"))?;
964    for step in &body {
965        if matches!(&step.kind, StepKind::WithIo { .. }) {
966            bail!(
967                "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)"
968            );
969        }
970    }
971    Ok(StepKind::AsyncBlock { body })
972}
973
974fn parse_block_elements_with_lower(
975    block_pair: Pair<Rule>,
976    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
977) -> Result<Vec<Step>> {
978    let mut steps = Vec::new();
979    for elem in block_pair.into_inner() {
980        match elem.as_rule() {
981            Rule::for_statement
982            | Rule::let_statement
983            | Rule::let_async_statement
984            | Rule::await_statement
985            | Rule::cancel_statement
986            | Rule::if_statement
987            | Rule::async_statement
988            | Rule::timeout_statement
989            | Rule::async_statement_block => {
990                let step_kind = parse_structural_command_with_lower(elem, lower)?;
991                steps.push(Step {
992                    guard: None,
993                    kind: step_kind,
994                    scope_enter: 0,
995                    scope_exit: 0,
996                });
997            }
998            Rule::guard_block => {
999                let mut guard_pair = None;
1000                let mut inner_block = None;
1001                for inner in elem.into_inner() {
1002                    match inner.as_rule() {
1003                        Rule::guard_line => guard_pair = Some(inner),
1004                        Rule::block => inner_block = Some(inner),
1005                        _ => {}
1006                    }
1007                }
1008                if let (Some(gp), Some(bp)) = (guard_pair, inner_block) {
1009                    let guard_expr = parse_guard_line(gp)?;
1010                    let mut inner_steps = parse_block_elements_with_lower(bp, lower)?;
1011                    for step in &mut inner_steps {
1012                        step.guard = Some(guard_expr.clone());
1013                    }
1014                    steps.extend(inner_steps);
1015                }
1016            }
1017            Rule::instruction | Rule::instruction_inner => {
1018                let (name, args) = extract_instruction(elem)?;
1019                let kind = lower(&name, args)?;
1020                steps.push(Step {
1021                    guard: None,
1022                    kind,
1023                    scope_enter: 0,
1024                    scope_exit: 0,
1025                });
1026            }
1027            Rule::with_io_command => {
1028                let step_kind = parse_structural_command_with_lower(elem, lower)?;
1029                steps.push(Step {
1030                    guard: None,
1031                    kind: step_kind,
1032                    scope_enter: 0,
1033                    scope_exit: 0,
1034                });
1035            }
1036            _ => {} // blank, hash_comment, semicolon, block_start, block_end, etc.
1037        }
1038    }
1039    Ok(steps)
1040}
1041
1042fn parse_argument(pair: Pair<Rule>) -> Result<Arg> {
1043    let inner: Vec<_> = pair.into_inner().collect();
1044    // Single expression — preserve as Arg::Expr for runtime evaluation
1045    if inner.len() == 1 && inner[0].as_rule() == Rule::expr {
1046        return Ok(Arg::Expr(parse_expr(inner.into_iter().next().unwrap())?));
1047    }
1048    // Single quoted string: preserve quote status and process escapes
1049    if inner.len() == 1 && inner[0].as_rule() == Rule::string_literal {
1050        return Ok(Arg::String(parse_fragments(&inner)?, true));
1051    }
1052    Ok(Arg::String(parse_fragments(&inner)?, false))
1053}
1054
1055fn parse_quoted_string(pair: Pair<Rule>) -> Result<String> {
1056    let s = pair.as_str();
1057    let content = &s[1..s.len() - 1];
1058    // Pass contents verbatim — all escape processing deferred to runtime expand_string
1059    Ok(content.to_string())
1060}
1061
1062/// Concatenate fragment pairs (string_literal, templated_arg, unquoted_arg, expr)
1063/// into a single String. Adjacent fragments without whitespace are joined directly;
1064/// fragments separated by whitespace get a space inserted.
1065fn parse_fragments(parts: &[Pair<Rule>]) -> Result<String> {
1066    // Single quoted string: unquote unconditionally
1067    if parts.len() == 1 && parts[0].as_rule() == Rule::string_literal {
1068        let s = parts[0].as_str();
1069        return Ok(s[1..s.len() - 1].to_string());
1070    }
1071
1072    let mut body = String::new();
1073    let mut last_end = None;
1074    for part in parts {
1075        let span = part.as_span();
1076        if let Some(end) = last_end
1077            && span.start() > end
1078        {
1079            body.push(' ');
1080        }
1081        match part.as_rule() {
1082            Rule::string_literal => {
1083                let s = part.as_str();
1084                let unquoted = &s[1..s.len() - 1];
1085                body.push_str(unquoted);
1086            }
1087            Rule::templated_arg | Rule::unquoted_arg => {
1088                body.push_str(part.as_str());
1089            }
1090            Rule::expr => body.push_str(part.as_str()),
1091            _ => {}
1092        }
1093        last_end = Some(span.end());
1094    }
1095    Ok(body)
1096}
1097
1098fn parse_guard_line(pair: Pair<Rule>) -> Result<GuardExpr> {
1099    for inner in pair.into_inner() {
1100        if inner.as_rule() == Rule::guard_expr {
1101            return parse_guard_expr(inner);
1102        }
1103    }
1104    bail!("guard line missing expression")
1105}
1106
1107fn parse_io_binding(pair: Pair<Rule>) -> Result<IoBinding> {
1108    let mut stream = None;
1109    let mut pipe = None;
1110    for inner in pair.into_inner() {
1111        match inner.as_rule() {
1112            Rule::io_stream => stream = Some(parse_io_stream(inner.as_str())),
1113            Rule::pipe_binding => pipe = Some(parse_pipe_binding(inner)?),
1114            _ => {}
1115        }
1116    }
1117    let stream = stream.ok_or_else(|| anyhow!("missing IO stream in WITH_IO"))?;
1118    Ok(IoBinding { stream, pipe })
1119}
1120
1121fn parse_io_stream(text: &str) -> IoStream {
1122    match text {
1123        "stdin" => IoStream::Stdin,
1124        "stdout" => IoStream::Stdout,
1125        "stderr" => IoStream::Stderr,
1126        _ => unreachable!("parser produced invalid io_stream token"),
1127    }
1128}
1129
1130fn parse_pipe_binding(pair: Pair<Rule>) -> Result<String> {
1131    for inner in pair.into_inner() {
1132        if inner.as_rule() == Rule::pipe_name {
1133            return Ok(inner.as_str().to_string());
1134        }
1135    }
1136    bail!("missing pipe identifier in WITH_IO binding");
1137}
1138
1139fn parse_guard_expr(pair: Pair<Rule>) -> Result<GuardExpr> {
1140    match pair.as_rule() {
1141        Rule::guard_expr => {
1142            let next = pair
1143                .into_inner()
1144                .next()
1145                .ok_or_else(|| anyhow!("guard expression missing body"))?;
1146            parse_guard_expr(next)
1147        }
1148        Rule::guard_seq => parse_guard_seq(pair),
1149        Rule::guard_factor => parse_guard_factor(pair),
1150        Rule::guard_not => {
1151            // guard_not is silent, so its inner pairs are the actual content
1152            bail!("guard_not should not create a pair")
1153        }
1154        Rule::guard_primary => parse_guard_primary(pair),
1155        Rule::guard_group => parse_guard_group(pair),
1156        Rule::guard_any_call => parse_guard_any_call(pair),
1157        Rule::guard_all_call => parse_guard_all_call(pair),
1158        Rule::not_call => parse_not_call(pair),
1159        Rule::guard_term => parse_guard_term(pair),
1160        _ => bail!("unexpected guard expression rule: {:?}", pair.as_rule()),
1161    }
1162}
1163
1164fn parse_guard_seq(pair: Pair<Rule>) -> Result<GuardExpr> {
1165    let mut exprs = Vec::new();
1166    for inner in pair.into_inner() {
1167        if inner.as_rule() == Rule::guard_factor {
1168            exprs.push(parse_guard_factor(inner)?);
1169        }
1170    }
1171    match exprs.len() {
1172        0 => bail!("guard list requires at least one entry"),
1173        1 => Ok(exprs.pop().unwrap()),
1174        _ => Ok(GuardExpr::all(exprs)),
1175    }
1176}
1177
1178fn parse_guard_factor(pair: Pair<Rule>) -> Result<GuardExpr> {
1179    let inner = pair
1180        .into_inner()
1181        .next()
1182        .ok_or_else(|| anyhow!("guard factor missing expression"))?;
1183    parse_guard_expr(inner)
1184}
1185
1186fn parse_not_call(pair: Pair<Rule>) -> Result<GuardExpr> {
1187    for inner in pair.into_inner() {
1188        if inner.as_rule() == Rule::guard_expr {
1189            return parse_guard_expr(inner).map(|e| GuardExpr::Not(Box::new(e)));
1190        }
1191    }
1192    bail!("not() missing expression")
1193}
1194
1195fn parse_guard_primary(pair: Pair<Rule>) -> Result<GuardExpr> {
1196    match pair.as_rule() {
1197        Rule::guard_primary => {
1198            let inner = pair
1199                .into_inner()
1200                .next()
1201                .ok_or_else(|| anyhow!("guard primary missing body"))?;
1202            parse_guard_primary(inner)
1203        }
1204        Rule::guard_group => parse_guard_group(pair),
1205        Rule::guard_any_call => parse_guard_any_call(pair),
1206        Rule::guard_all_call => parse_guard_all_call(pair),
1207        Rule::not_call => parse_not_call(pair),
1208        Rule::guard_term => parse_guard_term(pair),
1209        _ => bail!("unexpected guard primary rule: {:?}", pair.as_rule()),
1210    }
1211}
1212
1213fn parse_guard_group(pair: Pair<Rule>) -> Result<GuardExpr> {
1214    for inner in pair.into_inner() {
1215        if inner.as_rule() == Rule::guard_expr {
1216            return parse_guard_expr(inner);
1217        }
1218    }
1219    bail!("grouped guard missing expression")
1220}
1221
1222fn parse_guard_any_call(pair: Pair<Rule>) -> Result<GuardExpr> {
1223    let mut args = Vec::new();
1224    for inner in pair.into_inner() {
1225        if inner.as_rule() == Rule::guard_expr_list {
1226            args = parse_guard_expr_list(inner)?;
1227        }
1228    }
1229    if args.len() < 2 {
1230        bail!("any(...) requires at least two guard expressions");
1231    }
1232    Ok(GuardExpr::or(args))
1233}
1234
1235fn parse_guard_all_call(pair: Pair<Rule>) -> Result<GuardExpr> {
1236    let mut args = Vec::new();
1237    for inner in pair.into_inner() {
1238        if inner.as_rule() == Rule::guard_expr_list {
1239            args = parse_guard_expr_list(inner)?;
1240        }
1241    }
1242    if args.is_empty() {
1243        bail!("all(...) requires at least one guard expression");
1244    }
1245    Ok(GuardExpr::all(args))
1246}
1247
1248fn parse_guard_expr_list(pair: Pair<Rule>) -> Result<Vec<GuardExpr>> {
1249    let mut exprs = Vec::new();
1250    for inner in pair.into_inner() {
1251        if inner.as_rule() == Rule::guard_expr {
1252            push_guard_or_args_from_expr(inner, &mut exprs)?;
1253        }
1254    }
1255    Ok(exprs)
1256}
1257
1258fn push_guard_or_args_from_expr(expr_pair: Pair<Rule>, exprs: &mut Vec<GuardExpr>) -> Result<()> {
1259    if let Some(seq_pair) = expr_pair
1260        .clone()
1261        .into_inner()
1262        .find(|inner| inner.as_rule() == Rule::guard_seq)
1263    {
1264        let factors: Vec<Pair<Rule>> = seq_pair
1265            .into_inner()
1266            .filter(|inner| inner.as_rule() == Rule::guard_factor)
1267            .collect();
1268        if factors.len() > 1 {
1269            for factor in factors {
1270                exprs.push(parse_guard_factor(factor)?);
1271            }
1272            return Ok(());
1273        }
1274    }
1275    exprs.push(parse_guard_expr(expr_pair)?);
1276    Ok(())
1277}
1278
1279fn parse_guard_term(pair: Pair<Rule>) -> Result<GuardExpr> {
1280    for inner in pair.into_inner() {
1281        match inner.as_rule() {
1282            Rule::eq_guard => {
1283                return Ok(GuardExpr::Predicate(parse_func_guard(inner)?));
1284            }
1285            Rule::neq_guard => {
1286                let guard = parse_func_guard(inner)?;
1287                return Ok(GuardExpr::Not(Box::new(GuardExpr::Predicate(guard))));
1288            }
1289            Rule::bool_guard => {
1290                let val = inner
1291                    .into_inner()
1292                    .find(|p| p.as_rule() == Rule::bool_value)
1293                    .expect("grammar invariant violated: bool_guard missing bool_value")
1294                    .as_str()
1295                    .to_string();
1296                return Ok(GuardExpr::Predicate(Guard::StaticBool { value: val }));
1297            }
1298            Rule::env_guard => {
1299                return Ok(GuardExpr::Predicate(parse_env_guard(inner)?));
1300            }
1301            Rule::bare_guard_ident => {
1302                let tag = inner.as_str();
1303                if let Ok(g) = parse_platform_tag(tag) {
1304                    return Ok(GuardExpr::Predicate(g));
1305                }
1306                return Ok(GuardExpr::Predicate(Guard::EnvExists {
1307                    key: tag.to_string(),
1308                }));
1309            }
1310            _ => {}
1311        }
1312    }
1313    bail!("missing guard predicate")
1314}
1315
1316fn parse_func_guard(pair: Pair<Rule>) -> Result<Guard> {
1317    let mut key = String::new();
1318    let mut value = String::new();
1319    let mut saw_env_prefix = false;
1320    for inner in pair.into_inner() {
1321        match inner.as_rule() {
1322            Rule::env_prefix => saw_env_prefix = true,
1323            Rule::env_key if saw_env_prefix => {
1324                key = inner.as_str().trim().to_string();
1325            }
1326            Rule::bare_guard_value | Rule::quoted_string => {
1327                value = unquote(inner.as_str().trim()).to_string();
1328            }
1329            _ => {}
1330        }
1331    }
1332    Ok(Guard::EnvEquals { key, value })
1333}
1334
1335fn unquote(s: &str) -> &str {
1336    s.strip_prefix('"')
1337        .and_then(|s| s.strip_suffix('"'))
1338        .or_else(|| s.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1339        .unwrap_or(s)
1340}
1341
1342fn parse_env_guard(pair: Pair<Rule>) -> Result<Guard> {
1343    let mut key = String::new();
1344    for inner in pair.into_inner() {
1345        if inner.as_rule() == Rule::env_key {
1346            key = inner.as_str().trim().to_string();
1347        }
1348    }
1349    Ok(Guard::EnvExists { key })
1350}
1351
1352fn parse_platform_tag(tag: &str) -> Result<Guard> {
1353    let target = match tag.to_ascii_lowercase().as_str() {
1354        "unix" => PlatformGuard::Unix,
1355        "windows" => PlatformGuard::Windows,
1356        "mac" | "macos" => PlatformGuard::Macos,
1357        "linux" => PlatformGuard::Linux,
1358        _ => bail!("unknown platform '{}'", tag),
1359    };
1360    Ok(Guard::Platform { target })
1361}
1362
1363fn parse_dollar_ident(pair: Pair<Rule>) -> String {
1364    // Strip the leading '$' from the identifier
1365    let s = pair.as_str();
1366    s.strip_prefix('$').unwrap_or(s).to_string()
1367}
1368
1369use crate::ast::{CompareOp, Expr, LogicalOp, Value};
1370
1371fn parse_expr(pair: Pair<Rule>) -> Result<Expr> {
1372    let inner = pair.into_inner().next().unwrap();
1373    match inner.as_rule() {
1374        Rule::expr_logical_or => parse_expr_logical_or(inner),
1375        _ => bail!("unexpected expr rule: {:?}", inner.as_rule()),
1376    }
1377}
1378
1379fn parse_expr_logical_or(pair: Pair<Rule>) -> Result<Expr> {
1380    let mut inner = pair.into_inner();
1381    let mut left = parse_expr_logical_and(inner.next().unwrap())?;
1382    while let Some(op_pair) = inner.next() {
1383        let op = match op_pair.as_rule() {
1384            Rule::or_op => LogicalOp::Or,
1385            _ => bail!("unexpected operator in logical-or: {:?}", op_pair.as_rule()),
1386        };
1387        let right = parse_expr_logical_and(inner.next().unwrap())?;
1388        left = Expr::Logical {
1389            op,
1390            left: Box::new(left),
1391            right: Box::new(right),
1392        };
1393    }
1394    Ok(left)
1395}
1396
1397fn parse_expr_logical_and(pair: Pair<Rule>) -> Result<Expr> {
1398    let mut inner = pair.into_inner();
1399    let mut left = parse_expr_comparison(inner.next().unwrap())?;
1400    while let Some(op_pair) = inner.next() {
1401        let op = match op_pair.as_rule() {
1402            Rule::and_op => LogicalOp::And,
1403            _ => bail!(
1404                "unexpected operator in logical-and: {:?}",
1405                op_pair.as_rule()
1406            ),
1407        };
1408        let right = parse_expr_comparison(inner.next().unwrap())?;
1409        left = Expr::Logical {
1410            op,
1411            left: Box::new(left),
1412            right: Box::new(right),
1413        };
1414    }
1415    Ok(left)
1416}
1417
1418fn parse_expr_comparison(pair: Pair<Rule>) -> Result<Expr> {
1419    let mut inner = pair.into_inner();
1420    let left = parse_expr_unary(inner.next().unwrap())?;
1421    if let Some(op_pair) = inner.next() {
1422        let op = match op_pair.as_rule() {
1423            Rule::eq_op => CompareOp::Eq,
1424            Rule::neq_op => CompareOp::Ne,
1425            _ => bail!("unexpected comparison operator: {:?}", op_pair.as_rule()),
1426        };
1427        let right = parse_expr_unary(inner.next().unwrap())?;
1428        Ok(Expr::Compare {
1429            op,
1430            left: Box::new(left),
1431            right: Box::new(right),
1432        })
1433    } else {
1434        Ok(left)
1435    }
1436}
1437
1438fn parse_expr_unary(pair: Pair<Rule>) -> Result<Expr> {
1439    let mut bangs = 0u32;
1440    let mut atom = None;
1441    for inner in pair.into_inner() {
1442        match inner.as_rule() {
1443            Rule::not_op => bangs += 1,
1444            Rule::expr_atom => atom = Some(parse_expr_atom(inner)?),
1445            _ => bail!("unexpected unary operand rule: {:?}", inner.as_rule()),
1446        }
1447    }
1448    let mut expr = atom.ok_or_else(|| anyhow!("'!' requires an expression operand"))?;
1449    for _ in 0..bangs {
1450        expr = Expr::Not(Box::new(expr));
1451    }
1452    Ok(expr)
1453}
1454
1455fn parse_expr_atom(pair: Pair<Rule>) -> Result<Expr> {
1456    let inner = pair.into_inner().next().unwrap();
1457    match inner.as_rule() {
1458        Rule::parenthesized_expr => parse_expr(inner.into_inner().next().unwrap()),
1459        Rule::func_call => parse_func_call(inner),
1460        Rule::key_path => parse_key_path(inner),
1461        Rule::variable => {
1462            let name = inner.as_str();
1463            let name = name.strip_prefix('$').unwrap_or(name).to_string();
1464            Ok(Expr::Var(name))
1465        }
1466        Rule::list_literal => parse_list_literal(inner),
1467        Rule::map_literal => parse_map_literal(inner),
1468        Rule::string_literal | Rule::quoted_string => {
1469            let s = parse_quoted_string(inner)?;
1470            Ok(Expr::Literal(Value::String(s)))
1471        }
1472        Rule::bare_word => {
1473            let s = inner.as_str().to_string();
1474            match s.as_str() {
1475                "true" => Ok(Expr::Literal(Value::Bool(true))),
1476                "false" => Ok(Expr::Literal(Value::Bool(false))),
1477                _ => Ok(Expr::Literal(Value::String(s))),
1478            }
1479        }
1480        _ => bail!("unexpected expression atom rule: {:?}", inner.as_rule()),
1481    }
1482}
1483
1484fn parse_key_path(pair: Pair<Rule>) -> Result<Expr> {
1485    let mut base = None;
1486    let mut keys = Vec::new();
1487    for inner in pair.into_inner() {
1488        match inner.as_rule() {
1489            Rule::ident => {
1490                if base.is_none() {
1491                    base = Some(inner.as_str().to_string());
1492                }
1493            }
1494            Rule::key_path_segment => {
1495                keys.push(inner.as_str().to_string());
1496            }
1497            _ => {}
1498        }
1499    }
1500    Ok(Expr::KeyPath {
1501        base: base.ok_or_else(|| anyhow!("key path requires a base identifier"))?,
1502        keys,
1503    })
1504}
1505
1506fn parse_func_call(pair: Pair<Rule>) -> Result<Expr> {
1507    let mut name = None;
1508    let mut args = Vec::new();
1509    for inner in pair.into_inner() {
1510        match inner.as_rule() {
1511            Rule::ident => {
1512                name = Some(inner.as_str().to_string());
1513            }
1514            Rule::expr => {
1515                args.push(parse_expr(inner)?);
1516            }
1517            _ => {}
1518        }
1519    }
1520    Ok(Expr::Call {
1521        name: name.ok_or_else(|| anyhow!("function call requires a name"))?,
1522        args,
1523    })
1524}
1525
1526fn parse_list_literal(pair: Pair<Rule>) -> Result<Expr> {
1527    let mut items = Vec::new();
1528    for inner in pair.into_inner() {
1529        if inner.as_rule() == Rule::expr {
1530            items.push(parse_expr(inner)?);
1531        }
1532    }
1533    Ok(Expr::List(items))
1534}
1535
1536fn parse_map_literal(pair: Pair<Rule>) -> Result<Expr> {
1537    let mut entries = Vec::new();
1538    for inner in pair.into_inner() {
1539        if inner.as_rule() == Rule::map_entry {
1540            let mut key = String::new();
1541            let mut value = None;
1542            for entry_inner in inner.into_inner() {
1543                match entry_inner.as_rule() {
1544                    Rule::quoted_string => {
1545                        key = parse_quoted_string(entry_inner)?;
1546                    }
1547                    Rule::bare_word => {
1548                        key = entry_inner.as_str().to_string();
1549                    }
1550                    Rule::expr => {
1551                        value = Some(parse_expr(entry_inner)?);
1552                    }
1553                    _ => {}
1554                }
1555            }
1556            let val = value.ok_or_else(|| anyhow!("map entry missing value"))?;
1557            entries.push((key, val));
1558        }
1559    }
1560    Ok(Expr::Map(entries))
1561}