1use crate::ast::{Arg, 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 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 {
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 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 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
583enum InsToken {
588 Pos(Arg),
589 Assign(String, Arg),
590}
591
592fn 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
617fn 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
638fn 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
666fn 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
684fn 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
703fn 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)] => {
712 ArgType::KeyValue
715 .check_arg(&Arg::String(format!("{key}={}", value.render()), false))?;
716 Ok(StepKind::Env {
717 key: key.clone(),
718 value: value.clone(),
719 })
720 }
721 [InsToken::Pos(Arg::String(text, _))] => match crate::command::split_assignment(text)? {
722 Some((key, value)) => Ok(StepKind::Env { key, value }),
723 None => bail!("ENV requires KEY=value format"),
724 },
725 _ => bail!("ENV requires KEY=value format"),
726 }
727}
728
729fn lower_expand_command(tokens: Vec<InsToken>) -> Result<StepKind> {
733 let mut path = None;
734 let mut overrides = Vec::new();
735 for token in tokens {
736 match token {
737 InsToken::Assign(key, value) => {
738 if key.is_empty() {
739 bail!("EXPAND requires KEY=value format for overrides")
740 }
741 overrides.push((key, value));
742 }
743 InsToken::Pos(arg) => match &arg {
744 Arg::String(text, quoted) if !quoted && text.contains('=') => {
745 let Some((key, value)) = crate::command::split_assignment(text)? else {
746 bail!("EXPAND requires KEY=value format for overrides")
747 };
748 overrides.push((key, value));
749 }
750 _ => {
751 if path.is_none() {
752 ArgType::Path.check_arg(&arg)?;
756 path = Some(arg);
757 } else {
758 bail!("EXPAND accepts at most one path");
759 }
760 }
761 },
762 }
763 }
764 Ok(StepKind::Expand { path, overrides })
765}
766
767fn parse_for_statement_from_pair(
768 pair: Pair<Rule>,
769 lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
770) -> Result<StepKind> {
771 let mut idents = Vec::new();
772 let mut in_expr = None;
773 let mut body_steps = Vec::new();
774 for inner in pair.into_inner() {
775 match inner.as_rule() {
776 Rule::dollar_ident => {
777 idents.push(parse_dollar_ident(inner));
778 }
779 Rule::expr => {
780 in_expr = Some(parse_expr(inner)?);
781 }
782 Rule::block => {
783 body_steps = parse_block_elements_with_lower(inner, lower)?;
784 }
785 _ => {}
786 }
787 }
788 let (key_var, var) = match idents.len() {
789 1 => (None, idents.into_iter().next().unwrap()),
790 2 => {
791 let mut iter = idents.into_iter();
792 (Some(iter.next().unwrap()), iter.next().unwrap())
793 }
794 _ => bail!("FOR requires at least one variable"),
795 };
796 Ok(StepKind::For {
797 key_var,
798 var,
799 in_expr: in_expr.ok_or_else(|| anyhow!("FOR requires an iterable expression"))?,
800 body: body_steps,
801 })
802}
803
804fn parse_let_statement_from_pair(pair: Pair<Rule>) -> Result<StepKind> {
805 let mut var = None;
806 let mut expr = None;
807 for inner in pair.into_inner() {
808 match inner.as_rule() {
809 Rule::dollar_ident => {
810 var = Some(parse_dollar_ident(inner));
811 }
812 Rule::expr => {
813 expr = Some(parse_expr(inner)?);
814 }
815 _ => {}
816 }
817 }
818 Ok(StepKind::Assign {
819 var: var.ok_or_else(|| anyhow!("LET requires a variable"))?,
820 expr: expr.ok_or_else(|| anyhow!("LET requires an expression"))?,
821 })
822}
823
824fn parse_let_async_statement_from_pair(
825 pair: Pair<Rule>,
826 lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
827) -> Result<StepKind> {
828 let mut var = None;
829 let mut body = None;
830 for inner in pair.into_inner() {
831 match inner.as_rule() {
832 Rule::dollar_ident => {
833 var = Some(parse_dollar_ident(inner));
834 }
835 Rule::block => {
836 body = Some(parse_block_elements_with_lower(inner, lower)?);
837 }
838 Rule::command_inner => {
839 let inner = inner
842 .into_inner()
843 .next()
844 .ok_or_else(|| anyhow!("empty command_inner"))?;
845 let step_kind = parse_structural_command_with_lower(inner, lower)?;
846 body = Some(vec![Step {
847 guard: None,
848 kind: step_kind,
849 scope_enter: 0,
850 scope_exit: 0,
851 }]);
852 }
853 Rule::with_io_command => {
854 let kind = parse_structural_command_with_lower(inner, lower)?;
859 let StepKind::WithIo { bindings, cmd } = kind else {
860 bail!(
861 "LET $var = WITH_IO requires an ASYNC command (e.g. LET $t = WITH_IO [stdin=pipe:p] ASYNC WRITE \"f\")"
862 );
863 };
864 let StepKind::AsyncBlock { body: async_body } = *cmd else {
865 bail!(
866 "LET $var = WITH_IO requires an ASYNC command (e.g. LET $t = WITH_IO [stdin=pipe:p] ASYNC WRITE \"f\")"
867 );
868 };
869 if async_body.len() != 1 {
870 bail!(
871 "LET $var = WITH_IO [..] ASYNC accepts a single command; use LET $var = ASYNC {{ ... }} with WITH_IO inside the block for multi-step tasks"
872 );
873 }
874 let step = async_body
875 .into_iter()
876 .next()
877 .ok_or_else(|| anyhow!("LET $var = ASYNC requires a body"))?;
878 body = Some(vec![Step {
879 guard: step.guard,
880 kind: StepKind::WithIo {
881 bindings,
882 cmd: Box::new(step.kind),
883 },
884 scope_enter: step.scope_enter,
885 scope_exit: step.scope_exit,
886 }]);
887 }
888 _ => {}
889 }
890 }
891 Ok(StepKind::AssignAsync {
892 var: var.ok_or_else(|| anyhow!("LET $var = ASYNC requires a variable"))?,
893 body: body.ok_or_else(|| anyhow!("LET $var = ASYNC requires a body"))?,
894 })
895}
896
897fn parse_await_statement_from_pair(pair: Pair<Rule>) -> Result<StepKind> {
898 let mut var = None;
899 for inner in pair.into_inner() {
900 if inner.as_rule() == Rule::ident {
901 var = Some(inner.as_str().to_string());
902 }
903 }
904 Ok(StepKind::Await {
905 var: var.ok_or_else(|| anyhow!("AWAIT requires a variable"))?,
906 })
907}
908
909fn parse_cancel_statement_from_pair(pair: Pair<Rule>) -> Result<StepKind> {
910 let mut var = None;
911 for inner in pair.into_inner() {
912 if inner.as_rule() == Rule::ident {
913 var = Some(inner.as_str().to_string());
914 }
915 }
916 Ok(StepKind::Cancel {
917 var: var.ok_or_else(|| anyhow!("CANCEL requires a variable"))?,
918 })
919}
920
921fn parse_timeout_duration_arg(pair: Pair<Rule>) -> Result<Arg> {
925 for inner in pair.into_inner() {
926 let arg = match inner.as_rule() {
927 Rule::timeout_literal => Arg::String(inner.as_str().to_string(), false),
928 Rule::dollar_ident => Arg::Expr(Expr::Var(parse_dollar_ident(inner))),
929 Rule::quoted_string => Arg::String(
930 crate::command::strip_surrounding_quotes(inner.as_str()).to_string(),
931 true,
932 ),
933 Rule::templated_arg => Arg::String(inner.as_str().to_string(), false),
934 _ => continue,
935 };
936 ArgType::Duration.check_arg(&arg)?;
937 return Ok(arg);
938 }
939 bail!("TIMEOUT requires a duration")
940}
941
942fn parse_timeout_statement_from_pair(
943 pair: Pair<Rule>,
944 lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
945) -> Result<StepKind> {
946 let mut duration: Option<Arg> = None;
947 let mut body: Option<Vec<Step>> = None;
948 for inner in pair.into_inner() {
949 match inner.as_rule() {
950 Rule::timeout_duration => {
951 duration = Some(parse_timeout_duration_arg(inner)?);
952 }
953 Rule::block => {
954 body = Some(parse_block_elements_with_lower(inner, lower)?);
955 }
956 Rule::await_statement => {
957 let kind = parse_await_statement_from_pair(inner)?;
958 body = Some(vec![Step {
959 guard: None,
960 kind,
961 scope_enter: 0,
962 scope_exit: 0,
963 }]);
964 }
965 Rule::cancel_statement => {
966 let kind = parse_cancel_statement_from_pair(inner)?;
967 body = Some(vec![Step {
968 guard: None,
969 kind,
970 scope_enter: 0,
971 scope_exit: 0,
972 }]);
973 }
974 Rule::with_io_command
975 | Rule::inherit_env_command
976 | Rule::async_statement
977 | Rule::async_statement_block
978 | Rule::timeout_statement => {
979 let kind = parse_structural_command_with_lower(inner, lower)?;
980 body = Some(vec![Step {
981 guard: None,
982 kind,
983 scope_enter: 0,
984 scope_exit: 0,
985 }]);
986 }
987 Rule::instruction | Rule::instruction_inner => {
988 let kind = lower_instruction_pair(inner, lower)?;
989 body = Some(vec![Step {
990 guard: None,
991 kind,
992 scope_enter: 0,
993 scope_exit: 0,
994 }]);
995 }
996 _ => {}
997 }
998 }
999 Ok(StepKind::Timeout {
1000 duration: duration.ok_or_else(|| anyhow!("TIMEOUT requires a duration"))?,
1001 body: body.ok_or_else(|| anyhow!("TIMEOUT requires a command or block"))?,
1002 })
1003}
1004
1005fn parse_if_statement_from_pair(
1006 pair: Pair<Rule>,
1007 lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1008) -> Result<StepKind> {
1009 let mut cond = None;
1010 let mut then_body = Vec::new();
1011 let mut else_ifs = Vec::new();
1012 let mut else_body = None;
1013
1014 for inner in pair.into_inner() {
1015 match inner.as_rule() {
1016 Rule::expr => {
1017 if cond.is_none() {
1018 cond = Some(parse_expr(inner)?);
1019 }
1020 }
1021 Rule::block => {
1022 if then_body.is_empty() {
1023 then_body = parse_block_elements_with_lower(inner, lower)?;
1024 }
1025 }
1026 Rule::else_if_clause => {
1027 let (eif_cond, eif_body) = parse_else_if_clause(inner, lower)?;
1028 else_ifs.push((eif_cond, eif_body));
1029 }
1030 Rule::else_clause => {
1031 else_body = Some(parse_else_clause(inner, lower)?);
1032 }
1033 _ => {}
1034 }
1035 }
1036 Ok(StepKind::If {
1037 cond: Box::new(cond.ok_or_else(|| anyhow!("IF requires a condition"))?),
1038 then_body,
1039 else_ifs,
1040 else_body,
1041 })
1042}
1043
1044fn parse_else_if_clause(
1045 pair: Pair<Rule>,
1046 lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1047) -> Result<(Box<Expr>, Vec<Step>)> {
1048 let mut cond = None;
1049 let mut body = Vec::new();
1050 for inner in pair.into_inner() {
1051 match inner.as_rule() {
1052 Rule::expr => cond = Some(parse_expr(inner)?),
1053 Rule::block => body = parse_block_elements_with_lower(inner, lower)?,
1054 _ => {}
1055 }
1056 }
1057 Ok((
1058 Box::new(cond.ok_or_else(|| anyhow!("ELSE IF requires a condition"))?),
1059 body,
1060 ))
1061}
1062
1063fn parse_else_clause(
1064 pair: Pair<Rule>,
1065 lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1066) -> Result<Vec<Step>> {
1067 for inner in pair.into_inner() {
1068 if let Rule::block = inner.as_rule() {
1069 return parse_block_elements_with_lower(inner, lower);
1070 }
1071 }
1072 Ok(Vec::new())
1073}
1074
1075fn parse_async_statement_from_pair(
1076 pair: Pair<Rule>,
1077 lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1078) -> Result<StepKind> {
1079 let mut inner_cmd = None;
1080 let mut block_body = None;
1081 for inner in pair.into_inner() {
1082 match inner.as_rule() {
1083 Rule::command => {
1084 let cmd_text = inner.as_str();
1088 let steps = parse_script(cmd_text, |name, args| lower(name, args))?;
1089 if steps.len() == 1 {
1090 inner_cmd = Some(steps.into_iter().next().unwrap().kind);
1091 } else {
1092 bail!("unexpected multiple steps in async inner command");
1093 }
1094 }
1095 Rule::command_inner => {
1096 let child = inner
1098 .into_inner()
1099 .next()
1100 .ok_or_else(|| anyhow!("empty command_inner"))?;
1101 match child.as_rule() {
1102 Rule::inherit_env_command => {
1103 inner_cmd = Some(parse_structural_command_with_lower(child, lower)?);
1104 }
1105 Rule::async_statement | Rule::async_statement_block => {
1106 inner_cmd = Some(parse_structural_command_with_lower(child, lower)?);
1107 }
1108 Rule::timeout_statement | Rule::cancel_statement => {
1109 inner_cmd = Some(parse_structural_command_with_lower(child, lower)?);
1110 }
1111 Rule::instruction => {
1112 inner_cmd = Some(lower_instruction_pair(child, lower)?);
1113 }
1114 other => bail!("unexpected command_inner child: {:?}", other),
1115 }
1116 }
1117 Rule::instruction | Rule::instruction_inner => {
1118 inner_cmd = Some(lower_instruction_pair(inner, lower)?);
1119 }
1120 Rule::block => {
1121 block_body = Some(parse_block_elements_with_lower(inner, lower)?);
1122 }
1123 _ => {}
1124 }
1125 }
1126 if let Some(body) = block_body {
1127 for step in &body {
1128 if matches!(&step.kind, StepKind::WithIo { .. }) {
1129 bail!(
1130 "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)"
1131 );
1132 }
1133 }
1134 Ok(StepKind::AsyncBlock { body })
1135 } else if let Some(cmd) = inner_cmd {
1136 if matches!(&cmd, StepKind::WithIo { .. }) {
1137 bail!(
1138 "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)"
1139 );
1140 }
1141 Ok(StepKind::AsyncBlock {
1142 body: vec![Step {
1143 guard: None,
1144 kind: cmd,
1145 scope_enter: 0,
1146 scope_exit: 0,
1147 }],
1148 })
1149 } else {
1150 bail!("ASYNC requires either a command or a block");
1151 }
1152}
1153
1154fn parse_async_statement_block_from_pair(
1155 pair: Pair<Rule>,
1156 lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1157) -> Result<StepKind> {
1158 let mut block_body = None;
1159 for inner in pair.into_inner() {
1160 if inner.as_rule() == Rule::block {
1161 block_body = Some(parse_block_elements_with_lower(inner, lower)?);
1162 }
1163 }
1164 let body = block_body.ok_or_else(|| anyhow!("async_statement_block requires a block"))?;
1165 for step in &body {
1166 if matches!(&step.kind, StepKind::WithIo { .. }) {
1167 bail!(
1168 "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)"
1169 );
1170 }
1171 }
1172 Ok(StepKind::AsyncBlock { body })
1173}
1174
1175fn parse_block_elements_with_lower(
1176 block_pair: Pair<Rule>,
1177 lower: &dyn Fn(&str, Vec<Arg>) -> Result<StepKind>,
1178) -> Result<Vec<Step>> {
1179 let mut steps = Vec::new();
1180 for elem in block_pair.into_inner() {
1181 match elem.as_rule() {
1182 Rule::for_statement
1183 | Rule::let_statement
1184 | Rule::let_async_statement
1185 | Rule::await_statement
1186 | Rule::cancel_statement
1187 | Rule::if_statement
1188 | Rule::async_statement
1189 | Rule::timeout_statement
1190 | Rule::async_statement_block => {
1191 let step_kind = parse_structural_command_with_lower(elem, lower)?;
1192 steps.push(Step {
1193 guard: None,
1194 kind: step_kind,
1195 scope_enter: 0,
1196 scope_exit: 0,
1197 });
1198 }
1199 Rule::guard_block => {
1200 let mut guard_pair = None;
1201 let mut inner_block = None;
1202 for inner in elem.into_inner() {
1203 match inner.as_rule() {
1204 Rule::guard_line => guard_pair = Some(inner),
1205 Rule::block => inner_block = Some(inner),
1206 _ => {}
1207 }
1208 }
1209 if let (Some(gp), Some(bp)) = (guard_pair, inner_block) {
1210 let guard_expr = parse_guard_line(gp)?;
1211 let mut inner_steps = parse_block_elements_with_lower(bp, lower)?;
1212 for step in &mut inner_steps {
1213 step.guard = Some(guard_expr.clone());
1214 }
1215 steps.extend(inner_steps);
1216 }
1217 }
1218 Rule::instruction | Rule::instruction_inner => {
1219 let kind = lower_instruction_pair(elem, lower)?;
1220 steps.push(Step {
1221 guard: None,
1222 kind,
1223 scope_enter: 0,
1224 scope_exit: 0,
1225 });
1226 }
1227 Rule::with_io_command => {
1228 let step_kind = parse_structural_command_with_lower(elem, lower)?;
1229 steps.push(Step {
1230 guard: None,
1231 kind: step_kind,
1232 scope_enter: 0,
1233 scope_exit: 0,
1234 });
1235 }
1236 _ => {} }
1238 }
1239 Ok(steps)
1240}
1241
1242fn parse_argument(pair: Pair<Rule>) -> Result<Vec<Arg>> {
1243 let inners: Vec<_> = pair.into_inner().collect();
1244 let mut groups: Vec<Vec<Pair<Rule>>> = vec![Vec::new()];
1250 for fragment in inners {
1251 let glued = fragment.as_rule() == Rule::expr
1252 && fragment.as_str().ends_with(|c: char| c.is_whitespace());
1253 groups
1254 .last_mut()
1255 .expect("argument always holds a group")
1256 .push(fragment);
1257 if glued {
1258 groups.push(Vec::new());
1259 }
1260 }
1261 let mut args = Vec::new();
1262 for group in groups {
1263 if group.is_empty() {
1264 continue;
1265 }
1266 if group.len() == 1 && group[0].as_rule() == Rule::expr {
1268 args.push(Arg::Expr(parse_expr(
1269 group.into_iter().next().expect("group holds one pair"),
1270 )?));
1271 continue;
1272 }
1273 if group.len() == 1 && group[0].as_rule() == Rule::string_literal {
1275 args.push(Arg::String(parse_fragments(&group)?, true));
1276 continue;
1277 }
1278 args.push(Arg::String(parse_fragments(&group)?, false));
1279 }
1280 Ok(args)
1281}
1282
1283fn parse_quoted_string(pair: Pair<Rule>) -> Result<String> {
1284 let s = pair.as_str();
1285 let content = &s[1..s.len() - 1];
1286 Ok(content.to_string())
1288}
1289
1290fn parse_fragments(parts: &[Pair<Rule>]) -> Result<String> {
1294 if parts.len() == 1 && parts[0].as_rule() == Rule::string_literal {
1296 let s = parts[0].as_str();
1297 return Ok(s[1..s.len() - 1].to_string());
1298 }
1299
1300 let mut body = String::new();
1301 let mut last_end = None;
1302 for part in parts {
1303 let span = part.as_span();
1304 if let Some(end) = last_end
1305 && span.start() > end
1306 {
1307 body.push(' ');
1308 }
1309 match part.as_rule() {
1310 Rule::string_literal => {
1311 let s = part.as_str();
1312 let unquoted = &s[1..s.len() - 1];
1313 body.push_str(unquoted);
1314 }
1315 Rule::templated_arg | Rule::unquoted_arg => {
1316 body.push_str(part.as_str());
1317 }
1318 Rule::expr => body.push_str(part.as_str()),
1319 _ => {}
1320 }
1321 last_end = Some(span.end());
1322 }
1323 Ok(body)
1324}
1325
1326fn parse_guard_line(pair: Pair<Rule>) -> Result<GuardExpr> {
1327 for inner in pair.into_inner() {
1328 if inner.as_rule() == Rule::guard_expr {
1329 return parse_guard_expr(inner);
1330 }
1331 }
1332 bail!("guard line missing expression")
1333}
1334
1335fn parse_io_binding(pair: Pair<Rule>) -> Result<IoBinding> {
1336 let mut stream = None;
1337 let mut pipe = None;
1338 for inner in pair.into_inner() {
1339 match inner.as_rule() {
1340 Rule::io_stream => stream = Some(parse_io_stream(inner.as_str())),
1341 Rule::pipe_binding => pipe = Some(parse_pipe_binding(inner)?),
1342 _ => {}
1343 }
1344 }
1345 let stream = stream.ok_or_else(|| anyhow!("missing IO stream in WITH_IO"))?;
1346 Ok(IoBinding { stream, pipe })
1347}
1348
1349fn parse_io_stream(text: &str) -> IoStream {
1350 match text {
1351 "stdin" => IoStream::Stdin,
1352 "stdout" => IoStream::Stdout,
1353 "stderr" => IoStream::Stderr,
1354 _ => unreachable!("parser produced invalid io_stream token"),
1355 }
1356}
1357
1358fn parse_pipe_binding(pair: Pair<Rule>) -> Result<String> {
1359 for inner in pair.into_inner() {
1360 if inner.as_rule() == Rule::pipe_name {
1361 return Ok(inner.as_str().to_string());
1362 }
1363 }
1364 bail!("missing pipe identifier in WITH_IO binding");
1365}
1366
1367fn parse_guard_expr(pair: Pair<Rule>) -> Result<GuardExpr> {
1368 match pair.as_rule() {
1369 Rule::guard_expr => {
1370 let next = pair
1371 .into_inner()
1372 .next()
1373 .ok_or_else(|| anyhow!("guard expression missing body"))?;
1374 parse_guard_expr(next)
1375 }
1376 Rule::guard_seq => parse_guard_seq(pair),
1377 Rule::guard_factor => parse_guard_factor(pair),
1378 Rule::guard_not => {
1379 bail!("guard_not should not create a pair")
1381 }
1382 Rule::guard_primary => parse_guard_primary(pair),
1383 Rule::guard_group => parse_guard_group(pair),
1384 Rule::guard_any_call => parse_guard_any_call(pair),
1385 Rule::guard_all_call => parse_guard_all_call(pair),
1386 Rule::not_call => parse_not_call(pair),
1387 Rule::guard_term => parse_guard_term(pair),
1388 _ => bail!("unexpected guard expression rule: {:?}", pair.as_rule()),
1389 }
1390}
1391
1392fn parse_guard_seq(pair: Pair<Rule>) -> Result<GuardExpr> {
1393 let mut exprs = Vec::new();
1394 for inner in pair.into_inner() {
1395 if inner.as_rule() == Rule::guard_factor {
1396 exprs.push(parse_guard_factor(inner)?);
1397 }
1398 }
1399 match exprs.len() {
1400 0 => bail!("guard list requires at least one entry"),
1401 1 => Ok(exprs.pop().unwrap()),
1402 _ => Ok(GuardExpr::all(exprs)),
1403 }
1404}
1405
1406fn parse_guard_factor(pair: Pair<Rule>) -> Result<GuardExpr> {
1407 let inner = pair
1408 .into_inner()
1409 .next()
1410 .ok_or_else(|| anyhow!("guard factor missing expression"))?;
1411 parse_guard_expr(inner)
1412}
1413
1414fn parse_not_call(pair: Pair<Rule>) -> Result<GuardExpr> {
1415 for inner in pair.into_inner() {
1416 if inner.as_rule() == Rule::guard_expr {
1417 return parse_guard_expr(inner).map(|e| GuardExpr::Not(Box::new(e)));
1418 }
1419 }
1420 bail!("not() missing expression")
1421}
1422
1423fn parse_guard_primary(pair: Pair<Rule>) -> Result<GuardExpr> {
1424 match pair.as_rule() {
1425 Rule::guard_primary => {
1426 let inner = pair
1427 .into_inner()
1428 .next()
1429 .ok_or_else(|| anyhow!("guard primary missing body"))?;
1430 parse_guard_primary(inner)
1431 }
1432 Rule::guard_group => parse_guard_group(pair),
1433 Rule::guard_any_call => parse_guard_any_call(pair),
1434 Rule::guard_all_call => parse_guard_all_call(pair),
1435 Rule::not_call => parse_not_call(pair),
1436 Rule::guard_term => parse_guard_term(pair),
1437 _ => bail!("unexpected guard primary rule: {:?}", pair.as_rule()),
1438 }
1439}
1440
1441fn parse_guard_group(pair: Pair<Rule>) -> Result<GuardExpr> {
1442 for inner in pair.into_inner() {
1443 if inner.as_rule() == Rule::guard_expr {
1444 return parse_guard_expr(inner);
1445 }
1446 }
1447 bail!("grouped guard missing expression")
1448}
1449
1450fn parse_guard_any_call(pair: Pair<Rule>) -> Result<GuardExpr> {
1451 let mut args = Vec::new();
1452 for inner in pair.into_inner() {
1453 if inner.as_rule() == Rule::guard_expr_list {
1454 args = parse_guard_expr_list(inner)?;
1455 }
1456 }
1457 if args.len() < 2 {
1458 bail!("any(...) requires at least two guard expressions");
1459 }
1460 Ok(GuardExpr::or(args))
1461}
1462
1463fn parse_guard_all_call(pair: Pair<Rule>) -> Result<GuardExpr> {
1464 let mut args = Vec::new();
1465 for inner in pair.into_inner() {
1466 if inner.as_rule() == Rule::guard_expr_list {
1467 args = parse_guard_expr_list(inner)?;
1468 }
1469 }
1470 if args.is_empty() {
1471 bail!("all(...) requires at least one guard expression");
1472 }
1473 Ok(GuardExpr::all(args))
1474}
1475
1476fn parse_guard_expr_list(pair: Pair<Rule>) -> Result<Vec<GuardExpr>> {
1477 let mut exprs = Vec::new();
1478 for inner in pair.into_inner() {
1479 if inner.as_rule() == Rule::guard_expr {
1480 push_guard_or_args_from_expr(inner, &mut exprs)?;
1481 }
1482 }
1483 Ok(exprs)
1484}
1485
1486fn push_guard_or_args_from_expr(expr_pair: Pair<Rule>, exprs: &mut Vec<GuardExpr>) -> Result<()> {
1487 if let Some(seq_pair) = expr_pair
1488 .clone()
1489 .into_inner()
1490 .find(|inner| inner.as_rule() == Rule::guard_seq)
1491 {
1492 let factors: Vec<Pair<Rule>> = seq_pair
1493 .into_inner()
1494 .filter(|inner| inner.as_rule() == Rule::guard_factor)
1495 .collect();
1496 if factors.len() > 1 {
1497 for factor in factors {
1498 exprs.push(parse_guard_factor(factor)?);
1499 }
1500 return Ok(());
1501 }
1502 }
1503 exprs.push(parse_guard_expr(expr_pair)?);
1504 Ok(())
1505}
1506
1507fn parse_guard_term(pair: Pair<Rule>) -> Result<GuardExpr> {
1508 for inner in pair.into_inner() {
1509 match inner.as_rule() {
1510 Rule::eq_guard => {
1511 return Ok(GuardExpr::Predicate(parse_func_guard(inner)?));
1512 }
1513 Rule::neq_guard => {
1514 let guard = parse_func_guard(inner)?;
1515 return Ok(GuardExpr::Not(Box::new(GuardExpr::Predicate(guard))));
1516 }
1517 Rule::bool_guard => {
1518 let val = inner
1519 .into_inner()
1520 .find(|p| p.as_rule() == Rule::bool_value)
1521 .expect("grammar invariant violated: bool_guard missing bool_value")
1522 .as_str()
1523 .to_string();
1524 return Ok(GuardExpr::Predicate(Guard::StaticBool { value: val }));
1525 }
1526 Rule::env_guard => {
1527 return Ok(GuardExpr::Predicate(parse_env_guard(inner)?));
1528 }
1529 Rule::bare_guard_ident => {
1530 let tag = inner.as_str();
1531 if let Ok(g) = parse_platform_tag(tag) {
1532 return Ok(GuardExpr::Predicate(g));
1533 }
1534 return Ok(GuardExpr::Predicate(Guard::EnvExists {
1535 key: tag.to_string(),
1536 }));
1537 }
1538 _ => {}
1539 }
1540 }
1541 bail!("missing guard predicate")
1542}
1543
1544fn parse_func_guard(pair: Pair<Rule>) -> Result<Guard> {
1545 let mut key = String::new();
1546 let mut value = String::new();
1547 let mut saw_env_prefix = false;
1548 for inner in pair.into_inner() {
1549 match inner.as_rule() {
1550 Rule::env_prefix => saw_env_prefix = true,
1551 Rule::env_key if saw_env_prefix => {
1552 key = inner.as_str().trim().to_string();
1553 }
1554 Rule::bare_guard_value | Rule::quoted_string => {
1555 value = unquote(inner.as_str().trim()).to_string();
1556 }
1557 _ => {}
1558 }
1559 }
1560 Ok(Guard::EnvEquals { key, value })
1561}
1562
1563fn unquote(s: &str) -> &str {
1564 s.strip_prefix('"')
1565 .and_then(|s| s.strip_suffix('"'))
1566 .or_else(|| s.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1567 .unwrap_or(s)
1568}
1569
1570fn parse_env_guard(pair: Pair<Rule>) -> Result<Guard> {
1571 let mut key = String::new();
1572 for inner in pair.into_inner() {
1573 if inner.as_rule() == Rule::env_key {
1574 key = inner.as_str().trim().to_string();
1575 }
1576 }
1577 Ok(Guard::EnvExists { key })
1578}
1579
1580fn parse_platform_tag(tag: &str) -> Result<Guard> {
1581 let target = match tag.to_ascii_lowercase().as_str() {
1582 "unix" => PlatformGuard::Unix,
1583 "windows" => PlatformGuard::Windows,
1584 "mac" | "macos" => PlatformGuard::Macos,
1585 "linux" => PlatformGuard::Linux,
1586 _ => bail!("unknown platform '{}'", tag),
1587 };
1588 Ok(Guard::Platform { target })
1589}
1590
1591fn parse_dollar_ident(pair: Pair<Rule>) -> String {
1592 let s = pair.as_str();
1594 s.strip_prefix('$').unwrap_or(s).to_string()
1595}
1596
1597use crate::ast::{CompareOp, Expr, LogicalOp, Value};
1598
1599fn parse_expr(pair: Pair<Rule>) -> Result<Expr> {
1600 let inner = pair.into_inner().next().unwrap();
1601 match inner.as_rule() {
1602 Rule::expr_logical_or => parse_expr_logical_or(inner),
1603 _ => bail!("unexpected expr rule: {:?}", inner.as_rule()),
1604 }
1605}
1606
1607fn parse_expr_logical_or(pair: Pair<Rule>) -> Result<Expr> {
1608 let mut inner = pair.into_inner();
1609 let mut left = parse_expr_logical_and(inner.next().unwrap())?;
1610 while let Some(op_pair) = inner.next() {
1611 let op = match op_pair.as_rule() {
1612 Rule::or_op => LogicalOp::Or,
1613 _ => bail!("unexpected operator in logical-or: {:?}", op_pair.as_rule()),
1614 };
1615 let right = parse_expr_logical_and(inner.next().unwrap())?;
1616 left = Expr::Logical {
1617 op,
1618 left: Box::new(left),
1619 right: Box::new(right),
1620 };
1621 }
1622 Ok(left)
1623}
1624
1625fn parse_expr_logical_and(pair: Pair<Rule>) -> Result<Expr> {
1626 let mut inner = pair.into_inner();
1627 let mut left = parse_expr_comparison(inner.next().unwrap())?;
1628 while let Some(op_pair) = inner.next() {
1629 let op = match op_pair.as_rule() {
1630 Rule::and_op => LogicalOp::And,
1631 _ => bail!(
1632 "unexpected operator in logical-and: {:?}",
1633 op_pair.as_rule()
1634 ),
1635 };
1636 let right = parse_expr_comparison(inner.next().unwrap())?;
1637 left = Expr::Logical {
1638 op,
1639 left: Box::new(left),
1640 right: Box::new(right),
1641 };
1642 }
1643 Ok(left)
1644}
1645
1646fn parse_expr_comparison(pair: Pair<Rule>) -> Result<Expr> {
1647 let mut inner = pair.into_inner();
1648 let left = parse_expr_unary(inner.next().unwrap())?;
1649 if let Some(op_pair) = inner.next() {
1650 let op = match op_pair.as_rule() {
1651 Rule::eq_op => CompareOp::Eq,
1652 Rule::neq_op => CompareOp::Ne,
1653 _ => bail!("unexpected comparison operator: {:?}", op_pair.as_rule()),
1654 };
1655 let right = parse_expr_unary(inner.next().unwrap())?;
1656 Ok(Expr::Compare {
1657 op,
1658 left: Box::new(left),
1659 right: Box::new(right),
1660 })
1661 } else {
1662 Ok(left)
1663 }
1664}
1665
1666fn parse_expr_unary(pair: Pair<Rule>) -> Result<Expr> {
1667 let mut bangs = 0u32;
1668 let mut atom = None;
1669 for inner in pair.into_inner() {
1670 match inner.as_rule() {
1671 Rule::not_op => bangs += 1,
1672 Rule::expr_atom => atom = Some(parse_expr_atom(inner)?),
1673 _ => bail!("unexpected unary operand rule: {:?}", inner.as_rule()),
1674 }
1675 }
1676 let mut expr = atom.ok_or_else(|| anyhow!("'!' requires an expression operand"))?;
1677 for _ in 0..bangs {
1678 expr = Expr::Not(Box::new(expr));
1679 }
1680 Ok(expr)
1681}
1682
1683fn parse_expr_atom(pair: Pair<Rule>) -> Result<Expr> {
1684 let inner = pair.into_inner().next().unwrap();
1685 match inner.as_rule() {
1686 Rule::parenthesized_expr => parse_expr(inner.into_inner().next().unwrap()),
1687 Rule::func_call => parse_func_call(inner),
1688 Rule::key_path => parse_key_path(inner),
1689 Rule::variable => {
1690 let name = inner.as_str();
1691 let name = name.strip_prefix('$').unwrap_or(name).to_string();
1692 Ok(Expr::Var(name))
1693 }
1694 Rule::list_literal => parse_list_literal(inner),
1695 Rule::map_literal => parse_map_literal(inner),
1696 Rule::string_literal | Rule::quoted_string => {
1697 let s = parse_quoted_string(inner)?;
1698 Ok(Expr::Literal(Value::String(s)))
1699 }
1700 Rule::bare_word => {
1701 let s = inner.as_str().to_string();
1702 match s.as_str() {
1703 "true" => Ok(Expr::Literal(Value::Bool(true))),
1704 "false" => Ok(Expr::Literal(Value::Bool(false))),
1705 _ => Ok(Expr::Literal(Value::String(s))),
1706 }
1707 }
1708 _ => bail!("unexpected expression atom rule: {:?}", inner.as_rule()),
1709 }
1710}
1711
1712fn parse_key_path(pair: Pair<Rule>) -> Result<Expr> {
1713 let mut base = None;
1714 let mut keys = Vec::new();
1715 for inner in pair.into_inner() {
1716 match inner.as_rule() {
1717 Rule::ident => {
1718 if base.is_none() {
1719 base = Some(inner.as_str().to_string());
1720 }
1721 }
1722 Rule::key_path_segment => {
1723 keys.push(inner.as_str().to_string());
1724 }
1725 _ => {}
1726 }
1727 }
1728 Ok(Expr::KeyPath {
1729 base: base.ok_or_else(|| anyhow!("key path requires a base identifier"))?,
1730 keys,
1731 })
1732}
1733
1734fn parse_func_call(pair: Pair<Rule>) -> Result<Expr> {
1735 let mut name = None;
1736 let mut args = Vec::new();
1737 for inner in pair.into_inner() {
1738 match inner.as_rule() {
1739 Rule::ident => {
1740 name = Some(inner.as_str().to_string());
1741 }
1742 Rule::expr => {
1743 args.push(parse_expr(inner)?);
1744 }
1745 _ => {}
1746 }
1747 }
1748 Ok(Expr::Call {
1749 name: name.ok_or_else(|| anyhow!("function call requires a name"))?,
1750 args,
1751 })
1752}
1753
1754fn parse_list_literal(pair: Pair<Rule>) -> Result<Expr> {
1755 let mut items = Vec::new();
1756 for inner in pair.into_inner() {
1757 if inner.as_rule() == Rule::expr {
1758 items.push(parse_expr(inner)?);
1759 }
1760 }
1761 Ok(Expr::List(items))
1762}
1763
1764fn parse_map_literal(pair: Pair<Rule>) -> Result<Expr> {
1765 let mut entries = Vec::new();
1766 for inner in pair.into_inner() {
1767 if inner.as_rule() == Rule::map_entry {
1768 let mut key = String::new();
1769 let mut value = None;
1770 for entry_inner in inner.into_inner() {
1771 match entry_inner.as_rule() {
1772 Rule::quoted_string => {
1773 key = parse_quoted_string(entry_inner)?;
1774 }
1775 Rule::bare_word => {
1776 key = entry_inner.as_str().to_string();
1777 }
1778 Rule::expr => {
1779 value = Some(parse_expr(entry_inner)?);
1780 }
1781 _ => {}
1782 }
1783 }
1784 let val = value.ok_or_else(|| anyhow!("map entry missing value"))?;
1785 entries.push((key, val));
1786 }
1787 }
1788 Ok(Expr::Map(entries))
1789}