1use crate::ast::{
2 ARCH_VALUES, Arg, Expr, Guard, GuardExpr, IoBinding, IoStream, MathOp, ModuleTable, Ns,
3 PipeTarget, Step, StepKind,
4};
5use crate::command::ArgType;
6use crate::constants::{
7 KEYWORD_EXPORT, KEYWORD_IMPORT, KEYWORD_INSPECT, KEYWORD_TYPE, MODULE_SEPARATOR,
8 SCRIPT_MODULE_NAME, qualify, split_qualified,
9};
10use crate::error::{ParseError, ParseResult, SpanContext};
11use crate::lexer::{self, RawToken, Rule, parse_pest_error, refine_span, span_for_line, span_of};
12use pest::iterators::Pair;
13use std::cell::RefCell;
14use std::collections::{HashSet, VecDeque};
15
16pub(super) struct LowerCtx<'a> {
25 pub lower: &'a dyn Fn(&str, Vec<Arg>) -> ParseResult<StepKind>,
27 pub reserved_names: &'a HashSet<String>,
31 func_scopes: &'a RefCell<Vec<HashSet<String>>>,
37 modules: &'a ModuleTable,
39 import_scopes: &'a RefCell<Vec<Vec<String>>>,
43}
44
45impl<'a> LowerCtx<'a> {
46 pub(super) fn new(
47 lower: &'a dyn Fn(&str, Vec<Arg>) -> ParseResult<StepKind>,
48 reserved_names: &'a HashSet<String>,
49 func_scopes: &'a RefCell<Vec<HashSet<String>>>,
50 modules: &'a ModuleTable,
51 import_scopes: &'a RefCell<Vec<Vec<String>>>,
52 ) -> Self {
53 Self {
54 lower,
55 reserved_names,
56 func_scopes,
57 modules,
58 import_scopes,
59 }
60 }
61
62 pub(super) fn enter_scope(&self) {
63 self.func_scopes.borrow_mut().push(HashSet::new());
64 self.import_scopes.borrow_mut().push(Vec::new());
65 }
66
67 pub(super) fn exit_scope(&self) {
68 self.func_scopes.borrow_mut().pop();
69 self.import_scopes.borrow_mut().pop();
70 }
71
72 pub(super) fn declare_func(&self, name: &str) -> bool {
76 let mut scopes = self.func_scopes.borrow_mut();
77 match scopes.last_mut() {
78 Some(current) => current.insert(name.to_string()),
79 None => true,
80 }
81 }
82
83 pub(super) fn visible_func_names(&self) -> HashSet<String> {
88 self.func_scopes
89 .borrow()
90 .iter()
91 .flatten()
92 .cloned()
93 .collect()
94 }
95
96 pub(super) fn import_modules(&self, ctx: &SpanContext, modules: &[String]) -> ParseResult<()> {
98 let mut known: Vec<String> = self.modules.modules.keys().cloned().collect();
99 known.sort();
100 for module in modules {
101 if !self.modules.modules.contains_key(module) {
102 return Err(ParseError::validation(
103 KEYWORD_IMPORT,
104 format!(
105 "unknown module `{module}`; known modules: {}",
106 known.join(", ")
107 ),
108 ctx,
109 ));
110 }
111 let mut frames = self.import_scopes.borrow_mut();
112 match frames.last_mut() {
113 Some(frame) => {
114 if !frame.contains(module) {
115 frame.push(module.clone());
116 }
117 }
118 None => {
119 frames.push(vec![module.clone()]);
120 }
121 }
122 }
123 Ok(())
124 }
125
126 pub(super) fn resolve_call(&self, ctx: &SpanContext, name: &str) -> ParseResult<String> {
134 if let Some((module, base)) = split_qualified(name) {
135 if base == KEYWORD_INSPECT {
136 return Err(ParseError::validation(
137 "FUNC",
138 "INSPECT is a builtin keyword and cannot be module-qualified".to_string(),
139 ctx,
140 ));
141 }
142 check_func_ident(ctx, module)?;
143 check_func_ident(ctx, base)?;
144 match self.modules.modules.get(module) {
145 None => {
146 let mut known: Vec<String> = self.modules.modules.keys().cloned().collect();
147 known.sort();
148 Err(ParseError::validation(
149 "FUNC",
150 format!(
151 "unknown module `{module}`; known modules: {}",
152 known.join(", ")
153 ),
154 ctx,
155 ))
156 }
157 Some(None) => Ok(name.to_string()),
158 Some(Some(funcs)) => {
159 if funcs.functions.contains(base) {
160 Ok(qualify(module, base))
161 } else {
162 Err(ParseError::validation(
163 "FUNC",
164 format!("unknown function `{module}::{base}`"),
165 ctx,
166 ))
167 }
168 }
169 }
170 } else {
171 if name == KEYWORD_IMPORT || name == KEYWORD_EXPORT {
172 return Err(ParseError::validation(
173 "FUNC",
174 format!("`{name}` is a directive, not a function"),
175 ctx,
176 ));
177 }
178 if name == KEYWORD_INSPECT {
179 return Ok(name.to_string());
180 }
181 let scopes = self.func_scopes.borrow();
182 if scopes.iter().rev().any(|scope| scope.contains(name)) {
183 return Ok(qualify(SCRIPT_MODULE_NAME, name));
184 }
185 drop(scopes);
186 let frames = self.import_scopes.borrow();
189 let mut known_matches: Vec<String> = Vec::new();
190 let mut opaque_matches: Vec<String> = Vec::new();
191 for frame in frames.iter() {
192 for module in frame {
193 match self.modules.modules.get(module) {
194 Some(Some(funcs)) => {
195 if funcs.functions.contains(name) && !known_matches.contains(module) {
196 known_matches.push(module.clone());
197 }
198 }
199 Some(None) if !opaque_matches.contains(module) => {
200 opaque_matches.push(module.clone());
201 }
202 Some(None) | None => {}
203 }
204 }
205 }
206 drop(frames);
207 if known_matches.len() > 1 {
208 known_matches.sort();
209 return Err(ParseError::validation(
210 "FUNC",
211 format!(
212 "ambiguous function `{name}`: exported by {}; qualify it (e.g. `{}::{name}`)",
213 known_matches.join(", "),
214 known_matches[0],
215 ),
216 ctx,
217 ));
218 }
219 if let Some(module) = known_matches.pop() {
220 return Ok(qualify(&module, name));
221 }
222 if opaque_matches.len() > 1 {
223 opaque_matches.sort();
224 return Err(ParseError::validation(
225 "FUNC",
226 format!(
227 "ambiguous function `{name}`: imported opaque modules {}; qualify it",
228 opaque_matches.join(", "),
229 ),
230 ctx,
231 ));
232 }
233 if let Some(module) = opaque_matches.pop() {
234 return Ok(qualify(&module, name));
235 }
236 let mut exporters: Vec<String> = self
239 .modules
240 .modules
241 .iter()
242 .filter_map(|(module, funcs)| match funcs {
243 Some(funcs) if funcs.functions.contains(name) => Some(module.clone()),
244 _ => None,
245 })
246 .collect();
247 exporters.sort();
248 if let Some(first) = exporters.first() {
249 return Err(ParseError::validation(
250 "FUNC",
251 format!(
252 "unknown function `{name}`; qualify it (`{first}::{name}`) or add `IMPORT [{first}]`"
253 ),
254 ctx,
255 ));
256 }
257 Err(ParseError::validation(
258 "FUNC",
259 format!("unknown function `{name}`"),
260 ctx,
261 ))
262 }
263 }
264
265 pub(super) fn module_base_names(&self) -> HashSet<String> {
268 self.modules.reserved_base_names()
269 }
270
271 fn visible_snapshot(&self) -> (HashSet<String>, Vec<String>) {
276 let mut funcs = HashSet::new();
277 for scope in self.func_scopes.borrow().iter() {
278 funcs.extend(scope.iter().cloned());
279 }
280 let mut imports = Vec::new();
281 for frame in self.import_scopes.borrow().iter() {
282 for module in frame {
283 if !imports.contains(module) {
284 imports.push(module.clone());
285 }
286 }
287 }
288 (funcs, imports)
289 }
290}
291
292#[derive(Clone)]
293struct ScopeFrame {
294 line_no: usize,
295 had_command: bool,
296}
297
298#[derive(Clone)]
299struct PendingIoBlock<'a> {
300 line_no: usize,
301 span: SpanContext<'a>,
302 bindings: Vec<IoBinding>,
303 guards: Option<GuardExpr>,
304}
305
306#[derive(Clone)]
307struct IoScopeFrame {
308 line_no: usize,
309 had_command: bool,
310 bindings: Vec<IoBinding>,
311 guards: Option<GuardExpr>,
312 first_step: usize,
316}
317
318#[derive(Clone, Copy, Debug)]
319enum BlockKind {
320 Guard,
321 Io,
322}
323
324#[derive(Default)]
325struct IoBindingSet {
326 stdin: Option<IoBinding>,
327 stdout: Option<IoBinding>,
328 stderr: Option<IoBinding>,
329}
330
331impl IoBindingSet {
332 fn insert(&mut self, binding: IoBinding) {
333 match binding.stream {
334 IoStream::Stdin => self.stdin = Some(binding),
335 IoStream::Stdout => self.stdout = Some(binding),
336 IoStream::Stderr => self.stderr = Some(binding),
337 }
338 }
339
340 fn into_vec(self) -> Vec<IoBinding> {
341 let mut out = Vec::new();
342 if let Some(binding) = self.stdin {
343 out.push(binding);
344 }
345 if let Some(binding) = self.stdout {
346 out.push(binding);
347 }
348 if let Some(binding) = self.stderr {
349 out.push(binding);
350 }
351 out
352 }
353}
354
355pub struct ScriptParser<'a, F: Fn(&str, Vec<Arg>) -> ParseResult<StepKind>> {
356 input: &'a str,
357 tokens: VecDeque<RawToken<'a>>,
358 steps: Vec<Step>,
359 guard_stack: Vec<Option<GuardExpr>>,
360 pending_guards: Option<GuardExpr>,
361 pending_inline_guards: Option<GuardExpr>,
362 pending_can_open_block: bool,
363 pending_scope_enters: usize,
364 scope_stack: Vec<ScopeFrame>,
365 pending_io_block: Option<PendingIoBlock<'a>>,
366 io_scope_stack: Vec<IoScopeFrame>,
367 block_stack: Vec<BlockKind>,
368 lower: F,
369 reserved_names: HashSet<String>,
374 modules: ModuleTable,
378 preseed_imports: Vec<String>,
381 preseed_funcs: HashSet<String>,
384}
385
386impl<'a, F: Fn(&str, Vec<Arg>) -> ParseResult<StepKind>> ScriptParser<'a, F> {
387 pub fn new(input: &'a str, lower: F) -> ParseResult<Self> {
388 Self::new_with_hosts(input, lower, HashSet::new())
389 }
390
391 pub fn new_with_hosts(
392 input: &'a str,
393 lower: F,
394 reserved_names: HashSet<String>,
395 ) -> ParseResult<Self> {
396 Self::new_with_modules(input, lower, reserved_names, ModuleTable::default())
397 }
398
399 pub fn new_with_modules(
400 input: &'a str,
401 lower: F,
402 reserved_names: HashSet<String>,
403 modules: ModuleTable,
404 ) -> ParseResult<Self> {
405 Self::new_with_preseed(
406 input,
407 lower,
408 reserved_names,
409 modules,
410 HashSet::new(),
411 Vec::new(),
412 )
413 }
414
415 pub fn new_with_preseed(
416 input: &'a str,
417 lower: F,
418 reserved_names: HashSet<String>,
419 modules: ModuleTable,
420 preseed_funcs: HashSet<String>,
421 preseed_imports: Vec<String>,
422 ) -> ParseResult<Self> {
423 let tokens = VecDeque::from(lexer::tokenize(input)?);
424 Ok(Self {
425 input,
426 tokens,
427 steps: Vec::new(),
428 guard_stack: vec![None],
429 pending_guards: None,
430 pending_inline_guards: None,
431 pending_can_open_block: false,
432 pending_scope_enters: 0,
433 scope_stack: Vec::new(),
434 pending_io_block: None,
435 io_scope_stack: Vec::new(),
436 block_stack: Vec::new(),
437 lower,
438 reserved_names,
439 modules,
440 preseed_imports,
441 preseed_funcs,
442 })
443 }
444
445 fn eof_span(&self) -> SpanContext<'_> {
447 let lines = self.input.lines().count().max(1);
448 span_for_line(self.input, lines)
449 }
450
451 pub fn parse(mut self) -> ParseResult<Vec<Step>> {
452 let func_scopes: RefCell<Vec<HashSet<String>>> = RefCell::new(vec![HashSet::new()]);
461 let import_scopes: RefCell<Vec<Vec<String>>> = RefCell::new(vec![Vec::new()]);
465 func_scopes.borrow_mut()[0].extend(self.preseed_funcs.iter().cloned());
468 import_scopes.borrow_mut()[0].extend(self.preseed_imports.iter().cloned());
469 while let Some(token) = self.tokens.pop_front() {
470 let step_index = self.steps.len();
471 if self.pending_io_block.is_some()
472 && !matches!(
473 token,
474 RawToken::BlockStart { .. }
475 | RawToken::Command { .. }
476 | RawToken::Instruction { .. }
477 | RawToken::RunExec { .. }
478 )
479 {
480 let pending = self.pending_io_block.take().unwrap();
481 return Err(ParseError::structural(
482 "with_io",
483 format!(
484 "line {}: WITH_IO block must be followed by '{{'",
485 pending.line_no
486 ),
487 &pending.span,
488 ));
489 }
490 match token {
491 RawToken::Guard {
492 pair,
493 line_end,
494 span,
495 } => {
496 let span = span.with_step(step_index);
497 let groups = parse_guard_line(&span, pair)?;
498 self.handle_guard_token(line_end, groups)?;
499 }
500 RawToken::BlockStart { line_no, span } => {
501 let span = span.with_step(step_index);
502 self.start_block(&span, line_no)?;
503 LowerCtx::new(
504 &self.lower,
505 &self.reserved_names,
506 &func_scopes,
507 &self.modules,
508 &import_scopes,
509 )
510 .enter_scope();
511 }
512 RawToken::BlockEnd { line_no, span } => {
513 let span = span.with_step(step_index);
514 self.end_block(&span, line_no)?;
515 LowerCtx::new(
516 &self.lower,
517 &self.reserved_names,
518 &func_scopes,
519 &self.modules,
520 &import_scopes,
521 )
522 .exit_scope();
523 }
524 RawToken::Command {
525 pair,
526 line_no,
527 span,
528 } => {
529 let span = span.with_step(step_index);
530 let lctx = LowerCtx::new(
531 &self.lower,
532 &self.reserved_names,
533 &func_scopes,
534 &self.modules,
535 &import_scopes,
536 );
537 if pair.as_rule() == Rule::import_statement {
542 if self.pending_guards.is_some() || self.pending_inline_guards.is_some() {
543 return Err(ParseError::structural(
544 KEYWORD_IMPORT,
545 "IMPORT cannot be guarded".to_string(),
546 &span,
547 ));
548 }
549 self.lower_import(&span, pair, &lctx)?;
550 continue;
551 }
552 if pair.as_rule() == Rule::export_statement {
553 return Err(ParseError::validation(
554 KEYWORD_EXPORT,
555 "`EXPORT` is reserved for future script-module support and cannot be used yet.".to_string(),
556 &span,
557 ));
558 }
559 if pair.as_rule() == Rule::type_statement
560 && (self.pending_guards.is_some() || self.pending_inline_guards.is_some())
561 {
562 return Err(ParseError::structural(
563 KEYWORD_TYPE,
564 "TYPE cannot be guarded".to_string(),
565 &span,
566 ));
567 }
568 let kind = parse_structural_command_with_lower(&span, pair, &lctx)?;
569 self.handle_command_token(&span, line_no, kind)?;
570 }
571 RawToken::Instruction {
572 pair,
573 line_no,
574 span,
575 } => {
576 let span = span.with_step(step_index);
577 let lctx = LowerCtx::new(
578 &self.lower,
579 &self.reserved_names,
580 &func_scopes,
581 &self.modules,
582 &import_scopes,
583 );
584 let kind = self
585 .lower_instruction(&span, pair, &lctx)
586 .map_err(|e| e.with_span(&span))?;
587 self.handle_command_token(&span, line_no, kind)?;
588 }
589 RawToken::RunExec {
590 pair,
591 line_no,
592 span,
593 } => {
594 let span = span.with_step(step_index);
595 let lctx = LowerCtx::new(
596 &self.lower,
597 &self.reserved_names,
598 &func_scopes,
599 &self.modules,
600 &import_scopes,
601 );
602 let kind = lower_run_exec_pair(&span, pair, &lctx)?;
603 self.handle_command_token(&span, line_no, kind)?;
604 }
605 }
606 }
607
608 if let Some(pending) = self.pending_io_block.take() {
609 return Err(ParseError::structural(
610 "with_io",
611 format!(
612 "line {}: WITH_IO block must be followed by '{{'",
613 pending.line_no
614 ),
615 &pending.span,
616 ));
617 }
618
619 if self.guard_stack.len() != 1 {
620 let ctx = self.eof_span();
621 return Err(ParseError::structural(
622 "guard",
623 "unclosed guard block at end of script".to_string(),
624 &ctx,
625 ));
626 }
627 if self.pending_guards.is_some() {
628 let ctx = self.eof_span();
629 return Err(ParseError::structural(
630 "guard",
631 "guard declared on final lines without a following command".to_string(),
632 &ctx,
633 ));
634 }
635
636 if let Some(frame) = self.io_scope_stack.last() {
637 let ctx = span_for_line(self.input, frame.line_no);
638 return Err(ParseError::structural(
639 "with_io",
640 format!(
641 "WITH_IO block starting on line {} was not closed",
642 frame.line_no
643 ),
644 &ctx,
645 ));
646 }
647
648 {
651 let ctx = self.eof_span();
652 let mut seen_non_prelude = false;
653 let mut inherit_count = 0usize;
654 for step in &self.steps {
655 match &step.kind {
656 StepKind::InheritEnv { .. } => {
657 if seen_non_prelude {
658 return Err(ParseError::structural(
659 "inherit_env",
660 "INHERIT_ENV must appear before any other commands".to_string(),
661 &ctx,
662 ));
663 }
664 if step.guard.is_some() || step.scope_enter > 0 || step.scope_exit > 0 {
665 return Err(ParseError::structural(
666 "inherit_env",
667 "INHERIT_ENV cannot be guarded or nested inside blocks".to_string(),
668 &ctx,
669 ));
670 }
671 inherit_count += 1;
672 }
673 kind => {
674 if contains_inherit_env(kind) {
675 return Err(ParseError::structural(
676 "inherit_env",
677 "INHERIT_ENV cannot be nested inside other commands".to_string(),
678 &ctx,
679 ));
680 }
681 seen_non_prelude = true;
682 }
683 }
684 }
685 if inherit_count > 1 {
686 return Err(ParseError::structural(
687 "inherit_env",
688 "only one INHERIT_ENV directive is allowed".to_string(),
689 &ctx,
690 ));
691 }
692 }
693
694 {
699 let ctx = self.eof_span();
700 validate_remote_copy_placement(&self.steps, &ctx)?;
701 }
702
703 Ok(self.steps)
704 }
705
706 fn lower_instruction(
707 &self,
708 ctx: &SpanContext,
709 pair: Pair<Rule>,
710 lctx: &LowerCtx,
711 ) -> ParseResult<StepKind> {
712 lower_instruction_pair(ctx, pair, lctx)
713 }
714
715 fn lower_import(
719 &self,
720 ctx: &SpanContext,
721 pair: Pair<Rule>,
722 lctx: &LowerCtx,
723 ) -> ParseResult<()> {
724 lower_import_statement(ctx, pair, lctx)
725 }
726
727 fn handle_guard_token(&mut self, line_end: usize, expr: GuardExpr) -> ParseResult<()> {
728 if let Some(RawToken::Command { line_no, .. }) = self.tokens.front()
729 && *line_no == line_end
730 {
731 self.pending_inline_guards = Some(expr);
732 self.pending_can_open_block = false;
733 return Ok(());
734 }
735 self.stash_pending_guard(expr);
736 self.pending_can_open_block = true;
737 Ok(())
738 }
739
740 fn handle_command_token(
741 &mut self,
742 ctx: &SpanContext<'a>,
743 line_no: usize,
744 kind: StepKind,
745 ) -> ParseResult<()> {
746 let inline = self.pending_inline_guards.take();
747 self.handle_command(ctx, line_no, kind, inline)
748 }
749
750 fn stash_pending_guard(&mut self, guard: GuardExpr) {
751 self.pending_guards = Some(if let Some(existing) = self.pending_guards.take() {
752 GuardExpr::all(vec![existing, guard])
753 } else {
754 guard
755 });
756 }
757
758 fn start_guard_block_from_pending(
759 &mut self,
760 ctx: &SpanContext,
761 line_no: usize,
762 ) -> ParseResult<()> {
763 let guards = self.pending_guards.take().ok_or_else(|| {
764 ParseError::structural(
765 "guard",
766 format!("line {}: '{{' without a pending guard", line_no),
767 ctx,
768 )
769 })?;
770 if !self.pending_can_open_block {
771 return Err(ParseError::structural(
772 "guard",
773 format!("line {}: '{{' must directly follow a guard", line_no),
774 ctx,
775 ));
776 }
777 self.pending_can_open_block = false;
778 self.enter_guard_block(guards, line_no)
779 }
780
781 fn enter_guard_block(&mut self, guard: GuardExpr, line_no: usize) -> ParseResult<()> {
782 let composed = if let Some(pending) = self.pending_guards.take() {
783 GuardExpr::all(vec![pending, guard])
784 } else {
785 guard
786 };
787 let parent = self.guard_stack.last().cloned().unwrap_or(None);
788 let next = and_guard_exprs(parent, Some(composed));
789 self.guard_stack.push(next);
790 self.scope_stack.push(ScopeFrame {
791 line_no,
792 had_command: false,
793 });
794 self.pending_scope_enters += 1;
795 Ok(())
796 }
797
798 fn begin_io_block(
799 &mut self,
800 ctx: &SpanContext<'a>,
801 line_no: usize,
802 bindings: Vec<IoBinding>,
803 guards: Option<GuardExpr>,
804 ) -> ParseResult<()> {
805 if self.pending_io_block.is_some() {
806 return Err(ParseError::structural(
807 "with_io",
808 format!(
809 "line {}: previous WITH_IO block is still waiting for '{{'",
810 line_no
811 ),
812 ctx,
813 ));
814 }
815 self.pending_io_block = Some(PendingIoBlock {
816 line_no,
817 span: ctx.clone(),
818 bindings,
819 guards,
820 });
821 Ok(())
822 }
823
824 fn start_block(&mut self, ctx: &SpanContext, line_no: usize) -> ParseResult<()> {
825 if let Some(pending) = self.pending_io_block.take() {
826 self.block_stack.push(BlockKind::Io);
827 self.io_scope_stack.push(IoScopeFrame {
828 line_no: pending.line_no,
829 had_command: false,
830 bindings: pending.bindings,
831 guards: pending.guards,
832 first_step: self.steps.len(),
833 });
834 Ok(())
835 } else {
836 self.start_guard_block_from_pending(ctx, line_no)?;
837 self.block_stack.push(BlockKind::Guard);
838 Ok(())
839 }
840 }
841
842 fn end_block(&mut self, ctx: &SpanContext, line_no: usize) -> ParseResult<()> {
843 let kind = self.block_stack.pop().ok_or_else(|| {
844 ParseError::structural("block", format!("line {}: unexpected '}}'", line_no), ctx)
845 })?;
846 match kind {
847 BlockKind::Guard => self.end_guard_block(ctx, line_no),
848 BlockKind::Io => self.end_io_block(ctx, line_no),
849 }
850 }
851
852 fn end_guard_block(&mut self, ctx: &SpanContext, line_no: usize) -> ParseResult<()> {
853 if self.guard_stack.len() == 1 {
854 return Err(ParseError::structural(
855 "guard",
856 format!("line {}: unexpected '}}'", line_no),
857 ctx,
858 ));
859 }
860 if self.pending_guards.is_some() {
861 return Err(ParseError::structural(
862 "guard",
863 format!(
864 "line {}: guard declared immediately before '}}' without a command",
865 line_no
866 ),
867 ctx,
868 ));
869 }
870 let frame = self.scope_stack.last().cloned().ok_or_else(|| {
871 ParseError::structural(
872 "guard",
873 format!("line {}: scope stack underflow", line_no),
874 ctx,
875 )
876 })?;
877 if !frame.had_command {
878 return Err(ParseError::structural(
879 "guard",
880 format!(
881 "line {}: guard block starting on line {} must contain at least one command",
882 line_no, frame.line_no
883 ),
884 ctx,
885 ));
886 }
887 let step = self.steps.last_mut().ok_or_else(|| {
888 ParseError::structural(
889 "guard",
890 format!("line {}: guard block closed without any commands", line_no),
891 ctx,
892 )
893 })?;
894 step.scope_exit += 1;
895 self.scope_stack.pop();
896 self.guard_stack.pop();
897 Ok(())
898 }
899
900 fn end_io_block(&mut self, ctx: &SpanContext, line_no: usize) -> ParseResult<()> {
901 let frame = self.io_scope_stack.pop().ok_or_else(|| {
902 ParseError::structural("with_io", format!("line {}: unexpected '}}'", line_no), ctx)
903 })?;
904 if !frame.had_command {
905 return Err(ParseError::structural(
906 "with_io",
907 format!(
908 "line {}: WITH_IO block starting on line {} must contain at least one command",
909 line_no, frame.line_no
910 ),
911 ctx,
912 ));
913 }
914 if self.steps.len() > frame.first_step {
918 self.steps[frame.first_step].scope_enter += 1;
919 if let Some(last) = self.steps.last_mut() {
920 last.scope_exit += 1;
921 }
922 }
923 Ok(())
924 }
925
926 fn guard_context(&mut self, inline: Option<GuardExpr>) -> Option<GuardExpr> {
927 let mut context = self.guard_stack.last().cloned().unwrap_or(None);
928 if let Some(pending) = self.pending_guards.take() {
929 context = and_guard_exprs(context, Some(pending));
930 self.pending_can_open_block = false;
931 }
932 if let Some(inline_guard) = inline {
933 context = and_guard_exprs(context, Some(inline_guard));
934 }
935 context
936 }
937
938 fn handle_command(
939 &mut self,
940 ctx: &SpanContext<'a>,
941 line_no: usize,
942 kind: StepKind,
943 inline_guards: Option<GuardExpr>,
944 ) -> ParseResult<()> {
945 if let StepKind::WithIoBlock { bindings } = kind {
946 let guards = self.guard_context(inline_guards);
947 self.begin_io_block(ctx, line_no, bindings, guards)?;
948 return Ok(());
949 }
950
951 let guards = self.guard_context(inline_guards);
952 let guards = self.apply_io_guards(guards);
953 let scope_enter = self.pending_scope_enters;
954 self.pending_scope_enters = 0;
955 for frame in self.scope_stack.iter_mut() {
956 frame.had_command = true;
957 }
958 for frame in self.io_scope_stack.iter_mut() {
959 frame.had_command = true;
960 }
961 let kind = self.apply_io_defaults(kind);
962 self.steps.push(Step {
963 guard: guards,
964 kind,
965 scope_enter,
966 scope_exit: 0,
967 });
968 Ok(())
969 }
970
971 fn apply_io_defaults(&self, kind: StepKind) -> StepKind {
972 let defaults = self.current_io_defaults();
973 if defaults.is_empty() {
974 return kind;
975 }
976 match kind {
977 StepKind::WithIo { bindings, cmd } => StepKind::WithIo {
978 bindings: merge_bindings(&defaults, &bindings),
979 cmd,
980 },
981 other => StepKind::WithIo {
982 bindings: defaults,
983 cmd: Box::new(other),
984 },
985 }
986 }
987
988 fn current_io_defaults(&self) -> Vec<IoBinding> {
989 if self.io_scope_stack.is_empty() {
990 return Vec::new();
991 }
992 let mut set = IoBindingSet::default();
993 for frame in &self.io_scope_stack {
994 for binding in &frame.bindings {
995 set.insert(binding.clone());
996 }
997 }
998 set.into_vec()
999 }
1000
1001 fn apply_io_guards(&self, guard: Option<GuardExpr>) -> Option<GuardExpr> {
1002 self.io_scope_stack.iter().fold(guard, |acc, frame| {
1003 and_guard_exprs(acc, frame.guards.clone())
1004 })
1005 }
1006}
1007
1008pub fn parse_script(
1009 input: &str,
1010 lower: impl Fn(&str, Vec<Arg>) -> ParseResult<StepKind>,
1011) -> ParseResult<Vec<Step>> {
1012 ScriptParser::new(input, lower)?.parse()
1013}
1014
1015pub fn parse_script_with_preseed(
1019 input: &str,
1020 lower: impl Fn(&str, Vec<Arg>) -> ParseResult<StepKind>,
1021 reserved_names: HashSet<String>,
1022 modules: ModuleTable,
1023 preseed_funcs: HashSet<String>,
1024 preseed_imports: Vec<String>,
1025) -> ParseResult<Vec<Step>> {
1026 ScriptParser::new_with_preseed(
1027 input,
1028 lower,
1029 reserved_names,
1030 modules,
1031 preseed_funcs,
1032 preseed_imports,
1033 )?
1034 .parse()
1035}
1036
1037pub fn parse_script_with_modules(
1042 input: &str,
1043 lower: impl Fn(&str, Vec<Arg>) -> ParseResult<StepKind>,
1044 reserved_names: HashSet<String>,
1045 modules: ModuleTable,
1046) -> ParseResult<Vec<Step>> {
1047 ScriptParser::new_with_modules(input, lower, reserved_names, modules)?.parse()
1048}
1049
1050pub fn parse_guard_expr_str(input: &str) -> ParseResult<GuardExpr> {
1051 use pest::Parser;
1052 let pairs = lexer::LanguageParser::parse(Rule::guard_expr, input).map_err(parse_pest_error)?;
1053 let pair = pairs.into_iter().next().ok_or_else(|| {
1054 ParseError::structural("guard", "empty guard".to_string(), &span_for_line(input, 1))
1055 })?;
1056 let ctx = span_of(&pair, input);
1057 parse_guard_expr(&ctx, pair)
1058}
1059
1060fn and_guard_exprs(left: Option<GuardExpr>, right: Option<GuardExpr>) -> Option<GuardExpr> {
1061 match (left, right) {
1062 (None, None) => None,
1063 (Some(expr), None) | (None, Some(expr)) => Some(expr),
1064 (Some(lhs), Some(rhs)) => Some(GuardExpr::all(vec![lhs, rhs])),
1065 }
1066}
1067
1068fn merge_bindings(defaults: &[IoBinding], overrides: &[IoBinding]) -> Vec<IoBinding> {
1069 let mut set = IoBindingSet::default();
1070 for binding in defaults {
1071 set.insert(binding.clone());
1072 }
1073 for binding in overrides {
1074 set.insert(binding.clone());
1075 }
1076 set.into_vec()
1077}
1078
1079fn validate_remote_copy_placement(steps: &[Step], ctx: &SpanContext) -> ParseResult<()> {
1083 fn visit_kind(kind: &StepKind, in_remote: bool, ctx: &SpanContext) -> ParseResult<()> {
1084 if let StepKind::Copy {
1085 from_host, to_host, ..
1086 } = kind
1087 && (*from_host || *to_host)
1088 && !in_remote
1089 {
1090 return Err(ParseError::structural(
1091 "copy",
1092 "COPY --from-host and --to-host are only valid inside a REMOTE block".to_string(),
1093 ctx,
1094 ));
1095 }
1096 let child_remote = in_remote || matches!(kind, StepKind::RemoteBlock { .. });
1097 let mut result = Ok(());
1098 kind.walk_child_kinds(&mut |child| {
1099 if result.is_ok() {
1100 result = visit_kind(child, child_remote, ctx);
1101 }
1102 });
1103 result
1104 }
1105 for step in steps {
1106 visit_kind(&step.kind, false, ctx)?;
1107 }
1108 Ok(())
1109}
1110
1111fn contains_inherit_env(kind: &StepKind) -> bool {
1112 if matches!(kind, StepKind::InheritEnv { .. }) {
1113 return true;
1114 }
1115 let mut found = false;
1116 kind.walk_child_kinds(&mut |child| {
1117 found = found || contains_inherit_env(child);
1118 });
1119 found
1120}
1121
1122fn has_stdout_pipe(bindings: &[IoBinding]) -> bool {
1125 bindings
1126 .iter()
1127 .any(|b| b.stream == IoStream::Stdout && b.pipe.is_some())
1128}
1129
1130fn reject_async_in_capture(ctx: &SpanContext, kind: &StepKind) -> ParseResult<()> {
1133 let bad = match kind {
1134 StepKind::AsyncBlock { .. }
1135 | StepKind::AssignAsync { .. }
1136 | StepKind::Await { .. }
1137 | StepKind::AwaitCapture { .. }
1138 | StepKind::Cancel { .. } => true,
1139 _ => {
1140 let mut bad = false;
1141 kind.walk_child_kinds(&mut |child| {
1142 bad = bad || reject_async_in_capture(ctx, child).is_err();
1143 });
1144 bad
1145 }
1146 };
1147 if bad {
1148 return Err(ParseError::structural("let", "LET capture cannot run ASYNC/AWAIT/CANCEL inline; use LET $t: HANDLE = ASYNC ... then LET $o: STRING = AWAIT $t".to_string(), ctx));
1149 }
1150 Ok(())
1151}
1152
1153fn reject_pipe_stdout_in_capture(ctx: &SpanContext, kind: &StepKind) -> ParseResult<()> {
1156 if let StepKind::WithIo { bindings, .. } = kind
1157 && has_stdout_pipe(bindings)
1158 {
1159 return Err(ParseError::structural(
1160 "let",
1161 "LET capture cannot use WITH_IO [stdout=$var]; the capture sink owns stdout"
1162 .to_string(),
1163 ctx,
1164 ));
1165 }
1166 let mut result = Ok(());
1167 kind.walk_child_kinds(&mut |child| {
1168 if result.is_ok() {
1169 result = reject_pipe_stdout_in_capture(ctx, child);
1170 }
1171 });
1172 result
1173}
1174
1175fn parse_expr_str(ctx: &SpanContext, lctx: &LowerCtx, text: &str) -> ParseResult<Expr> {
1179 use pest::Parser;
1180 let mut pairs = lexer::LanguageParser::parse(Rule::expr, text).map_err(parse_pest_error)?;
1181 let pair = pairs.next().ok_or_else(|| {
1182 ParseError::validation("LET", "LET requires an expression".to_string(), ctx)
1183 })?;
1184 if pair.as_span().end() != text.len() {
1185 return Err(ParseError::structural(
1186 "expr",
1187 format!("invalid LET expression {text:?}"),
1188 ctx,
1189 ));
1190 }
1191 parse_expr(ctx, lctx, pair)
1192}
1193
1194fn parse_structural_command_with_lower(
1195 ctx: &SpanContext,
1196 pair: Pair<Rule>,
1197 lctx: &LowerCtx,
1198) -> ParseResult<StepKind> {
1199 let span = refine_span(ctx, &pair);
1200 let kind = match pair.as_rule() {
1201 Rule::inherit_env_command => {
1202 let mut keys = Vec::new();
1203 for inner in pair.into_inner() {
1204 if inner.as_rule() == Rule::inherit_list {
1205 for key in inner.into_inner() {
1206 if key.as_rule() == Rule::env_key {
1207 keys.push(key.as_str().trim().to_string());
1208 }
1209 }
1210 } else if inner.as_rule() == Rule::env_key {
1211 keys.push(inner.as_str().trim().to_string());
1212 }
1213 }
1214 StepKind::InheritEnv { keys }
1215 }
1216 Rule::with_io_command => {
1217 let mut bindings = Vec::new();
1218 let mut cmd = None;
1219 for inner in pair.into_inner() {
1220 match inner.as_rule() {
1221 Rule::io_flags => {
1222 for flag in inner.into_inner() {
1223 if flag.as_rule() == Rule::io_binding {
1224 bindings.push(parse_io_binding(ctx, flag)?);
1225 }
1226 }
1227 }
1228 Rule::with_io_command => {
1229 cmd = Some(Box::new(parse_structural_command_with_lower(
1230 ctx, inner, lctx,
1231 )?));
1232 }
1233 Rule::inherit_env_command => {
1234 cmd = Some(Box::new(parse_structural_command_with_lower(
1235 ctx, inner, lctx,
1236 )?));
1237 }
1238 Rule::async_statement | Rule::async_statement_block => {
1239 cmd = Some(Box::new(parse_structural_command_with_lower(
1240 ctx, inner, lctx,
1241 )?));
1242 }
1243 Rule::timeout_statement | Rule::cancel_statement => {
1244 cmd = Some(Box::new(parse_structural_command_with_lower(
1245 ctx, inner, lctx,
1246 )?));
1247 }
1248 Rule::remote_statement => {
1249 cmd = Some(Box::new(parse_structural_command_with_lower(
1250 ctx, inner, lctx,
1251 )?));
1252 }
1253 Rule::call_statement | Rule::while_statement => {
1254 cmd = Some(Box::new(parse_structural_command_with_lower(
1255 ctx, inner, lctx,
1256 )?));
1257 }
1258 Rule::func_def
1259 | Rule::return_statement
1260 | Rule::break_statement
1261 | Rule::continue_statement => {
1262 return Err(ParseError::structural(
1263 "parser",
1264 format!(
1265 "WITH_IO cannot wrap {:?}; place it around a command or block instead",
1266 inner.as_rule()
1267 ),
1268 &span,
1269 ));
1270 }
1271 Rule::instruction | Rule::instruction_inner => {
1272 cmd = Some(Box::new(lower_instruction_pair(ctx, inner, lctx)?));
1273 }
1274 Rule::run_exec_statement | Rule::run_exec_inner => {
1275 cmd = Some(Box::new(lower_run_exec_pair(ctx, inner, lctx)?));
1276 }
1277 _ => {}
1278 }
1279 }
1280 if let Some(cmd) = cmd {
1281 StepKind::WithIo { bindings, cmd }
1282 } else {
1283 StepKind::WithIoBlock { bindings }
1284 }
1285 }
1286 Rule::for_statement => parse_for_statement_from_pair(ctx, pair, lctx)?,
1287 Rule::while_statement => parse_while_statement_from_pair(ctx, pair, lctx)?,
1288 Rule::func_def => parse_func_def_from_pair(ctx, pair, lctx)?,
1289 Rule::call_statement => parse_call_statement_from_pair(ctx, lctx, pair)?,
1290 Rule::return_statement => parse_return_statement_from_pair(ctx, lctx, pair)?,
1291 Rule::break_statement => StepKind::Break,
1292 Rule::continue_statement => StepKind::Continue,
1293 Rule::let_statement => parse_let_statement_from_pair(ctx, lctx, pair)?,
1294 Rule::mutate_statement => parse_mutate_statement_from_pair(ctx, lctx, pair)?,
1295 Rule::let_async_statement => parse_let_async_statement_from_pair(ctx, pair, lctx)?,
1296 Rule::let_capture_statement => parse_let_capture_statement_from_pair(ctx, pair, lctx)?,
1297 Rule::await_statement => parse_await_statement_from_pair(ctx, pair)?,
1298 Rule::cancel_statement => parse_cancel_statement_from_pair(ctx, pair)?,
1299 Rule::if_statement => parse_if_statement_from_pair(ctx, pair, lctx)?,
1300 Rule::async_statement => parse_async_statement_from_pair(ctx, pair, lctx)?,
1301 Rule::async_statement_block => parse_async_statement_block_from_pair(ctx, pair, lctx)?,
1302 Rule::timeout_statement => parse_timeout_statement_from_pair(ctx, pair, lctx)?,
1303 Rule::remote_statement => parse_remote_statement_from_pair(ctx, pair, lctx)?,
1304 Rule::type_statement => parse_type_statement_from_pair(ctx, pair)?,
1305 Rule::command_inner => {
1306 let inner = pair.into_inner().next().ok_or_else(|| {
1309 ParseError::structural("parser", "empty command_inner".to_string(), &span)
1310 })?;
1311 parse_structural_command_with_lower(ctx, inner, lctx)?
1312 }
1313 Rule::instruction | Rule::instruction_inner => lower_instruction_pair(ctx, pair, lctx)?,
1314 Rule::run_exec_statement | Rule::run_exec_inner => lower_run_exec_pair(ctx, pair, lctx)?,
1315 _ => {
1316 return Err(ParseError::structural(
1317 "parser",
1318 format!("unexpected structural command rule: {:?}", pair.as_rule()),
1319 &span,
1320 ));
1321 }
1322 };
1323 Ok(kind)
1324}
1325
1326fn extract_instruction(
1327 ctx: &SpanContext,
1328 pair: Pair<Rule>,
1329 lctx: &LowerCtx,
1330) -> ParseResult<(String, Vec<InsToken>)> {
1331 let span = refine_span(ctx, &pair);
1332 let mut name = None;
1333 let mut args = Vec::new();
1334 for inner in pair.into_inner() {
1335 match inner.as_rule() {
1336 Rule::command_name => {
1337 name = Some(inner.as_str().to_string());
1338 }
1339 Rule::argument => {
1340 args.extend(
1341 parse_argument(ctx, lctx, inner)?
1342 .into_iter()
1343 .map(InsToken::Pos),
1344 );
1345 }
1346 Rule::assignment => {
1347 let (key, value) = parse_assignment(ctx, lctx, inner)?;
1348 args.push(InsToken::Assign(key, value));
1349 }
1350 _ => {}
1351 }
1352 }
1353 let name = name.ok_or_else(|| {
1354 ParseError::structural(
1355 "instruction",
1356 "instruction missing command name".to_string(),
1357 &span,
1358 )
1359 })?;
1360 Ok((name, args))
1361}
1362
1363enum InsToken {
1368 Pos(Arg),
1369 Assign(String, Arg),
1370}
1371
1372fn lower_instruction_pair(
1377 ctx: &SpanContext,
1378 pair: Pair<Rule>,
1379 lctx: &LowerCtx,
1380) -> ParseResult<StepKind> {
1381 let span = refine_span(ctx, &pair);
1382 let (name, tokens) = extract_instruction(ctx, pair, lctx)?;
1383 if name == "ENV" {
1384 return lower_env_command(ctx, tokens);
1385 }
1386 if name == "EXPAND" {
1387 return lower_expand_command(ctx, tokens);
1388 }
1389 let args = tokens
1390 .into_iter()
1391 .map(|token| match token {
1392 InsToken::Pos(arg) => arg,
1393 InsToken::Assign(key, value) => crate::commands::canonical_assignment_arg(&key, &value),
1394 })
1395 .collect();
1396 (lctx.lower)(&name, args).map_err(|e| e.with_span(&span))
1397}
1398
1399fn lower_run_exec_pair(
1405 ctx: &SpanContext,
1406 pair: Pair<Rule>,
1407 lctx: &LowerCtx,
1408) -> ParseResult<StepKind> {
1409 let span = refine_span(ctx, &pair);
1410 let mut list = None;
1411 for inner in pair.into_inner() {
1412 if inner.as_rule() == Rule::run_exec_list {
1413 list = Some(parse_run_exec_list(ctx, lctx, inner)?);
1414 }
1415 }
1416 let list = list.ok_or_else(|| {
1417 ParseError::structural(
1418 "run_exec",
1419 "RUN exec form missing list literal".to_string(),
1420 &span,
1421 )
1422 })?;
1423 (lctx.lower)("RUN", vec![Arg::Expr(list)]).map_err(|e| e.with_span(&span))
1424}
1425
1426fn parse_run_exec_list(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
1431 let mut items = Vec::new();
1432 for inner in pair.into_inner() {
1433 if inner.as_rule() == Rule::run_exec_arg {
1434 let item = parse_run_exec_arg(ctx, lctx, inner)?;
1435 reject_boundary(ctx, &item)?;
1436 items.push(item);
1437 }
1438 }
1439 Ok(Expr::List(items))
1440}
1441
1442fn parse_run_exec_arg(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
1443 let span = refine_span(ctx, &pair);
1444 let inner = pair.into_inner().next().ok_or_else(|| {
1445 ParseError::structural("run_exec", "RUN exec argument is empty".to_string(), &span)
1446 })?;
1447 match inner.as_rule() {
1448 Rule::parenthesized_expr => parse_expr_inner(ctx, lctx, inner.into_inner().next().unwrap()),
1449 Rule::func_call => parse_func_call(ctx, lctx, inner),
1450 Rule::key_path => parse_key_path(ctx, inner),
1451 Rule::variable => {
1452 let name = inner.as_str();
1453 let name = name.strip_prefix('$').unwrap_or(name).to_string();
1454 Ok(Expr::Var(name))
1455 }
1456 Rule::env_read => parse_env_read(ctx, inner).map(Expr::Env),
1457 Rule::list_literal => parse_list_literal(ctx, lctx, inner),
1458 Rule::map_literal => parse_map_literal(ctx, lctx, inner),
1459 Rule::block => Ok(Expr::Block(parse_block_elements_with_lower(
1460 ctx, inner, lctx,
1461 )?)),
1462 Rule::string_literal | Rule::quoted_string => {
1463 let s = parse_quoted_string(inner)?;
1464 Ok(Expr::Literal(Value::string(s)))
1465 }
1466 Rule::numeric_literal => parse_numeric_literal(ctx, inner),
1467 Rule::bare_word => {
1468 let s = inner.as_str().to_string();
1469 match s.as_str() {
1470 "true" => Ok(Expr::Literal(Value::bool(true))),
1471 "false" => Ok(Expr::Literal(Value::bool(false))),
1472 _ => Ok(Expr::Literal(Value::string(s))),
1473 }
1474 }
1475 _ => Err(ParseError::structural(
1476 "run_exec",
1477 format!("unexpected RUN exec argument rule: {:?}", inner.as_rule()),
1478 &span,
1479 )),
1480 }
1481}
1482
1483fn parse_assignment(
1485 ctx: &SpanContext,
1486 lctx: &LowerCtx,
1487 pair: Pair<Rule>,
1488) -> ParseResult<(String, Arg)> {
1489 let span = refine_span(ctx, &pair);
1490 let mut key = None;
1491 let mut value = None;
1492 for inner in pair.into_inner() {
1493 match inner.as_rule() {
1494 Rule::assign_key => {
1495 key = Some(inner.as_str().to_string());
1496 }
1497 Rule::assign_value => {
1498 value = Some(lower_command_value(ctx, lctx, inner)?);
1499 }
1500 _ => {
1501 return Err(ParseError::structural(
1502 "assignment",
1503 format!("unexpected assignment rule: {:?}", inner.as_rule()),
1504 &span,
1505 ));
1506 }
1507 }
1508 }
1509 Ok((
1510 key.ok_or_else(|| {
1511 ParseError::structural("assignment", "assignment missing key".to_string(), &span)
1512 })?,
1513 value.unwrap_or(Arg::String(String::new(), false)),
1514 ))
1515}
1516
1517fn lower_command_value(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Arg> {
1522 let span = refine_span(ctx, &pair);
1523 let inner = pair.into_inner().next().ok_or_else(|| {
1524 ParseError::structural("assignment", "assignment value is empty".to_string(), &span)
1525 })?;
1526 match inner.as_rule() {
1527 Rule::quoted_string => Ok(Arg::String(parse_quoted_string(inner)?, true)),
1528 Rule::assign_expr => {
1529 let shape = inner.into_inner().next().ok_or_else(|| {
1530 ParseError::structural(
1531 "assignment",
1532 "assignment expression is empty".to_string(),
1533 &span,
1534 )
1535 })?;
1536 match shape.as_rule() {
1537 Rule::variable => Ok(Arg::Expr(Expr::Var(parse_dollar_ident(shape)))),
1538 Rule::key_path => Ok(Arg::Expr(parse_key_path(ctx, shape)?)),
1539 Rule::env_read => Ok(Arg::Expr(Expr::Env(parse_env_read(ctx, shape)?))),
1540 Rule::func_call => Ok(Arg::Expr(parse_func_call(ctx, lctx, shape)?)),
1541 other => Err(ParseError::structural(
1542 "assignment",
1543 format!("unexpected assignment expression shape: {:?}", other),
1544 &span,
1545 )),
1546 }
1547 }
1548 Rule::raw_fragments => lower_raw_fragments(ctx, inner),
1549 other => Err(ParseError::structural(
1550 "assignment",
1551 format!("unexpected assignment value rule: {:?}", other),
1552 &span,
1553 )),
1554 }
1555}
1556
1557fn lower_raw_fragments(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Arg> {
1563 let span = refine_span(ctx, &pair);
1564 let mut body = String::new();
1565 for fragment in pair.into_inner() {
1566 match fragment.as_rule() {
1567 Rule::quoted_string => body.push_str(&parse_quoted_string(fragment)?),
1568 Rule::templated_arg => body.push_str(fragment.as_str()),
1569 Rule::raw_text => body.push_str(&collapse_ws(fragment.as_str())),
1570 other => {
1571 return Err(ParseError::structural(
1572 "assignment",
1573 format!("unexpected raw value fragment: {:?}", other),
1574 &span,
1575 ));
1576 }
1577 }
1578 }
1579 Ok(Arg::String(body.trim().to_string(), false))
1580}
1581
1582fn collapse_ws(s: &str) -> String {
1585 let mut out = String::with_capacity(s.len());
1586 let mut in_run = false;
1587 for c in s.chars() {
1588 if c.is_whitespace() {
1589 if !in_run {
1590 out.push(' ');
1591 in_run = true;
1592 }
1593 } else {
1594 out.push(c);
1595 in_run = false;
1596 }
1597 }
1598 out
1599}
1600
1601fn lower_env_command(ctx: &SpanContext, tokens: Vec<InsToken>) -> ParseResult<StepKind> {
1605 if tokens.is_empty() {
1606 return Err(ParseError::validation(
1607 "ENV",
1608 "ENV requires KEY=value".to_string(),
1609 ctx,
1610 ));
1611 }
1612 match tokens.as_slice() {
1613 [InsToken::Assign(key, value)] => {
1614 if crate::command::split_assignment(&format!("{key}={}", value.render()))
1617 .map_err(|e| ParseError::validation("ENV", e.to_string(), ctx))?
1618 .is_none()
1619 {
1620 return Err(ParseError::validation(
1621 "ENV",
1622 "ENV requires KEY=value format".to_string(),
1623 ctx,
1624 ));
1625 }
1626 Ok(StepKind::Env {
1627 key: key.clone(),
1628 value: value.clone(),
1629 })
1630 }
1631 [InsToken::Pos(Arg::String(text, _))] => match crate::command::split_assignment(text)
1632 .map_err(|e| ParseError::validation("ENV", e.to_string(), ctx))?
1633 {
1634 Some((key, value)) => Ok(StepKind::Env { key, value }),
1635 None => Err(ParseError::validation(
1636 "ENV",
1637 "ENV requires KEY=value format".to_string(),
1638 ctx,
1639 )),
1640 },
1641 _ => Err(ParseError::validation(
1642 "ENV",
1643 "ENV requires KEY=value format".to_string(),
1644 ctx,
1645 )),
1646 }
1647}
1648
1649fn lower_expand_command(ctx: &SpanContext, tokens: Vec<InsToken>) -> ParseResult<StepKind> {
1653 let mut path = None;
1654 let mut overrides = Vec::new();
1655 for token in tokens {
1656 match token {
1657 InsToken::Assign(key, value) => {
1658 if key.is_empty() {
1659 return Err(ParseError::validation(
1660 "EXPAND",
1661 "EXPAND requires KEY=value format for overrides".to_string(),
1662 ctx,
1663 ));
1664 }
1665 overrides.push((key, value));
1666 }
1667 InsToken::Pos(arg) => match &arg {
1668 Arg::String(text, quoted) if !quoted && text.contains('=') => {
1669 let Some((key, value)) = crate::command::split_assignment(text)
1670 .map_err(|e| ParseError::validation("EXPAND", e.to_string(), ctx))?
1671 else {
1672 return Err(ParseError::validation(
1673 "EXPAND",
1674 "EXPAND requires KEY=value format for overrides".to_string(),
1675 ctx,
1676 ));
1677 };
1678 overrides.push((key, value));
1679 }
1680 _ => {
1681 if path.is_none() {
1682 ArgType::Path
1686 .check_arg(&arg)
1687 .map_err(|e| ParseError::validation("EXPAND", e.to_string(), ctx))?;
1688 path = Some(arg);
1689 } else {
1690 return Err(ParseError::validation(
1691 "EXPAND",
1692 "EXPAND accepts at most one path".to_string(),
1693 ctx,
1694 ));
1695 }
1696 }
1697 },
1698 }
1699 }
1700 Ok(StepKind::Expand { path, overrides })
1701}
1702
1703fn parse_type_tag(pair: Pair<Rule>) -> String {
1704 crate::canonicalize_spelling(pair.as_str())
1710}
1711
1712fn parse_type_statement_from_pair(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<StepKind> {
1718 let span = refine_span(ctx, &pair);
1719 let mut name: Option<String> = None;
1720 let mut target: Option<String> = None;
1721 for inner in pair.into_inner() {
1722 match inner.as_rule() {
1723 Rule::type_name => name = Some(inner.as_str().trim().to_string()),
1724 Rule::type_tag => target = Some(parse_type_tag(inner)),
1725 _ => {}
1726 }
1727 }
1728 let (Some(name), Some(target)) = (name, target) else {
1729 return Err(ParseError::structural(
1730 KEYWORD_TYPE,
1731 "TYPE requires a name and a shape (`TYPE NAME = SHAPE`)".to_string(),
1732 &span,
1733 ));
1734 };
1735 Ok(StepKind::TypeAlias { name, target })
1736}
1737
1738fn check_func_ident(ctx: &SpanContext, name: &str) -> ParseResult<()> {
1739 let ok = name
1740 .chars()
1741 .next()
1742 .map(|c| c.is_ascii_uppercase())
1743 .unwrap_or(false)
1744 && name
1745 .chars()
1746 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_');
1747 if !ok {
1748 return Err(ParseError::validation(
1749 "FUNC",
1750 format!(
1751 "function names must be UPPERCASE (ASCII_ALPHA_UPPER, digits, _), got `{name}`"
1752 ),
1753 ctx,
1754 ));
1755 }
1756 Ok(())
1757}
1758
1759fn parse_while_statement_from_pair(
1760 ctx: &SpanContext,
1761 pair: Pair<Rule>,
1762 lctx: &LowerCtx,
1763) -> ParseResult<StepKind> {
1764 let span = refine_span(ctx, &pair);
1765 let mut cond = None;
1766 let mut body = None;
1767 for inner in pair.into_inner() {
1768 match inner.as_rule() {
1769 Rule::expr => {
1770 if cond.is_none() {
1771 cond = Some(parse_expr(ctx, lctx, inner)?);
1772 }
1773 }
1774 Rule::block => {
1775 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
1776 }
1777 _ => {}
1778 }
1779 }
1780 Ok(StepKind::While {
1781 cond: Box::new(cond.ok_or_else(|| {
1782 ParseError::validation("WHILE", "WHILE requires a condition".to_string(), &span)
1783 })?),
1784 body: body.ok_or_else(|| {
1785 ParseError::validation("WHILE", "WHILE requires a block".to_string(), &span)
1786 })?,
1787 })
1788}
1789
1790fn parse_func_def_from_pair(
1791 ctx: &SpanContext,
1792 pair: Pair<Rule>,
1793 lctx: &LowerCtx,
1794) -> ParseResult<StepKind> {
1795 let span = refine_span(ctx, &pair);
1796 let def_name = pair
1801 .clone()
1802 .into_inner()
1803 .find(|inner| inner.as_rule() == Rule::func_ident)
1804 .map(|inner| inner.as_str().to_string())
1805 .ok_or_else(|| ParseError::validation("FUNC", "FUNC requires a name".to_string(), &span))?;
1806 check_func_ident(ctx, &def_name)?;
1807 if lctx.reserved_names.contains(&def_name) || lctx.module_base_names().contains(&def_name) {
1814 return Err(ParseError::validation(
1815 "FUNC",
1816 format!("FUNC {def_name} cannot shadow reserved function `{def_name}`"),
1817 &span,
1818 ));
1819 }
1820 if !lctx.declare_func(&def_name) {
1821 return Err(ParseError::validation(
1822 "FUNC",
1823 format!("duplicate function `{def_name}` in same scope"),
1824 &span,
1825 ));
1826 }
1827 let mut name: Option<String> = None;
1828 let mut param_names: Vec<String> = Vec::new();
1829 let mut param_types: Vec<String> = Vec::new();
1830 let mut body = None;
1831 for inner in pair.into_inner() {
1832 match inner.as_rule() {
1833 Rule::func_ident => {
1834 if name.is_none() {
1835 name = Some(inner.as_str().to_string());
1836 }
1837 }
1838 Rule::func_param => {
1839 let mut pname = None;
1840 let mut ptype = None;
1841 for part in inner.into_inner() {
1842 match part.as_rule() {
1843 Rule::dollar_ident => {
1844 pname = Some(parse_dollar_ident(part));
1845 }
1846 Rule::type_tag => {
1847 ptype = Some(parse_type_tag(part));
1848 }
1849 _ => {}
1850 }
1851 }
1852 param_names.push(pname.ok_or_else(|| {
1853 ParseError::validation(
1854 "FUNC",
1855 "FUNC parameter requires a $variable".to_string(),
1856 &span,
1857 )
1858 })?);
1859 param_types.push(ptype.ok_or_else(|| {
1860 ParseError::validation(
1861 "FUNC",
1862 "FUNC parameters require explicit types: FUNC NAME($p: TYPE, ...)"
1863 .to_string(),
1864 &span,
1865 )
1866 })?);
1867 }
1868 Rule::block => {
1869 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
1870 }
1871 _ => {}
1872 }
1873 }
1874 let name = name
1875 .ok_or_else(|| ParseError::validation("FUNC", "FUNC requires a name".to_string(), &span))?;
1876 check_func_ident(ctx, &name)?;
1877 if param_names.len() != param_types.len() {
1878 return Err(ParseError::validation(
1879 "FUNC",
1880 format!("FUNC {name} has mismatched parameter names and types"),
1881 &span,
1882 ));
1883 }
1884 let mut seen = std::collections::HashSet::new();
1885 for pname in ¶m_names {
1886 if !seen.insert(pname.clone()) {
1887 return Err(ParseError::validation(
1888 "FUNC",
1889 format!("FUNC {name} declares duplicate parameter ${pname}"),
1890 &span,
1891 ));
1892 }
1893 }
1894 Ok(StepKind::FuncDef {
1895 name,
1896 params: param_names.into_iter().zip(param_types).collect(),
1897 body: body.ok_or_else(|| {
1898 ParseError::validation("FUNC", "FUNC requires a block".to_string(), &span)
1899 })?,
1900 })
1901}
1902
1903fn parse_call_statement_from_pair(
1904 ctx: &SpanContext,
1905 lctx: &LowerCtx,
1906 pair: Pair<Rule>,
1907) -> ParseResult<StepKind> {
1908 let span = refine_span(ctx, &pair);
1909 let mut name: Option<String> = None;
1910 let mut args = Vec::new();
1911 for inner in pair.into_inner() {
1915 match inner.as_rule() {
1916 Rule::call_head_paren => {
1917 if name.is_none() {
1918 let text = inner.as_str();
1919 name = Some(text.strip_suffix('(').unwrap_or(text).to_string());
1920 }
1921 }
1922 Rule::func_call_head => {
1923 if name.is_none() {
1924 name = Some(inner.as_str().to_string());
1925 }
1926 }
1927 Rule::expr => {
1928 args.push(parse_expr(ctx, lctx, inner)?);
1929 }
1930 _ => {}
1931 }
1932 }
1933 let name = name.ok_or_else(|| {
1934 ParseError::validation(
1935 "FUNC",
1936 "function call requires a function name".to_string(),
1937 &span,
1938 )
1939 })?;
1940 if !name.contains(MODULE_SEPARATOR) {
1943 check_func_ident(ctx, &name)?;
1944 }
1945 let qualified = lctx.resolve_call(&span, &name)?;
1946 Ok(StepKind::Call {
1947 name: qualified,
1948 args,
1949 })
1950}
1951
1952fn parse_return_statement_from_pair(
1953 ctx: &SpanContext,
1954 lctx: &LowerCtx,
1955 pair: Pair<Rule>,
1956) -> ParseResult<StepKind> {
1957 use crate::ast::Value;
1958 for inner in pair.into_inner() {
1959 if inner.as_rule() == Rule::expr {
1960 return Ok(StepKind::Return {
1961 expr: Box::new(parse_expr(ctx, lctx, inner)?),
1962 });
1963 }
1964 }
1965 Ok(StepKind::Return {
1966 expr: Box::new(Expr::Literal(Value::string(String::new()))),
1967 })
1968}
1969
1970fn parse_for_statement_from_pair(
1971 ctx: &SpanContext,
1972 pair: Pair<Rule>,
1973 lctx: &LowerCtx,
1974) -> ParseResult<StepKind> {
1975 let span = refine_span(ctx, &pair);
1976 let mut idents: Vec<String> = Vec::new();
1977 let mut types: Vec<String> = Vec::new();
1978 let mut type_spans: Vec<SpanContext> = Vec::new();
1979 let mut in_expr = None;
1980 let mut body_steps = Vec::new();
1981 for inner in pair.into_inner() {
1982 match inner.as_rule() {
1983 Rule::dollar_ident => {
1984 idents.push(parse_dollar_ident(inner));
1985 }
1986 Rule::type_tag => {
1987 type_spans.push(refine_span(ctx, &inner));
1988 types.push(parse_type_tag(inner));
1989 }
1990 Rule::expr => {
1991 in_expr = Some(parse_expr(ctx, lctx, inner)?);
1992 }
1993 Rule::block => {
1994 body_steps = parse_block_elements_with_lower(ctx, inner, lctx)?;
1995 }
1996 _ => {}
1997 }
1998 }
1999 if idents.len() != types.len() {
2000 return Err(ParseError::validation(
2001 "FOR",
2002 format!(
2003 "FOR requires explicit types: FOR $item: TYPE IN <expr> (got {} vars, {} types)",
2004 idents.len(),
2005 types.len()
2006 ),
2007 &span,
2008 ));
2009 }
2010 let (key_var, key_type, var, var_type) = match idents.len() {
2011 1 => (
2012 None,
2013 None,
2014 idents.into_iter().next().unwrap(),
2015 types.into_iter().next().unwrap(),
2016 ),
2017 2 => {
2018 let mut iv = idents.into_iter();
2019 let mut tv = types.into_iter();
2020 (
2021 Some(iv.next().unwrap()),
2022 Some(tv.next().unwrap()),
2023 iv.next().unwrap(),
2024 tv.next().unwrap(),
2025 )
2026 }
2027 _ => {
2028 return Err(ParseError::validation(
2029 "FOR",
2030 "FOR requires one or two variables".to_string(),
2031 &span,
2032 ));
2033 }
2034 };
2035 if let Some(kt) = &key_type
2036 && kt != "STRING"
2037 && kt != "INT"
2038 {
2039 let at = type_spans.first().unwrap_or(&span);
2041 return Err(ParseError::validation(
2042 "FOR",
2043 format!("FOR key variable must be INT or STRING, got {kt}"),
2044 at,
2045 ));
2046 }
2047 Ok(StepKind::For {
2048 key_var,
2049 key_type,
2050 var,
2051 var_type,
2052 in_expr: in_expr.ok_or_else(|| {
2053 ParseError::validation(
2054 "FOR",
2055 "FOR requires an iterable expression".to_string(),
2056 &span,
2057 )
2058 })?,
2059 body: body_steps,
2060 })
2061}
2062
2063fn parse_let_statement_from_pair(
2064 ctx: &SpanContext,
2065 lctx: &LowerCtx,
2066 pair: Pair<Rule>,
2067) -> ParseResult<StepKind> {
2068 let span = refine_span(ctx, &pair);
2069 let mut var = None;
2070 let mut decl_type = None;
2071 let mut expr = None;
2072 for inner in pair.into_inner() {
2073 match inner.as_rule() {
2074 Rule::dollar_ident => {
2075 var = Some(parse_dollar_ident(inner));
2076 }
2077 Rule::type_tag => {
2078 decl_type = Some(parse_type_tag(inner));
2079 }
2080 Rule::expr => {
2081 expr = Some(parse_expr(ctx, lctx, inner)?);
2082 }
2083 _ => {}
2084 }
2085 }
2086 let var = var.ok_or_else(|| {
2087 ParseError::validation("LET", "LET requires a variable".to_string(), &span)
2088 })?;
2089 let decl_type = decl_type.ok_or_else(|| {
2090 ParseError::validation(
2091 "LET",
2092 "LET requires explicit type: LET $var: TYPE = <expr>".to_string(),
2093 &span,
2094 )
2095 })?;
2096 let expr = match expr {
2099 Some(e) => e,
2100 None if decl_type == "PIPE" => Expr::FreshPipe,
2101 None => {
2102 return Err(ParseError::validation(
2103 "LET",
2104 "LET requires an expression: LET $var: TYPE = <expr> (only LET $p: PIPE omits the initializer)".to_string(),
2105 &span,
2106 ));
2107 }
2108 };
2109 Ok(StepKind::Assign {
2110 var,
2111 decl_type,
2112 expr,
2113 })
2114}
2115
2116fn parse_mutate_statement_from_pair(
2117 ctx: &SpanContext,
2118 lctx: &LowerCtx,
2119 pair: Pair<Rule>,
2120) -> ParseResult<StepKind> {
2121 let span = refine_span(ctx, &pair);
2122 let mut var = None;
2123 let mut expr = None;
2124 for inner in pair.into_inner() {
2125 match inner.as_rule() {
2126 Rule::dollar_ident => {
2127 var = Some(parse_dollar_ident(inner));
2128 }
2129 Rule::expr => {
2130 expr = Some(parse_expr(ctx, lctx, inner)?);
2131 }
2132 _ => {}
2133 }
2134 }
2135 Ok(StepKind::Set {
2136 var: var.ok_or_else(|| {
2137 ParseError::validation(
2138 "mutate",
2139 "mutation requires a variable: $var = <expr>".to_string(),
2140 &span,
2141 )
2142 })?,
2143 expr: expr.ok_or_else(|| {
2144 ParseError::validation(
2145 "mutate",
2146 "mutation requires an expression: $var = <expr>".to_string(),
2147 &span,
2148 )
2149 })?,
2150 })
2151}
2152
2153fn parse_let_async_statement_from_pair(
2154 ctx: &SpanContext,
2155 pair: Pair<Rule>,
2156 lctx: &LowerCtx,
2157) -> ParseResult<StepKind> {
2158 let span = refine_span(ctx, &pair);
2159 let mut var = None;
2160 let mut decl_type: Option<String> = None;
2161 let mut body = None;
2162 for inner in pair.into_inner() {
2163 match inner.as_rule() {
2164 Rule::dollar_ident => {
2165 var = Some(parse_dollar_ident(inner));
2166 }
2167 Rule::type_tag => {
2168 decl_type = Some(parse_type_tag(inner));
2169 }
2170 Rule::block => {
2171 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
2172 }
2173 Rule::command_inner => {
2174 let inner = inner.into_inner().next().ok_or_else(|| {
2177 ParseError::structural("let", "empty command_inner".to_string(), &span)
2178 })?;
2179 let step_kind = parse_structural_command_with_lower(ctx, inner, lctx)?;
2180 body = Some(vec![Step {
2181 guard: None,
2182 kind: step_kind,
2183 scope_enter: 0,
2184 scope_exit: 0,
2185 }]);
2186 }
2187 Rule::with_io_command => {
2188 let kind = parse_structural_command_with_lower(ctx, inner, lctx)?;
2198 let StepKind::WithIo { bindings, cmd } = kind else {
2199 return Err(ParseError::validation("LET", "LET $var: TYPE = WITH_IO requires an ASYNC command (e.g. LET $t = WITH_IO [stdin=$p] ASYNC WRITE \"f\")".to_string(), &span));
2200 };
2201 match *cmd {
2202 StepKind::AsyncBlock { body: async_body } => {
2203 if async_body.len() != 1 {
2204 return Err(ParseError::structural("let", "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".to_string(), &span));
2205 }
2206 let step = async_body.into_iter().next().ok_or_else(|| {
2207 ParseError::validation(
2208 "LET",
2209 "LET $var: HANDLE = ASYNC requires a body".to_string(),
2210 &span,
2211 )
2212 })?;
2213 body = Some(vec![Step {
2214 guard: step.guard,
2215 kind: StepKind::WithIo {
2216 bindings,
2217 cmd: Box::new(step.kind),
2218 },
2219 scope_enter: step.scope_enter,
2220 scope_exit: step.scope_exit,
2221 }]);
2222 }
2223 sync_cmd => {
2224 if has_stdout_pipe(&bindings) {
2225 return Err(ParseError::structural("let", "LET capture cannot use WITH_IO [stdout=$var]; the capture sink owns stdout".to_string(), &span));
2226 }
2227 reject_async_in_capture(ctx, &sync_cmd)?;
2228 let name = var.clone().ok_or_else(|| {
2229 ParseError::validation(
2230 "LET",
2231 "LET $var: TYPE = WITH_IO requires a variable".to_string(),
2232 &span,
2233 )
2234 })?;
2235 let dtype = decl_type.ok_or_else(|| {
2236 ParseError::validation(
2237 "LET",
2238 "LET requires explicit type: LET $var: TYPE = ...".to_string(),
2239 &span,
2240 )
2241 })?;
2242 return Ok(StepKind::AssignCapture {
2243 var: name,
2244 decl_type: dtype,
2245 cmd: Box::new(StepKind::WithIo {
2246 bindings,
2247 cmd: Box::new(sync_cmd),
2248 }),
2249 });
2250 }
2251 }
2252 }
2253 _ => {}
2254 }
2255 }
2256 Ok(StepKind::AssignAsync {
2257 var: var.ok_or_else(|| {
2258 ParseError::validation(
2259 "LET",
2260 "LET $var: HANDLE = ASYNC requires a variable".to_string(),
2261 &span,
2262 )
2263 })?,
2264 decl_type: decl_type.ok_or_else(|| {
2265 ParseError::validation(
2266 "LET",
2267 "LET requires explicit type: LET $var: TYPE = ...".to_string(),
2268 &span,
2269 )
2270 })?,
2271 body: body.ok_or_else(|| {
2272 ParseError::validation(
2273 "LET",
2274 "LET $var: HANDLE = ASYNC requires a body".to_string(),
2275 &span,
2276 )
2277 })?,
2278 })
2279}
2280
2281fn parse_let_capture_statement_from_pair(
2289 ctx: &SpanContext,
2290 pair: Pair<Rule>,
2291 lctx: &LowerCtx,
2292) -> ParseResult<StepKind> {
2293 let span = refine_span(ctx, &pair);
2294 use pest::Parser;
2295 let mut var = None;
2296 let mut decl_type: Option<String> = None;
2297 let mut await_pair = None;
2298 let mut timeout_pair = None;
2299 let mut instruction_pair = None;
2300 for inner in pair.into_inner() {
2301 match inner.as_rule() {
2302 Rule::dollar_ident => {
2303 var = Some(parse_dollar_ident(inner));
2304 }
2305 Rule::type_tag => {
2306 decl_type = Some(parse_type_tag(inner));
2307 }
2308 Rule::await_statement => {
2309 await_pair = Some(inner);
2310 }
2311 Rule::timeout_statement => {
2312 timeout_pair = Some(inner);
2313 }
2314 Rule::instruction => {
2315 instruction_pair = Some(inner);
2316 }
2317 _ => {}
2318 }
2319 }
2320 let var = var.ok_or_else(|| {
2321 ParseError::validation("LET", "LET requires a variable".to_string(), &span)
2322 })?;
2323 let dtype: String = decl_type.ok_or_else(|| {
2324 ParseError::validation(
2325 "LET",
2326 "LET requires explicit type: LET $var: TYPE = ...".to_string(),
2327 &span,
2328 )
2329 })?;
2330 if let Some(awaited) = await_pair {
2331 let mut task_var = None;
2332 for inner in awaited.into_inner() {
2333 if inner.as_rule() == Rule::ident {
2334 task_var = Some(inner.as_str().to_string());
2335 }
2336 }
2337 return Ok(StepKind::AwaitCapture {
2338 out_var: var,
2339 out_type: dtype,
2340 task_var: task_var.ok_or_else(|| {
2341 ParseError::validation(
2342 "LET",
2343 "LET $out = AWAIT requires a task variable".to_string(),
2344 &span,
2345 )
2346 })?,
2347 });
2348 }
2349 if let Some(timeouted) = timeout_pair {
2350 let kind = parse_structural_command_with_lower(ctx, timeouted, lctx)?;
2351 reject_async_in_capture(ctx, &kind)?;
2352 reject_pipe_stdout_in_capture(ctx, &kind)?;
2353 return Ok(StepKind::AssignCapture {
2354 var,
2355 decl_type: dtype,
2356 cmd: Box::new(kind),
2357 });
2358 }
2359 if let Some(ins) = instruction_pair {
2360 let text = ins.as_str().to_string();
2361 let mut lead = None;
2362 for token in ins.into_inner() {
2363 if token.as_rule() == Rule::command_name {
2364 lead = Some(token.as_str().to_string());
2365 break;
2366 }
2367 }
2368 let lead = lead.ok_or_else(|| {
2369 ParseError::validation("LET", "LET capture requires a command".to_string(), &span)
2370 })?;
2371 if crate::commands::is_known_command(&lead) {
2372 let kind = lower_instruction_pair(
2373 ctx,
2374 lexer::LanguageParser::parse(Rule::instruction, &text)
2375 .map_err(parse_pest_error)?
2376 .next()
2377 .ok_or_else(|| {
2378 ParseError::validation(
2379 "LET",
2380 "LET capture requires a command".to_string(),
2381 &span,
2382 )
2383 })?,
2384 lctx,
2385 )?;
2386 reject_async_in_capture(ctx, &kind)?;
2387 reject_pipe_stdout_in_capture(ctx, &kind)?;
2388 return Ok(StepKind::AssignCapture {
2389 var,
2390 decl_type: dtype,
2391 cmd: Box::new(kind),
2392 });
2393 }
2394 let expr = parse_expr_str(&span, lctx, &text)?;
2395 return Ok(StepKind::Assign {
2396 var,
2397 decl_type: dtype,
2398 expr,
2399 });
2400 }
2401 Err(ParseError::structural(
2402 "let",
2403 "LET requires a value".to_string(),
2404 &span,
2405 ))
2406}
2407
2408fn parse_await_statement_from_pair(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<StepKind> {
2409 let span = refine_span(ctx, &pair);
2410 let mut var = None;
2411 for inner in pair.into_inner() {
2412 if inner.as_rule() == Rule::ident {
2413 var = Some(inner.as_str().to_string());
2414 }
2415 }
2416 Ok(StepKind::Await {
2417 var: var.ok_or_else(|| {
2418 ParseError::validation("AWAIT", "AWAIT requires a variable".to_string(), &span)
2419 })?,
2420 })
2421}
2422
2423fn parse_cancel_statement_from_pair(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<StepKind> {
2424 let span = refine_span(ctx, &pair);
2425 let mut var = None;
2426 for inner in pair.into_inner() {
2427 if inner.as_rule() == Rule::ident {
2428 var = Some(inner.as_str().to_string());
2429 }
2430 }
2431 Ok(StepKind::Cancel {
2432 var: var.ok_or_else(|| {
2433 ParseError::validation("CANCEL", "CANCEL requires a variable".to_string(), &span)
2434 })?,
2435 })
2436}
2437
2438fn parse_timeout_duration_arg(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Arg> {
2442 let span = refine_span(ctx, &pair);
2443 for inner in pair.into_inner() {
2444 let arg = match inner.as_rule() {
2445 Rule::timeout_literal => Arg::String(inner.as_str().to_string(), false),
2446 Rule::dollar_ident => Arg::Expr(Expr::Var(parse_dollar_ident(inner))),
2447 Rule::quoted_string => Arg::String(
2448 crate::command::strip_surrounding_quotes(inner.as_str()).to_string(),
2449 true,
2450 ),
2451 Rule::templated_arg => Arg::String(inner.as_str().to_string(), false),
2452 _ => continue,
2453 };
2454 ArgType::Duration
2455 .check_arg(&arg)
2456 .map_err(|e| ParseError::validation("TIMEOUT", e.to_string(), &span))?;
2457 return Ok(arg);
2458 }
2459 Err(ParseError::validation(
2460 "TIMEOUT",
2461 "TIMEOUT requires a duration".to_string(),
2462 &span,
2463 ))
2464}
2465
2466fn parse_timeout_statement_from_pair(
2467 ctx: &SpanContext,
2468 pair: Pair<Rule>,
2469 lctx: &LowerCtx,
2470) -> ParseResult<StepKind> {
2471 let span = refine_span(ctx, &pair);
2472 let mut duration: Option<Arg> = None;
2473 let mut body: Option<Vec<Step>> = None;
2474 for inner in pair.into_inner() {
2475 match inner.as_rule() {
2476 Rule::timeout_duration => {
2477 duration = Some(parse_timeout_duration_arg(ctx, inner)?);
2478 }
2479 Rule::block => {
2480 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
2481 }
2482 Rule::await_statement => {
2483 let kind = parse_await_statement_from_pair(ctx, inner)?;
2484 body = Some(vec![Step {
2485 guard: None,
2486 kind,
2487 scope_enter: 0,
2488 scope_exit: 0,
2489 }]);
2490 }
2491 Rule::cancel_statement => {
2492 let kind = parse_cancel_statement_from_pair(ctx, inner)?;
2493 body = Some(vec![Step {
2494 guard: None,
2495 kind,
2496 scope_enter: 0,
2497 scope_exit: 0,
2498 }]);
2499 }
2500 Rule::with_io_command
2501 | Rule::inherit_env_command
2502 | Rule::async_statement
2503 | Rule::async_statement_block
2504 | Rule::call_statement
2505 | Rule::while_statement
2506 | Rule::func_def
2507 | Rule::return_statement
2508 | Rule::break_statement
2509 | Rule::continue_statement
2510 | Rule::timeout_statement
2511 | Rule::remote_statement => {
2512 let kind = parse_structural_command_with_lower(ctx, inner, lctx)?;
2513 body = Some(vec![Step {
2514 guard: None,
2515 kind,
2516 scope_enter: 0,
2517 scope_exit: 0,
2518 }]);
2519 }
2520 Rule::instruction | Rule::instruction_inner => {
2521 let kind = lower_instruction_pair(ctx, inner, lctx)?;
2522 body = Some(vec![Step {
2523 guard: None,
2524 kind,
2525 scope_enter: 0,
2526 scope_exit: 0,
2527 }]);
2528 }
2529 Rule::run_exec_statement | Rule::run_exec_inner => {
2530 let kind = lower_run_exec_pair(ctx, inner, lctx)?;
2531 body = Some(vec![Step {
2532 guard: None,
2533 kind,
2534 scope_enter: 0,
2535 scope_exit: 0,
2536 }]);
2537 }
2538 _ => {}
2539 }
2540 }
2541 Ok(StepKind::Timeout {
2542 duration: duration.ok_or_else(|| {
2543 ParseError::validation("TIMEOUT", "TIMEOUT requires a duration".to_string(), &span)
2544 })?,
2545 body: body.ok_or_else(|| {
2546 ParseError::validation(
2547 "TIMEOUT",
2548 "TIMEOUT requires a command or block".to_string(),
2549 &span,
2550 )
2551 })?,
2552 })
2553}
2554
2555fn contains_remote_block(steps: &[Step]) -> bool {
2559 steps.iter().any(|step| contains_remote_kind(&step.kind))
2560}
2561
2562fn contains_remote_kind(kind: &StepKind) -> bool {
2563 if matches!(kind, StepKind::RemoteBlock { .. }) {
2564 return true;
2565 }
2566 let mut found = false;
2567 kind.walk_child_kinds(&mut |child| {
2568 found = found || contains_remote_kind(child);
2569 });
2570 found
2571}
2572
2573fn collect_remote_body_symbols(
2579 steps: &[Step],
2580 defs: &mut HashSet<String>,
2581 calls: &mut Vec<String>,
2582) {
2583 for step in steps {
2584 collect_remote_kind_symbols(&step.kind, defs, calls);
2585 }
2586}
2587
2588fn collect_remote_kind_symbols(
2589 kind: &StepKind,
2590 defs: &mut HashSet<String>,
2591 calls: &mut Vec<String>,
2592) {
2593 if let StepKind::FuncDef { name, .. } = kind {
2594 defs.insert(name.clone());
2595 }
2596 if let StepKind::Call { name, .. } = kind {
2597 calls.push(name.clone());
2598 }
2599 kind.walk_step_exprs(&mut |expr| match expr {
2600 Expr::Call { name, .. } => calls.push(name.clone()),
2601 Expr::CompiledMath(ops) => {
2602 for op in ops {
2603 if let MathOp::Call { name, .. } = op {
2604 calls.push(name.clone());
2605 }
2606 }
2607 }
2608 Expr::Block(steps) => collect_remote_body_symbols(steps, defs, calls),
2609 _ => {}
2610 });
2611}
2612
2613fn parse_remote_statement_from_pair(
2614 ctx: &SpanContext,
2615 pair: Pair<Rule>,
2616 lctx: &LowerCtx,
2617) -> ParseResult<StepKind> {
2618 let span = refine_span(ctx, &pair);
2619 let mut target: Option<String> = None;
2620 let mut vars: Vec<String> = Vec::new();
2621 let mut env: Vec<String> = Vec::new();
2622 let mut body: Option<Vec<Step>> = None;
2623 for inner in pair.into_inner() {
2624 match inner.as_rule() {
2625 Rule::remote_target => {
2626 target = Some(inner.as_str().to_string());
2627 }
2628 Rule::remote_bindings => {
2629 for binding in inner.into_inner() {
2630 let entry = if binding.as_rule() == Rule::remote_binding {
2633 binding.into_inner().next()
2634 } else {
2635 Some(binding)
2636 };
2637 let Some(entry) = entry else {
2638 continue;
2639 };
2640 match entry.as_rule() {
2641 Rule::remote_var_binding => {
2642 let mut name = None;
2643 for part in entry.into_inner() {
2644 if part.as_rule() == Rule::ident {
2645 name = Some(part.as_str().to_string());
2646 }
2647 }
2648 vars.push(name.ok_or_else(|| {
2649 ParseError::validation(
2650 "REMOTE",
2651 "REMOTE header entries must be `$var` or `env:NAME`"
2652 .to_string(),
2653 &span,
2654 )
2655 })?);
2656 }
2657 Rule::remote_env_binding => {
2658 let mut name = None;
2659 for part in entry.into_inner() {
2660 if part.as_rule() == Rule::env_read_key {
2661 name = Some(part.as_str().to_string());
2662 }
2663 }
2664 env.push(name.ok_or_else(|| {
2665 ParseError::validation(
2666 "REMOTE",
2667 "REMOTE header entries must be `$var` or `env:NAME`"
2668 .to_string(),
2669 &span,
2670 )
2671 })?);
2672 }
2673 _ => {}
2674 }
2675 }
2676 }
2677 Rule::block => {
2678 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
2679 }
2680 _ => {}
2681 }
2682 }
2683 let target = target.ok_or_else(|| {
2684 ParseError::validation("REMOTE", "REMOTE requires a target".to_string(), &span)
2685 })?;
2686 if !crate::commands::is_valid_remote_target(&target) {
2687 return Err(ParseError::validation(
2688 "REMOTE",
2689 format!(
2690 "invalid REMOTE target `{target}`: start alphanumeric, then alphanumeric, `_`, or `-`, max 64 chars, never SNAPSHOT, LOCAL, CACHE, SYSTEM, or REMOTE"
2691 ),
2692 &span,
2693 ));
2694 }
2695 let body = body.ok_or_else(|| {
2696 ParseError::validation(
2697 "REMOTE",
2698 "REMOTE requires a braced block, e.g. `REMOTE prod [$version] { ... }`".to_string(),
2699 &span,
2700 )
2701 })?;
2702 if body.is_empty() {
2703 return Err(ParseError::validation(
2704 "REMOTE",
2705 "REMOTE requires a non-empty braced block".to_string(),
2706 &span,
2707 ));
2708 }
2709 if contains_remote_block(&body) {
2710 return Err(ParseError::validation(
2711 "REMOTE",
2712 format!("REMOTE blocks cannot nest (target `{target}` contains another REMOTE block)"),
2713 &span,
2714 ));
2715 }
2716 if body.iter().any(|s| contains_inherit_env(&s.kind)) {
2717 return Err(ParseError::validation(
2718 "REMOTE",
2719 "INHERIT_ENV cannot appear inside a REMOTE block: the guest inherits its own process environment".to_string(),
2720 &span,
2721 ));
2722 }
2723 let mut inner_defs = HashSet::new();
2724 let mut calls = Vec::new();
2725 collect_remote_body_symbols(&body, &mut inner_defs, &mut calls);
2726 let outer_defs = lctx.visible_func_names();
2727 for call in &calls {
2728 let bare = crate::base_name(call);
2729 let is_script_call = match crate::split_qualified(call) {
2730 Some((module, _)) => module == crate::constants::SCRIPT_MODULE_NAME,
2731 None => true,
2732 };
2733 if is_script_call && outer_defs.contains(bare) && !inner_defs.contains(bare) {
2734 return Err(ParseError::validation(
2735 "REMOTE",
2736 format!(
2737 "REMOTE '{target}' calls outer function '{bare}' (functions must be defined inside the block)"
2738 ),
2739 &span,
2740 ));
2741 }
2742 }
2743 Ok(StepKind::RemoteBlock {
2744 target,
2745 vars,
2746 env,
2747 body,
2748 })
2749}
2750
2751fn parse_if_statement_from_pair(
2752 ctx: &SpanContext,
2753 pair: Pair<Rule>,
2754 lctx: &LowerCtx,
2755) -> ParseResult<StepKind> {
2756 let span = refine_span(ctx, &pair);
2757 let mut cond = None;
2758 let mut then_body = Vec::new();
2759 let mut else_ifs = Vec::new();
2760 let mut else_body = None;
2761
2762 for inner in pair.into_inner() {
2763 match inner.as_rule() {
2764 Rule::expr => {
2765 if cond.is_none() {
2766 cond = Some(parse_expr(ctx, lctx, inner)?);
2767 }
2768 }
2769 Rule::block => {
2770 if then_body.is_empty() {
2771 then_body = parse_block_elements_with_lower(ctx, inner, lctx)?;
2772 }
2773 }
2774 Rule::else_if_clause => {
2775 let (eif_cond, eif_body) = parse_else_if_clause(ctx, inner, lctx)?;
2776 else_ifs.push((eif_cond, eif_body));
2777 }
2778 Rule::else_clause => {
2779 else_body = Some(parse_else_clause(ctx, inner, lctx)?);
2780 }
2781 _ => {}
2782 }
2783 }
2784 Ok(StepKind::If {
2785 cond: Box::new(cond.ok_or_else(|| {
2786 ParseError::structural("if", "IF requires a condition".to_string(), &span)
2787 })?),
2788 then_body,
2789 else_ifs,
2790 else_body,
2791 })
2792}
2793
2794fn parse_else_if_clause(
2795 ctx: &SpanContext,
2796 pair: Pair<Rule>,
2797 lctx: &LowerCtx,
2798) -> ParseResult<(Box<Expr>, Vec<Step>)> {
2799 let span = refine_span(ctx, &pair);
2800 let mut cond = None;
2801 let mut body = Vec::new();
2802 for inner in pair.into_inner() {
2803 match inner.as_rule() {
2804 Rule::expr => cond = Some(parse_expr(ctx, lctx, inner)?),
2805 Rule::block => body = parse_block_elements_with_lower(ctx, inner, lctx)?,
2806 _ => {}
2807 }
2808 }
2809 Ok((
2810 Box::new(cond.ok_or_else(|| {
2811 ParseError::structural("if", "ELSE IF requires a condition".to_string(), &span)
2812 })?),
2813 body,
2814 ))
2815}
2816
2817fn parse_else_clause(
2818 ctx: &SpanContext,
2819 pair: Pair<Rule>,
2820 lctx: &LowerCtx,
2821) -> ParseResult<Vec<Step>> {
2822 for inner in pair.into_inner() {
2823 if let Rule::block = inner.as_rule() {
2824 return parse_block_elements_with_lower(ctx, inner, lctx);
2825 }
2826 }
2827 Ok(Vec::new())
2828}
2829
2830fn parse_async_statement_from_pair(
2831 ctx: &SpanContext,
2832 pair: Pair<Rule>,
2833 lctx: &LowerCtx,
2834) -> ParseResult<StepKind> {
2835 let span = refine_span(ctx, &pair);
2836 let mut inner_cmd = None;
2837 let mut block_body = None;
2838 for inner in pair.into_inner() {
2839 match inner.as_rule() {
2840 Rule::command => {
2841 let cmd_text = inner.as_str();
2846 let (preseed_funcs, preseed_imports) = lctx.visible_snapshot();
2847 let steps = parse_script_with_preseed(
2848 cmd_text,
2849 |name, args| (lctx.lower)(name, args),
2850 lctx.reserved_names.clone(),
2851 lctx.modules.clone(),
2852 preseed_funcs,
2853 preseed_imports,
2854 )?;
2855 if steps.len() == 1 {
2856 inner_cmd = Some(steps.into_iter().next().unwrap().kind);
2857 } else {
2858 return Err(ParseError::structural(
2859 "async",
2860 "unexpected multiple steps in async inner command".to_string(),
2861 &span,
2862 ));
2863 }
2864 }
2865 Rule::command_inner => {
2866 let child = inner.into_inner().next().ok_or_else(|| {
2868 ParseError::structural("async", "empty command_inner".to_string(), &span)
2869 })?;
2870 match child.as_rule() {
2871 Rule::inherit_env_command => {
2872 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2873 }
2874 Rule::async_statement | Rule::async_statement_block => {
2875 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2876 }
2877 Rule::timeout_statement | Rule::cancel_statement => {
2878 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2879 }
2880 Rule::remote_statement => {
2881 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2882 }
2883 Rule::call_statement | Rule::while_statement => {
2884 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2885 }
2886 Rule::func_def
2887 | Rule::return_statement
2888 | Rule::break_statement
2889 | Rule::continue_statement => {
2890 return Err(ParseError::structural(
2891 "async",
2892 format!(
2893 "{:?} cannot run as a lone ASYNC command; use ASYNC {{ ... }} block form if needed",
2894 child.as_rule()
2895 ),
2896 &span,
2897 ));
2898 }
2899 Rule::instruction => {
2900 inner_cmd = Some(lower_instruction_pair(ctx, child, lctx)?);
2901 }
2902 Rule::run_exec_statement | Rule::run_exec_inner => {
2903 inner_cmd = Some(lower_run_exec_pair(ctx, child, lctx)?);
2904 }
2905 other => {
2906 return Err(ParseError::structural(
2907 "async",
2908 format!("unexpected command_inner child: {:?}", other),
2909 &span,
2910 ));
2911 }
2912 }
2913 }
2914 Rule::instruction | Rule::instruction_inner => {
2915 inner_cmd = Some(lower_instruction_pair(ctx, inner, lctx)?);
2916 }
2917 Rule::run_exec_statement | Rule::run_exec_inner => {
2918 inner_cmd = Some(lower_run_exec_pair(ctx, inner, lctx)?);
2919 }
2920 Rule::block => {
2921 block_body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
2922 }
2923 _ => {}
2924 }
2925 }
2926 if let Some(body) = block_body {
2927 for step in &body {
2928 if matches!(&step.kind, StepKind::WithIo { .. }) {
2929 return Err(ParseError::structural("async", "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)".to_string(), &span));
2930 }
2931 }
2932 Ok(StepKind::AsyncBlock { body })
2933 } else if let Some(cmd) = inner_cmd {
2934 if matches!(&cmd, StepKind::WithIo { .. }) {
2935 return Err(ParseError::structural("async", "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)".to_string(), &span));
2936 }
2937 Ok(StepKind::AsyncBlock {
2938 body: vec![Step {
2939 guard: None,
2940 kind: cmd,
2941 scope_enter: 0,
2942 scope_exit: 0,
2943 }],
2944 })
2945 } else {
2946 Err(ParseError::structural(
2947 "async",
2948 "ASYNC requires either a command or a block".to_string(),
2949 &span,
2950 ))
2951 }
2952}
2953
2954fn parse_async_statement_block_from_pair(
2955 ctx: &SpanContext,
2956 pair: Pair<Rule>,
2957 lctx: &LowerCtx,
2958) -> ParseResult<StepKind> {
2959 let span = refine_span(ctx, &pair);
2960 let mut block_body = None;
2961 for inner in pair.into_inner() {
2962 if inner.as_rule() == Rule::block {
2963 block_body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
2964 }
2965 }
2966 let body = block_body.ok_or_else(|| {
2967 ParseError::structural(
2968 "async",
2969 "async_statement_block requires a block".to_string(),
2970 &span,
2971 )
2972 })?;
2973 for step in &body {
2974 if matches!(&step.kind, StepKind::WithIo { .. }) {
2975 return Err(ParseError::structural("async", "WITH_IO cannot be placed inside ASYNC. Place WITH_IO outside ASYNC instead (e.g. WITH_IO [...] ASYNC RUN ...)".to_string(), &span));
2976 }
2977 }
2978 Ok(StepKind::AsyncBlock { body })
2979}
2980
2981fn lower_import_statement(ctx: &SpanContext, pair: Pair<Rule>, lctx: &LowerCtx) -> ParseResult<()> {
2983 let mut modules = Vec::new();
2984 for inner in pair.into_inner() {
2985 match inner.as_rule() {
2986 Rule::import_list => {
2987 for module in inner.into_inner() {
2988 if module.as_rule() == Rule::import_module {
2989 modules.push(module.as_str().to_string());
2990 }
2991 }
2992 }
2993 Rule::import_module => modules.push(inner.as_str().to_string()),
2994 _ => {}
2995 }
2996 }
2997 lctx.import_modules(ctx, &modules)
2998}
2999
3000fn parse_block_elements_with_lower(
3001 ctx: &SpanContext,
3002 block_pair: Pair<Rule>,
3003 lctx: &LowerCtx,
3004) -> ParseResult<Vec<Step>> {
3005 lctx.enter_scope();
3009 let mut steps = Vec::new();
3010 let mut pending_guard: Option<GuardExpr> = None;
3015 for elem in block_pair.into_inner() {
3016 match elem.as_rule() {
3017 Rule::guard_line => {
3018 let guard = parse_guard_line(ctx, elem)?;
3019 pending_guard = Some(match pending_guard.take() {
3020 Some(existing) => GuardExpr::all(vec![existing, guard]),
3021 None => guard,
3022 });
3023 continue;
3024 }
3025 Rule::for_statement
3026 | Rule::while_statement
3027 | Rule::func_def
3028 | Rule::call_statement
3029 | Rule::return_statement
3030 | Rule::break_statement
3031 | Rule::continue_statement
3032 | Rule::let_statement
3033 | Rule::mutate_statement
3034 | Rule::let_async_statement
3035 | Rule::let_capture_statement
3036 | Rule::await_statement
3037 | Rule::cancel_statement
3038 | Rule::if_statement
3039 | Rule::async_statement
3040 | Rule::timeout_statement
3041 | Rule::remote_statement
3042 | Rule::async_statement_block => {
3043 let step_kind = parse_structural_command_with_lower(ctx, elem, lctx)?;
3044 steps.push(Step {
3045 guard: pending_guard.take(),
3046 kind: step_kind,
3047 scope_enter: 0,
3048 scope_exit: 0,
3049 });
3050 }
3051 Rule::type_statement => {
3052 return Err(ParseError::structural(
3053 KEYWORD_TYPE,
3054 "TYPE must appear at top level".to_string(),
3055 ctx,
3056 ));
3057 }
3058 Rule::guard_block => {
3059 let mut guard_pair = None;
3060 let mut inner_block = None;
3061 for inner in elem.into_inner() {
3062 match inner.as_rule() {
3063 Rule::guard_line => guard_pair = Some(inner),
3064 Rule::block => inner_block = Some(inner),
3065 _ => {}
3066 }
3067 }
3068 if let (Some(gp), Some(bp)) = (guard_pair, inner_block) {
3069 let guard_expr = parse_guard_line(ctx, gp)?;
3070 let mut inner_steps = parse_block_elements_with_lower(ctx, bp, lctx)?;
3071 for step in &mut inner_steps {
3072 let inner = step.guard.take();
3073 step.guard = Some(match (pending_guard.clone(), inner) {
3074 (Some(pending), Some(existing)) => {
3075 GuardExpr::all(vec![pending, existing])
3076 }
3077 (Some(pending), None) => {
3078 GuardExpr::all(vec![pending, guard_expr.clone()])
3079 }
3080 (None, Some(existing)) => existing,
3081 (None, None) => guard_expr.clone(),
3082 });
3083 }
3084 pending_guard = None;
3087 steps.extend(inner_steps);
3088 }
3089 }
3090 Rule::instruction | Rule::instruction_inner => {
3091 let kind = lower_instruction_pair(ctx, elem, lctx)?;
3092 steps.push(Step {
3093 guard: pending_guard.take(),
3094 kind,
3095 scope_enter: 0,
3096 scope_exit: 0,
3097 });
3098 }
3099 Rule::run_exec_statement | Rule::run_exec_inner => {
3100 let kind = lower_run_exec_pair(ctx, elem, lctx)?;
3101 steps.push(Step {
3102 guard: pending_guard.take(),
3103 kind,
3104 scope_enter: 0,
3105 scope_exit: 0,
3106 });
3107 }
3108 Rule::with_io_command => {
3109 let step_kind = parse_structural_command_with_lower(ctx, elem, lctx)?;
3110 steps.push(Step {
3111 guard: pending_guard.take(),
3112 kind: step_kind,
3113 scope_enter: 0,
3114 scope_exit: 0,
3115 });
3116 }
3117 Rule::inherit_env_command => {
3118 let step_kind = parse_structural_command_with_lower(ctx, elem, lctx)?;
3119 steps.push(Step {
3120 guard: pending_guard.take(),
3121 kind: step_kind,
3122 scope_enter: 0,
3123 scope_exit: 0,
3124 });
3125 }
3126 Rule::import_statement => {
3127 lower_import_statement(ctx, elem, lctx)?;
3131 }
3132 Rule::export_statement => {
3133 return Err(ParseError::validation(
3134 KEYWORD_EXPORT,
3135 "`EXPORT` is reserved for future script-module support and cannot be used yet."
3136 .to_string(),
3137 ctx,
3138 ));
3139 }
3140 _ => {} }
3142 }
3143 if pending_guard.is_some() {
3144 return Err(ParseError::structural(
3145 "guard",
3146 "guard declared immediately before '}' without a command".to_string(),
3147 ctx,
3148 ));
3149 }
3150 lctx.exit_scope();
3151 Ok(steps)
3152}
3153
3154fn parse_argument(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Vec<Arg>> {
3155 let inners: Vec<_> = pair.into_inner().collect();
3156 let mut groups: Vec<Vec<Pair<Rule>>> = vec![Vec::new()];
3162 for fragment in inners {
3163 let glued = fragment.as_rule() == Rule::expr
3164 && fragment.as_str().ends_with(|c: char| c.is_whitespace());
3165 groups
3166 .last_mut()
3167 .expect("argument always holds a group")
3168 .push(fragment);
3169 if glued {
3170 groups.push(Vec::new());
3171 }
3172 }
3173 let mut args = Vec::new();
3174 for group in groups {
3175 if group.is_empty() {
3176 continue;
3177 }
3178 if group.len() == 1 && group[0].as_rule() == Rule::expr {
3180 args.push(Arg::Expr(parse_expr(
3181 ctx,
3182 lctx,
3183 group.into_iter().next().expect("group holds one pair"),
3184 )?));
3185 continue;
3186 }
3187 if group.len() == 1 && group[0].as_rule() == Rule::string_literal {
3189 args.push(Arg::String(parse_fragments(&group)?, true));
3190 continue;
3191 }
3192 args.push(Arg::String(parse_fragments(&group)?, false));
3193 }
3194 Ok(args)
3195}
3196
3197fn parse_quoted_string(pair: Pair<Rule>) -> ParseResult<String> {
3198 let s = pair.as_str();
3199 let content = &s[1..s.len() - 1];
3200 Ok(content.to_string())
3202}
3203
3204fn parse_fragments(parts: &[Pair<Rule>]) -> ParseResult<String> {
3208 if parts.len() == 1 && parts[0].as_rule() == Rule::string_literal {
3210 let s = parts[0].as_str();
3211 return Ok(s[1..s.len() - 1].to_string());
3212 }
3213
3214 let mut body = String::new();
3215 let mut last_end = None;
3216 for part in parts {
3217 let span = part.as_span();
3218 if let Some(end) = last_end
3219 && span.start() > end
3220 {
3221 body.push(' ');
3222 }
3223 match part.as_rule() {
3224 Rule::string_literal => {
3225 let s = part.as_str();
3226 let unquoted = &s[1..s.len() - 1];
3227 body.push_str(unquoted);
3228 }
3229 Rule::templated_arg | Rule::unquoted_arg => {
3230 body.push_str(part.as_str());
3231 }
3232 Rule::expr => body.push_str(part.as_str()),
3233 _ => {}
3234 }
3235 last_end = Some(span.end());
3236 }
3237 Ok(body)
3238}
3239
3240fn parse_guard_line(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3241 let span = refine_span(ctx, &pair);
3242 for inner in pair.into_inner() {
3243 if inner.as_rule() == Rule::guard_expr {
3244 return parse_guard_expr(ctx, inner);
3245 }
3246 }
3247 Err(ParseError::structural(
3248 "guard",
3249 "guard line missing expression".to_string(),
3250 &span,
3251 ))
3252}
3253
3254fn parse_io_binding(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<IoBinding> {
3255 let span = refine_span(ctx, &pair);
3256 let mut stream = None;
3257 let mut pipe = None;
3258 for inner in pair.into_inner() {
3259 match inner.as_rule() {
3260 Rule::io_stream => stream = Some(parse_io_stream(inner.as_str())),
3261 Rule::pipe_binding => pipe = Some(parse_pipe_binding(ctx, inner)?),
3262 _ => {}
3263 }
3264 }
3265 let stream = stream.ok_or_else(|| {
3266 ParseError::structural("with_io", "missing IO stream in WITH_IO".to_string(), &span)
3267 })?;
3268 Ok(IoBinding { stream, pipe })
3269}
3270
3271fn parse_io_stream(text: &str) -> IoStream {
3272 match text {
3273 "stdin" => IoStream::Stdin,
3274 "stdout" => IoStream::Stdout,
3275 "stderr" => IoStream::Stderr,
3276 _ => unreachable!("parser produced invalid io_stream token"),
3277 }
3278}
3279
3280fn parse_pipe_binding(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<PipeTarget> {
3281 let span = refine_span(ctx, &pair);
3282 for inner in pair.into_inner() {
3283 if inner.as_rule() == Rule::dollar_ident {
3284 return Ok(PipeTarget::Var(parse_dollar_ident(inner)));
3285 }
3286 }
3287 Err(ParseError::structural(
3288 "with_io",
3289 "missing pipe identifier in WITH_IO binding".to_string(),
3290 &span,
3291 ))
3292}
3293
3294fn parse_guard_expr(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3295 let span = refine_span(ctx, &pair);
3296 match pair.as_rule() {
3297 Rule::guard_expr => {
3298 let next = pair.into_inner().next().ok_or_else(|| {
3299 ParseError::structural("guard", "guard expression missing body".to_string(), &span)
3300 })?;
3301 parse_guard_expr(ctx, next)
3302 }
3303 Rule::guard_seq => parse_guard_seq(ctx, pair),
3304 Rule::guard_factor => parse_guard_factor(ctx, pair),
3305 Rule::guard_not => {
3306 Err(ParseError::structural(
3308 "guard",
3309 "guard_not should not create a pair".to_string(),
3310 &span,
3311 ))
3312 }
3313 Rule::guard_primary => parse_guard_primary(ctx, pair),
3314 Rule::guard_group => parse_guard_group(ctx, pair),
3315 Rule::guard_any_call => parse_guard_any_call(ctx, pair),
3316 Rule::guard_all_call => parse_guard_all_call(ctx, pair),
3317 Rule::not_call => parse_not_call(ctx, pair),
3318 Rule::guard_term => parse_guard_term(ctx, pair),
3319 _ => Err(ParseError::structural(
3320 "guard",
3321 format!("unexpected guard expression rule: {:?}", pair.as_rule()),
3322 &span,
3323 )),
3324 }
3325}
3326
3327fn parse_guard_seq(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3328 let span = refine_span(ctx, &pair);
3329 let mut exprs = Vec::new();
3330 for inner in pair.into_inner() {
3331 if inner.as_rule() == Rule::guard_factor {
3332 exprs.push(parse_guard_factor(ctx, inner)?);
3333 }
3334 }
3335 match exprs.len() {
3336 0 => Err(ParseError::structural(
3337 "guard",
3338 "guard list requires at least one entry".to_string(),
3339 &span,
3340 )),
3341 1 => Ok(exprs.pop().unwrap()),
3342 _ => Ok(GuardExpr::all(exprs)),
3343 }
3344}
3345
3346fn parse_guard_factor(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3347 let span = refine_span(ctx, &pair);
3348 let inner = pair.into_inner().next().ok_or_else(|| {
3349 ParseError::structural(
3350 "guard",
3351 "guard factor missing expression".to_string(),
3352 &span,
3353 )
3354 })?;
3355 parse_guard_expr(ctx, inner)
3356}
3357
3358fn parse_not_call(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3359 let span = refine_span(ctx, &pair);
3360 for inner in pair.into_inner() {
3361 if inner.as_rule() == Rule::guard_expr {
3362 return parse_guard_expr(ctx, inner).map(|e| GuardExpr::Not(Box::new(e)));
3363 }
3364 }
3365 Err(ParseError::structural(
3366 "guard",
3367 "not() missing expression".to_string(),
3368 &span,
3369 ))
3370}
3371
3372fn parse_guard_primary(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3373 let span = refine_span(ctx, &pair);
3374 match pair.as_rule() {
3375 Rule::guard_primary => {
3376 let inner = pair.into_inner().next().ok_or_else(|| {
3377 ParseError::structural("guard", "guard primary missing body".to_string(), &span)
3378 })?;
3379 parse_guard_primary(ctx, inner)
3380 }
3381 Rule::guard_group => parse_guard_group(ctx, pair),
3382 Rule::guard_any_call => parse_guard_any_call(ctx, pair),
3383 Rule::guard_all_call => parse_guard_all_call(ctx, pair),
3384 Rule::not_call => parse_not_call(ctx, pair),
3385 Rule::guard_term => parse_guard_term(ctx, pair),
3386 _ => Err(ParseError::structural(
3387 "guard",
3388 format!("unexpected guard primary rule: {:?}", pair.as_rule()),
3389 &span,
3390 )),
3391 }
3392}
3393
3394fn parse_guard_group(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3395 let span = refine_span(ctx, &pair);
3396 for inner in pair.into_inner() {
3397 if inner.as_rule() == Rule::guard_expr {
3398 return parse_guard_expr(ctx, inner);
3399 }
3400 }
3401 Err(ParseError::structural(
3402 "guard",
3403 "grouped guard missing expression".to_string(),
3404 &span,
3405 ))
3406}
3407
3408fn parse_guard_any_call(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3409 let span = refine_span(ctx, &pair);
3410 let mut args = Vec::new();
3411 for inner in pair.into_inner() {
3412 if inner.as_rule() == Rule::guard_expr_list {
3413 args = parse_guard_expr_list(ctx, inner)?;
3414 }
3415 }
3416 if args.len() < 2 {
3417 return Err(ParseError::structural(
3418 "guard",
3419 "any(...) requires at least two guard expressions".to_string(),
3420 &span,
3421 ));
3422 }
3423 Ok(GuardExpr::or(args))
3424}
3425
3426fn parse_guard_all_call(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3427 let span = refine_span(ctx, &pair);
3428 let mut args = Vec::new();
3429 for inner in pair.into_inner() {
3430 if inner.as_rule() == Rule::guard_expr_list {
3431 args = parse_guard_expr_list(ctx, inner)?;
3432 }
3433 }
3434 if args.is_empty() {
3435 return Err(ParseError::structural(
3436 "guard",
3437 "all(...) requires at least one guard expression".to_string(),
3438 &span,
3439 ));
3440 }
3441 Ok(GuardExpr::all(args))
3442}
3443
3444fn parse_guard_expr_list(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Vec<GuardExpr>> {
3445 let mut exprs = Vec::new();
3446 for inner in pair.into_inner() {
3447 if inner.as_rule() == Rule::guard_expr {
3448 push_guard_or_args_from_expr(ctx, inner, &mut exprs)?;
3449 }
3450 }
3451 Ok(exprs)
3452}
3453
3454fn push_guard_or_args_from_expr(
3455 ctx: &SpanContext,
3456 expr_pair: Pair<Rule>,
3457 exprs: &mut Vec<GuardExpr>,
3458) -> ParseResult<()> {
3459 if let Some(seq_pair) = expr_pair
3460 .clone()
3461 .into_inner()
3462 .find(|inner| inner.as_rule() == Rule::guard_seq)
3463 {
3464 let factors: Vec<Pair<Rule>> = seq_pair
3465 .into_inner()
3466 .filter(|inner| inner.as_rule() == Rule::guard_factor)
3467 .collect();
3468 if factors.len() > 1 {
3469 for factor in factors {
3470 exprs.push(parse_guard_factor(ctx, factor)?);
3471 }
3472 return Ok(());
3473 }
3474 }
3475 exprs.push(parse_guard_expr(ctx, expr_pair)?);
3476 Ok(())
3477}
3478
3479fn parse_guard_term(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3480 let span = refine_span(ctx, &pair);
3481 for inner in pair.into_inner() {
3482 match inner.as_rule() {
3483 Rule::inject_guard => {
3484 return Ok(GuardExpr::Predicate(Guard::Attr {
3485 ns: Ns::Env,
3486 key: Some(inner.as_str().to_string()),
3487 val: None,
3488 }));
3489 }
3490 Rule::eq_guard => {
3491 return Ok(GuardExpr::Predicate(parse_func_guard(inner)?));
3492 }
3493 Rule::neq_guard => {
3494 let guard = parse_func_guard(inner)?;
3495 return Ok(GuardExpr::Not(Box::new(GuardExpr::Predicate(guard))));
3496 }
3497 Rule::bool_guard => {
3498 let val = inner
3499 .into_inner()
3500 .find(|p| p.as_rule() == Rule::bool_value)
3501 .expect("grammar invariant violated: bool_guard missing bool_value")
3502 .as_str()
3503 .to_string();
3504 return Ok(GuardExpr::Predicate(Guard::Attr {
3505 ns: Ns::Bool,
3506 key: None,
3507 val: Some(val),
3508 }));
3509 }
3510 Rule::env_guard => {
3511 return Ok(GuardExpr::Predicate(parse_env_guard(inner)?));
3512 }
3513 Rule::ns_guard => {
3514 return parse_ns_guard(inner, &span);
3515 }
3516 _ => {}
3517 }
3518 }
3519 Err(ParseError::structural(
3520 "guard",
3521 "missing guard predicate".to_string(),
3522 &span,
3523 ))
3524}
3525
3526fn parse_func_guard(pair: Pair<Rule>) -> ParseResult<Guard> {
3527 let mut key = String::new();
3528 let mut value = String::new();
3529 let mut saw_env_prefix = false;
3530 for inner in pair.into_inner() {
3531 match inner.as_rule() {
3532 Rule::env_prefix => saw_env_prefix = true,
3533 Rule::env_key if saw_env_prefix => {
3534 key = inner.as_str().trim().to_string();
3535 }
3536 Rule::bare_guard_value | Rule::quoted_string => {
3537 value = unquote(inner.as_str().trim()).to_string();
3538 }
3539 _ => {}
3540 }
3541 }
3542 Ok(Guard::Attr {
3543 ns: Ns::Env,
3544 key: Some(key),
3545 val: Some(value),
3546 })
3547}
3548
3549fn unquote(s: &str) -> &str {
3550 s.strip_prefix('"')
3551 .and_then(|s| s.strip_suffix('"'))
3552 .or_else(|| s.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
3553 .unwrap_or(s)
3554}
3555
3556fn parse_env_guard(pair: Pair<Rule>) -> ParseResult<Guard> {
3557 let mut key = String::new();
3558 for inner in pair.into_inner() {
3559 if inner.as_rule() == Rule::env_key {
3560 key = inner.as_str().trim().to_string();
3561 }
3562 }
3563 Ok(Guard::Attr {
3564 ns: Ns::Env,
3565 key: Some(key),
3566 val: None,
3567 })
3568}
3569
3570fn parse_ns_guard(pair: Pair<Rule>, span: &SpanContext) -> ParseResult<GuardExpr> {
3578 fn os(value: &str) -> GuardExpr {
3579 GuardExpr::Predicate(Guard::Attr {
3580 ns: Ns::Os,
3581 key: None,
3582 val: Some(value.to_string()),
3583 })
3584 }
3585 let mut ns = String::new();
3586 let mut val = String::new();
3587 for inner in pair.into_inner() {
3588 match inner.as_rule() {
3589 Rule::ns_name => ns = inner.as_str().trim().to_string(),
3590 Rule::ns_value => val = inner.as_str().trim().to_string(),
3591 _ => {}
3592 }
3593 }
3594 match ns.as_str() {
3595 "family" => match val.as_str() {
3596 "unix" => Ok(GuardExpr::or(vec![os("macos"), os("linux")])),
3597 "windows" => Ok(os("windows")),
3598 _ => Err(ParseError::structural(
3599 "guard",
3600 format!("unknown family '{val}'; known values: unix, windows"),
3601 span,
3602 )),
3603 },
3604 "os" => match val.as_str() {
3605 "macos" | "linux" | "windows" => Ok(os(val.as_str())),
3606 _ => Err(ParseError::structural(
3607 "guard",
3608 format!("unknown os '{val}'; known values: macos, linux, windows"),
3609 span,
3610 )),
3611 },
3612 "arch" => {
3613 if ARCH_VALUES.contains(&val.as_str()) {
3614 Ok(GuardExpr::Predicate(Guard::Attr {
3615 ns: Ns::Arch,
3616 key: None,
3617 val: Some(val),
3618 }))
3619 } else {
3620 Err(ParseError::structural(
3621 "guard",
3622 format!(
3623 "unknown arch '{val}'; known values: {}",
3624 ARCH_VALUES.join(", ")
3625 ),
3626 span,
3627 ))
3628 }
3629 }
3630 _ => Err(ParseError::structural(
3631 "guard",
3632 format!(
3633 "unknown guard namespace '{ns}'; known namespaces: family, os, arch, env, bool"
3634 ),
3635 span,
3636 )),
3637 }
3638}
3639
3640fn parse_dollar_ident(pair: Pair<Rule>) -> String {
3641 let s = pair.as_str();
3643 s.strip_prefix('$').unwrap_or(s).to_string()
3644}
3645
3646use crate::ast::{ArithOp, CompareOp, LogicalOp, Value};
3647
3648fn parse_expr(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3649 let span = refine_span(ctx, &pair);
3650 let expr = parse_expr_inner(ctx, lctx, pair)?;
3651 if matches!(expr, Expr::UnsignedIntBoundary(_)) {
3652 return Err(ParseError::structural(
3653 "expr",
3654 "integer overflow: 9223372036854775808 exceeds i64::MAX".to_string(),
3655 &span,
3656 ));
3657 }
3658 Ok(expr)
3659}
3660
3661fn parse_expr_inner(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3662 let span = refine_span(ctx, &pair);
3663 let inner = pair.into_inner().next().unwrap();
3664 match inner.as_rule() {
3665 Rule::expr_logical_or => parse_expr_logical_or(ctx, lctx, inner),
3666 _ => Err(ParseError::structural(
3667 "expr",
3668 format!("unexpected expr rule: {:?}", inner.as_rule()),
3669 &span,
3670 )),
3671 }
3672}
3673
3674fn parse_expr_logical_or(
3675 ctx: &SpanContext,
3676 lctx: &LowerCtx,
3677 pair: Pair<Rule>,
3678) -> ParseResult<Expr> {
3679 let span = refine_span(ctx, &pair);
3680 let mut inner = pair.into_inner();
3681 let mut left = parse_expr_logical_and(ctx, lctx, inner.next().unwrap())?;
3682 while let Some(op_pair) = inner.next() {
3683 let op = match op_pair.as_rule() {
3684 Rule::or_op => LogicalOp::Or,
3685 _ => {
3686 return Err(ParseError::structural(
3687 "expr",
3688 format!("unexpected operator in logical-or: {:?}", op_pair.as_rule()),
3689 &span,
3690 ));
3691 }
3692 };
3693 let right = parse_expr_logical_and(ctx, lctx, inner.next().unwrap())?;
3694 left = Expr::Logical {
3695 op,
3696 left: Box::new(left),
3697 right: Box::new(right),
3698 };
3699 }
3700 Ok(left)
3701}
3702
3703fn parse_expr_logical_and(
3704 ctx: &SpanContext,
3705 lctx: &LowerCtx,
3706 pair: Pair<Rule>,
3707) -> ParseResult<Expr> {
3708 let span = refine_span(ctx, &pair);
3709 let mut inner = pair.into_inner();
3710 let mut left = parse_expr_comparison(ctx, lctx, inner.next().unwrap())?;
3711 while let Some(op_pair) = inner.next() {
3712 let op = match op_pair.as_rule() {
3713 Rule::and_op => LogicalOp::And,
3714 _ => {
3715 return Err(ParseError::structural(
3716 "expr",
3717 format!(
3718 "unexpected operator in logical-and: {:?}",
3719 op_pair.as_rule()
3720 ),
3721 &span,
3722 ));
3723 }
3724 };
3725 let right = parse_expr_comparison(ctx, lctx, inner.next().unwrap())?;
3726 reject_boundary(ctx, &left)?;
3727 reject_boundary(ctx, &right)?;
3728 left = Expr::Logical {
3729 op,
3730 left: Box::new(left),
3731 right: Box::new(right),
3732 };
3733 }
3734 Ok(left)
3735}
3736
3737fn parse_expr_comparison(
3738 ctx: &SpanContext,
3739 lctx: &LowerCtx,
3740 pair: Pair<Rule>,
3741) -> ParseResult<Expr> {
3742 let span = refine_span(ctx, &pair);
3743 let mut inner = pair.into_inner();
3744 let left = parse_expr_ordering(ctx, lctx, inner.next().unwrap())?;
3745 if let Some(op_pair) = inner.next() {
3746 let op = match op_pair.as_rule() {
3747 Rule::eq_op => CompareOp::Eq,
3748 Rule::neq_op => CompareOp::Ne,
3749 _ => {
3750 return Err(ParseError::structural(
3751 "expr",
3752 format!("unexpected comparison operator: {:?}", op_pair.as_rule()),
3753 &span,
3754 ));
3755 }
3756 };
3757 let right = parse_expr_ordering(ctx, lctx, inner.next().unwrap())?;
3758 return make_compare(ctx, op, left, right);
3759 }
3760 Ok(left)
3761}
3762
3763fn parse_expr_ordering(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3764 let span = refine_span(ctx, &pair);
3765 let mut inner = pair.into_inner();
3766 let left = parse_expr_add_sub(ctx, lctx, inner.next().unwrap())?;
3767 if let Some(op_pair) = inner.next() {
3768 let op = match op_pair.as_rule() {
3769 Rule::lt_op => CompareOp::Lt,
3770 Rule::le_op => CompareOp::Le,
3771 Rule::gt_op => CompareOp::Gt,
3772 Rule::ge_op => CompareOp::Ge,
3773 _ => {
3774 return Err(ParseError::structural(
3775 "expr",
3776 format!("unexpected ordering operator: {:?}", op_pair.as_rule()),
3777 &span,
3778 ));
3779 }
3780 };
3781 let right = parse_expr_add_sub(ctx, lctx, inner.next().unwrap())?;
3782 return make_compare(ctx, op, left, right);
3783 }
3784 Ok(left)
3785}
3786
3787fn parse_expr_add_sub(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3788 let span = refine_span(ctx, &pair);
3789 let mut inner = pair.into_inner();
3790 let mut left = parse_expr_mul_div(ctx, lctx, inner.next().unwrap())?;
3791 while let Some(op_pair) = inner.next() {
3792 let op = match op_pair.as_rule() {
3793 Rule::plus_op => ArithOp::Add,
3794 Rule::minus_op => ArithOp::Sub,
3795 _ => {
3796 return Err(ParseError::structural(
3797 "expr",
3798 format!("unexpected additive operator: {:?}", op_pair.as_rule()),
3799 &span,
3800 ));
3801 }
3802 };
3803 let right = parse_expr_mul_div(ctx, lctx, inner.next().unwrap())?;
3804 left = make_arith(ctx, op, left, right)?;
3805 }
3806 Ok(left)
3807}
3808
3809fn parse_expr_mul_div(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3810 let span = refine_span(ctx, &pair);
3811 let mut inner = pair.into_inner();
3812 let mut left = parse_expr_unary(ctx, lctx, inner.next().unwrap())?;
3813 while let Some(op_pair) = inner.next() {
3814 let op = match op_pair.as_rule() {
3815 Rule::star_op => ArithOp::Mul,
3816 Rule::slash_op => ArithOp::Div,
3817 _ => {
3818 return Err(ParseError::structural(
3819 "expr",
3820 format!(
3821 "unexpected multiplicative operator: {:?}",
3822 op_pair.as_rule()
3823 ),
3824 &span,
3825 ));
3826 }
3827 };
3828 let right = parse_expr_unary(ctx, lctx, inner.next().unwrap())?;
3829 left = make_arith(ctx, op, left, right)?;
3830 }
3831 Ok(left)
3832}
3833
3834fn parse_expr_unary(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3835 let span = refine_span(ctx, &pair);
3836 let mut prefixes = Vec::new();
3837 let mut atom = None;
3838 for inner in pair.into_inner() {
3839 match inner.as_rule() {
3840 Rule::not_op => prefixes.push(false),
3841 Rule::neg_op => prefixes.push(true),
3842 Rule::expr_atom => atom = Some(parse_expr_atom(ctx, lctx, inner)?),
3843 _ => {
3844 return Err(ParseError::structural(
3845 "expr",
3846 format!("unexpected unary operand rule: {:?}", inner.as_rule()),
3847 &span,
3848 ));
3849 }
3850 }
3851 }
3852 let mut expr = atom.ok_or_else(|| {
3853 ParseError::structural(
3854 "expr",
3855 "'!'/'-' requires an expression operand".to_string(),
3856 &span,
3857 )
3858 })?;
3859 for is_neg in prefixes.into_iter().rev() {
3861 if is_neg {
3862 expr = apply_unary_neg(ctx, expr)?;
3863 } else {
3864 reject_boundary(ctx, &expr)?;
3865 expr = Expr::Not(Box::new(expr));
3866 }
3867 }
3868 Ok(expr)
3869}
3870
3871fn reject_boundary(ctx: &SpanContext, expr: &Expr) -> ParseResult<()> {
3874 if matches!(expr, Expr::UnsignedIntBoundary(_)) {
3875 return Err(ParseError::structural(
3876 "expr",
3877 "integer overflow: 9223372036854775808 exceeds i64::MAX".to_string(),
3878 ctx,
3879 ));
3880 }
3881 Ok(())
3882}
3883
3884fn apply_unary_neg(ctx: &SpanContext, expr: Expr) -> ParseResult<Expr> {
3887 match expr {
3888 Expr::Literal(v) => match (v.as_i64(), v.as_f64()) {
3889 (Some(n), _) => match n.checked_neg() {
3890 Some(neg) => Ok(Expr::Literal(Value::int(neg))),
3891 None => Ok(Expr::CompiledMath(vec![MathOp::PushConst(v), MathOp::Neg])),
3892 },
3893 (None, Some(f)) => Ok(Expr::Literal(Value::float(-f))),
3894 (None, None) => {
3895 let other = Expr::Literal(v);
3896 if let Some(mut ops) = expr_to_rpn(&other) {
3897 ops.push(MathOp::Neg);
3898 Ok(Expr::CompiledMath(ops))
3899 } else {
3900 Ok(Expr::Arithmetic {
3903 op: ArithOp::Sub,
3904 left: Box::new(Expr::Literal(Value::int(0))),
3905 right: Box::new(other),
3906 })
3907 }
3908 }
3909 },
3910 Expr::UnsignedIntBoundary(n) => {
3911 if n == i64::MAX as u64 + 1 {
3912 Ok(Expr::Literal(Value::int(i64::MIN)))
3913 } else {
3914 Err(ParseError::structural(
3915 "expr",
3916 format!("integer overflow: {} exceeds i64::MAX", n),
3917 ctx,
3918 ))
3919 }
3920 }
3921 other => {
3922 if let Some(mut ops) = expr_to_rpn(&other) {
3923 ops.push(MathOp::Neg);
3924 Ok(Expr::CompiledMath(ops))
3925 } else {
3926 Ok(Expr::Arithmetic {
3929 op: ArithOp::Sub,
3930 left: Box::new(Expr::Literal(Value::int(0))),
3931 right: Box::new(other),
3932 })
3933 }
3934 }
3935 }
3936}
3937
3938fn try_fold_arith(op: ArithOp, left: &Expr, right: &Expr) -> Option<Expr> {
3943 let (Expr::Literal(lv), Expr::Literal(rv)) = (left, right) else {
3944 return None;
3945 };
3946 fold_arith_values(op, lv, rv).map(Expr::Literal)
3947}
3948
3949fn fold_arith_values(op: ArithOp, left: &Value, right: &Value) -> Option<Value> {
3950 match (left.as_i64(), right.as_i64()) {
3951 (Some(a), Some(b)) => {
3952 let v = match op {
3953 ArithOp::Add => a.checked_add(b)?,
3954 ArithOp::Sub => a.checked_sub(b)?,
3955 ArithOp::Mul => a.checked_mul(b)?,
3956 ArithOp::Div => a.checked_div(b)?,
3957 };
3958 Some(Value::int(v))
3959 }
3960 _ => {
3961 let (af, bf) = (as_f64(left)?, as_f64(right)?);
3962 fold_float(op, af, bf)
3963 }
3964 }
3965}
3966
3967fn fold_float(op: ArithOp, a: f64, b: f64) -> Option<Value> {
3968 if !a.is_finite() || !b.is_finite() {
3969 return None;
3970 }
3971 let v = match op {
3972 ArithOp::Add => a + b,
3973 ArithOp::Sub => a - b,
3974 ArithOp::Mul => a * b,
3975 ArithOp::Div => {
3976 if b == 0.0 {
3977 return None;
3978 }
3979 a / b
3980 }
3981 };
3982 if v.is_finite() {
3983 Some(Value::float(v))
3984 } else {
3985 None
3986 }
3987}
3988
3989fn try_fold_compare(op: CompareOp, left: &Expr, right: &Expr) -> Option<Expr> {
3990 let (Expr::Literal(lv), Expr::Literal(rv)) = (left, right) else {
3991 return None;
3992 };
3993 match (lv.as_i64(), rv.as_i64()) {
3994 (Some(a), Some(b)) => {
3995 let r = match op {
3996 CompareOp::Eq => a == b,
3997 CompareOp::Ne => a != b,
3998 CompareOp::Lt => a < b,
3999 CompareOp::Le => a <= b,
4000 CompareOp::Gt => a > b,
4001 CompareOp::Ge => a >= b,
4002 };
4003 Some(Expr::Literal(Value::bool(r)))
4004 }
4005 _ => {
4006 if let (Some(a), Some(b)) = (lv.as_bool(), rv.as_bool()) {
4007 return match op {
4008 CompareOp::Eq => Some(Expr::Literal(Value::bool(a == b))),
4009 CompareOp::Ne => Some(Expr::Literal(Value::bool(a != b))),
4010 _ => None,
4011 };
4012 }
4013 let (af, bf) = (as_f64(lv)?, as_f64(rv)?);
4014 let r = match op {
4015 CompareOp::Eq => af == bf,
4016 CompareOp::Ne => af != bf,
4017 CompareOp::Lt => af < bf,
4018 CompareOp::Le => af <= bf,
4019 CompareOp::Gt => af > bf,
4020 CompareOp::Ge => af >= bf,
4021 };
4022 Some(Expr::Literal(Value::bool(r)))
4023 }
4024 }
4025}
4026
4027fn as_f64(v: &Value) -> Option<f64> {
4028 if let Some(n) = v.as_i64() {
4029 return Some(n as f64);
4030 }
4031 match v.as_f64() {
4032 Some(f) if f.is_finite() => Some(f),
4033 _ => None,
4034 }
4035}
4036
4037fn make_arith(ctx: &SpanContext, op: ArithOp, left: Expr, right: Expr) -> ParseResult<Expr> {
4038 reject_boundary(ctx, &left)?;
4039 reject_boundary(ctx, &right)?;
4040 if let Some(folded) = try_fold_arith(op, &left, &right) {
4041 return Ok(folded);
4042 }
4043 if let (Some(mut lops), Some(mut rops)) = (expr_to_rpn(&left), expr_to_rpn(&right)) {
4044 lops.append(&mut rops);
4045 lops.push(match op {
4046 ArithOp::Add => MathOp::Add,
4047 ArithOp::Sub => MathOp::Sub,
4048 ArithOp::Mul => MathOp::Mul,
4049 ArithOp::Div => MathOp::Div,
4050 });
4051 return Ok(Expr::CompiledMath(lops));
4052 }
4053 Ok(Expr::Arithmetic {
4054 op,
4055 left: Box::new(left),
4056 right: Box::new(right),
4057 })
4058}
4059
4060fn make_compare(ctx: &SpanContext, op: CompareOp, left: Expr, right: Expr) -> ParseResult<Expr> {
4061 reject_boundary(ctx, &left)?;
4062 reject_boundary(ctx, &right)?;
4063 if let Some(folded) = try_fold_compare(op, &left, &right) {
4064 return Ok(folded);
4065 }
4066 if let (Some(mut lops), Some(mut rops)) = (expr_to_rpn(&left), expr_to_rpn(&right)) {
4067 lops.append(&mut rops);
4068 lops.push(match op {
4069 CompareOp::Eq => MathOp::Eq,
4070 CompareOp::Ne => MathOp::Ne,
4071 CompareOp::Lt => MathOp::Lt,
4072 CompareOp::Le => MathOp::Le,
4073 CompareOp::Gt => MathOp::Gt,
4074 CompareOp::Ge => MathOp::Ge,
4075 });
4076 return Ok(Expr::CompiledMath(lops));
4077 }
4078 Ok(Expr::Compare {
4079 op,
4080 left: Box::new(left),
4081 right: Box::new(right),
4082 })
4083}
4084
4085fn expr_to_rpn(expr: &Expr) -> Option<Vec<MathOp>> {
4089 match expr {
4090 Expr::Literal(v) => Some(vec![MathOp::PushConst(v.clone())]),
4091 Expr::Var(name) => Some(vec![MathOp::LoadVar(name.clone())]),
4092 Expr::Env(key) => Some(vec![MathOp::LoadEnv(key.clone())]),
4093 Expr::KeyPath { base, keys } => Some(vec![MathOp::LoadKeyPath {
4094 base: base.clone(),
4095 keys: keys.clone(),
4096 }]),
4097 Expr::Call { name, args } => {
4098 let mut ops = Vec::new();
4099 for arg in args {
4100 ops.extend(expr_to_rpn(arg)?);
4101 }
4102 ops.push(MathOp::Call {
4103 name: name.clone(),
4104 arity: args.len(),
4105 });
4106 Some(ops)
4107 }
4108 Expr::Inspect(var) => Some(vec![MathOp::Inspect(var.clone())]),
4109 Expr::Arithmetic { op, left, right } => {
4110 let mut ops = expr_to_rpn(left)?;
4111 ops.extend(expr_to_rpn(right)?);
4112 ops.push(match op {
4113 ArithOp::Add => MathOp::Add,
4114 ArithOp::Sub => MathOp::Sub,
4115 ArithOp::Mul => MathOp::Mul,
4116 ArithOp::Div => MathOp::Div,
4117 });
4118 Some(ops)
4119 }
4120 Expr::Compare { op, left, right } => {
4121 let mut ops = expr_to_rpn(left)?;
4122 ops.extend(expr_to_rpn(right)?);
4123 ops.push(match op {
4124 CompareOp::Eq => MathOp::Eq,
4125 CompareOp::Ne => MathOp::Ne,
4126 CompareOp::Lt => MathOp::Lt,
4127 CompareOp::Le => MathOp::Le,
4128 CompareOp::Gt => MathOp::Gt,
4129 CompareOp::Ge => MathOp::Ge,
4130 });
4131 Some(ops)
4132 }
4133 Expr::CompiledMath(ops) => Some(ops.clone()),
4134 Expr::FreshPipe => None,
4135 Expr::Not(_) | Expr::Logical { .. } | Expr::List(_) | Expr::Map(_) | Expr::Block(_) => None,
4136 Expr::UnsignedIntBoundary(_) => None,
4137 }
4138}
4139
4140fn parse_expr_atom(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
4141 let span = refine_span(ctx, &pair);
4142 let inner = pair.into_inner().next().unwrap();
4143 match inner.as_rule() {
4144 Rule::parenthesized_expr => parse_expr_inner(ctx, lctx, inner.into_inner().next().unwrap()),
4145 Rule::func_call => parse_func_call(ctx, lctx, inner),
4146 Rule::key_path => parse_key_path(ctx, inner),
4147 Rule::variable => {
4148 let name = inner.as_str();
4149 let name = name.strip_prefix('$').unwrap_or(name).to_string();
4150 Ok(Expr::Var(name))
4151 }
4152 Rule::env_read => parse_env_read(ctx, inner).map(Expr::Env),
4153 Rule::list_literal => parse_list_literal(ctx, lctx, inner),
4154 Rule::map_literal => parse_map_literal(ctx, lctx, inner),
4155 Rule::block => Ok(Expr::Block(parse_block_elements_with_lower(
4156 ctx, inner, lctx,
4157 )?)),
4158 Rule::string_literal | Rule::quoted_string => {
4159 let s = parse_quoted_string(inner)?;
4160 Ok(Expr::Literal(Value::string(s)))
4161 }
4162 Rule::numeric_literal => parse_numeric_literal(ctx, inner),
4163 Rule::bare_word => {
4164 let s = inner.as_str().to_string();
4165 match s.as_str() {
4166 "true" => Ok(Expr::Literal(Value::bool(true))),
4167 "false" => Ok(Expr::Literal(Value::bool(false))),
4168 _ => Ok(Expr::Literal(Value::string(s))),
4169 }
4170 }
4171 _ => Err(ParseError::structural(
4172 "expr",
4173 format!("unexpected expression atom rule: {:?}", inner.as_rule()),
4174 &span,
4175 )),
4176 }
4177}
4178
4179fn parse_numeric_literal(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Expr> {
4184 let span = refine_span(ctx, &pair);
4185 let text = pair.as_str();
4186 if text.contains('.') {
4187 let parsed: f64 = text.parse().map_err(|_| {
4188 ParseError::structural("expr", format!("invalid float literal {text:?}"), &span)
4189 })?;
4190 if !parsed.is_finite() {
4191 return Err(ParseError::structural(
4192 "expr",
4193 format!("invalid float literal {text:?}"),
4194 &span,
4195 ));
4196 }
4197 return Ok(Expr::Literal(Value::float(parsed)));
4198 }
4199 let digits: u64 = text.parse().map_err(|_| {
4200 ParseError::structural(
4201 "expr",
4202 format!("integer overflow: {text:?} exceeds i64::MAX"),
4203 &span,
4204 )
4205 })?;
4206 if digits <= i64::MAX as u64 {
4207 Ok(Expr::Literal(Value::int(digits as i64)))
4208 } else if digits == i64::MAX as u64 + 1 {
4209 Ok(Expr::UnsignedIntBoundary(digits))
4210 } else {
4211 Err(ParseError::structural(
4212 "expr",
4213 format!("integer overflow: {text:?} exceeds i64::MAX"),
4214 &span,
4215 ))
4216 }
4217}
4218
4219fn parse_env_read(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<String> {
4220 let span = refine_span(ctx, &pair);
4221 for inner in pair.into_inner() {
4222 if inner.as_rule() == Rule::env_read_key {
4223 return Ok(inner.as_str().trim().to_string());
4224 }
4225 }
4226 Err(ParseError::structural(
4227 "expr",
4228 "env read requires a key: env:KEY".to_string(),
4229 &span,
4230 ))
4231}
4232
4233fn parse_key_path(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Expr> {
4234 let span = refine_span(ctx, &pair);
4235 let mut base = None;
4236 let mut keys = Vec::new();
4237 for inner in pair.into_inner() {
4238 match inner.as_rule() {
4239 Rule::ident => {
4240 if base.is_none() {
4241 base = Some(inner.as_str().to_string());
4242 }
4243 }
4244 Rule::key_path_segment => {
4245 keys.push(inner.as_str().to_string());
4246 }
4247 _ => {}
4248 }
4249 }
4250 Ok(Expr::KeyPath {
4251 base: base.ok_or_else(|| {
4252 ParseError::structural(
4253 "expr",
4254 "key path requires a base identifier".to_string(),
4255 &span,
4256 )
4257 })?,
4258 keys,
4259 })
4260}
4261
4262fn parse_func_call(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
4263 let span = refine_span(ctx, &pair);
4264 let mut name = None;
4265 let mut args = Vec::new();
4266 for inner in pair.into_inner() {
4267 match inner.as_rule() {
4268 Rule::qualified_func_name => {
4269 name = Some(inner.as_str().to_string());
4270 }
4271 Rule::expr => {
4272 let arg = parse_expr_inner(ctx, lctx, inner)?;
4273 reject_boundary(ctx, &arg)?;
4274 args.push(arg);
4275 }
4276 _ => {}
4277 }
4278 }
4279 let name = name.ok_or_else(|| {
4280 ParseError::structural("expr", "function call requires a name".to_string(), &span)
4281 })?;
4282 if name == KEYWORD_INSPECT {
4287 let [arg] = args.as_slice() else {
4288 return Err(ParseError::structural(
4289 "expr",
4290 "INSPECT requires exactly one argument: INSPECT($var)".to_string(),
4291 &span,
4292 ));
4293 };
4294 if let Expr::Var(var) = arg {
4295 return Ok(Expr::Inspect(var.clone()));
4296 }
4297 return Err(ParseError::structural(
4298 "expr",
4299 format!("INSPECT requires a $variable argument, found {arg:?}"),
4300 &span,
4301 ));
4302 }
4303 let qualified = lctx.resolve_call(&span, &name)?;
4304 Ok(Expr::Call {
4305 name: qualified,
4306 args,
4307 })
4308}
4309
4310fn parse_list_literal(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
4311 let mut items = Vec::new();
4312 for inner in pair.into_inner() {
4313 if inner.as_rule() == Rule::expr {
4314 let item = parse_expr_inner(ctx, lctx, inner)?;
4315 reject_boundary(ctx, &item)?;
4316 items.push(item);
4317 }
4318 }
4319 Ok(Expr::List(items))
4320}
4321
4322fn parse_map_literal(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
4323 let span = refine_span(ctx, &pair);
4324 let mut entries = Vec::new();
4325 for inner in pair.into_inner() {
4326 if inner.as_rule() == Rule::map_entry {
4327 let mut key = String::new();
4328 let mut value = None;
4329 for entry_inner in inner.into_inner() {
4330 match entry_inner.as_rule() {
4331 Rule::quoted_string => {
4332 key = parse_quoted_string(entry_inner)?;
4333 }
4334 Rule::bare_word => {
4335 key = entry_inner.as_str().to_string();
4336 }
4337 Rule::expr => {
4338 let val = parse_expr_inner(ctx, lctx, entry_inner)?;
4339 reject_boundary(ctx, &val)?;
4340 value = Some(val);
4341 }
4342 _ => {}
4343 }
4344 }
4345 let val = value.ok_or_else(|| {
4346 ParseError::structural("expr", "map entry missing value".to_string(), &span)
4347 })?;
4348 entries.push((key, val));
4349 }
4350 }
4351 Ok(Expr::Map(entries))
4352}