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 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)] => 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
725fn 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 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 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 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 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 _ => {} }
1204 }
1205 Ok(steps)
1206}
1207
1208fn parse_argument(pair: Pair<Rule>) -> Result<Vec<Arg>> {
1209 let inners: Vec<_> = pair.into_inner().collect();
1210 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 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 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 Ok(content.to_string())
1254}
1255
1256fn parse_fragments(parts: &[Pair<Rule>]) -> Result<String> {
1260 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 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 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}