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