Skip to main content

oxdock_parser/
parser.rs

1use crate::ast::{Arg, Expr, Guard, GuardExpr, IoBinding, IoStream, PlatformGuard, Step, StepKind};
2use crate::command::ArgType;
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                        | RawToken::RunExec { .. }
113                )
114            {
115                let pending = self.pending_io_block.take().unwrap();
116                bail!(
117                    "line {}: WITH_IO block must be followed by '{{'",
118                    pending.line_no
119                );
120            }
121            match token {
122                RawToken::Guard { pair, line_end } => {
123                    let groups = parse_guard_line(pair)?;
124                    self.handle_guard_token(line_end, groups)?
125                }
126                RawToken::BlockStart { line_no } => self.start_block(line_no)?,
127                RawToken::BlockEnd { line_no } => self.end_block(line_no)?,
128                RawToken::Command { pair, line_no } => {
129                    let kind = parse_structural_command_with_lower(pair, &self.lower)?;
130                    self.handle_command_token(line_no, kind)?
131                }
132                RawToken::Instruction { pair, line_no } => {
133                    let kind = self.lower_instruction(pair)?;
134                    self.handle_command_token(line_no, kind)?
135                }
136                RawToken::RunExec { pair, line_no } => {
137                    let kind = lower_run_exec_pair(pair, &self.lower)?;
138                    self.handle_command_token(line_no, kind)?
139                }
140            }
141        }
142
143        if let Some(pending) = self.pending_io_block.take() {
144            bail!(
145                "line {}: WITH_IO block must be followed by '{{'",
146                pending.line_no
147            );
148        }
149
150        if self.guard_stack.len() != 1 {
151            bail!("unclosed guard block at end of script");
152        }
153        if self.pending_guards.is_some() {
154            bail!("guard declared on final lines without a following command");
155        }
156
157        if let Some(frame) = self.io_scope_stack.last() {
158            bail!(
159                "WITH_IO block starting on line {} was not closed",
160                frame.line_no
161            );
162        }
163
164        // Validate `INHERIT_ENV` directives: only allowed in the prelude (before
165        // any other commands) and at most one occurrence.
166        {
167            let mut seen_non_prelude = false;
168            let mut inherit_count = 0usize;
169            for step in &self.steps {
170                match &step.kind {
171                    StepKind::InheritEnv { .. } => {
172                        if seen_non_prelude {
173                            bail!("INHERIT_ENV must appear before any other commands");
174                        }
175                        if step.guard.is_some() || step.scope_enter > 0 || step.scope_exit > 0 {
176                            bail!("INHERIT_ENV cannot be guarded or nested inside blocks");
177                        }
178                        inherit_count += 1;
179                    }
180                    kind => {
181                        if contains_inherit_env(kind) {
182                            bail!("INHERIT_ENV cannot be nested inside other commands");
183                        }
184                        seen_non_prelude = true;
185                    }
186                }
187            }
188            if inherit_count > 1 {
189                bail!("only one INHERIT_ENV directive is allowed");
190            }
191        }
192
193        Ok(self.steps)
194    }
195
196    fn lower_instruction(&self, pair: Pair<Rule>) -> Result<StepKind> {
197        lower_instruction_pair(pair, &self.lower)
198    }
199
200    fn handle_guard_token(&mut self, line_end: usize, expr: GuardExpr) -> Result<()> {
201        if let Some(RawToken::Command { line_no, .. }) = self.tokens.front()
202            && *line_no == line_end
203        {
204            self.pending_inline_guards = Some(expr);
205            self.pending_can_open_block = false;
206            return Ok(());
207        }
208        self.stash_pending_guard(expr);
209        self.pending_can_open_block = true;
210        Ok(())
211    }
212
213    fn handle_command_token(&mut self, line_no: usize, kind: StepKind) -> Result<()> {
214        let inline = self.pending_inline_guards.take();
215        self.handle_command(line_no, kind, inline)
216    }
217
218    fn stash_pending_guard(&mut self, guard: GuardExpr) {
219        self.pending_guards = Some(if let Some(existing) = self.pending_guards.take() {
220            GuardExpr::all(vec![existing, guard])
221        } else {
222            guard
223        });
224    }
225
226    fn start_guard_block_from_pending(&mut self, line_no: usize) -> Result<()> {
227        let guards = self
228            .pending_guards
229            .take()
230            .ok_or_else(|| anyhow!("line {}: '{{' without a pending guard", line_no))?;
231        if !self.pending_can_open_block {
232            bail!("line {}: '{{' must directly follow a guard", line_no);
233        }
234        self.pending_can_open_block = false;
235        self.enter_guard_block(guards, line_no)
236    }
237
238    fn enter_guard_block(&mut self, guard: GuardExpr, line_no: usize) -> Result<()> {
239        let composed = if let Some(pending) = self.pending_guards.take() {
240            GuardExpr::all(vec![pending, guard])
241        } else {
242            guard
243        };
244        let parent = self.guard_stack.last().cloned().unwrap_or(None);
245        let next = and_guard_exprs(parent, Some(composed));
246        self.guard_stack.push(next);
247        self.scope_stack.push(ScopeFrame {
248            line_no,
249            had_command: false,
250        });
251        self.pending_scope_enters += 1;
252        Ok(())
253    }
254
255    fn begin_io_block(
256        &mut self,
257        line_no: usize,
258        bindings: Vec<IoBinding>,
259        guards: Option<GuardExpr>,
260    ) -> Result<()> {
261        if self.pending_io_block.is_some() {
262            bail!(
263                "line {}: previous WITH_IO block is still waiting for '{{'",
264                line_no
265            );
266        }
267        self.pending_io_block = Some(PendingIoBlock {
268            line_no,
269            bindings,
270            guards,
271        });
272        Ok(())
273    }
274
275    fn start_block(&mut self, line_no: usize) -> Result<()> {
276        if let Some(pending) = self.pending_io_block.take() {
277            self.block_stack.push(BlockKind::Io);
278            self.io_scope_stack.push(IoScopeFrame {
279                line_no: pending.line_no,
280                had_command: false,
281                bindings: pending.bindings,
282                guards: pending.guards,
283                first_step: self.steps.len(),
284            });
285            Ok(())
286        } else {
287            self.start_guard_block_from_pending(line_no)?;
288            self.block_stack.push(BlockKind::Guard);
289            Ok(())
290        }
291    }
292
293    fn end_block(&mut self, line_no: usize) -> Result<()> {
294        let kind = self
295            .block_stack
296            .pop()
297            .ok_or_else(|| anyhow!("line {}: unexpected '}}'", line_no))?;
298        match kind {
299            BlockKind::Guard => self.end_guard_block(line_no),
300            BlockKind::Io => self.end_io_block(line_no),
301        }
302    }
303
304    fn end_guard_block(&mut self, line_no: usize) -> Result<()> {
305        if self.guard_stack.len() == 1 {
306            bail!("line {}: unexpected '}}'", line_no);
307        }
308        if self.pending_guards.is_some() {
309            bail!(
310                "line {}: guard declared immediately before '}}' without a command",
311                line_no
312            );
313        }
314        let frame = self
315            .scope_stack
316            .last()
317            .cloned()
318            .ok_or_else(|| anyhow!("line {}: scope stack underflow", line_no))?;
319        if !frame.had_command {
320            bail!(
321                "line {}: guard block starting on line {} must contain at least one command",
322                line_no,
323                frame.line_no
324            );
325        }
326        let step = self
327            .steps
328            .last_mut()
329            .ok_or_else(|| anyhow!("line {}: guard block closed without any commands", line_no))?;
330        step.scope_exit += 1;
331        self.scope_stack.pop();
332        self.guard_stack.pop();
333        Ok(())
334    }
335
336    fn end_io_block(&mut self, line_no: usize) -> Result<()> {
337        let frame = self
338            .io_scope_stack
339            .pop()
340            .ok_or_else(|| anyhow!("line {}: unexpected '}}'", line_no))?;
341        if !frame.had_command {
342            bail!(
343                "line {}: WITH_IO block starting on line {} must contain at least one command",
344                line_no,
345                frame.line_no
346            );
347        }
348        // WITH_IO block bodies are lexical scopes like guard blocks: mark
349        // scope boundaries so LET/ENV/WORKDIR/WORKSPACE revert on exit.
350        // Pipe registrations live in ExecIo and are unaffected (they leak).
351        if self.steps.len() > frame.first_step {
352            self.steps[frame.first_step].scope_enter += 1;
353            if let Some(last) = self.steps.last_mut() {
354                last.scope_exit += 1;
355            }
356        }
357        Ok(())
358    }
359
360    fn guard_context(&mut self, inline: Option<GuardExpr>) -> Option<GuardExpr> {
361        let mut context = self.guard_stack.last().cloned().unwrap_or(None);
362        if let Some(pending) = self.pending_guards.take() {
363            context = and_guard_exprs(context, Some(pending));
364            self.pending_can_open_block = false;
365        }
366        if let Some(inline_guard) = inline {
367            context = and_guard_exprs(context, Some(inline_guard));
368        }
369        context
370    }
371
372    fn handle_command(
373        &mut self,
374        line_no: usize,
375        kind: StepKind,
376        inline_guards: Option<GuardExpr>,
377    ) -> Result<()> {
378        if let StepKind::WithIoBlock { bindings } = kind {
379            let guards = self.guard_context(inline_guards);
380            self.begin_io_block(line_no, bindings, guards)?;
381            return Ok(());
382        }
383
384        let guards = self.guard_context(inline_guards);
385        let guards = self.apply_io_guards(guards);
386        let scope_enter = self.pending_scope_enters;
387        self.pending_scope_enters = 0;
388        for frame in self.scope_stack.iter_mut() {
389            frame.had_command = true;
390        }
391        for frame in self.io_scope_stack.iter_mut() {
392            frame.had_command = true;
393        }
394        let kind = self.apply_io_defaults(kind);
395        self.steps.push(Step {
396            guard: guards,
397            kind,
398            scope_enter,
399            scope_exit: 0,
400        });
401        Ok(())
402    }
403
404    fn apply_io_defaults(&self, kind: StepKind) -> StepKind {
405        let defaults = self.current_io_defaults();
406        if defaults.is_empty() {
407            return kind;
408        }
409        match kind {
410            StepKind::WithIo { bindings, cmd } => StepKind::WithIo {
411                bindings: merge_bindings(&defaults, &bindings),
412                cmd,
413            },
414            other => StepKind::WithIo {
415                bindings: defaults,
416                cmd: Box::new(other),
417            },
418        }
419    }
420
421    fn current_io_defaults(&self) -> Vec<IoBinding> {
422        if self.io_scope_stack.is_empty() {
423            return Vec::new();
424        }
425        let mut set = IoBindingSet::default();
426        for frame in &self.io_scope_stack {
427            for binding in &frame.bindings {
428                set.insert(binding.clone());
429            }
430        }
431        set.into_vec()
432    }
433
434    fn apply_io_guards(&self, guard: Option<GuardExpr>) -> Option<GuardExpr> {
435        self.io_scope_stack.iter().fold(guard, |acc, frame| {
436            and_guard_exprs(acc, frame.guards.clone())
437        })
438    }
439}
440
441pub fn parse_script(
442    input: &str,
443    lower: impl Fn(&str, Vec<Arg>) -> Result<StepKind>,
444) -> Result<Vec<Step>> {
445    ScriptParser::new(input, lower)?.parse()
446}
447
448pub fn parse_guard_expr_str(input: &str) -> Result<GuardExpr> {
449    use pest::Parser;
450    let pairs = lexer::LanguageParser::parse(Rule::guard_expr, input)
451        .map_err(|e| anyhow!("guard parse error: {e}"))?;
452    let pair = pairs
453        .into_iter()
454        .next()
455        .ok_or_else(|| anyhow!("empty guard"))?;
456    parse_guard_expr(pair)
457}
458
459fn and_guard_exprs(left: Option<GuardExpr>, right: Option<GuardExpr>) -> Option<GuardExpr> {
460    match (left, right) {
461        (None, None) => None,
462        (Some(expr), None) | (None, Some(expr)) => Some(expr),
463        (Some(lhs), Some(rhs)) => Some(GuardExpr::all(vec![lhs, rhs])),
464    }
465}
466
467fn merge_bindings(defaults: &[IoBinding], overrides: &[IoBinding]) -> Vec<IoBinding> {
468    let mut set = IoBindingSet::default();
469    for binding in defaults {
470        set.insert(binding.clone());
471    }
472    for binding in overrides {
473        set.insert(binding.clone());
474    }
475    set.into_vec()
476}
477
478fn contains_inherit_env(kind: &StepKind) -> bool {
479    match kind {
480        StepKind::InheritEnv { .. } => true,
481        StepKind::WithIo { cmd, .. } => contains_inherit_env(cmd),
482        StepKind::AssignCapture { cmd, .. } => contains_inherit_env(cmd),
483        _ => false,
484    }
485}
486
487/// True when bindings reroute stdout into a named pipe. A `LET`-capture owns
488/// the step's stdout, so combining the two is a parse error.
489fn has_stdout_pipe(bindings: &[IoBinding]) -> bool {
490    bindings
491        .iter()
492        .any(|b| b.stream == IoStream::Stdout && b.pipe.is_some())
493}
494
495/// Reject async machinery inside a capture body: background tasks are
496/// captured via `LET $o = AWAIT $t`, never inline.
497fn reject_async_in_capture(kind: &StepKind) -> Result<()> {
498    let bad = match kind {
499        StepKind::AsyncBlock { .. }
500        | StepKind::AssignAsync { .. }
501        | StepKind::Await { .. }
502        | StepKind::AwaitCapture { .. }
503        | StepKind::Cancel { .. } => true,
504        StepKind::WithIo { cmd, .. } => reject_async_in_capture(cmd).is_err(),
505        StepKind::Timeout { body, .. } => body
506            .iter()
507            .any(|s| reject_async_in_capture(&s.kind).is_err()),
508        _ => false,
509    };
510    if bad {
511        bail!(
512            "LET capture cannot run ASYNC/AWAIT/CANCEL inline; use LET $t = ASYNC ... then LET $o = AWAIT $t"
513        );
514    }
515    Ok(())
516}
517
518/// Reject `WITH_IO [stdout=pipe:...]` anywhere inside a capture body: the
519/// capture sink owns stdout.
520fn reject_pipe_stdout_in_capture(kind: &StepKind) -> Result<()> {
521    match kind {
522        StepKind::WithIo { bindings, cmd } => {
523            if has_stdout_pipe(bindings) {
524                bail!(
525                    "LET capture cannot use WITH_IO [stdout=pipe:...]; the capture sink owns stdout"
526                );
527            }
528            reject_pipe_stdout_in_capture(cmd)
529        }
530        StepKind::Timeout { body, .. } => {
531            for step in body {
532                reject_pipe_stdout_in_capture(&step.kind)?;
533            }
534            Ok(())
535        }
536        _ => Ok(()),
537    }
538}
539
540/// Re-parse raw RHS text as an expression (fallback when the `LET` RHS lead
541/// token is not a known command). Requires the expression to consume the
542/// full text so `LET $x = FOO bar` stays an error instead of binding `FOO`.
543fn parse_expr_str(text: &str) -> Result<Expr> {
544    use pest::Parser;
545    let mut pairs = lexer::LanguageParser::parse(Rule::expr, text)
546        .map_err(|e| anyhow!("invalid LET expression {text:?}: {e}"))?;
547    let pair = pairs
548        .next()
549        .ok_or_else(|| anyhow!("LET requires an expression"))?;
550    if pair.as_span().end() != text.len() {
551        bail!("invalid LET expression {text:?}");
552    }
553    parse_expr(pair)
554}
555
556fn parse_structural_command_with_lower(
557    pair: Pair<Rule>,
558    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
559) -> Result<StepKind> {
560    let kind = match pair.as_rule() {
561        Rule::inherit_env_command => {
562            let mut keys = Vec::new();
563            for inner in pair.into_inner() {
564                if inner.as_rule() == Rule::inherit_list {
565                    for key in inner.into_inner() {
566                        if key.as_rule() == Rule::env_key {
567                            keys.push(key.as_str().trim().to_string());
568                        }
569                    }
570                } else if inner.as_rule() == Rule::env_key {
571                    keys.push(inner.as_str().trim().to_string());
572                }
573            }
574            StepKind::InheritEnv { keys }
575        }
576        Rule::with_io_command => {
577            let mut bindings = Vec::new();
578            let mut cmd = None;
579            for inner in pair.into_inner() {
580                match inner.as_rule() {
581                    Rule::io_flags => {
582                        for flag in inner.into_inner() {
583                            if flag.as_rule() == Rule::io_binding {
584                                bindings.push(parse_io_binding(flag)?);
585                            }
586                        }
587                    }
588                    Rule::with_io_command => {
589                        cmd = Some(Box::new(parse_structural_command_with_lower(inner, lower)?));
590                    }
591                    Rule::inherit_env_command => {
592                        cmd = Some(Box::new(parse_structural_command_with_lower(inner, lower)?));
593                    }
594                    Rule::async_statement | Rule::async_statement_block => {
595                        cmd = Some(Box::new(parse_structural_command_with_lower(inner, lower)?));
596                    }
597                    Rule::timeout_statement | Rule::cancel_statement => {
598                        cmd = Some(Box::new(parse_structural_command_with_lower(inner, lower)?));
599                    }
600                    Rule::instruction | Rule::instruction_inner => {
601                        cmd = Some(Box::new(lower_instruction_pair(inner, lower)?));
602                    }
603                    Rule::run_exec_statement | Rule::run_exec_inner => {
604                        cmd = Some(Box::new(lower_run_exec_pair(inner, lower)?));
605                    }
606                    _ => {}
607                }
608            }
609            if let Some(cmd) = cmd {
610                StepKind::WithIo { bindings, cmd }
611            } else {
612                StepKind::WithIoBlock { bindings }
613            }
614        }
615        Rule::for_statement => parse_for_statement_from_pair(pair, lower)?,
616        Rule::let_statement => parse_let_statement_from_pair(pair)?,
617        Rule::let_async_statement => parse_let_async_statement_from_pair(pair, lower)?,
618        Rule::let_capture_statement => parse_let_capture_statement_from_pair(pair, lower)?,
619        Rule::await_statement => parse_await_statement_from_pair(pair)?,
620        Rule::cancel_statement => parse_cancel_statement_from_pair(pair)?,
621        Rule::if_statement => parse_if_statement_from_pair(pair, lower)?,
622        Rule::async_statement => parse_async_statement_from_pair(pair, lower)?,
623        Rule::async_statement_block => parse_async_statement_block_from_pair(pair, lower)?,
624        Rule::timeout_statement => parse_timeout_statement_from_pair(pair, lower)?,
625        Rule::command_inner => {
626            // command_inner = { inherit_env_command | instruction }
627            // Unwrap to the inner rule
628            let inner = pair
629                .into_inner()
630                .next()
631                .ok_or_else(|| anyhow!("empty command_inner"))?;
632            parse_structural_command_with_lower(inner, lower)?
633        }
634        Rule::instruction | Rule::instruction_inner => lower_instruction_pair(pair, lower)?,
635        Rule::run_exec_statement | Rule::run_exec_inner => lower_run_exec_pair(pair, lower)?,
636        _ => bail!("unexpected structural command rule: {:?}", pair.as_rule()),
637    };
638    Ok(kind)
639}
640
641fn extract_instruction(pair: Pair<Rule>) -> Result<(String, Vec<InsToken>)> {
642    let mut name = None;
643    let mut args = Vec::new();
644    for inner in pair.into_inner() {
645        match inner.as_rule() {
646            Rule::command_name => {
647                name = Some(inner.as_str().to_string());
648            }
649            Rule::argument => {
650                args.extend(parse_argument(inner)?.into_iter().map(InsToken::Pos));
651            }
652            Rule::assignment => {
653                let (key, value) = parse_assignment(inner)?;
654                args.push(InsToken::Assign(key, value));
655            }
656            _ => {}
657        }
658    }
659    let name = name.ok_or_else(|| anyhow!("instruction missing command name"))?;
660    Ok((name, args))
661}
662
663/// One lowered instruction token: a positional argument, or a pre-split
664/// `KEY=value` assignment from the unified grammar rule. Assignments reach
665/// ENV/EXPAND lowerings intact; every other command sees them collapsed to
666/// canonical `key=value` text (see `lower_instruction_pair`).
667enum InsToken {
668    Pos(Arg),
669    Assign(String, Arg),
670}
671
672/// Lower one generic instruction pair: ENV/EXPAND build `StepKind` directly
673/// from pre-split assignments (never via the injected `lower`, mirroring how
674/// LET/FOR/IF bypass it); all other commands flow through `lower` with
675/// assignments in canonical text form.
676fn lower_instruction_pair(
677    pair: Pair<Rule>,
678    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
679) -> Result<StepKind> {
680    let (name, tokens) = extract_instruction(pair)?;
681    if name == "ENV" {
682        return lower_env_command(tokens);
683    }
684    if name == "EXPAND" {
685        return lower_expand_command(tokens);
686    }
687    let args = tokens
688        .into_iter()
689        .map(|token| match token {
690            InsToken::Pos(arg) => arg,
691            InsToken::Assign(key, value) => crate::commands::canonical_assignment_arg(&key, &value),
692        })
693        .collect();
694    lower(&name, args)
695}
696
697/// Lower a `run_exec` grammar pair: the PEG engine has already validated the
698/// full `RUN [...]` span, so extract the inner `list_literal` and route the
699/// structured `Expr::List` through the injected `lower` as `RUN` with one
700/// typed argument (production `lower_command` maps it to `StepKind::RunExec`;
701/// the grammar-test mock wraps it in `StepKind::Run`).
702fn lower_run_exec_pair(
703    pair: Pair<Rule>,
704    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
705) -> Result<StepKind> {
706    let mut list = None;
707    for inner in pair.into_inner() {
708        if inner.as_rule() == Rule::list_literal {
709            list = Some(parse_list_literal(inner)?);
710        }
711    }
712    let list = list.ok_or_else(|| anyhow!("RUN exec form missing list literal"))?;
713    lower("RUN", vec![Arg::Expr(list)])
714}
715
716/// Split one `assignment` pair into its key and lowered value.
717fn parse_assignment(pair: Pair<Rule>) -> Result<(String, Arg)> {
718    let mut key = None;
719    let mut value = None;
720    for inner in pair.into_inner() {
721        match inner.as_rule() {
722            Rule::assign_key => {
723                key = Some(inner.as_str().to_string());
724            }
725            Rule::assign_value => {
726                value = Some(lower_command_value(inner)?);
727            }
728            _ => bail!("unexpected assignment rule: {:?}", inner.as_rule()),
729        }
730    }
731    Ok((
732        key.ok_or_else(|| anyhow!("assignment missing key"))?,
733        value.unwrap_or(Arg::String(String::new(), false)),
734    ))
735}
736
737/// Single unified value lowering: every command's free-text value flows through
738/// here on raw pest spans. Quoted bytes stay exact, lone `$var`/`$a.b`/`CALL()`
739/// stay typed `Arg::Expr`, and anything else becomes literal text with only
740/// `{{ }}` as the interpolation trigger. No heuristic rewriting, ever.
741fn lower_command_value(pair: Pair<Rule>) -> Result<Arg> {
742    let inner = pair
743        .into_inner()
744        .next()
745        .ok_or_else(|| anyhow!("assignment value is empty"))?;
746    match inner.as_rule() {
747        Rule::quoted_string => Ok(Arg::String(parse_quoted_string(inner)?, true)),
748        Rule::assign_expr => {
749            let shape = inner
750                .into_inner()
751                .next()
752                .ok_or_else(|| anyhow!("assignment expression is empty"))?;
753            match shape.as_rule() {
754                Rule::variable => Ok(Arg::Expr(Expr::Var(parse_dollar_ident(shape)))),
755                Rule::key_path => Ok(Arg::Expr(parse_key_path(shape)?)),
756                Rule::func_call => Ok(Arg::Expr(parse_func_call(shape)?)),
757                other => bail!("unexpected assignment expression shape: {:?}", other),
758            }
759        }
760        Rule::raw_fragments => lower_raw_fragments(inner),
761        other => bail!("unexpected assignment value rule: {:?}", other),
762    }
763}
764
765/// Assemble a bounded raw span into one literal `Arg::String`: `{{ }}` template
766/// chunks pass through verbatim for `expand_string`, quoted chunks unquote
767/// once with exact bytes, and unquoted runs collapse whitespace to single
768/// spaces (trailing/leading edges trimmed). Pure text needs no `Parts` — every
769/// fragment resolves through the same `expand_string` pass.
770fn lower_raw_fragments(pair: Pair<Rule>) -> Result<Arg> {
771    let mut body = String::new();
772    for fragment in pair.into_inner() {
773        match fragment.as_rule() {
774            Rule::quoted_string => body.push_str(&parse_quoted_string(fragment)?),
775            Rule::templated_arg => body.push_str(fragment.as_str()),
776            Rule::raw_text => body.push_str(&collapse_ws(fragment.as_str())),
777            other => bail!("unexpected raw value fragment: {:?}", other),
778        }
779    }
780    Ok(Arg::String(body.trim().to_string(), false))
781}
782
783/// Collapse every whitespace run to a single space, preserving edge positions
784/// (callers trim the assembled value).
785fn collapse_ws(s: &str) -> String {
786    let mut out = String::with_capacity(s.len());
787    let mut in_run = false;
788    for c in s.chars() {
789        if c.is_whitespace() {
790            if !in_run {
791                out.push(' ');
792                in_run = true;
793            }
794        } else {
795            out.push(c);
796            in_run = false;
797        }
798    }
799    out
800}
801
802/// Parser-direct `ENV` lowering: exactly one assignment. A lone positional
803/// holding `=` is the exotic-key fringe (keys the grammar cannot classify);
804/// anything else is a precise error instead of a silent drop.
805fn lower_env_command(tokens: Vec<InsToken>) -> Result<StepKind> {
806    if tokens.is_empty() {
807        bail!("ENV requires KEY=value");
808    }
809    match tokens.as_slice() {
810        [InsToken::Assign(key, value)] => {
811            // Same KeyValue check the central validator applies on the
812            // `lower_command` path, over the joined assignment form.
813            ArgType::KeyValue
814                .check_arg(&Arg::String(format!("{key}={}", value.render()), false))?;
815            Ok(StepKind::Env {
816                key: key.clone(),
817                value: value.clone(),
818            })
819        }
820        [InsToken::Pos(Arg::String(text, _))] => match crate::command::split_assignment(text)? {
821            Some((key, value)) => Ok(StepKind::Env { key, value }),
822            None => bail!("ENV requires KEY=value format"),
823        },
824        _ => bail!("ENV requires KEY=value format"),
825    }
826}
827
828/// Parser-direct `EXPAND` lowering: positional tokens are the optional path,
829/// assignments are overrides. Split quoted values can never masquerade as
830/// extra paths — tokenize time already proved they are one value.
831fn lower_expand_command(tokens: Vec<InsToken>) -> Result<StepKind> {
832    let mut path = None;
833    let mut overrides = Vec::new();
834    for token in tokens {
835        match token {
836            InsToken::Assign(key, value) => {
837                if key.is_empty() {
838                    bail!("EXPAND requires KEY=value format for overrides")
839                }
840                overrides.push((key, value));
841            }
842            InsToken::Pos(arg) => match &arg {
843                Arg::String(text, quoted) if !quoted && text.contains('=') => {
844                    let Some((key, value)) = crate::command::split_assignment(text)? else {
845                        bail!("EXPAND requires KEY=value format for overrides")
846                    };
847                    overrides.push((key, value));
848                }
849                _ => {
850                    if path.is_none() {
851                        // Path-typed positional, checked like every other
852                        // `lower_command` path arg (literals always pass;
853                        // resolution stays runtime).
854                        ArgType::Path.check_arg(&arg)?;
855                        path = Some(arg);
856                    } else {
857                        bail!("EXPAND accepts at most one path");
858                    }
859                }
860            },
861        }
862    }
863    Ok(StepKind::Expand { path, overrides })
864}
865
866fn parse_for_statement_from_pair(
867    pair: Pair<Rule>,
868    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
869) -> Result<StepKind> {
870    let mut idents = Vec::new();
871    let mut in_expr = None;
872    let mut body_steps = Vec::new();
873    for inner in pair.into_inner() {
874        match inner.as_rule() {
875            Rule::dollar_ident => {
876                idents.push(parse_dollar_ident(inner));
877            }
878            Rule::expr => {
879                in_expr = Some(parse_expr(inner)?);
880            }
881            Rule::block => {
882                body_steps = parse_block_elements_with_lower(inner, lower)?;
883            }
884            _ => {}
885        }
886    }
887    let (key_var, var) = match idents.len() {
888        1 => (None, idents.into_iter().next().unwrap()),
889        2 => {
890            let mut iter = idents.into_iter();
891            (Some(iter.next().unwrap()), iter.next().unwrap())
892        }
893        _ => bail!("FOR requires at least one variable"),
894    };
895    Ok(StepKind::For {
896        key_var,
897        var,
898        in_expr: in_expr.ok_or_else(|| anyhow!("FOR requires an iterable expression"))?,
899        body: body_steps,
900    })
901}
902
903fn parse_let_statement_from_pair(pair: Pair<Rule>) -> Result<StepKind> {
904    let mut var = None;
905    let mut expr = None;
906    for inner in pair.into_inner() {
907        match inner.as_rule() {
908            Rule::dollar_ident => {
909                var = Some(parse_dollar_ident(inner));
910            }
911            Rule::expr => {
912                expr = Some(parse_expr(inner)?);
913            }
914            _ => {}
915        }
916    }
917    Ok(StepKind::Assign {
918        var: var.ok_or_else(|| anyhow!("LET requires a variable"))?,
919        expr: expr.ok_or_else(|| anyhow!("LET requires an expression"))?,
920    })
921}
922
923fn parse_let_async_statement_from_pair(
924    pair: Pair<Rule>,
925    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
926) -> Result<StepKind> {
927    let mut var = None;
928    let mut body = None;
929    for inner in pair.into_inner() {
930        match inner.as_rule() {
931            Rule::dollar_ident => {
932                var = Some(parse_dollar_ident(inner));
933            }
934            Rule::block => {
935                body = Some(parse_block_elements_with_lower(inner, lower)?);
936            }
937            Rule::command_inner => {
938                // command_inner = { inherit_env_command | async_statement | async_statement_block | instruction }
939                // Unwrap to the inner rule
940                let inner = inner
941                    .into_inner()
942                    .next()
943                    .ok_or_else(|| anyhow!("empty command_inner"))?;
944                let step_kind = parse_structural_command_with_lower(inner, lower)?;
945                body = Some(vec![Step {
946                    guard: None,
947                    kind: step_kind,
948                    scope_enter: 0,
949                    scope_exit: 0,
950                }]);
951            }
952            Rule::with_io_command => {
953                // LET $var = WITH_IO [flags] ... — two shapes share this rule
954                // (`let_async_statement` precedes `let_capture_statement` in
955                // the grammar, so every WITH_IO-led LET lands here):
956                // - wrapping ASYNC binds a pipe-wired background task. The
957                //   bindings apply inside the task thread — the same shape as
958                //   a braced body holding one WITH_IO step, which the
959                //   AssignAsync runtime path supports.
960                // - wrapping a synchronous command captures its stdout into
961                //   the variable (same semantics as LET $x = <command>).
962                let kind = parse_structural_command_with_lower(inner, lower)?;
963                let StepKind::WithIo { bindings, cmd } = kind else {
964                    bail!(
965                        "LET $var = WITH_IO requires an ASYNC command (e.g. LET $t = WITH_IO [stdin=pipe:p] ASYNC WRITE \"f\")"
966                    );
967                };
968                match *cmd {
969                    StepKind::AsyncBlock { body: async_body } => {
970                        if async_body.len() != 1 {
971                            bail!(
972                                "LET $var = WITH_IO [..] ASYNC accepts a single command; use LET $var = ASYNC {{ ... }} with WITH_IO inside the block for multi-step tasks"
973                            );
974                        }
975                        let step = async_body
976                            .into_iter()
977                            .next()
978                            .ok_or_else(|| anyhow!("LET $var = ASYNC requires a body"))?;
979                        body = Some(vec![Step {
980                            guard: step.guard,
981                            kind: StepKind::WithIo {
982                                bindings,
983                                cmd: Box::new(step.kind),
984                            },
985                            scope_enter: step.scope_enter,
986                            scope_exit: step.scope_exit,
987                        }]);
988                    }
989                    sync_cmd => {
990                        if has_stdout_pipe(&bindings) {
991                            bail!(
992                                "LET capture cannot use WITH_IO [stdout=pipe:...]; the capture sink owns stdout"
993                            );
994                        }
995                        reject_async_in_capture(&sync_cmd)?;
996                        let name = var
997                            .clone()
998                            .ok_or_else(|| anyhow!("LET $var = WITH_IO requires a variable"))?;
999                        return Ok(StepKind::AssignCapture {
1000                            var: name,
1001                            cmd: Box::new(StepKind::WithIo {
1002                                bindings,
1003                                cmd: Box::new(sync_cmd),
1004                            }),
1005                        });
1006                    }
1007                }
1008            }
1009            _ => {}
1010        }
1011    }
1012    Ok(StepKind::AssignAsync {
1013        var: var.ok_or_else(|| anyhow!("LET $var = ASYNC requires a variable"))?,
1014        body: body.ok_or_else(|| anyhow!("LET $var = ASYNC requires a body"))?,
1015    })
1016}
1017
1018/// Lower `LET $var = <sync command>` / `LET $out = AWAIT $task`.
1019///
1020/// Shadow-safe by construction: the grammar only routes UPPERCASE-led
1021/// `instruction` lines here (`let_async_statement` claims ASYNC-led and
1022/// WITH_IO-led lines first; lowercase/digit/sigil RHSs never match). Rust
1023/// then branches on the lead token: known commands lower to capture,
1024/// unknown leads re-parse as plain expressions.
1025fn parse_let_capture_statement_from_pair(
1026    pair: Pair<Rule>,
1027    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1028) -> Result<StepKind> {
1029    use pest::Parser;
1030    let mut var = None;
1031    let mut await_pair = None;
1032    let mut timeout_pair = None;
1033    let mut instruction_pair = None;
1034    for inner in pair.into_inner() {
1035        match inner.as_rule() {
1036            Rule::dollar_ident => {
1037                var = Some(parse_dollar_ident(inner));
1038            }
1039            Rule::await_statement => {
1040                await_pair = Some(inner);
1041            }
1042            Rule::timeout_statement => {
1043                timeout_pair = Some(inner);
1044            }
1045            Rule::instruction => {
1046                instruction_pair = Some(inner);
1047            }
1048            _ => {}
1049        }
1050    }
1051    let var = var.ok_or_else(|| anyhow!("LET requires a variable"))?;
1052    if let Some(awaited) = await_pair {
1053        let mut task_var = None;
1054        for inner in awaited.into_inner() {
1055            if inner.as_rule() == Rule::ident {
1056                task_var = Some(inner.as_str().to_string());
1057            }
1058        }
1059        return Ok(StepKind::AwaitCapture {
1060            out_var: var,
1061            task_var: task_var
1062                .ok_or_else(|| anyhow!("LET $out = AWAIT requires a task variable"))?,
1063        });
1064    }
1065    if let Some(timeouted) = timeout_pair {
1066        let kind = parse_structural_command_with_lower(timeouted, lower)?;
1067        reject_async_in_capture(&kind)?;
1068        reject_pipe_stdout_in_capture(&kind)?;
1069        return Ok(StepKind::AssignCapture {
1070            var,
1071            cmd: Box::new(kind),
1072        });
1073    }
1074    if let Some(ins) = instruction_pair {
1075        let text = ins.as_str().to_string();
1076        let mut lead = None;
1077        for token in ins.into_inner() {
1078            if token.as_rule() == Rule::command_name {
1079                lead = Some(token.as_str().to_string());
1080                break;
1081            }
1082        }
1083        let lead = lead.ok_or_else(|| anyhow!("LET capture requires a command"))?;
1084        if crate::commands::is_known_command(&lead) {
1085            let kind = lower_instruction_pair(
1086                lexer::LanguageParser::parse(Rule::instruction, &text)
1087                    .map_err(|e| anyhow!("invalid LET capture {text:?}: {e}"))?
1088                    .next()
1089                    .ok_or_else(|| anyhow!("LET capture requires a command"))?,
1090                lower,
1091            )?;
1092            reject_async_in_capture(&kind)?;
1093            reject_pipe_stdout_in_capture(&kind)?;
1094            return Ok(StepKind::AssignCapture {
1095                var,
1096                cmd: Box::new(kind),
1097            });
1098        }
1099        let expr = parse_expr_str(&text)?;
1100        return Ok(StepKind::Assign { var, expr });
1101    }
1102    bail!("LET requires a value")
1103}
1104
1105fn parse_await_statement_from_pair(pair: Pair<Rule>) -> Result<StepKind> {
1106    let mut var = None;
1107    for inner in pair.into_inner() {
1108        if inner.as_rule() == Rule::ident {
1109            var = Some(inner.as_str().to_string());
1110        }
1111    }
1112    Ok(StepKind::Await {
1113        var: var.ok_or_else(|| anyhow!("AWAIT requires a variable"))?,
1114    })
1115}
1116
1117fn parse_cancel_statement_from_pair(pair: Pair<Rule>) -> Result<StepKind> {
1118    let mut var = None;
1119    for inner in pair.into_inner() {
1120        if inner.as_rule() == Rule::ident {
1121            var = Some(inner.as_str().to_string());
1122        }
1123    }
1124    Ok(StepKind::Cancel {
1125        var: var.ok_or_else(|| anyhow!("CANCEL requires a variable"))?,
1126    })
1127}
1128
1129/// Build a TIMEOUT duration [`Arg`] from the widened `timeout_duration`
1130/// alternatives. Static literals type-check now via the declared Duration
1131/// arg type; dynamics (`$var`, templates) resolve at runtime.
1132fn parse_timeout_duration_arg(pair: Pair<Rule>) -> Result<Arg> {
1133    for inner in pair.into_inner() {
1134        let arg = match inner.as_rule() {
1135            Rule::timeout_literal => Arg::String(inner.as_str().to_string(), false),
1136            Rule::dollar_ident => Arg::Expr(Expr::Var(parse_dollar_ident(inner))),
1137            Rule::quoted_string => Arg::String(
1138                crate::command::strip_surrounding_quotes(inner.as_str()).to_string(),
1139                true,
1140            ),
1141            Rule::templated_arg => Arg::String(inner.as_str().to_string(), false),
1142            _ => continue,
1143        };
1144        ArgType::Duration.check_arg(&arg)?;
1145        return Ok(arg);
1146    }
1147    bail!("TIMEOUT requires a duration")
1148}
1149
1150fn parse_timeout_statement_from_pair(
1151    pair: Pair<Rule>,
1152    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1153) -> Result<StepKind> {
1154    let mut duration: Option<Arg> = None;
1155    let mut body: Option<Vec<Step>> = None;
1156    for inner in pair.into_inner() {
1157        match inner.as_rule() {
1158            Rule::timeout_duration => {
1159                duration = Some(parse_timeout_duration_arg(inner)?);
1160            }
1161            Rule::block => {
1162                body = Some(parse_block_elements_with_lower(inner, lower)?);
1163            }
1164            Rule::await_statement => {
1165                let kind = parse_await_statement_from_pair(inner)?;
1166                body = Some(vec![Step {
1167                    guard: None,
1168                    kind,
1169                    scope_enter: 0,
1170                    scope_exit: 0,
1171                }]);
1172            }
1173            Rule::cancel_statement => {
1174                let kind = parse_cancel_statement_from_pair(inner)?;
1175                body = Some(vec![Step {
1176                    guard: None,
1177                    kind,
1178                    scope_enter: 0,
1179                    scope_exit: 0,
1180                }]);
1181            }
1182            Rule::with_io_command
1183            | Rule::inherit_env_command
1184            | Rule::async_statement
1185            | Rule::async_statement_block
1186            | Rule::timeout_statement => {
1187                let kind = parse_structural_command_with_lower(inner, lower)?;
1188                body = Some(vec![Step {
1189                    guard: None,
1190                    kind,
1191                    scope_enter: 0,
1192                    scope_exit: 0,
1193                }]);
1194            }
1195            Rule::instruction | Rule::instruction_inner => {
1196                let kind = lower_instruction_pair(inner, lower)?;
1197                body = Some(vec![Step {
1198                    guard: None,
1199                    kind,
1200                    scope_enter: 0,
1201                    scope_exit: 0,
1202                }]);
1203            }
1204            Rule::run_exec_statement | Rule::run_exec_inner => {
1205                let kind = lower_run_exec_pair(inner, lower)?;
1206                body = Some(vec![Step {
1207                    guard: None,
1208                    kind,
1209                    scope_enter: 0,
1210                    scope_exit: 0,
1211                }]);
1212            }
1213            _ => {}
1214        }
1215    }
1216    Ok(StepKind::Timeout {
1217        duration: duration.ok_or_else(|| anyhow!("TIMEOUT requires a duration"))?,
1218        body: body.ok_or_else(|| anyhow!("TIMEOUT requires a command or block"))?,
1219    })
1220}
1221
1222fn parse_if_statement_from_pair(
1223    pair: Pair<Rule>,
1224    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1225) -> Result<StepKind> {
1226    let mut cond = None;
1227    let mut then_body = Vec::new();
1228    let mut else_ifs = Vec::new();
1229    let mut else_body = None;
1230
1231    for inner in pair.into_inner() {
1232        match inner.as_rule() {
1233            Rule::expr => {
1234                if cond.is_none() {
1235                    cond = Some(parse_expr(inner)?);
1236                }
1237            }
1238            Rule::block => {
1239                if then_body.is_empty() {
1240                    then_body = parse_block_elements_with_lower(inner, lower)?;
1241                }
1242            }
1243            Rule::else_if_clause => {
1244                let (eif_cond, eif_body) = parse_else_if_clause(inner, lower)?;
1245                else_ifs.push((eif_cond, eif_body));
1246            }
1247            Rule::else_clause => {
1248                else_body = Some(parse_else_clause(inner, lower)?);
1249            }
1250            _ => {}
1251        }
1252    }
1253    Ok(StepKind::If {
1254        cond: Box::new(cond.ok_or_else(|| anyhow!("IF requires a condition"))?),
1255        then_body,
1256        else_ifs,
1257        else_body,
1258    })
1259}
1260
1261fn parse_else_if_clause(
1262    pair: Pair<Rule>,
1263    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1264) -> Result<(Box<Expr>, Vec<Step>)> {
1265    let mut cond = None;
1266    let mut body = Vec::new();
1267    for inner in pair.into_inner() {
1268        match inner.as_rule() {
1269            Rule::expr => cond = Some(parse_expr(inner)?),
1270            Rule::block => body = parse_block_elements_with_lower(inner, lower)?,
1271            _ => {}
1272        }
1273    }
1274    Ok((
1275        Box::new(cond.ok_or_else(|| anyhow!("ELSE IF requires a condition"))?),
1276        body,
1277    ))
1278}
1279
1280fn parse_else_clause(
1281    pair: Pair<Rule>,
1282    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1283) -> Result<Vec<Step>> {
1284    for inner in pair.into_inner() {
1285        if let Rule::block = inner.as_rule() {
1286            return parse_block_elements_with_lower(inner, lower);
1287        }
1288    }
1289    Ok(Vec::new())
1290}
1291
1292fn parse_async_statement_from_pair(
1293    pair: Pair<Rule>,
1294    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1295) -> Result<StepKind> {
1296    let mut inner_cmd = None;
1297    let mut block_body = None;
1298    for inner in pair.into_inner() {
1299        match inner.as_rule() {
1300            Rule::command => {
1301                // command is _{} = silent, so its children aren't visible as pairs
1302                // when nested inside compound-atomic async_statement.
1303                // Parse the command text directly.
1304                let cmd_text = inner.as_str();
1305                let steps = parse_script(cmd_text, |name, args| lower(name, args))?;
1306                if steps.len() == 1 {
1307                    inner_cmd = Some(steps.into_iter().next().unwrap().kind);
1308                } else {
1309                    bail!("unexpected multiple steps in async inner command");
1310                }
1311            }
1312            Rule::command_inner => {
1313                // command_inner = { inherit_env_command | async_statement | async_statement_block | instruction }
1314                let child = inner
1315                    .into_inner()
1316                    .next()
1317                    .ok_or_else(|| anyhow!("empty command_inner"))?;
1318                match child.as_rule() {
1319                    Rule::inherit_env_command => {
1320                        inner_cmd = Some(parse_structural_command_with_lower(child, lower)?);
1321                    }
1322                    Rule::async_statement | Rule::async_statement_block => {
1323                        inner_cmd = Some(parse_structural_command_with_lower(child, lower)?);
1324                    }
1325                    Rule::timeout_statement | Rule::cancel_statement => {
1326                        inner_cmd = Some(parse_structural_command_with_lower(child, lower)?);
1327                    }
1328                    Rule::instruction => {
1329                        inner_cmd = Some(lower_instruction_pair(child, lower)?);
1330                    }
1331                    Rule::run_exec_statement | Rule::run_exec_inner => {
1332                        inner_cmd = Some(lower_run_exec_pair(child, lower)?);
1333                    }
1334                    other => bail!("unexpected command_inner child: {:?}", other),
1335                }
1336            }
1337            Rule::instruction | Rule::instruction_inner => {
1338                inner_cmd = Some(lower_instruction_pair(inner, lower)?);
1339            }
1340            Rule::run_exec_statement | Rule::run_exec_inner => {
1341                inner_cmd = Some(lower_run_exec_pair(inner, lower)?);
1342            }
1343            Rule::block => {
1344                block_body = Some(parse_block_elements_with_lower(inner, lower)?);
1345            }
1346            _ => {}
1347        }
1348    }
1349    if let Some(body) = block_body {
1350        for step in &body {
1351            if matches!(&step.kind, StepKind::WithIo { .. }) {
1352                bail!(
1353                    "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)"
1354                );
1355            }
1356        }
1357        Ok(StepKind::AsyncBlock { body })
1358    } else if let Some(cmd) = inner_cmd {
1359        if matches!(&cmd, StepKind::WithIo { .. }) {
1360            bail!(
1361                "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)"
1362            );
1363        }
1364        Ok(StepKind::AsyncBlock {
1365            body: vec![Step {
1366                guard: None,
1367                kind: cmd,
1368                scope_enter: 0,
1369                scope_exit: 0,
1370            }],
1371        })
1372    } else {
1373        bail!("ASYNC requires either a command or a block");
1374    }
1375}
1376
1377fn parse_async_statement_block_from_pair(
1378    pair: Pair<Rule>,
1379    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1380) -> Result<StepKind> {
1381    let mut block_body = None;
1382    for inner in pair.into_inner() {
1383        if inner.as_rule() == Rule::block {
1384            block_body = Some(parse_block_elements_with_lower(inner, lower)?);
1385        }
1386    }
1387    let body = block_body.ok_or_else(|| anyhow!("async_statement_block requires a block"))?;
1388    for step in &body {
1389        if matches!(&step.kind, StepKind::WithIo { .. }) {
1390            bail!(
1391                "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)"
1392            );
1393        }
1394    }
1395    Ok(StepKind::AsyncBlock { body })
1396}
1397
1398fn parse_block_elements_with_lower(
1399    block_pair: Pair<Rule>,
1400    lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1401) -> Result<Vec<Step>> {
1402    let mut steps = Vec::new();
1403    for elem in block_pair.into_inner() {
1404        match elem.as_rule() {
1405            Rule::for_statement
1406            | Rule::let_statement
1407            | Rule::let_async_statement
1408            | Rule::let_capture_statement
1409            | Rule::await_statement
1410            | Rule::cancel_statement
1411            | Rule::if_statement
1412            | Rule::async_statement
1413            | Rule::timeout_statement
1414            | Rule::async_statement_block => {
1415                let step_kind = parse_structural_command_with_lower(elem, lower)?;
1416                steps.push(Step {
1417                    guard: None,
1418                    kind: step_kind,
1419                    scope_enter: 0,
1420                    scope_exit: 0,
1421                });
1422            }
1423            Rule::guard_block => {
1424                let mut guard_pair = None;
1425                let mut inner_block = None;
1426                for inner in elem.into_inner() {
1427                    match inner.as_rule() {
1428                        Rule::guard_line => guard_pair = Some(inner),
1429                        Rule::block => inner_block = Some(inner),
1430                        _ => {}
1431                    }
1432                }
1433                if let (Some(gp), Some(bp)) = (guard_pair, inner_block) {
1434                    let guard_expr = parse_guard_line(gp)?;
1435                    let mut inner_steps = parse_block_elements_with_lower(bp, lower)?;
1436                    for step in &mut inner_steps {
1437                        step.guard = Some(guard_expr.clone());
1438                    }
1439                    steps.extend(inner_steps);
1440                }
1441            }
1442            Rule::instruction | Rule::instruction_inner => {
1443                let kind = lower_instruction_pair(elem, lower)?;
1444                steps.push(Step {
1445                    guard: None,
1446                    kind,
1447                    scope_enter: 0,
1448                    scope_exit: 0,
1449                });
1450            }
1451            Rule::run_exec_statement | Rule::run_exec_inner => {
1452                let kind = lower_run_exec_pair(elem, lower)?;
1453                steps.push(Step {
1454                    guard: None,
1455                    kind,
1456                    scope_enter: 0,
1457                    scope_exit: 0,
1458                });
1459            }
1460            Rule::with_io_command => {
1461                let step_kind = parse_structural_command_with_lower(elem, lower)?;
1462                steps.push(Step {
1463                    guard: None,
1464                    kind: step_kind,
1465                    scope_enter: 0,
1466                    scope_exit: 0,
1467                });
1468            }
1469            _ => {} // blank, hash_comment, semicolon, block_start, block_end, etc.
1470        }
1471    }
1472    Ok(steps)
1473}
1474
1475fn parse_argument(pair: Pair<Rule>) -> Result<Vec<Arg>> {
1476    let inners: Vec<_> = pair.into_inner().collect();
1477    // An `expr` fragment can swallow its trailing separator through inner
1478    // `gap` rules, gluing following text into one argument pair
1479    // (`ECHO $x hello` lexes as `[expr("$x "), unquoted("hello")]`). Split
1480    // groups there so expressions survive as typed `Arg::Expr`; every other
1481    // fragment kind is whitespace-tight by construction.
1482    let mut groups: Vec<Vec<Pair<Rule>>> = vec![Vec::new()];
1483    for fragment in inners {
1484        let glued = fragment.as_rule() == Rule::expr
1485            && fragment.as_str().ends_with(|c: char| c.is_whitespace());
1486        groups
1487            .last_mut()
1488            .expect("argument always holds a group")
1489            .push(fragment);
1490        if glued {
1491            groups.push(Vec::new());
1492        }
1493    }
1494    let mut args = Vec::new();
1495    for group in groups {
1496        if group.is_empty() {
1497            continue;
1498        }
1499        // Single expression — preserve as Arg::Expr for runtime evaluation
1500        if group.len() == 1 && group[0].as_rule() == Rule::expr {
1501            args.push(Arg::Expr(parse_expr(
1502                group.into_iter().next().expect("group holds one pair"),
1503            )?));
1504            continue;
1505        }
1506        // Single quoted string: preserve quote status and process escapes
1507        if group.len() == 1 && group[0].as_rule() == Rule::string_literal {
1508            args.push(Arg::String(parse_fragments(&group)?, true));
1509            continue;
1510        }
1511        args.push(Arg::String(parse_fragments(&group)?, false));
1512    }
1513    Ok(args)
1514}
1515
1516fn parse_quoted_string(pair: Pair<Rule>) -> Result<String> {
1517    let s = pair.as_str();
1518    let content = &s[1..s.len() - 1];
1519    // Pass contents verbatim — all escape processing deferred to runtime expand_string
1520    Ok(content.to_string())
1521}
1522
1523/// Concatenate fragment pairs (string_literal, templated_arg, unquoted_arg, expr)
1524/// into a single String. Adjacent fragments without whitespace are joined directly;
1525/// fragments separated by whitespace get a space inserted.
1526fn parse_fragments(parts: &[Pair<Rule>]) -> Result<String> {
1527    // Single quoted string: unquote unconditionally
1528    if parts.len() == 1 && parts[0].as_rule() == Rule::string_literal {
1529        let s = parts[0].as_str();
1530        return Ok(s[1..s.len() - 1].to_string());
1531    }
1532
1533    let mut body = String::new();
1534    let mut last_end = None;
1535    for part in parts {
1536        let span = part.as_span();
1537        if let Some(end) = last_end
1538            && span.start() > end
1539        {
1540            body.push(' ');
1541        }
1542        match part.as_rule() {
1543            Rule::string_literal => {
1544                let s = part.as_str();
1545                let unquoted = &s[1..s.len() - 1];
1546                body.push_str(unquoted);
1547            }
1548            Rule::templated_arg | Rule::unquoted_arg => {
1549                body.push_str(part.as_str());
1550            }
1551            Rule::expr => body.push_str(part.as_str()),
1552            _ => {}
1553        }
1554        last_end = Some(span.end());
1555    }
1556    Ok(body)
1557}
1558
1559fn parse_guard_line(pair: Pair<Rule>) -> Result<GuardExpr> {
1560    for inner in pair.into_inner() {
1561        if inner.as_rule() == Rule::guard_expr {
1562            return parse_guard_expr(inner);
1563        }
1564    }
1565    bail!("guard line missing expression")
1566}
1567
1568fn parse_io_binding(pair: Pair<Rule>) -> Result<IoBinding> {
1569    let mut stream = None;
1570    let mut pipe = None;
1571    for inner in pair.into_inner() {
1572        match inner.as_rule() {
1573            Rule::io_stream => stream = Some(parse_io_stream(inner.as_str())),
1574            Rule::pipe_binding => pipe = Some(parse_pipe_binding(inner)?),
1575            _ => {}
1576        }
1577    }
1578    let stream = stream.ok_or_else(|| anyhow!("missing IO stream in WITH_IO"))?;
1579    Ok(IoBinding { stream, pipe })
1580}
1581
1582fn parse_io_stream(text: &str) -> IoStream {
1583    match text {
1584        "stdin" => IoStream::Stdin,
1585        "stdout" => IoStream::Stdout,
1586        "stderr" => IoStream::Stderr,
1587        _ => unreachable!("parser produced invalid io_stream token"),
1588    }
1589}
1590
1591fn parse_pipe_binding(pair: Pair<Rule>) -> Result<String> {
1592    for inner in pair.into_inner() {
1593        if inner.as_rule() == Rule::pipe_name {
1594            return Ok(inner.as_str().to_string());
1595        }
1596    }
1597    bail!("missing pipe identifier in WITH_IO binding");
1598}
1599
1600fn parse_guard_expr(pair: Pair<Rule>) -> Result<GuardExpr> {
1601    match pair.as_rule() {
1602        Rule::guard_expr => {
1603            let next = pair
1604                .into_inner()
1605                .next()
1606                .ok_or_else(|| anyhow!("guard expression missing body"))?;
1607            parse_guard_expr(next)
1608        }
1609        Rule::guard_seq => parse_guard_seq(pair),
1610        Rule::guard_factor => parse_guard_factor(pair),
1611        Rule::guard_not => {
1612            // guard_not is silent, so its inner pairs are the actual content
1613            bail!("guard_not should not create a pair")
1614        }
1615        Rule::guard_primary => parse_guard_primary(pair),
1616        Rule::guard_group => parse_guard_group(pair),
1617        Rule::guard_any_call => parse_guard_any_call(pair),
1618        Rule::guard_all_call => parse_guard_all_call(pair),
1619        Rule::not_call => parse_not_call(pair),
1620        Rule::guard_term => parse_guard_term(pair),
1621        _ => bail!("unexpected guard expression rule: {:?}", pair.as_rule()),
1622    }
1623}
1624
1625fn parse_guard_seq(pair: Pair<Rule>) -> Result<GuardExpr> {
1626    let mut exprs = Vec::new();
1627    for inner in pair.into_inner() {
1628        if inner.as_rule() == Rule::guard_factor {
1629            exprs.push(parse_guard_factor(inner)?);
1630        }
1631    }
1632    match exprs.len() {
1633        0 => bail!("guard list requires at least one entry"),
1634        1 => Ok(exprs.pop().unwrap()),
1635        _ => Ok(GuardExpr::all(exprs)),
1636    }
1637}
1638
1639fn parse_guard_factor(pair: Pair<Rule>) -> Result<GuardExpr> {
1640    let inner = pair
1641        .into_inner()
1642        .next()
1643        .ok_or_else(|| anyhow!("guard factor missing expression"))?;
1644    parse_guard_expr(inner)
1645}
1646
1647fn parse_not_call(pair: Pair<Rule>) -> Result<GuardExpr> {
1648    for inner in pair.into_inner() {
1649        if inner.as_rule() == Rule::guard_expr {
1650            return parse_guard_expr(inner).map(|e| GuardExpr::Not(Box::new(e)));
1651        }
1652    }
1653    bail!("not() missing expression")
1654}
1655
1656fn parse_guard_primary(pair: Pair<Rule>) -> Result<GuardExpr> {
1657    match pair.as_rule() {
1658        Rule::guard_primary => {
1659            let inner = pair
1660                .into_inner()
1661                .next()
1662                .ok_or_else(|| anyhow!("guard primary missing body"))?;
1663            parse_guard_primary(inner)
1664        }
1665        Rule::guard_group => parse_guard_group(pair),
1666        Rule::guard_any_call => parse_guard_any_call(pair),
1667        Rule::guard_all_call => parse_guard_all_call(pair),
1668        Rule::not_call => parse_not_call(pair),
1669        Rule::guard_term => parse_guard_term(pair),
1670        _ => bail!("unexpected guard primary rule: {:?}", pair.as_rule()),
1671    }
1672}
1673
1674fn parse_guard_group(pair: Pair<Rule>) -> Result<GuardExpr> {
1675    for inner in pair.into_inner() {
1676        if inner.as_rule() == Rule::guard_expr {
1677            return parse_guard_expr(inner);
1678        }
1679    }
1680    bail!("grouped guard missing expression")
1681}
1682
1683fn parse_guard_any_call(pair: Pair<Rule>) -> Result<GuardExpr> {
1684    let mut args = Vec::new();
1685    for inner in pair.into_inner() {
1686        if inner.as_rule() == Rule::guard_expr_list {
1687            args = parse_guard_expr_list(inner)?;
1688        }
1689    }
1690    if args.len() < 2 {
1691        bail!("any(...) requires at least two guard expressions");
1692    }
1693    Ok(GuardExpr::or(args))
1694}
1695
1696fn parse_guard_all_call(pair: Pair<Rule>) -> Result<GuardExpr> {
1697    let mut args = Vec::new();
1698    for inner in pair.into_inner() {
1699        if inner.as_rule() == Rule::guard_expr_list {
1700            args = parse_guard_expr_list(inner)?;
1701        }
1702    }
1703    if args.is_empty() {
1704        bail!("all(...) requires at least one guard expression");
1705    }
1706    Ok(GuardExpr::all(args))
1707}
1708
1709fn parse_guard_expr_list(pair: Pair<Rule>) -> Result<Vec<GuardExpr>> {
1710    let mut exprs = Vec::new();
1711    for inner in pair.into_inner() {
1712        if inner.as_rule() == Rule::guard_expr {
1713            push_guard_or_args_from_expr(inner, &mut exprs)?;
1714        }
1715    }
1716    Ok(exprs)
1717}
1718
1719fn push_guard_or_args_from_expr(expr_pair: Pair<Rule>, exprs: &mut Vec<GuardExpr>) -> Result<()> {
1720    if let Some(seq_pair) = expr_pair
1721        .clone()
1722        .into_inner()
1723        .find(|inner| inner.as_rule() == Rule::guard_seq)
1724    {
1725        let factors: Vec<Pair<Rule>> = seq_pair
1726            .into_inner()
1727            .filter(|inner| inner.as_rule() == Rule::guard_factor)
1728            .collect();
1729        if factors.len() > 1 {
1730            for factor in factors {
1731                exprs.push(parse_guard_factor(factor)?);
1732            }
1733            return Ok(());
1734        }
1735    }
1736    exprs.push(parse_guard_expr(expr_pair)?);
1737    Ok(())
1738}
1739
1740fn parse_guard_term(pair: Pair<Rule>) -> Result<GuardExpr> {
1741    for inner in pair.into_inner() {
1742        match inner.as_rule() {
1743            Rule::eq_guard => {
1744                return Ok(GuardExpr::Predicate(parse_func_guard(inner)?));
1745            }
1746            Rule::neq_guard => {
1747                let guard = parse_func_guard(inner)?;
1748                return Ok(GuardExpr::Not(Box::new(GuardExpr::Predicate(guard))));
1749            }
1750            Rule::bool_guard => {
1751                let val = inner
1752                    .into_inner()
1753                    .find(|p| p.as_rule() == Rule::bool_value)
1754                    .expect("grammar invariant violated: bool_guard missing bool_value")
1755                    .as_str()
1756                    .to_string();
1757                return Ok(GuardExpr::Predicate(Guard::StaticBool { value: val }));
1758            }
1759            Rule::env_guard => {
1760                return Ok(GuardExpr::Predicate(parse_env_guard(inner)?));
1761            }
1762            Rule::bare_guard_ident => {
1763                let tag = inner.as_str();
1764                if let Ok(g) = parse_platform_tag(tag) {
1765                    return Ok(GuardExpr::Predicate(g));
1766                }
1767                return Ok(GuardExpr::Predicate(Guard::EnvExists {
1768                    key: tag.to_string(),
1769                }));
1770            }
1771            _ => {}
1772        }
1773    }
1774    bail!("missing guard predicate")
1775}
1776
1777fn parse_func_guard(pair: Pair<Rule>) -> Result<Guard> {
1778    let mut key = String::new();
1779    let mut value = String::new();
1780    let mut saw_env_prefix = false;
1781    for inner in pair.into_inner() {
1782        match inner.as_rule() {
1783            Rule::env_prefix => saw_env_prefix = true,
1784            Rule::env_key if saw_env_prefix => {
1785                key = inner.as_str().trim().to_string();
1786            }
1787            Rule::bare_guard_value | Rule::quoted_string => {
1788                value = unquote(inner.as_str().trim()).to_string();
1789            }
1790            _ => {}
1791        }
1792    }
1793    Ok(Guard::EnvEquals { key, value })
1794}
1795
1796fn unquote(s: &str) -> &str {
1797    s.strip_prefix('"')
1798        .and_then(|s| s.strip_suffix('"'))
1799        .or_else(|| s.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1800        .unwrap_or(s)
1801}
1802
1803fn parse_env_guard(pair: Pair<Rule>) -> Result<Guard> {
1804    let mut key = String::new();
1805    for inner in pair.into_inner() {
1806        if inner.as_rule() == Rule::env_key {
1807            key = inner.as_str().trim().to_string();
1808        }
1809    }
1810    Ok(Guard::EnvExists { key })
1811}
1812
1813fn parse_platform_tag(tag: &str) -> Result<Guard> {
1814    let target = match tag.to_ascii_lowercase().as_str() {
1815        "unix" => PlatformGuard::Unix,
1816        "windows" => PlatformGuard::Windows,
1817        "mac" | "macos" => PlatformGuard::Macos,
1818        "linux" => PlatformGuard::Linux,
1819        _ => bail!("unknown platform '{}'", tag),
1820    };
1821    Ok(Guard::Platform { target })
1822}
1823
1824fn parse_dollar_ident(pair: Pair<Rule>) -> String {
1825    // Strip the leading '$' from the identifier
1826    let s = pair.as_str();
1827    s.strip_prefix('$').unwrap_or(s).to_string()
1828}
1829
1830use crate::ast::{CompareOp, LogicalOp, Value};
1831
1832fn parse_expr(pair: Pair<Rule>) -> Result<Expr> {
1833    let inner = pair.into_inner().next().unwrap();
1834    match inner.as_rule() {
1835        Rule::expr_logical_or => parse_expr_logical_or(inner),
1836        _ => bail!("unexpected expr rule: {:?}", inner.as_rule()),
1837    }
1838}
1839
1840fn parse_expr_logical_or(pair: Pair<Rule>) -> Result<Expr> {
1841    let mut inner = pair.into_inner();
1842    let mut left = parse_expr_logical_and(inner.next().unwrap())?;
1843    while let Some(op_pair) = inner.next() {
1844        let op = match op_pair.as_rule() {
1845            Rule::or_op => LogicalOp::Or,
1846            _ => bail!("unexpected operator in logical-or: {:?}", op_pair.as_rule()),
1847        };
1848        let right = parse_expr_logical_and(inner.next().unwrap())?;
1849        left = Expr::Logical {
1850            op,
1851            left: Box::new(left),
1852            right: Box::new(right),
1853        };
1854    }
1855    Ok(left)
1856}
1857
1858fn parse_expr_logical_and(pair: Pair<Rule>) -> Result<Expr> {
1859    let mut inner = pair.into_inner();
1860    let mut left = parse_expr_comparison(inner.next().unwrap())?;
1861    while let Some(op_pair) = inner.next() {
1862        let op = match op_pair.as_rule() {
1863            Rule::and_op => LogicalOp::And,
1864            _ => bail!(
1865                "unexpected operator in logical-and: {:?}",
1866                op_pair.as_rule()
1867            ),
1868        };
1869        let right = parse_expr_comparison(inner.next().unwrap())?;
1870        left = Expr::Logical {
1871            op,
1872            left: Box::new(left),
1873            right: Box::new(right),
1874        };
1875    }
1876    Ok(left)
1877}
1878
1879fn parse_expr_comparison(pair: Pair<Rule>) -> Result<Expr> {
1880    let mut inner = pair.into_inner();
1881    let left = parse_expr_unary(inner.next().unwrap())?;
1882    if let Some(op_pair) = inner.next() {
1883        let op = match op_pair.as_rule() {
1884            Rule::eq_op => CompareOp::Eq,
1885            Rule::neq_op => CompareOp::Ne,
1886            _ => bail!("unexpected comparison operator: {:?}", op_pair.as_rule()),
1887        };
1888        let right = parse_expr_unary(inner.next().unwrap())?;
1889        Ok(Expr::Compare {
1890            op,
1891            left: Box::new(left),
1892            right: Box::new(right),
1893        })
1894    } else {
1895        Ok(left)
1896    }
1897}
1898
1899fn parse_expr_unary(pair: Pair<Rule>) -> Result<Expr> {
1900    let mut bangs = 0u32;
1901    let mut atom = None;
1902    for inner in pair.into_inner() {
1903        match inner.as_rule() {
1904            Rule::not_op => bangs += 1,
1905            Rule::expr_atom => atom = Some(parse_expr_atom(inner)?),
1906            _ => bail!("unexpected unary operand rule: {:?}", inner.as_rule()),
1907        }
1908    }
1909    let mut expr = atom.ok_or_else(|| anyhow!("'!' requires an expression operand"))?;
1910    for _ in 0..bangs {
1911        expr = Expr::Not(Box::new(expr));
1912    }
1913    Ok(expr)
1914}
1915
1916fn parse_expr_atom(pair: Pair<Rule>) -> Result<Expr> {
1917    let inner = pair.into_inner().next().unwrap();
1918    match inner.as_rule() {
1919        Rule::parenthesized_expr => parse_expr(inner.into_inner().next().unwrap()),
1920        Rule::func_call => parse_func_call(inner),
1921        Rule::key_path => parse_key_path(inner),
1922        Rule::variable => {
1923            let name = inner.as_str();
1924            let name = name.strip_prefix('$').unwrap_or(name).to_string();
1925            Ok(Expr::Var(name))
1926        }
1927        Rule::list_literal => parse_list_literal(inner),
1928        Rule::map_literal => parse_map_literal(inner),
1929        Rule::string_literal | Rule::quoted_string => {
1930            let s = parse_quoted_string(inner)?;
1931            Ok(Expr::Literal(Value::String(s)))
1932        }
1933        Rule::bare_word => {
1934            let s = inner.as_str().to_string();
1935            match s.as_str() {
1936                "true" => Ok(Expr::Literal(Value::Bool(true))),
1937                "false" => Ok(Expr::Literal(Value::Bool(false))),
1938                _ => Ok(Expr::Literal(Value::String(s))),
1939            }
1940        }
1941        _ => bail!("unexpected expression atom rule: {:?}", inner.as_rule()),
1942    }
1943}
1944
1945fn parse_key_path(pair: Pair<Rule>) -> Result<Expr> {
1946    let mut base = None;
1947    let mut keys = Vec::new();
1948    for inner in pair.into_inner() {
1949        match inner.as_rule() {
1950            Rule::ident => {
1951                if base.is_none() {
1952                    base = Some(inner.as_str().to_string());
1953                }
1954            }
1955            Rule::key_path_segment => {
1956                keys.push(inner.as_str().to_string());
1957            }
1958            _ => {}
1959        }
1960    }
1961    Ok(Expr::KeyPath {
1962        base: base.ok_or_else(|| anyhow!("key path requires a base identifier"))?,
1963        keys,
1964    })
1965}
1966
1967fn parse_func_call(pair: Pair<Rule>) -> Result<Expr> {
1968    let mut name = None;
1969    let mut args = Vec::new();
1970    for inner in pair.into_inner() {
1971        match inner.as_rule() {
1972            Rule::ident => {
1973                name = Some(inner.as_str().to_string());
1974            }
1975            Rule::expr => {
1976                args.push(parse_expr(inner)?);
1977            }
1978            _ => {}
1979        }
1980    }
1981    Ok(Expr::Call {
1982        name: name.ok_or_else(|| anyhow!("function call requires a name"))?,
1983        args,
1984    })
1985}
1986
1987fn parse_list_literal(pair: Pair<Rule>) -> Result<Expr> {
1988    let mut items = Vec::new();
1989    for inner in pair.into_inner() {
1990        if inner.as_rule() == Rule::expr {
1991            items.push(parse_expr(inner)?);
1992        }
1993    }
1994    Ok(Expr::List(items))
1995}
1996
1997fn parse_map_literal(pair: Pair<Rule>) -> Result<Expr> {
1998    let mut entries = Vec::new();
1999    for inner in pair.into_inner() {
2000        if inner.as_rule() == Rule::map_entry {
2001            let mut key = String::new();
2002            let mut value = None;
2003            for entry_inner in inner.into_inner() {
2004                match entry_inner.as_rule() {
2005                    Rule::quoted_string => {
2006                        key = parse_quoted_string(entry_inner)?;
2007                    }
2008                    Rule::bare_word => {
2009                        key = entry_inner.as_str().to_string();
2010                    }
2011                    Rule::expr => {
2012                        value = Some(parse_expr(entry_inner)?);
2013                    }
2014                    _ => {}
2015                }
2016            }
2017            let val = value.ok_or_else(|| anyhow!("map entry missing value"))?;
2018            entries.push((key, val));
2019        }
2020    }
2021    Ok(Expr::Map(entries))
2022}