1use crate::ast::{
2 Guard, GuardExpr, IoBinding, IoStream, PlatformGuard, Step, StepKind, WorkspaceTarget,
3};
4use crate::lexer::{self, RawToken, Rule};
5use anyhow::{Result, anyhow, bail};
6use pest::iterators::Pair;
7use std::collections::VecDeque;
8
9#[derive(Clone)]
10struct ScopeFrame {
11 line_no: usize,
12 had_command: bool,
13}
14
15#[derive(Clone)]
16struct PendingIoBlock {
17 line_no: usize,
18 bindings: Vec<IoBinding>,
19 guards: Option<GuardExpr>,
20}
21
22#[derive(Clone)]
23struct IoScopeFrame {
24 line_no: usize,
25 had_command: bool,
26 bindings: Vec<IoBinding>,
27 guards: Option<GuardExpr>,
28}
29
30#[derive(Clone, Copy)]
31enum BlockKind {
32 Guard,
33 Io,
34}
35
36#[derive(Default)]
37struct IoBindingSet {
38 stdin: Option<IoBinding>,
39 stdout: Option<IoBinding>,
40 stderr: Option<IoBinding>,
41}
42
43impl IoBindingSet {
44 fn insert(&mut self, binding: IoBinding) {
45 match binding.stream {
46 IoStream::Stdin => self.stdin = Some(binding),
47 IoStream::Stdout => self.stdout = Some(binding),
48 IoStream::Stderr => self.stderr = Some(binding),
49 }
50 }
51
52 fn into_vec(self) -> Vec<IoBinding> {
53 let mut out = Vec::new();
54 if let Some(binding) = self.stdin {
55 out.push(binding);
56 }
57 if let Some(binding) = self.stdout {
58 out.push(binding);
59 }
60 if let Some(binding) = self.stderr {
61 out.push(binding);
62 }
63 out
64 }
65}
66
67pub struct ScriptParser<'a> {
68 tokens: VecDeque<RawToken<'a>>,
69 steps: Vec<Step>,
70 guard_stack: Vec<Option<GuardExpr>>,
71 pending_guards: Option<GuardExpr>,
72 pending_inline_guards: Option<GuardExpr>,
73 pending_can_open_block: bool,
74 pending_scope_enters: usize,
75 scope_stack: Vec<ScopeFrame>,
76 pending_io_block: Option<PendingIoBlock>,
77 io_scope_stack: Vec<IoScopeFrame>,
78 block_stack: Vec<BlockKind>,
79}
80
81impl<'a> ScriptParser<'a> {
82 pub fn new(input: &'a str) -> Result<Self> {
83 let tokens = VecDeque::from(lexer::tokenize(input)?);
84 Ok(Self {
85 tokens,
86 steps: Vec::new(),
87 guard_stack: vec![None],
88 pending_guards: None,
89 pending_inline_guards: None,
90 pending_can_open_block: false,
91 pending_scope_enters: 0,
92 scope_stack: Vec::new(),
93 pending_io_block: None,
94 io_scope_stack: Vec::new(),
95 block_stack: Vec::new(),
96 })
97 }
98
99 pub fn parse(mut self) -> Result<Vec<Step>> {
100 while let Some(token) = self.tokens.pop_front() {
101 if self.pending_io_block.is_some() && !matches!(token, RawToken::BlockStart { .. }) {
102 let pending = self.pending_io_block.take().unwrap();
103 bail!(
104 "line {}: WITH_IO block must be followed by '{{'",
105 pending.line_no
106 );
107 }
108 match token {
109 RawToken::Guard { pair, line_end } => {
110 let groups = parse_guard_line(pair)?;
111 self.handle_guard_token(line_end, groups)?
112 }
113 RawToken::BlockStart { line_no } => self.start_block(line_no)?,
114 RawToken::BlockEnd { line_no } => self.end_block(line_no)?,
115 RawToken::Command { pair, line_no } => {
116 let kind = parse_command(pair)?;
117 self.handle_command_token(line_no, kind)?
118 }
119 }
120 }
121
122 if let Some(pending) = self.pending_io_block.take() {
123 bail!(
124 "line {}: WITH_IO block must be followed by '{{'",
125 pending.line_no
126 );
127 }
128
129 if self.guard_stack.len() != 1 {
130 bail!("unclosed guard block at end of script");
131 }
132 if self.pending_guards.is_some() {
133 bail!("guard declared on final lines without a following command");
134 }
135
136 if let Some(frame) = self.io_scope_stack.last() {
137 bail!(
138 "WITH_IO block starting on line {} was not closed",
139 frame.line_no
140 );
141 }
142
143 {
146 let mut seen_non_prelude = false;
147 let mut inherit_count = 0usize;
148 for step in &self.steps {
149 match &step.kind {
150 StepKind::InheritEnv { .. } => {
151 if seen_non_prelude {
152 bail!("INHERIT_ENV must appear before any other commands");
153 }
154 if step.guard.is_some() || step.scope_enter > 0 || step.scope_exit > 0 {
155 bail!("INHERIT_ENV cannot be guarded or nested inside blocks");
156 }
157 inherit_count += 1;
158 }
159 kind => {
160 if contains_inherit_env(kind) {
161 bail!("INHERIT_ENV cannot be nested inside other commands");
162 }
163 seen_non_prelude = true;
164 }
165 }
166 }
167 if inherit_count > 1 {
168 bail!("only one INHERIT_ENV directive is allowed");
169 }
170 }
171
172 Ok(self.steps)
173 }
174
175 fn handle_guard_token(&mut self, line_end: usize, expr: GuardExpr) -> Result<()> {
176 if let Some(RawToken::Command { line_no, .. }) = self.tokens.front()
177 && *line_no == line_end
178 {
179 self.pending_inline_guards = Some(expr);
180 self.pending_can_open_block = false;
181 return Ok(());
182 }
183 self.stash_pending_guard(expr);
184 self.pending_can_open_block = true;
185 Ok(())
186 }
187
188 fn handle_command_token(&mut self, line_no: usize, kind: StepKind) -> Result<()> {
189 let inline = self.pending_inline_guards.take();
190 self.handle_command(line_no, kind, inline)
191 }
192
193 fn stash_pending_guard(&mut self, guard: GuardExpr) {
194 self.pending_guards = Some(if let Some(existing) = self.pending_guards.take() {
195 GuardExpr::all(vec![existing, guard])
196 } else {
197 guard
198 });
199 }
200
201 fn start_guard_block_from_pending(&mut self, line_no: usize) -> Result<()> {
202 let guards = self
203 .pending_guards
204 .take()
205 .ok_or_else(|| anyhow!("line {}: '{{' without a pending guard", line_no))?;
206 if !self.pending_can_open_block {
207 bail!("line {}: '{{' must directly follow a guard", line_no);
208 }
209 self.pending_can_open_block = false;
210 self.enter_guard_block(guards, line_no)
211 }
212
213 fn enter_guard_block(&mut self, guard: GuardExpr, line_no: usize) -> Result<()> {
214 let composed = if let Some(pending) = self.pending_guards.take() {
215 GuardExpr::all(vec![pending, guard])
216 } else {
217 guard
218 };
219 let parent = self.guard_stack.last().cloned().unwrap_or(None);
220 let next = and_guard_exprs(parent, Some(composed));
221 self.guard_stack.push(next);
222 self.scope_stack.push(ScopeFrame {
223 line_no,
224 had_command: false,
225 });
226 self.pending_scope_enters += 1;
227 Ok(())
228 }
229
230 fn begin_io_block(
231 &mut self,
232 line_no: usize,
233 bindings: Vec<IoBinding>,
234 guards: Option<GuardExpr>,
235 ) -> Result<()> {
236 if self.pending_io_block.is_some() {
237 bail!(
238 "line {}: previous WITH_IO block is still waiting for '{{'",
239 line_no
240 );
241 }
242 self.pending_io_block = Some(PendingIoBlock {
243 line_no,
244 bindings,
245 guards,
246 });
247 Ok(())
248 }
249
250 fn start_block(&mut self, line_no: usize) -> Result<()> {
251 if let Some(pending) = self.pending_io_block.take() {
252 self.block_stack.push(BlockKind::Io);
253 self.io_scope_stack.push(IoScopeFrame {
254 line_no: pending.line_no,
255 had_command: false,
256 bindings: pending.bindings,
257 guards: pending.guards,
258 });
259 Ok(())
260 } else {
261 self.start_guard_block_from_pending(line_no)?;
262 self.block_stack.push(BlockKind::Guard);
263 Ok(())
264 }
265 }
266
267 fn end_block(&mut self, line_no: usize) -> Result<()> {
268 let kind = self
269 .block_stack
270 .pop()
271 .ok_or_else(|| anyhow!("line {}: unexpected '}}'", line_no))?;
272 match kind {
273 BlockKind::Guard => self.end_guard_block(line_no),
274 BlockKind::Io => self.end_io_block(line_no),
275 }
276 }
277
278 fn end_guard_block(&mut self, line_no: usize) -> Result<()> {
279 if self.guard_stack.len() == 1 {
280 bail!("line {}: unexpected '}}'", line_no);
281 }
282 if self.pending_guards.is_some() {
283 bail!(
284 "line {}: guard declared immediately before '}}' without a command",
285 line_no
286 );
287 }
288 let frame = self
289 .scope_stack
290 .last()
291 .cloned()
292 .ok_or_else(|| anyhow!("line {}: scope stack underflow", line_no))?;
293 if !frame.had_command {
294 bail!(
295 "line {}: guard block starting on line {} must contain at least one command",
296 line_no,
297 frame.line_no
298 );
299 }
300 let step = self
301 .steps
302 .last_mut()
303 .ok_or_else(|| anyhow!("line {}: guard block closed without any commands", line_no))?;
304 step.scope_exit += 1;
305 self.scope_stack.pop();
306 self.guard_stack.pop();
307 Ok(())
308 }
309
310 fn end_io_block(&mut self, line_no: usize) -> Result<()> {
311 let frame = self
312 .io_scope_stack
313 .pop()
314 .ok_or_else(|| anyhow!("line {}: unexpected '}}'", line_no))?;
315 if !frame.had_command {
316 bail!(
317 "line {}: WITH_IO block starting on line {} must contain at least one command",
318 line_no,
319 frame.line_no
320 );
321 }
322 Ok(())
323 }
324
325 fn guard_context(&mut self, inline: Option<GuardExpr>) -> Option<GuardExpr> {
326 let mut context = self.guard_stack.last().cloned().unwrap_or(None);
327 if let Some(pending) = self.pending_guards.take() {
328 context = and_guard_exprs(context, Some(pending));
329 self.pending_can_open_block = false;
330 }
331 if let Some(inline_guard) = inline {
332 context = and_guard_exprs(context, Some(inline_guard));
333 }
334 context
335 }
336
337 fn handle_command(
338 &mut self,
339 line_no: usize,
340 kind: StepKind,
341 inline_guards: Option<GuardExpr>,
342 ) -> Result<()> {
343 if let StepKind::WithIoBlock { bindings } = kind {
344 let guards = self.guard_context(inline_guards);
345 self.begin_io_block(line_no, bindings, guards)?;
346 return Ok(());
347 }
348
349 let guards = self.guard_context(inline_guards);
350 let guards = self.apply_io_guards(guards);
351 let scope_enter = self.pending_scope_enters;
352 self.pending_scope_enters = 0;
353 for frame in self.scope_stack.iter_mut() {
354 frame.had_command = true;
355 }
356 for frame in self.io_scope_stack.iter_mut() {
357 frame.had_command = true;
358 }
359 let kind = self.apply_io_defaults(kind);
360 self.steps.push(Step {
361 guard: guards,
362 kind,
363 scope_enter,
364 scope_exit: 0,
365 });
366 Ok(())
367 }
368
369 fn apply_io_defaults(&self, kind: StepKind) -> StepKind {
370 let defaults = self.current_io_defaults();
371 if defaults.is_empty() {
372 return kind;
373 }
374 match kind {
375 StepKind::WithIo { bindings, cmd } => StepKind::WithIo {
376 bindings: merge_bindings(&defaults, &bindings),
377 cmd,
378 },
379 other => StepKind::WithIo {
380 bindings: defaults,
381 cmd: Box::new(other),
382 },
383 }
384 }
385
386 fn current_io_defaults(&self) -> Vec<IoBinding> {
387 if self.io_scope_stack.is_empty() {
388 return Vec::new();
389 }
390 let mut set = IoBindingSet::default();
391 for frame in &self.io_scope_stack {
392 for binding in &frame.bindings {
393 set.insert(binding.clone());
394 }
395 }
396 set.into_vec()
397 }
398
399 fn apply_io_guards(&self, guard: Option<GuardExpr>) -> Option<GuardExpr> {
400 self.io_scope_stack.iter().fold(guard, |acc, frame| {
401 and_guard_exprs(acc, frame.guards.clone())
402 })
403 }
404}
405
406pub fn parse_script(input: &str) -> Result<Vec<Step>> {
407 ScriptParser::new(input)?.parse()
408}
409
410fn and_guard_exprs(left: Option<GuardExpr>, right: Option<GuardExpr>) -> Option<GuardExpr> {
411 match (left, right) {
412 (None, None) => None,
413 (Some(expr), None) | (None, Some(expr)) => Some(expr),
414 (Some(lhs), Some(rhs)) => Some(GuardExpr::all(vec![lhs, rhs])),
415 }
416}
417
418fn merge_bindings(defaults: &[IoBinding], overrides: &[IoBinding]) -> Vec<IoBinding> {
419 let mut set = IoBindingSet::default();
420 for binding in defaults {
421 set.insert(binding.clone());
422 }
423 for binding in overrides {
424 set.insert(binding.clone());
425 }
426 set.into_vec()
427}
428
429fn contains_inherit_env(kind: &StepKind) -> bool {
430 match kind {
431 StepKind::InheritEnv { .. } => true,
432 StepKind::WithIo { cmd, .. } => contains_inherit_env(cmd),
433 _ => false,
434 }
435}
436
437fn parse_command(pair: Pair<Rule>) -> Result<StepKind> {
438 let kind = match pair.as_rule() {
439 Rule::workdir_command => {
440 let arg = parse_single_arg(pair)?;
441 StepKind::Workdir(arg.into())
442 }
443 Rule::workspace_command => {
444 let target = parse_workspace_target(pair)?;
445 StepKind::Workspace(target)
446 }
447 Rule::env_command => {
448 let (key, value) = parse_env_pair(pair)?;
449 StepKind::Env {
450 key,
451 value: value.into(),
452 }
453 }
454 Rule::echo_command => {
455 let msg = parse_message(pair)?;
456 StepKind::Echo(msg.into())
457 }
458 Rule::run_command => {
459 let cmd = parse_run_args(pair)?;
460 StepKind::Run(cmd.into())
461 }
462 Rule::run_bg_command => {
463 let cmd = parse_run_args(pair)?;
464 StepKind::RunBg(cmd.into())
465 }
466 Rule::copy_command => {
467 let mut args: Vec<String> = Vec::new();
468 let mut from_current_workspace = false;
469 for inner in pair.into_inner() {
470 match inner.as_rule() {
471 Rule::from_current_workspace_flag => from_current_workspace = true,
472 Rule::argument => args.push(parse_argument(inner)?),
473 _ => {}
474 }
475 }
476 if args.len() != 2 {
477 bail!("COPY expects 2 arguments (from, to)");
478 }
479 StepKind::Copy {
480 from_current_workspace,
481 from: args.remove(0).into(),
482 to: args.remove(0).into(),
483 }
484 }
485 Rule::with_io_command => {
486 let mut bindings = Vec::new();
487 let mut cmd = None;
488 for inner in pair.into_inner() {
489 match inner.as_rule() {
490 Rule::io_flags => {
491 for flag in inner.into_inner() {
492 if flag.as_rule() == Rule::io_binding {
493 bindings.push(parse_io_binding(flag)?);
494 }
495 }
496 }
497 _ => {
498 cmd = Some(Box::new(parse_command(inner)?));
499 }
500 }
501 }
502 if let Some(cmd) = cmd {
503 StepKind::WithIo { bindings, cmd }
504 } else {
505 StepKind::WithIoBlock { bindings }
506 }
507 }
508 Rule::copy_git_command => {
509 let mut args = Vec::new();
510 let mut include_dirty = false;
511 for inner in pair.into_inner() {
512 match inner.as_rule() {
513 Rule::include_dirty_flag => include_dirty = true,
514 Rule::argument => args.push(parse_argument(inner)?),
515 _ => {}
516 }
517 }
518 if args.len() != 3 {
519 bail!("COPY_GIT expects 3 arguments (rev, from, to)");
520 }
521 StepKind::CopyGit {
522 rev: args.remove(0).into(),
523 from: args.remove(0).into(),
524 to: args.remove(0).into(),
525 include_dirty,
526 }
527 }
528 Rule::hash_sha256_command => {
529 let arg = parse_single_arg(pair)?;
530 StepKind::HashSha256 { path: arg.into() }
531 }
532 Rule::inherit_env_command => {
533 let mut keys: Vec<String> = Vec::new();
534 for inner in pair.into_inner() {
535 match inner.as_rule() {
536 Rule::inherit_list => {
537 for key in inner.into_inner() {
538 if key.as_rule() == Rule::env_key {
539 keys.push(key.as_str().trim().to_string());
540 }
541 }
542 }
543 Rule::env_key => keys.push(inner.as_str().trim().to_string()),
544 _ => {}
545 }
546 }
547 StepKind::InheritEnv { keys }
548 }
549 Rule::symlink_command => {
550 let mut args = parse_args(pair)?;
551 StepKind::Symlink {
552 from: args.remove(0).into(),
553 to: args.remove(0).into(),
554 }
555 }
556 Rule::mkdir_command => {
557 let arg = parse_single_arg(pair)?;
558 StepKind::Mkdir(arg.into())
559 }
560 Rule::ls_command => {
561 let args = parse_args(pair)?;
562 StepKind::Ls(args.into_iter().next().map(Into::into))
563 }
564 Rule::cwd_command => StepKind::Cwd,
565 Rule::read_command => {
566 let args = parse_args(pair)?;
567 StepKind::Read(args.into_iter().next().map(Into::into))
568 }
569 Rule::write_command => {
570 let mut path = None;
571 let mut contents = None;
572 for inner in pair.into_inner() {
573 match inner.as_rule() {
574 Rule::argument if path.is_none() => {
575 path = Some(parse_argument(inner)?);
576 }
577 Rule::message => {
578 contents = Some(parse_concatenated_string(inner)?);
579 }
580 _ => {}
581 }
582 }
583 StepKind::Write {
584 path: path
585 .ok_or_else(|| anyhow!("WRITE expects a path argument"))?
586 .into(),
587 contents: contents.map(Into::into),
588 }
589 }
590 Rule::exit_command => {
591 let code = parse_exit_code(pair)?;
592 StepKind::Exit(code)
593 }
594 _ => bail!("unknown command rule: {:?}", pair.as_rule()),
595 };
596 Ok(kind)
597}
598
599fn parse_single_arg(pair: Pair<Rule>) -> Result<String> {
600 for inner in pair.into_inner() {
601 if inner.as_rule() == Rule::argument {
602 return parse_argument(inner);
603 }
604 }
605 bail!("missing argument")
606}
607
608fn parse_args(pair: Pair<Rule>) -> Result<Vec<String>> {
609 let mut args = Vec::new();
610 for inner in pair.into_inner() {
611 if inner.as_rule() == Rule::argument {
612 args.push(parse_argument(inner)?);
613 }
614 }
615 Ok(args)
616}
617
618fn parse_argument(pair: Pair<Rule>) -> Result<String> {
619 let inner = pair.into_inner().next().unwrap();
620 match inner.as_rule() {
621 Rule::quoted_string => parse_quoted_string(inner),
622 Rule::templated_arg => Ok(inner.as_str().to_string()),
623 Rule::unquoted_arg => Ok(inner.as_str().to_string()),
624 _ => unreachable!(),
625 }
626}
627
628fn parse_quoted_string(pair: Pair<Rule>) -> Result<String> {
629 let s = pair.as_str();
630 let _quote = s.chars().next().unwrap();
631 let content = &s[1..s.len() - 1];
632
633 let mut out = String::with_capacity(content.len());
634 let mut escape = false;
635 for ch in content.chars() {
636 if escape {
637 out.push(ch);
638 escape = false;
639 } else if ch == '\\' {
640 escape = true;
641 } else {
642 out.push(ch);
643 }
644 }
645 Ok(out)
646}
647
648fn parse_workspace_target(pair: Pair<Rule>) -> Result<WorkspaceTarget> {
649 for inner in pair.into_inner() {
650 if inner.as_rule() == Rule::workspace_target {
651 return match inner.as_str().to_ascii_lowercase().as_str() {
652 "snapshot" => Ok(WorkspaceTarget::Snapshot),
653 "local" => Ok(WorkspaceTarget::Local),
654 _ => bail!("unknown workspace target"),
655 };
656 }
657 }
658 bail!("missing workspace target")
659}
660
661fn parse_env_pair(pair: Pair<Rule>) -> Result<(String, String)> {
662 for inner in pair.into_inner() {
663 if inner.as_rule() == Rule::env_pair {
664 let mut parts = inner.into_inner();
665 let key = parts.next().unwrap().as_str().to_string();
666 let value_pair = parts.next().unwrap();
667 let value = match value_pair.as_rule() {
668 Rule::env_value_part => {
669 let inner_val = value_pair.into_inner().next().unwrap();
670 match inner_val.as_rule() {
671 Rule::quoted_string => parse_quoted_string(inner_val)?,
672 Rule::unquoted_env_value => inner_val.as_str().to_string(),
673 _ => unreachable!(
674 "unexpected rule in env_value_part: {:?}",
675 inner_val.as_rule()
676 ),
677 }
678 }
679 _ => unreachable!("expected env_value_part"),
680 };
681 return Ok((key, value));
682 }
683 }
684 bail!("missing env pair")
685}
686
687fn parse_message(pair: Pair<Rule>) -> Result<String> {
688 for inner in pair.into_inner() {
689 if inner.as_rule() == Rule::message {
690 return parse_concatenated_string(inner);
691 }
692 }
693 bail!("missing message")
694}
695
696fn parse_run_args(pair: Pair<Rule>) -> Result<String> {
697 for inner in pair.into_inner() {
698 if inner.as_rule() == Rule::run_args {
699 return parse_smart_concatenated_string(inner);
700 }
701 }
702 bail!("missing run args")
703}
704
705fn parse_smart_concatenated_string(pair: Pair<Rule>) -> Result<String> {
706 let parts: Vec<_> = pair.into_inner().collect();
707
708 if parts.len() == 1 && parts[0].as_rule() == Rule::quoted_string {
712 return parse_quoted_string(parts[0].clone());
713 }
714
715 let mut body = String::new();
716 let mut last_end = None;
717 for part in parts {
718 let span = part.as_span();
719 if let Some(end) = last_end
720 && span.start() > end
721 {
722 body.push(' ');
723 }
724 match part.as_rule() {
725 Rule::quoted_string => {
726 let raw = part.as_str();
727 let unquoted = parse_quoted_string(part.clone())?;
728 let needs_quotes = unquoted.is_empty()
731 || unquoted
732 .chars()
733 .any(|c| c.is_whitespace() || c == ';' || c == '\n' || c == '\r')
734 || unquoted.contains("//")
735 || unquoted.contains("/*");
736
737 if needs_quotes {
738 body.push_str(raw);
739 } else {
740 body.push_str(&unquoted);
741 }
742 }
743 Rule::unquoted_msg_content | Rule::unquoted_run_content => body.push_str(part.as_str()),
744 _ => {}
745 }
746 last_end = Some(span.end());
747 }
748 Ok(body)
749}
750
751fn parse_concatenated_string(pair: Pair<Rule>) -> Result<String> {
752 let mut body = String::new();
753 let mut last_end = None;
754 for part in pair.into_inner() {
755 let span = part.as_span();
756 if let Some(end) = last_end
757 && span.start() > end
758 {
759 body.push(' ');
760 }
761 match part.as_rule() {
762 Rule::quoted_string => body.push_str(&parse_quoted_string(part)?),
763 Rule::unquoted_msg_content | Rule::unquoted_run_content => body.push_str(part.as_str()),
764 _ => {}
765 }
766 last_end = Some(span.end());
767 }
768 Ok(body)
769}
770
771fn parse_exit_code(pair: Pair<Rule>) -> Result<i32> {
772 for inner in pair.into_inner() {
773 if inner.as_rule() == Rule::exit_code {
774 return inner
775 .as_str()
776 .parse()
777 .map_err(|_| anyhow!("invalid exit code"));
778 }
779 }
780 bail!("missing exit code")
781}
782
783fn parse_guard_line(pair: Pair<Rule>) -> Result<GuardExpr> {
784 for inner in pair.into_inner() {
785 if inner.as_rule() == Rule::guard_expr {
786 return parse_guard_expr(inner);
787 }
788 }
789 bail!("guard line missing expression")
790}
791
792fn parse_io_binding(pair: Pair<Rule>) -> Result<IoBinding> {
793 let mut stream = None;
794 let mut pipe = None;
795 for inner in pair.into_inner() {
796 match inner.as_rule() {
797 Rule::io_stream => stream = Some(parse_io_stream(inner.as_str())),
798 Rule::pipe_binding => pipe = Some(parse_pipe_binding(inner)?),
799 _ => {}
800 }
801 }
802 let stream = stream.ok_or_else(|| anyhow!("missing IO stream in WITH_IO"))?;
803 Ok(IoBinding { stream, pipe })
804}
805
806fn parse_io_stream(text: &str) -> IoStream {
807 match text {
808 "stdin" => IoStream::Stdin,
809 "stdout" => IoStream::Stdout,
810 "stderr" => IoStream::Stderr,
811 _ => unreachable!("parser produced invalid io_stream token"),
812 }
813}
814
815fn parse_pipe_binding(pair: Pair<Rule>) -> Result<String> {
816 for inner in pair.into_inner() {
817 if inner.as_rule() == Rule::pipe_name {
818 return Ok(inner.as_str().to_string());
819 }
820 }
821 bail!("missing pipe identifier in WITH_IO binding");
822}
823
824fn parse_guard_expr(pair: Pair<Rule>) -> Result<GuardExpr> {
825 match pair.as_rule() {
826 Rule::guard_expr => {
827 let next = pair
828 .into_inner()
829 .next()
830 .ok_or_else(|| anyhow!("guard expression missing body"))?;
831 parse_guard_expr(next)
832 }
833 Rule::guard_seq => parse_guard_seq(pair),
834 Rule::guard_factor => parse_guard_factor(pair),
835 Rule::guard_not => parse_guard_not(pair),
836 Rule::guard_primary => parse_guard_primary(pair),
837 Rule::guard_group => parse_guard_group(pair),
838 Rule::guard_or_call => parse_guard_or_call(pair),
839 Rule::guard_term => Ok(GuardExpr::Predicate(parse_guard_term(pair)?)),
840 _ => bail!("unexpected guard expression rule: {:?}", pair.as_rule()),
841 }
842}
843
844fn parse_guard_seq(pair: Pair<Rule>) -> Result<GuardExpr> {
845 let mut exprs = Vec::new();
846 for inner in pair.into_inner() {
847 if inner.as_rule() == Rule::guard_factor {
848 exprs.push(parse_guard_factor(inner)?);
849 }
850 }
851 match exprs.len() {
852 0 => bail!("guard list requires at least one entry"),
853 1 => Ok(exprs.pop().unwrap()),
854 _ => Ok(GuardExpr::all(exprs)),
855 }
856}
857
858fn parse_guard_factor(pair: Pair<Rule>) -> Result<GuardExpr> {
859 for inner in pair.into_inner() {
860 if inner.as_rule() == Rule::guard_not {
861 return parse_guard_not(inner);
862 }
863 }
864 bail!("guard factor missing expression")
865}
866
867fn parse_guard_not(pair: Pair<Rule>) -> Result<GuardExpr> {
868 let mut invert_count = 0usize;
869 let mut primary = None;
870 for inner in pair.into_inner() {
871 match inner.as_rule() {
872 Rule::invert => invert_count += 1,
873 _ => primary = Some(parse_guard_primary(inner)?),
874 }
875 }
876 let expr = primary.ok_or_else(|| anyhow!("guard expression missing predicate"))?;
877 apply_inversion(expr, invert_count % 2 == 1)
878}
879
880fn parse_guard_primary(pair: Pair<Rule>) -> Result<GuardExpr> {
881 match pair.as_rule() {
882 Rule::guard_primary => {
883 let inner = pair
884 .into_inner()
885 .next()
886 .ok_or_else(|| anyhow!("guard primary missing body"))?;
887 parse_guard_primary(inner)
888 }
889 Rule::guard_group => parse_guard_group(pair),
890 Rule::guard_or_call => parse_guard_or_call(pair),
891 Rule::guard_term => Ok(GuardExpr::Predicate(parse_guard_term(pair)?)),
892 _ => bail!("unexpected guard primary rule: {:?}", pair.as_rule()),
893 }
894}
895
896fn parse_guard_group(pair: Pair<Rule>) -> Result<GuardExpr> {
897 for inner in pair.into_inner() {
898 if inner.as_rule() == Rule::guard_expr {
899 return parse_guard_expr(inner);
900 }
901 }
902 bail!("grouped guard missing expression")
903}
904
905fn parse_guard_or_call(pair: Pair<Rule>) -> Result<GuardExpr> {
906 let mut args = Vec::new();
907 for inner in pair.into_inner() {
908 if inner.as_rule() == Rule::guard_expr_list {
909 args = parse_guard_expr_list(inner)?;
910 }
911 }
912 if args.len() < 2 {
913 bail!("or(...) requires at least two guard expressions");
914 }
915 Ok(GuardExpr::or(args))
916}
917
918fn parse_guard_expr_list(pair: Pair<Rule>) -> Result<Vec<GuardExpr>> {
919 let mut exprs = Vec::new();
920 for inner in pair.into_inner() {
921 if inner.as_rule() == Rule::guard_expr {
922 push_guard_or_args_from_expr(inner, &mut exprs)?;
923 }
924 }
925 Ok(exprs)
926}
927
928fn push_guard_or_args_from_expr(expr_pair: Pair<Rule>, exprs: &mut Vec<GuardExpr>) -> Result<()> {
929 if let Some(seq_pair) = expr_pair
930 .clone()
931 .into_inner()
932 .find(|inner| inner.as_rule() == Rule::guard_seq)
933 {
934 let factors: Vec<Pair<Rule>> = seq_pair
935 .into_inner()
936 .filter(|inner| inner.as_rule() == Rule::guard_factor)
937 .collect();
938 if factors.len() > 1 {
939 for factor in factors {
940 exprs.push(parse_guard_factor(factor)?);
941 }
942 return Ok(());
943 }
944 }
945 exprs.push(parse_guard_expr(expr_pair)?);
946 Ok(())
947}
948
949fn apply_inversion(expr: GuardExpr, invert: bool) -> Result<GuardExpr> {
950 if !invert {
951 return Ok(expr);
952 }
953 match expr {
954 GuardExpr::Predicate(guard) => {
955 if let Guard::EnvEquals {
956 key,
957 value,
958 invert: false,
959 } = &guard
960 {
961 bail!(
962 "inverted env equality is not allowed: use 'env:{}!={}' or '!env:{}'",
963 key,
964 value,
965 key
966 );
967 }
968 Ok(GuardExpr::Predicate(invert_guard(guard)))
969 }
970 other => Ok(!other),
971 }
972}
973
974fn invert_guard(guard: Guard) -> Guard {
975 match guard {
976 Guard::Platform { target, invert } => Guard::Platform {
977 target,
978 invert: !invert,
979 },
980 Guard::EnvExists { key, invert } => Guard::EnvExists {
981 key,
982 invert: !invert,
983 },
984 Guard::EnvEquals { key, value, invert } => Guard::EnvEquals {
985 key,
986 value,
987 invert: !invert,
988 },
989 }
990}
991
992fn parse_guard_term(pair: Pair<Rule>) -> Result<Guard> {
993 for inner in pair.into_inner() {
994 match inner.as_rule() {
995 Rule::env_guard => return parse_env_guard(inner),
996 Rule::bare_platform => return parse_bare_platform(inner, false),
997 _ => {}
998 }
999 }
1000 bail!("missing guard predicate")
1001}
1002
1003fn parse_env_guard(pair: Pair<Rule>) -> Result<Guard> {
1004 let mut key = String::new();
1005 let mut value = None;
1006 let mut is_not_equals = false;
1007
1008 for inner in pair.into_inner() {
1009 match inner.as_rule() {
1010 Rule::env_key => key = inner.as_str().trim().to_string(),
1011 Rule::env_comparison => {
1012 for comp_part in inner.into_inner() {
1013 match comp_part.as_rule() {
1014 Rule::equals_env | Rule::not_equals_env => {
1015 for part in comp_part.into_inner() {
1016 match part.as_rule() {
1017 Rule::eq_op => {}
1018 Rule::neq_op => is_not_equals = true,
1019 Rule::env_value => {
1020 value = Some(part.as_str().trim().to_string());
1021 }
1022 _ => {}
1023 }
1024 }
1025 }
1026 Rule::eq_op => {}
1027 Rule::neq_op => is_not_equals = true,
1028 Rule::env_value => {
1029 value = Some(comp_part.as_str().trim().to_string());
1030 }
1031 _ => {}
1032 }
1033 }
1034 }
1035 _ => {}
1036 }
1037 }
1038
1039 if let Some(val) = value {
1040 Ok(Guard::EnvEquals {
1041 key,
1042 value: val,
1043 invert: is_not_equals,
1044 })
1045 } else {
1046 Ok(Guard::EnvExists { key, invert: false })
1047 }
1048}
1049
1050fn parse_bare_platform(pair: Pair<Rule>, invert: bool) -> Result<Guard> {
1051 let tag = pair.into_inner().next().unwrap().as_str();
1052 parse_platform_tag(tag, invert)
1053}
1054
1055fn parse_platform_tag(tag: &str, invert: bool) -> Result<Guard> {
1056 let target = match tag.to_ascii_lowercase().as_str() {
1057 "unix" => PlatformGuard::Unix,
1058 "windows" => PlatformGuard::Windows,
1059 "mac" | "macos" => PlatformGuard::Macos,
1060 "linux" => PlatformGuard::Linux,
1061 _ => bail!("unknown platform '{}'", tag),
1062 };
1063 Ok(Guard::Platform { target, invert })
1064}