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