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