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_expression_str(input: &str) -> ParseResult<Expr> {
1057 use pest::Parser;
1058 let trimmed = input.trim();
1062 let pairs = lexer::LanguageParser::parse(Rule::expr, trimmed).map_err(parse_pest_error)?;
1063 let pair = pairs.into_iter().next().ok_or_else(|| {
1064 ParseError::structural(
1065 "expr",
1066 "empty expression".to_string(),
1067 &span_for_line(trimmed, 1),
1068 )
1069 })?;
1070 if pair.as_span().end() != trimmed.len() {
1071 return Err(ParseError::structural(
1072 "expr",
1073 format!("invalid expression {input:?}"),
1074 &span_of(&pair, trimmed),
1075 ));
1076 }
1077 let ctx = span_of(&pair, trimmed);
1078 let reserved = HashSet::new();
1079 let func_scopes = RefCell::new(vec![HashSet::new()]);
1080 let modules = ModuleTable {
1081 modules: std::collections::HashMap::new(),
1082 };
1083 let import_scopes = RefCell::new(vec![Vec::new()]);
1084 let lower = crate::commands::lower_command as fn(&str, Vec<Arg>) -> ParseResult<StepKind>;
1085 let lctx = LowerCtx::new(&lower, &reserved, &func_scopes, &modules, &import_scopes);
1086 parse_expr(&ctx, &lctx, pair)
1087}
1088
1089pub fn parse_guard_expr_str(input: &str) -> ParseResult<GuardExpr> {
1090 use pest::Parser;
1091 let pairs = lexer::LanguageParser::parse(Rule::guard_expr, input).map_err(parse_pest_error)?;
1092 let pair = pairs.into_iter().next().ok_or_else(|| {
1093 ParseError::structural("guard", "empty guard".to_string(), &span_for_line(input, 1))
1094 })?;
1095 let ctx = span_of(&pair, input);
1096 parse_guard_expr(&ctx, pair)
1097}
1098
1099fn and_guard_exprs(left: Option<GuardExpr>, right: Option<GuardExpr>) -> Option<GuardExpr> {
1100 match (left, right) {
1101 (None, None) => None,
1102 (Some(expr), None) | (None, Some(expr)) => Some(expr),
1103 (Some(lhs), Some(rhs)) => Some(GuardExpr::all(vec![lhs, rhs])),
1104 }
1105}
1106
1107fn merge_bindings(defaults: &[IoBinding], overrides: &[IoBinding]) -> Vec<IoBinding> {
1108 let mut set = IoBindingSet::default();
1109 for binding in defaults {
1110 set.insert(binding.clone());
1111 }
1112 for binding in overrides {
1113 set.insert(binding.clone());
1114 }
1115 set.into_vec()
1116}
1117
1118fn validate_remote_copy_placement(steps: &[Step], ctx: &SpanContext) -> ParseResult<()> {
1122 fn visit_kind(kind: &StepKind, in_remote: bool, ctx: &SpanContext) -> ParseResult<()> {
1123 if let StepKind::Copy {
1124 from_host, to_host, ..
1125 } = kind
1126 && (*from_host || *to_host)
1127 && !in_remote
1128 {
1129 return Err(ParseError::structural(
1130 "copy",
1131 "COPY --from-host and --to-host are only valid inside a REMOTE block".to_string(),
1132 ctx,
1133 ));
1134 }
1135 let child_remote = in_remote || matches!(kind, StepKind::RemoteBlock { .. });
1136 let mut result = Ok(());
1137 kind.walk_child_kinds(&mut |child| {
1138 if result.is_ok() {
1139 result = visit_kind(child, child_remote, ctx);
1140 }
1141 });
1142 result
1143 }
1144 for step in steps {
1145 visit_kind(&step.kind, false, ctx)?;
1146 }
1147 Ok(())
1148}
1149
1150fn contains_inherit_env(kind: &StepKind) -> bool {
1151 if matches!(kind, StepKind::InheritEnv { .. }) {
1152 return true;
1153 }
1154 let mut found = false;
1155 kind.walk_child_kinds(&mut |child| {
1156 found = found || contains_inherit_env(child);
1157 });
1158 found
1159}
1160
1161fn has_stdout_pipe(bindings: &[IoBinding]) -> bool {
1164 bindings
1165 .iter()
1166 .any(|b| b.stream == IoStream::Stdout && b.pipe.is_some())
1167}
1168
1169fn reject_async_in_capture(ctx: &SpanContext, kind: &StepKind) -> ParseResult<()> {
1172 let bad = match kind {
1173 StepKind::AsyncBlock { .. }
1174 | StepKind::AssignAsync { .. }
1175 | StepKind::Await { .. }
1176 | StepKind::AwaitCapture { .. }
1177 | StepKind::Cancel { .. } => true,
1178 _ => {
1179 let mut bad = false;
1180 kind.walk_child_kinds(&mut |child| {
1181 bad = bad || reject_async_in_capture(ctx, child).is_err();
1182 });
1183 bad
1184 }
1185 };
1186 if bad {
1187 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));
1188 }
1189 Ok(())
1190}
1191
1192fn reject_pipe_stdout_in_capture(ctx: &SpanContext, kind: &StepKind) -> ParseResult<()> {
1195 if let StepKind::WithIo { bindings, .. } = kind
1196 && has_stdout_pipe(bindings)
1197 {
1198 return Err(ParseError::structural(
1199 "let",
1200 "LET capture cannot use WITH_IO [stdout=$var]; the capture sink owns stdout"
1201 .to_string(),
1202 ctx,
1203 ));
1204 }
1205 let mut result = Ok(());
1206 kind.walk_child_kinds(&mut |child| {
1207 if result.is_ok() {
1208 result = reject_pipe_stdout_in_capture(ctx, child);
1209 }
1210 });
1211 result
1212}
1213
1214fn parse_expr_str(ctx: &SpanContext, lctx: &LowerCtx, text: &str) -> ParseResult<Expr> {
1218 use pest::Parser;
1219 let mut pairs = lexer::LanguageParser::parse(Rule::expr, text).map_err(parse_pest_error)?;
1220 let pair = pairs.next().ok_or_else(|| {
1221 ParseError::validation("LET", "LET requires an expression".to_string(), ctx)
1222 })?;
1223 if pair.as_span().end() != text.len() {
1224 return Err(ParseError::structural(
1225 "expr",
1226 format!("invalid LET expression {text:?}"),
1227 ctx,
1228 ));
1229 }
1230 parse_expr(ctx, lctx, pair)
1231}
1232
1233fn parse_structural_command_with_lower(
1234 ctx: &SpanContext,
1235 pair: Pair<Rule>,
1236 lctx: &LowerCtx,
1237) -> ParseResult<StepKind> {
1238 let span = refine_span(ctx, &pair);
1239 let kind = match pair.as_rule() {
1240 Rule::inherit_env_command => {
1241 let mut keys = Vec::new();
1242 for inner in pair.into_inner() {
1243 if inner.as_rule() == Rule::inherit_list {
1244 for key in inner.into_inner() {
1245 if key.as_rule() == Rule::env_key {
1246 keys.push(key.as_str().trim().to_string());
1247 }
1248 }
1249 } else if inner.as_rule() == Rule::env_key {
1250 keys.push(inner.as_str().trim().to_string());
1251 }
1252 }
1253 StepKind::InheritEnv { keys }
1254 }
1255 Rule::with_io_command => {
1256 let mut bindings = Vec::new();
1257 let mut cmd = None;
1258 for inner in pair.into_inner() {
1259 match inner.as_rule() {
1260 Rule::io_flags => {
1261 for flag in inner.into_inner() {
1262 if flag.as_rule() == Rule::io_binding {
1263 bindings.push(parse_io_binding(ctx, flag)?);
1264 }
1265 }
1266 }
1267 Rule::with_io_command => {
1268 cmd = Some(Box::new(parse_structural_command_with_lower(
1269 ctx, inner, lctx,
1270 )?));
1271 }
1272 Rule::inherit_env_command => {
1273 cmd = Some(Box::new(parse_structural_command_with_lower(
1274 ctx, inner, lctx,
1275 )?));
1276 }
1277 Rule::async_statement | Rule::async_statement_block => {
1278 cmd = Some(Box::new(parse_structural_command_with_lower(
1279 ctx, inner, lctx,
1280 )?));
1281 }
1282 Rule::timeout_statement | Rule::cancel_statement => {
1283 cmd = Some(Box::new(parse_structural_command_with_lower(
1284 ctx, inner, lctx,
1285 )?));
1286 }
1287 Rule::remote_statement => {
1288 cmd = Some(Box::new(parse_structural_command_with_lower(
1289 ctx, inner, lctx,
1290 )?));
1291 }
1292 Rule::call_statement | Rule::while_statement => {
1293 cmd = Some(Box::new(parse_structural_command_with_lower(
1294 ctx, inner, lctx,
1295 )?));
1296 }
1297 Rule::func_def
1298 | Rule::return_statement
1299 | Rule::break_statement
1300 | Rule::continue_statement => {
1301 return Err(ParseError::structural(
1302 "parser",
1303 format!(
1304 "WITH_IO cannot wrap {:?}; place it around a command or block instead",
1305 inner.as_rule()
1306 ),
1307 &span,
1308 ));
1309 }
1310 Rule::instruction | Rule::instruction_inner => {
1311 cmd = Some(Box::new(lower_instruction_pair(ctx, inner, lctx)?));
1312 }
1313 Rule::run_exec_statement | Rule::run_exec_inner => {
1314 cmd = Some(Box::new(lower_run_exec_pair(ctx, inner, lctx)?));
1315 }
1316 _ => {}
1317 }
1318 }
1319 if let Some(cmd) = cmd {
1320 StepKind::WithIo { bindings, cmd }
1321 } else {
1322 StepKind::WithIoBlock { bindings }
1323 }
1324 }
1325 Rule::for_statement => parse_for_statement_from_pair(ctx, pair, lctx)?,
1326 Rule::while_statement => parse_while_statement_from_pair(ctx, pair, lctx)?,
1327 Rule::func_def => parse_func_def_from_pair(ctx, pair, lctx)?,
1328 Rule::call_statement => parse_call_statement_from_pair(ctx, lctx, pair)?,
1329 Rule::return_statement => parse_return_statement_from_pair(ctx, lctx, pair)?,
1330 Rule::break_statement => StepKind::Break,
1331 Rule::continue_statement => StepKind::Continue,
1332 Rule::let_statement => parse_let_statement_from_pair(ctx, lctx, pair)?,
1333 Rule::mutate_statement => parse_mutate_statement_from_pair(ctx, lctx, pair)?,
1334 Rule::let_async_statement => parse_let_async_statement_from_pair(ctx, pair, lctx)?,
1335 Rule::let_capture_statement => parse_let_capture_statement_from_pair(ctx, pair, lctx)?,
1336 Rule::await_statement => parse_await_statement_from_pair(ctx, pair)?,
1337 Rule::cancel_statement => parse_cancel_statement_from_pair(ctx, pair)?,
1338 Rule::if_statement => parse_if_statement_from_pair(ctx, pair, lctx)?,
1339 Rule::async_statement => parse_async_statement_from_pair(ctx, pair, lctx)?,
1340 Rule::async_statement_block => parse_async_statement_block_from_pair(ctx, pair, lctx)?,
1341 Rule::timeout_statement => parse_timeout_statement_from_pair(ctx, pair, lctx)?,
1342 Rule::remote_statement => parse_remote_statement_from_pair(ctx, pair, lctx)?,
1343 Rule::type_statement => parse_type_statement_from_pair(ctx, pair)?,
1344 Rule::command_inner => {
1345 let inner = pair.into_inner().next().ok_or_else(|| {
1348 ParseError::structural("parser", "empty command_inner".to_string(), &span)
1349 })?;
1350 parse_structural_command_with_lower(ctx, inner, lctx)?
1351 }
1352 Rule::instruction | Rule::instruction_inner => lower_instruction_pair(ctx, pair, lctx)?,
1353 Rule::run_exec_statement | Rule::run_exec_inner => lower_run_exec_pair(ctx, pair, lctx)?,
1354 _ => {
1355 return Err(ParseError::structural(
1356 "parser",
1357 format!("unexpected structural command rule: {:?}", pair.as_rule()),
1358 &span,
1359 ));
1360 }
1361 };
1362 Ok(kind)
1363}
1364
1365fn extract_instruction(
1366 ctx: &SpanContext,
1367 pair: Pair<Rule>,
1368 lctx: &LowerCtx,
1369) -> ParseResult<(String, Vec<InsToken>)> {
1370 let span = refine_span(ctx, &pair);
1371 let mut name = None;
1372 let mut args = Vec::new();
1373 for inner in pair.into_inner() {
1374 match inner.as_rule() {
1375 Rule::command_name => {
1376 name = Some(inner.as_str().to_string());
1377 }
1378 Rule::argument => {
1379 args.extend(
1380 parse_argument(ctx, lctx, inner)?
1381 .into_iter()
1382 .map(InsToken::Pos),
1383 );
1384 }
1385 Rule::assignment => {
1386 let (key, value) = parse_assignment(ctx, lctx, inner)?;
1387 args.push(InsToken::Assign(key, value));
1388 }
1389 _ => {}
1390 }
1391 }
1392 let name = name.ok_or_else(|| {
1393 ParseError::structural(
1394 "instruction",
1395 "instruction missing command name".to_string(),
1396 &span,
1397 )
1398 })?;
1399 Ok((name, args))
1400}
1401
1402enum InsToken {
1407 Pos(Arg),
1408 Assign(String, Arg),
1409}
1410
1411fn lower_instruction_pair(
1416 ctx: &SpanContext,
1417 pair: Pair<Rule>,
1418 lctx: &LowerCtx,
1419) -> ParseResult<StepKind> {
1420 let span = refine_span(ctx, &pair);
1421 let (name, tokens) = extract_instruction(ctx, pair, lctx)?;
1422 if name == "ENV" {
1423 return lower_env_command(ctx, tokens);
1424 }
1425 if name == "EXPAND" {
1426 return lower_expand_command(ctx, tokens);
1427 }
1428 let args = tokens
1429 .into_iter()
1430 .map(|token| match token {
1431 InsToken::Pos(arg) => arg,
1432 InsToken::Assign(key, value) => crate::commands::canonical_assignment_arg(&key, &value),
1433 })
1434 .collect();
1435 (lctx.lower)(&name, args).map_err(|e| e.with_span(&span))
1436}
1437
1438fn lower_run_exec_pair(
1444 ctx: &SpanContext,
1445 pair: Pair<Rule>,
1446 lctx: &LowerCtx,
1447) -> ParseResult<StepKind> {
1448 let span = refine_span(ctx, &pair);
1449 let mut list = None;
1450 for inner in pair.into_inner() {
1451 if inner.as_rule() == Rule::run_exec_list {
1452 list = Some(parse_run_exec_list(ctx, lctx, inner)?);
1453 }
1454 }
1455 let list = list.ok_or_else(|| {
1456 ParseError::structural(
1457 "run_exec",
1458 "RUN exec form missing list literal".to_string(),
1459 &span,
1460 )
1461 })?;
1462 (lctx.lower)("RUN", vec![Arg::Expr(list)]).map_err(|e| e.with_span(&span))
1463}
1464
1465fn parse_run_exec_list(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
1470 let mut items = Vec::new();
1471 for inner in pair.into_inner() {
1472 if inner.as_rule() == Rule::run_exec_arg {
1473 let item = parse_run_exec_arg(ctx, lctx, inner)?;
1474 reject_boundary(ctx, &item)?;
1475 items.push(item);
1476 }
1477 }
1478 Ok(Expr::List(items))
1479}
1480
1481fn parse_run_exec_arg(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
1482 let span = refine_span(ctx, &pair);
1483 let inner = pair.into_inner().next().ok_or_else(|| {
1484 ParseError::structural("run_exec", "RUN exec argument is empty".to_string(), &span)
1485 })?;
1486 match inner.as_rule() {
1487 Rule::parenthesized_expr => parse_expr_inner(ctx, lctx, inner.into_inner().next().unwrap()),
1488 Rule::func_call => parse_func_call(ctx, lctx, inner),
1489 Rule::key_path => parse_key_path(ctx, inner),
1490 Rule::variable => {
1491 let name = inner.as_str();
1492 let name = name.strip_prefix('$').unwrap_or(name).to_string();
1493 Ok(Expr::Var(name))
1494 }
1495 Rule::env_read => parse_env_read(ctx, inner).map(Expr::Env),
1496 Rule::list_literal => parse_list_literal(ctx, lctx, inner),
1497 Rule::map_literal => parse_map_literal(ctx, lctx, inner),
1498 Rule::block => Ok(Expr::Block(parse_block_elements_with_lower(
1499 ctx, inner, lctx,
1500 )?)),
1501 Rule::string_literal | Rule::quoted_string => {
1502 let s = parse_quoted_string(inner)?;
1503 Ok(Expr::Literal(Value::string(s)))
1504 }
1505 Rule::numeric_literal => parse_numeric_literal(ctx, inner),
1506 Rule::bare_word => {
1507 let s = inner.as_str().to_string();
1508 match s.as_str() {
1509 "true" => Ok(Expr::Literal(Value::bool(true))),
1510 "false" => Ok(Expr::Literal(Value::bool(false))),
1511 _ => Ok(Expr::Literal(Value::string(s))),
1512 }
1513 }
1514 _ => Err(ParseError::structural(
1515 "run_exec",
1516 format!("unexpected RUN exec argument rule: {:?}", inner.as_rule()),
1517 &span,
1518 )),
1519 }
1520}
1521
1522fn parse_assignment(
1524 ctx: &SpanContext,
1525 lctx: &LowerCtx,
1526 pair: Pair<Rule>,
1527) -> ParseResult<(String, Arg)> {
1528 let span = refine_span(ctx, &pair);
1529 let mut key = None;
1530 let mut value = None;
1531 for inner in pair.into_inner() {
1532 match inner.as_rule() {
1533 Rule::assign_key => {
1534 key = Some(inner.as_str().to_string());
1535 }
1536 Rule::assign_value => {
1537 value = Some(lower_command_value(ctx, lctx, inner)?);
1538 }
1539 _ => {
1540 return Err(ParseError::structural(
1541 "assignment",
1542 format!("unexpected assignment rule: {:?}", inner.as_rule()),
1543 &span,
1544 ));
1545 }
1546 }
1547 }
1548 Ok((
1549 key.ok_or_else(|| {
1550 ParseError::structural("assignment", "assignment missing key".to_string(), &span)
1551 })?,
1552 value.unwrap_or(Arg::String(String::new(), false)),
1553 ))
1554}
1555
1556fn lower_command_value(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Arg> {
1561 let span = refine_span(ctx, &pair);
1562 let inner = pair.into_inner().next().ok_or_else(|| {
1563 ParseError::structural("assignment", "assignment value is empty".to_string(), &span)
1564 })?;
1565 match inner.as_rule() {
1566 Rule::quoted_string => Ok(Arg::String(parse_quoted_string(inner)?, true)),
1567 Rule::assign_expr => {
1568 let shape = inner.into_inner().next().ok_or_else(|| {
1569 ParseError::structural(
1570 "assignment",
1571 "assignment expression is empty".to_string(),
1572 &span,
1573 )
1574 })?;
1575 match shape.as_rule() {
1576 Rule::variable => Ok(Arg::Expr(Expr::Var(parse_dollar_ident(shape)))),
1577 Rule::key_path => Ok(Arg::Expr(parse_key_path(ctx, shape)?)),
1578 Rule::env_read => Ok(Arg::Expr(Expr::Env(parse_env_read(ctx, shape)?))),
1579 Rule::func_call => Ok(Arg::Expr(parse_func_call(ctx, lctx, shape)?)),
1580 other => Err(ParseError::structural(
1581 "assignment",
1582 format!("unexpected assignment expression shape: {:?}", other),
1583 &span,
1584 )),
1585 }
1586 }
1587 Rule::raw_fragments => lower_raw_fragments(ctx, inner),
1588 other => Err(ParseError::structural(
1589 "assignment",
1590 format!("unexpected assignment value rule: {:?}", other),
1591 &span,
1592 )),
1593 }
1594}
1595
1596fn lower_raw_fragments(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Arg> {
1602 let span = refine_span(ctx, &pair);
1603 let mut body = String::new();
1604 for fragment in pair.into_inner() {
1605 match fragment.as_rule() {
1606 Rule::quoted_string => body.push_str(&parse_quoted_string(fragment)?),
1607 Rule::templated_arg => body.push_str(fragment.as_str()),
1608 Rule::raw_text => body.push_str(&collapse_ws(fragment.as_str())),
1609 other => {
1610 return Err(ParseError::structural(
1611 "assignment",
1612 format!("unexpected raw value fragment: {:?}", other),
1613 &span,
1614 ));
1615 }
1616 }
1617 }
1618 Ok(Arg::String(body.trim().to_string(), false))
1619}
1620
1621fn collapse_ws(s: &str) -> String {
1624 let mut out = String::with_capacity(s.len());
1625 let mut in_run = false;
1626 for c in s.chars() {
1627 if c.is_whitespace() {
1628 if !in_run {
1629 out.push(' ');
1630 in_run = true;
1631 }
1632 } else {
1633 out.push(c);
1634 in_run = false;
1635 }
1636 }
1637 out
1638}
1639
1640fn lower_env_command(ctx: &SpanContext, tokens: Vec<InsToken>) -> ParseResult<StepKind> {
1644 if tokens.is_empty() {
1645 return Err(ParseError::validation(
1646 "ENV",
1647 "ENV requires KEY=value".to_string(),
1648 ctx,
1649 ));
1650 }
1651 match tokens.as_slice() {
1652 [InsToken::Assign(key, value)] => {
1653 if crate::command::split_assignment(&format!("{key}={}", value.render()))
1656 .map_err(|e| ParseError::validation("ENV", e.to_string(), ctx))?
1657 .is_none()
1658 {
1659 return Err(ParseError::validation(
1660 "ENV",
1661 "ENV requires KEY=value format".to_string(),
1662 ctx,
1663 ));
1664 }
1665 Ok(StepKind::Env {
1666 key: key.clone(),
1667 value: value.clone(),
1668 })
1669 }
1670 [InsToken::Pos(Arg::String(text, _))] => match crate::command::split_assignment(text)
1671 .map_err(|e| ParseError::validation("ENV", e.to_string(), ctx))?
1672 {
1673 Some((key, value)) => Ok(StepKind::Env { key, value }),
1674 None => Err(ParseError::validation(
1675 "ENV",
1676 "ENV requires KEY=value format".to_string(),
1677 ctx,
1678 )),
1679 },
1680 _ => Err(ParseError::validation(
1681 "ENV",
1682 "ENV requires KEY=value format".to_string(),
1683 ctx,
1684 )),
1685 }
1686}
1687
1688fn lower_expand_command(ctx: &SpanContext, tokens: Vec<InsToken>) -> ParseResult<StepKind> {
1692 let mut path = None;
1693 let mut overrides = Vec::new();
1694 for token in tokens {
1695 match token {
1696 InsToken::Assign(key, value) => {
1697 if key.is_empty() {
1698 return Err(ParseError::validation(
1699 "EXPAND",
1700 "EXPAND requires KEY=value format for overrides".to_string(),
1701 ctx,
1702 ));
1703 }
1704 overrides.push((key, value));
1705 }
1706 InsToken::Pos(arg) => match &arg {
1707 Arg::String(text, quoted) if !quoted && text.contains('=') => {
1708 let Some((key, value)) = crate::command::split_assignment(text)
1709 .map_err(|e| ParseError::validation("EXPAND", e.to_string(), ctx))?
1710 else {
1711 return Err(ParseError::validation(
1712 "EXPAND",
1713 "EXPAND requires KEY=value format for overrides".to_string(),
1714 ctx,
1715 ));
1716 };
1717 overrides.push((key, value));
1718 }
1719 _ => {
1720 if path.is_none() {
1721 ArgType::Path
1725 .check_arg(&arg)
1726 .map_err(|e| ParseError::validation("EXPAND", e.to_string(), ctx))?;
1727 path = Some(arg);
1728 } else {
1729 return Err(ParseError::validation(
1730 "EXPAND",
1731 "EXPAND accepts at most one path".to_string(),
1732 ctx,
1733 ));
1734 }
1735 }
1736 },
1737 }
1738 }
1739 Ok(StepKind::Expand { path, overrides })
1740}
1741
1742fn parse_type_tag(pair: Pair<Rule>) -> String {
1743 crate::canonicalize_spelling(pair.as_str())
1749}
1750
1751fn parse_type_statement_from_pair(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<StepKind> {
1757 let span = refine_span(ctx, &pair);
1758 let mut name: Option<String> = None;
1759 let mut target: Option<String> = None;
1760 for inner in pair.into_inner() {
1761 match inner.as_rule() {
1762 Rule::type_name => name = Some(inner.as_str().trim().to_string()),
1763 Rule::type_tag => target = Some(parse_type_tag(inner)),
1764 _ => {}
1765 }
1766 }
1767 let (Some(name), Some(target)) = (name, target) else {
1768 return Err(ParseError::structural(
1769 KEYWORD_TYPE,
1770 "TYPE requires a name and a shape (`TYPE NAME = SHAPE`)".to_string(),
1771 &span,
1772 ));
1773 };
1774 Ok(StepKind::TypeAlias { name, target })
1775}
1776
1777fn check_func_ident(ctx: &SpanContext, name: &str) -> ParseResult<()> {
1778 let ok = name
1779 .chars()
1780 .next()
1781 .map(|c| c.is_ascii_uppercase())
1782 .unwrap_or(false)
1783 && name
1784 .chars()
1785 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_');
1786 if !ok {
1787 return Err(ParseError::validation(
1788 "FUNC",
1789 format!(
1790 "function names must be UPPERCASE (ASCII_ALPHA_UPPER, digits, _), got `{name}`"
1791 ),
1792 ctx,
1793 ));
1794 }
1795 Ok(())
1796}
1797
1798fn parse_while_statement_from_pair(
1799 ctx: &SpanContext,
1800 pair: Pair<Rule>,
1801 lctx: &LowerCtx,
1802) -> ParseResult<StepKind> {
1803 let span = refine_span(ctx, &pair);
1804 let mut cond = None;
1805 let mut body = None;
1806 for inner in pair.into_inner() {
1807 match inner.as_rule() {
1808 Rule::expr => {
1809 if cond.is_none() {
1810 cond = Some(parse_expr(ctx, lctx, inner)?);
1811 }
1812 }
1813 Rule::block => {
1814 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
1815 }
1816 _ => {}
1817 }
1818 }
1819 Ok(StepKind::While {
1820 cond: Box::new(cond.ok_or_else(|| {
1821 ParseError::validation("WHILE", "WHILE requires a condition".to_string(), &span)
1822 })?),
1823 body: body.ok_or_else(|| {
1824 ParseError::validation("WHILE", "WHILE requires a block".to_string(), &span)
1825 })?,
1826 })
1827}
1828
1829fn parse_func_def_from_pair(
1830 ctx: &SpanContext,
1831 pair: Pair<Rule>,
1832 lctx: &LowerCtx,
1833) -> ParseResult<StepKind> {
1834 let span = refine_span(ctx, &pair);
1835 let def_name = pair
1840 .clone()
1841 .into_inner()
1842 .find(|inner| inner.as_rule() == Rule::func_ident)
1843 .map(|inner| inner.as_str().to_string())
1844 .ok_or_else(|| ParseError::validation("FUNC", "FUNC requires a name".to_string(), &span))?;
1845 check_func_ident(ctx, &def_name)?;
1846 if lctx.reserved_names.contains(&def_name) || lctx.module_base_names().contains(&def_name) {
1853 return Err(ParseError::validation(
1854 "FUNC",
1855 format!("FUNC {def_name} cannot shadow reserved function `{def_name}`"),
1856 &span,
1857 ));
1858 }
1859 if !lctx.declare_func(&def_name) {
1860 return Err(ParseError::validation(
1861 "FUNC",
1862 format!("duplicate function `{def_name}` in same scope"),
1863 &span,
1864 ));
1865 }
1866 let mut name: Option<String> = None;
1867 let mut param_names: Vec<String> = Vec::new();
1868 let mut param_types: Vec<String> = Vec::new();
1869 let mut body = None;
1870 for inner in pair.into_inner() {
1871 match inner.as_rule() {
1872 Rule::func_ident => {
1873 if name.is_none() {
1874 name = Some(inner.as_str().to_string());
1875 }
1876 }
1877 Rule::func_param => {
1878 let mut pname = None;
1879 let mut ptype = None;
1880 for part in inner.into_inner() {
1881 match part.as_rule() {
1882 Rule::dollar_ident => {
1883 pname = Some(parse_dollar_ident(part));
1884 }
1885 Rule::type_tag => {
1886 ptype = Some(parse_type_tag(part));
1887 }
1888 _ => {}
1889 }
1890 }
1891 param_names.push(pname.ok_or_else(|| {
1892 ParseError::validation(
1893 "FUNC",
1894 "FUNC parameter requires a $variable".to_string(),
1895 &span,
1896 )
1897 })?);
1898 param_types.push(ptype.ok_or_else(|| {
1899 ParseError::validation(
1900 "FUNC",
1901 "FUNC parameters require explicit types: FUNC NAME($p: TYPE, ...)"
1902 .to_string(),
1903 &span,
1904 )
1905 })?);
1906 }
1907 Rule::block => {
1908 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
1909 }
1910 _ => {}
1911 }
1912 }
1913 let name = name
1914 .ok_or_else(|| ParseError::validation("FUNC", "FUNC requires a name".to_string(), &span))?;
1915 check_func_ident(ctx, &name)?;
1916 if param_names.len() != param_types.len() {
1917 return Err(ParseError::validation(
1918 "FUNC",
1919 format!("FUNC {name} has mismatched parameter names and types"),
1920 &span,
1921 ));
1922 }
1923 let mut seen = std::collections::HashSet::new();
1924 for pname in ¶m_names {
1925 if !seen.insert(pname.clone()) {
1926 return Err(ParseError::validation(
1927 "FUNC",
1928 format!("FUNC {name} declares duplicate parameter ${pname}"),
1929 &span,
1930 ));
1931 }
1932 }
1933 Ok(StepKind::FuncDef {
1934 name,
1935 params: param_names.into_iter().zip(param_types).collect(),
1936 body: body.ok_or_else(|| {
1937 ParseError::validation("FUNC", "FUNC requires a block".to_string(), &span)
1938 })?,
1939 })
1940}
1941
1942fn parse_call_statement_from_pair(
1943 ctx: &SpanContext,
1944 lctx: &LowerCtx,
1945 pair: Pair<Rule>,
1946) -> ParseResult<StepKind> {
1947 let span = refine_span(ctx, &pair);
1948 let mut name: Option<String> = None;
1949 let mut args = Vec::new();
1950 for inner in pair.into_inner() {
1954 match inner.as_rule() {
1955 Rule::call_head_paren => {
1956 if name.is_none() {
1957 let text = inner.as_str();
1958 name = Some(text.strip_suffix('(').unwrap_or(text).to_string());
1959 }
1960 }
1961 Rule::func_call_head => {
1962 if name.is_none() {
1963 name = Some(inner.as_str().to_string());
1964 }
1965 }
1966 Rule::expr => {
1967 args.push(parse_expr(ctx, lctx, inner)?);
1968 }
1969 _ => {}
1970 }
1971 }
1972 let name = name.ok_or_else(|| {
1973 ParseError::validation(
1974 "FUNC",
1975 "function call requires a function name".to_string(),
1976 &span,
1977 )
1978 })?;
1979 if !name.contains(MODULE_SEPARATOR) {
1982 check_func_ident(ctx, &name)?;
1983 }
1984 let qualified = lctx.resolve_call(&span, &name)?;
1985 Ok(StepKind::Call {
1986 name: qualified,
1987 args,
1988 })
1989}
1990
1991fn parse_return_statement_from_pair(
1992 ctx: &SpanContext,
1993 lctx: &LowerCtx,
1994 pair: Pair<Rule>,
1995) -> ParseResult<StepKind> {
1996 use crate::ast::Value;
1997 for inner in pair.into_inner() {
1998 if inner.as_rule() == Rule::expr {
1999 return Ok(StepKind::Return {
2000 expr: Box::new(parse_expr(ctx, lctx, inner)?),
2001 });
2002 }
2003 }
2004 Ok(StepKind::Return {
2005 expr: Box::new(Expr::Literal(Value::string(String::new()))),
2006 })
2007}
2008
2009fn parse_for_statement_from_pair(
2010 ctx: &SpanContext,
2011 pair: Pair<Rule>,
2012 lctx: &LowerCtx,
2013) -> ParseResult<StepKind> {
2014 let span = refine_span(ctx, &pair);
2015 let mut idents: Vec<String> = Vec::new();
2016 let mut types: Vec<String> = Vec::new();
2017 let mut type_spans: Vec<SpanContext> = Vec::new();
2018 let mut in_expr = None;
2019 let mut body_steps = Vec::new();
2020 for inner in pair.into_inner() {
2021 match inner.as_rule() {
2022 Rule::dollar_ident => {
2023 idents.push(parse_dollar_ident(inner));
2024 }
2025 Rule::type_tag => {
2026 type_spans.push(refine_span(ctx, &inner));
2027 types.push(parse_type_tag(inner));
2028 }
2029 Rule::expr => {
2030 in_expr = Some(parse_expr(ctx, lctx, inner)?);
2031 }
2032 Rule::block => {
2033 body_steps = parse_block_elements_with_lower(ctx, inner, lctx)?;
2034 }
2035 _ => {}
2036 }
2037 }
2038 if idents.len() != types.len() {
2039 return Err(ParseError::validation(
2040 "FOR",
2041 format!(
2042 "FOR requires explicit types: FOR $item: TYPE IN <expr> (got {} vars, {} types)",
2043 idents.len(),
2044 types.len()
2045 ),
2046 &span,
2047 ));
2048 }
2049 let (key_var, key_type, var, var_type) = match idents.len() {
2050 1 => (
2051 None,
2052 None,
2053 idents.into_iter().next().unwrap(),
2054 types.into_iter().next().unwrap(),
2055 ),
2056 2 => {
2057 let mut iv = idents.into_iter();
2058 let mut tv = types.into_iter();
2059 (
2060 Some(iv.next().unwrap()),
2061 Some(tv.next().unwrap()),
2062 iv.next().unwrap(),
2063 tv.next().unwrap(),
2064 )
2065 }
2066 _ => {
2067 return Err(ParseError::validation(
2068 "FOR",
2069 "FOR requires one or two variables".to_string(),
2070 &span,
2071 ));
2072 }
2073 };
2074 if let Some(kt) = &key_type
2075 && kt != "STRING"
2076 && kt != "INT"
2077 {
2078 let at = type_spans.first().unwrap_or(&span);
2080 return Err(ParseError::validation(
2081 "FOR",
2082 format!("FOR key variable must be INT or STRING, got {kt}"),
2083 at,
2084 ));
2085 }
2086 Ok(StepKind::For {
2087 key_var,
2088 key_type,
2089 var,
2090 var_type,
2091 in_expr: in_expr.ok_or_else(|| {
2092 ParseError::validation(
2093 "FOR",
2094 "FOR requires an iterable expression".to_string(),
2095 &span,
2096 )
2097 })?,
2098 body: body_steps,
2099 })
2100}
2101
2102fn parse_let_statement_from_pair(
2103 ctx: &SpanContext,
2104 lctx: &LowerCtx,
2105 pair: Pair<Rule>,
2106) -> ParseResult<StepKind> {
2107 let span = refine_span(ctx, &pair);
2108 let mut var = None;
2109 let mut decl_type = None;
2110 let mut expr = None;
2111 for inner in pair.into_inner() {
2112 match inner.as_rule() {
2113 Rule::dollar_ident => {
2114 var = Some(parse_dollar_ident(inner));
2115 }
2116 Rule::type_tag => {
2117 decl_type = Some(parse_type_tag(inner));
2118 }
2119 Rule::expr => {
2120 expr = Some(parse_expr(ctx, lctx, inner)?);
2121 }
2122 _ => {}
2123 }
2124 }
2125 let var = var.ok_or_else(|| {
2126 ParseError::validation("LET", "LET requires a variable".to_string(), &span)
2127 })?;
2128 let decl_type = decl_type.ok_or_else(|| {
2129 ParseError::validation(
2130 "LET",
2131 "LET requires explicit type: LET $var: TYPE = <expr>".to_string(),
2132 &span,
2133 )
2134 })?;
2135 let expr = match expr {
2138 Some(e) => e,
2139 None if decl_type == "PIPE" => Expr::FreshPipe,
2140 None => {
2141 return Err(ParseError::validation(
2142 "LET",
2143 "LET requires an expression: LET $var: TYPE = <expr> (only LET $p: PIPE omits the initializer)".to_string(),
2144 &span,
2145 ));
2146 }
2147 };
2148 Ok(StepKind::Assign {
2149 var,
2150 decl_type,
2151 expr,
2152 })
2153}
2154
2155fn parse_mutate_statement_from_pair(
2156 ctx: &SpanContext,
2157 lctx: &LowerCtx,
2158 pair: Pair<Rule>,
2159) -> ParseResult<StepKind> {
2160 let span = refine_span(ctx, &pair);
2161 let mut var = None;
2162 let mut expr = None;
2163 for inner in pair.into_inner() {
2164 match inner.as_rule() {
2165 Rule::dollar_ident => {
2166 var = Some(parse_dollar_ident(inner));
2167 }
2168 Rule::expr => {
2169 expr = Some(parse_expr(ctx, lctx, inner)?);
2170 }
2171 _ => {}
2172 }
2173 }
2174 Ok(StepKind::Set {
2175 var: var.ok_or_else(|| {
2176 ParseError::validation(
2177 "mutate",
2178 "mutation requires a variable: $var = <expr>".to_string(),
2179 &span,
2180 )
2181 })?,
2182 expr: expr.ok_or_else(|| {
2183 ParseError::validation(
2184 "mutate",
2185 "mutation requires an expression: $var = <expr>".to_string(),
2186 &span,
2187 )
2188 })?,
2189 })
2190}
2191
2192fn parse_let_async_statement_from_pair(
2193 ctx: &SpanContext,
2194 pair: Pair<Rule>,
2195 lctx: &LowerCtx,
2196) -> ParseResult<StepKind> {
2197 let span = refine_span(ctx, &pair);
2198 let mut var = None;
2199 let mut decl_type: Option<String> = None;
2200 let mut body = None;
2201 for inner in pair.into_inner() {
2202 match inner.as_rule() {
2203 Rule::dollar_ident => {
2204 var = Some(parse_dollar_ident(inner));
2205 }
2206 Rule::type_tag => {
2207 decl_type = Some(parse_type_tag(inner));
2208 }
2209 Rule::block => {
2210 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
2211 }
2212 Rule::command_inner => {
2213 let inner = inner.into_inner().next().ok_or_else(|| {
2216 ParseError::structural("let", "empty command_inner".to_string(), &span)
2217 })?;
2218 let step_kind = parse_structural_command_with_lower(ctx, inner, lctx)?;
2219 body = Some(vec![Step {
2220 guard: None,
2221 kind: step_kind,
2222 scope_enter: 0,
2223 scope_exit: 0,
2224 }]);
2225 }
2226 Rule::with_io_command => {
2227 let kind = parse_structural_command_with_lower(ctx, inner, lctx)?;
2237 let StepKind::WithIo { bindings, cmd } = kind else {
2238 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));
2239 };
2240 match *cmd {
2241 StepKind::AsyncBlock { body: async_body } => {
2242 if async_body.len() != 1 {
2243 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));
2244 }
2245 let step = async_body.into_iter().next().ok_or_else(|| {
2246 ParseError::validation(
2247 "LET",
2248 "LET $var: HANDLE = ASYNC requires a body".to_string(),
2249 &span,
2250 )
2251 })?;
2252 body = Some(vec![Step {
2253 guard: step.guard,
2254 kind: StepKind::WithIo {
2255 bindings,
2256 cmd: Box::new(step.kind),
2257 },
2258 scope_enter: step.scope_enter,
2259 scope_exit: step.scope_exit,
2260 }]);
2261 }
2262 sync_cmd => {
2263 if has_stdout_pipe(&bindings) {
2264 return Err(ParseError::structural("let", "LET capture cannot use WITH_IO [stdout=$var]; the capture sink owns stdout".to_string(), &span));
2265 }
2266 reject_async_in_capture(ctx, &sync_cmd)?;
2267 let name = var.clone().ok_or_else(|| {
2268 ParseError::validation(
2269 "LET",
2270 "LET $var: TYPE = WITH_IO requires a variable".to_string(),
2271 &span,
2272 )
2273 })?;
2274 let dtype = decl_type.ok_or_else(|| {
2275 ParseError::validation(
2276 "LET",
2277 "LET requires explicit type: LET $var: TYPE = ...".to_string(),
2278 &span,
2279 )
2280 })?;
2281 return Ok(StepKind::AssignCapture {
2282 var: name,
2283 decl_type: dtype,
2284 cmd: Box::new(StepKind::WithIo {
2285 bindings,
2286 cmd: Box::new(sync_cmd),
2287 }),
2288 });
2289 }
2290 }
2291 }
2292 _ => {}
2293 }
2294 }
2295 Ok(StepKind::AssignAsync {
2296 var: var.ok_or_else(|| {
2297 ParseError::validation(
2298 "LET",
2299 "LET $var: HANDLE = ASYNC requires a variable".to_string(),
2300 &span,
2301 )
2302 })?,
2303 decl_type: decl_type.ok_or_else(|| {
2304 ParseError::validation(
2305 "LET",
2306 "LET requires explicit type: LET $var: TYPE = ...".to_string(),
2307 &span,
2308 )
2309 })?,
2310 body: body.ok_or_else(|| {
2311 ParseError::validation(
2312 "LET",
2313 "LET $var: HANDLE = ASYNC requires a body".to_string(),
2314 &span,
2315 )
2316 })?,
2317 })
2318}
2319
2320fn parse_let_capture_statement_from_pair(
2328 ctx: &SpanContext,
2329 pair: Pair<Rule>,
2330 lctx: &LowerCtx,
2331) -> ParseResult<StepKind> {
2332 let span = refine_span(ctx, &pair);
2333 use pest::Parser;
2334 let mut var = None;
2335 let mut decl_type: Option<String> = None;
2336 let mut await_pair = None;
2337 let mut timeout_pair = None;
2338 let mut run_exec_pair = None;
2339 let mut instruction_pair = None;
2340 for inner in pair.into_inner() {
2341 match inner.as_rule() {
2342 Rule::dollar_ident => {
2343 var = Some(parse_dollar_ident(inner));
2344 }
2345 Rule::type_tag => {
2346 decl_type = Some(parse_type_tag(inner));
2347 }
2348 Rule::await_statement => {
2349 await_pair = Some(inner);
2350 }
2351 Rule::timeout_statement => {
2352 timeout_pair = Some(inner);
2353 }
2354 Rule::run_exec_statement => {
2355 run_exec_pair = Some(inner);
2356 }
2357 Rule::instruction => {
2358 instruction_pair = Some(inner);
2359 }
2360 _ => {}
2361 }
2362 }
2363 let var = var.ok_or_else(|| {
2364 ParseError::validation("LET", "LET requires a variable".to_string(), &span)
2365 })?;
2366 let dtype: String = decl_type.ok_or_else(|| {
2367 ParseError::validation(
2368 "LET",
2369 "LET requires explicit type: LET $var: TYPE = ...".to_string(),
2370 &span,
2371 )
2372 })?;
2373 if let Some(awaited) = await_pair {
2374 let mut task_var = None;
2375 for inner in awaited.into_inner() {
2376 if inner.as_rule() == Rule::ident {
2377 task_var = Some(inner.as_str().to_string());
2378 }
2379 }
2380 return Ok(StepKind::AwaitCapture {
2381 out_var: var,
2382 out_type: dtype,
2383 task_var: task_var.ok_or_else(|| {
2384 ParseError::validation(
2385 "LET",
2386 "LET $out = AWAIT requires a task variable".to_string(),
2387 &span,
2388 )
2389 })?,
2390 });
2391 }
2392 if let Some(timeouted) = timeout_pair {
2393 let kind = parse_structural_command_with_lower(ctx, timeouted, lctx)?;
2394 reject_async_in_capture(ctx, &kind)?;
2395 reject_pipe_stdout_in_capture(ctx, &kind)?;
2396 return Ok(StepKind::AssignCapture {
2397 var,
2398 decl_type: dtype,
2399 cmd: Box::new(kind),
2400 });
2401 }
2402 if let Some(exec) = run_exec_pair {
2403 let kind = lower_run_exec_pair(ctx, exec, lctx)?;
2404 reject_async_in_capture(ctx, &kind)?;
2405 reject_pipe_stdout_in_capture(ctx, &kind)?;
2406 return Ok(StepKind::AssignCapture {
2407 var,
2408 decl_type: dtype,
2409 cmd: Box::new(kind),
2410 });
2411 }
2412 if let Some(ins) = instruction_pair {
2413 let text = ins.as_str().to_string();
2414 let mut lead = None;
2415 for token in ins.into_inner() {
2416 if token.as_rule() == Rule::command_name {
2417 lead = Some(token.as_str().to_string());
2418 break;
2419 }
2420 }
2421 let lead = lead.ok_or_else(|| {
2422 ParseError::validation("LET", "LET capture requires a command".to_string(), &span)
2423 })?;
2424 if crate::commands::is_known_command(&lead) {
2425 let kind = lower_instruction_pair(
2426 ctx,
2427 lexer::LanguageParser::parse(Rule::instruction, &text)
2428 .map_err(parse_pest_error)?
2429 .next()
2430 .ok_or_else(|| {
2431 ParseError::validation(
2432 "LET",
2433 "LET capture requires a command".to_string(),
2434 &span,
2435 )
2436 })?,
2437 lctx,
2438 )?;
2439 reject_async_in_capture(ctx, &kind)?;
2440 reject_pipe_stdout_in_capture(ctx, &kind)?;
2441 return Ok(StepKind::AssignCapture {
2442 var,
2443 decl_type: dtype,
2444 cmd: Box::new(kind),
2445 });
2446 }
2447 let expr = parse_expr_str(&span, lctx, &text)?;
2448 return Ok(StepKind::Assign {
2449 var,
2450 decl_type: dtype,
2451 expr,
2452 });
2453 }
2454 Err(ParseError::structural(
2455 "let",
2456 "LET requires a value".to_string(),
2457 &span,
2458 ))
2459}
2460
2461fn parse_await_statement_from_pair(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<StepKind> {
2462 let span = refine_span(ctx, &pair);
2463 let mut var = None;
2464 for inner in pair.into_inner() {
2465 if inner.as_rule() == Rule::ident {
2466 var = Some(inner.as_str().to_string());
2467 }
2468 }
2469 Ok(StepKind::Await {
2470 var: var.ok_or_else(|| {
2471 ParseError::validation("AWAIT", "AWAIT requires a variable".to_string(), &span)
2472 })?,
2473 })
2474}
2475
2476fn parse_cancel_statement_from_pair(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<StepKind> {
2477 let span = refine_span(ctx, &pair);
2478 let mut var = None;
2479 for inner in pair.into_inner() {
2480 if inner.as_rule() == Rule::ident {
2481 var = Some(inner.as_str().to_string());
2482 }
2483 }
2484 Ok(StepKind::Cancel {
2485 var: var.ok_or_else(|| {
2486 ParseError::validation("CANCEL", "CANCEL requires a variable".to_string(), &span)
2487 })?,
2488 })
2489}
2490
2491fn parse_timeout_duration_arg(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Arg> {
2495 let span = refine_span(ctx, &pair);
2496 for inner in pair.into_inner() {
2497 let arg = match inner.as_rule() {
2498 Rule::timeout_literal => Arg::String(inner.as_str().to_string(), false),
2499 Rule::dollar_ident => Arg::Expr(Expr::Var(parse_dollar_ident(inner))),
2500 Rule::quoted_string => Arg::String(
2501 crate::command::strip_surrounding_quotes(inner.as_str()).to_string(),
2502 true,
2503 ),
2504 Rule::templated_arg => Arg::String(inner.as_str().to_string(), false),
2505 _ => continue,
2506 };
2507 ArgType::Duration
2508 .check_arg(&arg)
2509 .map_err(|e| ParseError::validation("TIMEOUT", e.to_string(), &span))?;
2510 return Ok(arg);
2511 }
2512 Err(ParseError::validation(
2513 "TIMEOUT",
2514 "TIMEOUT requires a duration".to_string(),
2515 &span,
2516 ))
2517}
2518
2519fn parse_timeout_statement_from_pair(
2520 ctx: &SpanContext,
2521 pair: Pair<Rule>,
2522 lctx: &LowerCtx,
2523) -> ParseResult<StepKind> {
2524 let span = refine_span(ctx, &pair);
2525 let mut duration: Option<Arg> = None;
2526 let mut body: Option<Vec<Step>> = None;
2527 for inner in pair.into_inner() {
2528 match inner.as_rule() {
2529 Rule::timeout_duration => {
2530 duration = Some(parse_timeout_duration_arg(ctx, inner)?);
2531 }
2532 Rule::block => {
2533 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
2534 }
2535 Rule::await_statement => {
2536 let kind = parse_await_statement_from_pair(ctx, inner)?;
2537 body = Some(vec![Step {
2538 guard: None,
2539 kind,
2540 scope_enter: 0,
2541 scope_exit: 0,
2542 }]);
2543 }
2544 Rule::cancel_statement => {
2545 let kind = parse_cancel_statement_from_pair(ctx, inner)?;
2546 body = Some(vec![Step {
2547 guard: None,
2548 kind,
2549 scope_enter: 0,
2550 scope_exit: 0,
2551 }]);
2552 }
2553 Rule::with_io_command
2554 | Rule::inherit_env_command
2555 | Rule::async_statement
2556 | Rule::async_statement_block
2557 | Rule::call_statement
2558 | Rule::while_statement
2559 | Rule::func_def
2560 | Rule::return_statement
2561 | Rule::break_statement
2562 | Rule::continue_statement
2563 | Rule::timeout_statement
2564 | Rule::remote_statement => {
2565 let kind = parse_structural_command_with_lower(ctx, inner, lctx)?;
2566 body = Some(vec![Step {
2567 guard: None,
2568 kind,
2569 scope_enter: 0,
2570 scope_exit: 0,
2571 }]);
2572 }
2573 Rule::instruction | Rule::instruction_inner => {
2574 let kind = lower_instruction_pair(ctx, inner, lctx)?;
2575 body = Some(vec![Step {
2576 guard: None,
2577 kind,
2578 scope_enter: 0,
2579 scope_exit: 0,
2580 }]);
2581 }
2582 Rule::run_exec_statement | Rule::run_exec_inner => {
2583 let kind = lower_run_exec_pair(ctx, inner, lctx)?;
2584 body = Some(vec![Step {
2585 guard: None,
2586 kind,
2587 scope_enter: 0,
2588 scope_exit: 0,
2589 }]);
2590 }
2591 _ => {}
2592 }
2593 }
2594 Ok(StepKind::Timeout {
2595 duration: duration.ok_or_else(|| {
2596 ParseError::validation("TIMEOUT", "TIMEOUT requires a duration".to_string(), &span)
2597 })?,
2598 body: body.ok_or_else(|| {
2599 ParseError::validation(
2600 "TIMEOUT",
2601 "TIMEOUT requires a command or block".to_string(),
2602 &span,
2603 )
2604 })?,
2605 })
2606}
2607
2608fn contains_remote_block(steps: &[Step]) -> bool {
2612 steps.iter().any(|step| contains_remote_kind(&step.kind))
2613}
2614
2615fn contains_remote_kind(kind: &StepKind) -> bool {
2616 if matches!(kind, StepKind::RemoteBlock { .. }) {
2617 return true;
2618 }
2619 let mut found = false;
2620 kind.walk_child_kinds(&mut |child| {
2621 found = found || contains_remote_kind(child);
2622 });
2623 found
2624}
2625
2626fn collect_remote_body_symbols(
2632 steps: &[Step],
2633 defs: &mut HashSet<String>,
2634 calls: &mut Vec<String>,
2635) {
2636 for step in steps {
2637 collect_remote_kind_symbols(&step.kind, defs, calls);
2638 }
2639}
2640
2641fn collect_remote_kind_symbols(
2642 kind: &StepKind,
2643 defs: &mut HashSet<String>,
2644 calls: &mut Vec<String>,
2645) {
2646 if let StepKind::FuncDef { name, .. } = kind {
2647 defs.insert(name.clone());
2648 }
2649 if let StepKind::Call { name, .. } = kind {
2650 calls.push(name.clone());
2651 }
2652 kind.walk_step_exprs(&mut |expr| match expr {
2653 Expr::Call { name, .. } => calls.push(name.clone()),
2654 Expr::CompiledMath(ops) => {
2655 for op in ops {
2656 if let MathOp::Call { name, .. } = op {
2657 calls.push(name.clone());
2658 }
2659 }
2660 }
2661 Expr::Block(steps) => collect_remote_body_symbols(steps, defs, calls),
2662 _ => {}
2663 });
2664}
2665
2666fn parse_remote_statement_from_pair(
2667 ctx: &SpanContext,
2668 pair: Pair<Rule>,
2669 lctx: &LowerCtx,
2670) -> ParseResult<StepKind> {
2671 let span = refine_span(ctx, &pair);
2672 let mut target: Option<String> = None;
2673 let mut vars: Vec<String> = Vec::new();
2674 let mut env: Vec<String> = Vec::new();
2675 let mut body: Option<Vec<Step>> = None;
2676 for inner in pair.into_inner() {
2677 match inner.as_rule() {
2678 Rule::remote_target => {
2679 target = Some(inner.as_str().to_string());
2680 }
2681 Rule::remote_bindings => {
2682 for binding in inner.into_inner() {
2683 let entry = if binding.as_rule() == Rule::remote_binding {
2686 binding.into_inner().next()
2687 } else {
2688 Some(binding)
2689 };
2690 let Some(entry) = entry else {
2691 continue;
2692 };
2693 match entry.as_rule() {
2694 Rule::remote_var_binding => {
2695 let mut name = None;
2696 for part in entry.into_inner() {
2697 if part.as_rule() == Rule::ident {
2698 name = Some(part.as_str().to_string());
2699 }
2700 }
2701 vars.push(name.ok_or_else(|| {
2702 ParseError::validation(
2703 "REMOTE",
2704 "REMOTE header entries must be `$var` or `env:NAME`"
2705 .to_string(),
2706 &span,
2707 )
2708 })?);
2709 }
2710 Rule::remote_env_binding => {
2711 let mut name = None;
2712 for part in entry.into_inner() {
2713 if part.as_rule() == Rule::env_read_key {
2714 name = Some(part.as_str().to_string());
2715 }
2716 }
2717 env.push(name.ok_or_else(|| {
2718 ParseError::validation(
2719 "REMOTE",
2720 "REMOTE header entries must be `$var` or `env:NAME`"
2721 .to_string(),
2722 &span,
2723 )
2724 })?);
2725 }
2726 _ => {}
2727 }
2728 }
2729 }
2730 Rule::block => {
2731 body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
2732 }
2733 _ => {}
2734 }
2735 }
2736 let target = target.ok_or_else(|| {
2737 ParseError::validation("REMOTE", "REMOTE requires a target".to_string(), &span)
2738 })?;
2739 if !crate::commands::is_valid_remote_target(&target) {
2740 return Err(ParseError::validation(
2741 "REMOTE",
2742 format!(
2743 "invalid REMOTE target `{target}`: start alphanumeric, then alphanumeric, `_`, or `-`, max 64 chars, never SNAPSHOT, LOCAL, CACHE, SYSTEM, or REMOTE"
2744 ),
2745 &span,
2746 ));
2747 }
2748 let body = body.ok_or_else(|| {
2749 ParseError::validation(
2750 "REMOTE",
2751 "REMOTE requires a braced block, e.g. `REMOTE prod [$version] { ... }`".to_string(),
2752 &span,
2753 )
2754 })?;
2755 if body.is_empty() {
2756 return Err(ParseError::validation(
2757 "REMOTE",
2758 "REMOTE requires a non-empty braced block".to_string(),
2759 &span,
2760 ));
2761 }
2762 if contains_remote_block(&body) {
2763 return Err(ParseError::validation(
2764 "REMOTE",
2765 format!("REMOTE blocks cannot nest (target `{target}` contains another REMOTE block)"),
2766 &span,
2767 ));
2768 }
2769 if body.iter().any(|s| contains_inherit_env(&s.kind)) {
2770 return Err(ParseError::validation(
2771 "REMOTE",
2772 "INHERIT_ENV cannot appear inside a REMOTE block: the guest inherits its own process environment".to_string(),
2773 &span,
2774 ));
2775 }
2776 let mut inner_defs = HashSet::new();
2777 let mut calls = Vec::new();
2778 collect_remote_body_symbols(&body, &mut inner_defs, &mut calls);
2779 let outer_defs = lctx.visible_func_names();
2780 for call in &calls {
2781 let bare = crate::base_name(call);
2782 let is_script_call = match crate::split_qualified(call) {
2783 Some((module, _)) => module == crate::constants::SCRIPT_MODULE_NAME,
2784 None => true,
2785 };
2786 if is_script_call && outer_defs.contains(bare) && !inner_defs.contains(bare) {
2787 return Err(ParseError::validation(
2788 "REMOTE",
2789 format!(
2790 "REMOTE '{target}' calls outer function '{bare}' (functions must be defined inside the block)"
2791 ),
2792 &span,
2793 ));
2794 }
2795 }
2796 Ok(StepKind::RemoteBlock {
2797 target,
2798 vars,
2799 env,
2800 body,
2801 })
2802}
2803
2804fn parse_if_statement_from_pair(
2805 ctx: &SpanContext,
2806 pair: Pair<Rule>,
2807 lctx: &LowerCtx,
2808) -> ParseResult<StepKind> {
2809 let span = refine_span(ctx, &pair);
2810 let mut cond = None;
2811 let mut then_body = Vec::new();
2812 let mut else_ifs = Vec::new();
2813 let mut else_body = None;
2814
2815 for inner in pair.into_inner() {
2816 match inner.as_rule() {
2817 Rule::expr => {
2818 if cond.is_none() {
2819 cond = Some(parse_expr(ctx, lctx, inner)?);
2820 }
2821 }
2822 Rule::block => {
2823 if then_body.is_empty() {
2824 then_body = parse_block_elements_with_lower(ctx, inner, lctx)?;
2825 }
2826 }
2827 Rule::else_if_clause => {
2828 let (eif_cond, eif_body) = parse_else_if_clause(ctx, inner, lctx)?;
2829 else_ifs.push((eif_cond, eif_body));
2830 }
2831 Rule::else_clause => {
2832 else_body = Some(parse_else_clause(ctx, inner, lctx)?);
2833 }
2834 _ => {}
2835 }
2836 }
2837 Ok(StepKind::If {
2838 cond: Box::new(cond.ok_or_else(|| {
2839 ParseError::structural("if", "IF requires a condition".to_string(), &span)
2840 })?),
2841 then_body,
2842 else_ifs,
2843 else_body,
2844 })
2845}
2846
2847fn parse_else_if_clause(
2848 ctx: &SpanContext,
2849 pair: Pair<Rule>,
2850 lctx: &LowerCtx,
2851) -> ParseResult<(Box<Expr>, Vec<Step>)> {
2852 let span = refine_span(ctx, &pair);
2853 let mut cond = None;
2854 let mut body = Vec::new();
2855 for inner in pair.into_inner() {
2856 match inner.as_rule() {
2857 Rule::expr => cond = Some(parse_expr(ctx, lctx, inner)?),
2858 Rule::block => body = parse_block_elements_with_lower(ctx, inner, lctx)?,
2859 _ => {}
2860 }
2861 }
2862 Ok((
2863 Box::new(cond.ok_or_else(|| {
2864 ParseError::structural("if", "ELSE IF requires a condition".to_string(), &span)
2865 })?),
2866 body,
2867 ))
2868}
2869
2870fn parse_else_clause(
2871 ctx: &SpanContext,
2872 pair: Pair<Rule>,
2873 lctx: &LowerCtx,
2874) -> ParseResult<Vec<Step>> {
2875 for inner in pair.into_inner() {
2876 if let Rule::block = inner.as_rule() {
2877 return parse_block_elements_with_lower(ctx, inner, lctx);
2878 }
2879 }
2880 Ok(Vec::new())
2881}
2882
2883fn parse_async_statement_from_pair(
2884 ctx: &SpanContext,
2885 pair: Pair<Rule>,
2886 lctx: &LowerCtx,
2887) -> ParseResult<StepKind> {
2888 let span = refine_span(ctx, &pair);
2889 let mut inner_cmd = None;
2890 let mut block_body = None;
2891 for inner in pair.into_inner() {
2892 match inner.as_rule() {
2893 Rule::command => {
2894 let cmd_text = inner.as_str();
2899 let (preseed_funcs, preseed_imports) = lctx.visible_snapshot();
2900 let steps = parse_script_with_preseed(
2901 cmd_text,
2902 |name, args| (lctx.lower)(name, args),
2903 lctx.reserved_names.clone(),
2904 lctx.modules.clone(),
2905 preseed_funcs,
2906 preseed_imports,
2907 )?;
2908 if steps.len() == 1 {
2909 inner_cmd = Some(steps.into_iter().next().unwrap().kind);
2910 } else {
2911 return Err(ParseError::structural(
2912 "async",
2913 "unexpected multiple steps in async inner command".to_string(),
2914 &span,
2915 ));
2916 }
2917 }
2918 Rule::command_inner => {
2919 let child = inner.into_inner().next().ok_or_else(|| {
2921 ParseError::structural("async", "empty command_inner".to_string(), &span)
2922 })?;
2923 match child.as_rule() {
2924 Rule::inherit_env_command => {
2925 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2926 }
2927 Rule::async_statement | Rule::async_statement_block => {
2928 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2929 }
2930 Rule::timeout_statement | Rule::cancel_statement => {
2931 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2932 }
2933 Rule::remote_statement => {
2934 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2935 }
2936 Rule::call_statement | Rule::while_statement => {
2937 inner_cmd = Some(parse_structural_command_with_lower(ctx, child, lctx)?);
2938 }
2939 Rule::func_def
2940 | Rule::return_statement
2941 | Rule::break_statement
2942 | Rule::continue_statement => {
2943 return Err(ParseError::structural(
2944 "async",
2945 format!(
2946 "{:?} cannot run as a lone ASYNC command; use ASYNC {{ ... }} block form if needed",
2947 child.as_rule()
2948 ),
2949 &span,
2950 ));
2951 }
2952 Rule::instruction => {
2953 inner_cmd = Some(lower_instruction_pair(ctx, child, lctx)?);
2954 }
2955 Rule::run_exec_statement | Rule::run_exec_inner => {
2956 inner_cmd = Some(lower_run_exec_pair(ctx, child, lctx)?);
2957 }
2958 other => {
2959 return Err(ParseError::structural(
2960 "async",
2961 format!("unexpected command_inner child: {:?}", other),
2962 &span,
2963 ));
2964 }
2965 }
2966 }
2967 Rule::instruction | Rule::instruction_inner => {
2968 inner_cmd = Some(lower_instruction_pair(ctx, inner, lctx)?);
2969 }
2970 Rule::run_exec_statement | Rule::run_exec_inner => {
2971 inner_cmd = Some(lower_run_exec_pair(ctx, inner, lctx)?);
2972 }
2973 Rule::block => {
2974 block_body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
2975 }
2976 _ => {}
2977 }
2978 }
2979 if let Some(body) = block_body {
2980 for step in &body {
2981 if matches!(&step.kind, StepKind::WithIo { .. }) {
2982 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));
2983 }
2984 }
2985 Ok(StepKind::AsyncBlock { body })
2986 } else if let Some(cmd) = inner_cmd {
2987 if matches!(&cmd, StepKind::WithIo { .. }) {
2988 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));
2989 }
2990 Ok(StepKind::AsyncBlock {
2991 body: vec![Step {
2992 guard: None,
2993 kind: cmd,
2994 scope_enter: 0,
2995 scope_exit: 0,
2996 }],
2997 })
2998 } else {
2999 Err(ParseError::structural(
3000 "async",
3001 "ASYNC requires either a command or a block".to_string(),
3002 &span,
3003 ))
3004 }
3005}
3006
3007fn parse_async_statement_block_from_pair(
3008 ctx: &SpanContext,
3009 pair: Pair<Rule>,
3010 lctx: &LowerCtx,
3011) -> ParseResult<StepKind> {
3012 let span = refine_span(ctx, &pair);
3013 let mut block_body = None;
3014 for inner in pair.into_inner() {
3015 if inner.as_rule() == Rule::block {
3016 block_body = Some(parse_block_elements_with_lower(ctx, inner, lctx)?);
3017 }
3018 }
3019 let body = block_body.ok_or_else(|| {
3020 ParseError::structural(
3021 "async",
3022 "async_statement_block requires a block".to_string(),
3023 &span,
3024 )
3025 })?;
3026 for step in &body {
3027 if matches!(&step.kind, StepKind::WithIo { .. }) {
3028 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));
3029 }
3030 }
3031 Ok(StepKind::AsyncBlock { body })
3032}
3033
3034fn lower_import_statement(ctx: &SpanContext, pair: Pair<Rule>, lctx: &LowerCtx) -> ParseResult<()> {
3036 let mut modules = Vec::new();
3037 for inner in pair.into_inner() {
3038 match inner.as_rule() {
3039 Rule::import_list => {
3040 for module in inner.into_inner() {
3041 if module.as_rule() == Rule::import_module {
3042 modules.push(module.as_str().to_string());
3043 }
3044 }
3045 }
3046 Rule::import_module => modules.push(inner.as_str().to_string()),
3047 _ => {}
3048 }
3049 }
3050 lctx.import_modules(ctx, &modules)
3051}
3052
3053fn parse_block_elements_with_lower(
3054 ctx: &SpanContext,
3055 block_pair: Pair<Rule>,
3056 lctx: &LowerCtx,
3057) -> ParseResult<Vec<Step>> {
3058 lctx.enter_scope();
3062 let mut steps = Vec::new();
3063 let mut pending_guard: Option<GuardExpr> = None;
3068 for elem in block_pair.into_inner() {
3069 match elem.as_rule() {
3070 Rule::guard_line => {
3071 let guard = parse_guard_line(ctx, elem)?;
3072 pending_guard = Some(match pending_guard.take() {
3073 Some(existing) => GuardExpr::all(vec![existing, guard]),
3074 None => guard,
3075 });
3076 continue;
3077 }
3078 Rule::for_statement
3079 | Rule::while_statement
3080 | Rule::func_def
3081 | Rule::call_statement
3082 | Rule::return_statement
3083 | Rule::break_statement
3084 | Rule::continue_statement
3085 | Rule::let_statement
3086 | Rule::mutate_statement
3087 | Rule::let_async_statement
3088 | Rule::let_capture_statement
3089 | Rule::await_statement
3090 | Rule::cancel_statement
3091 | Rule::if_statement
3092 | Rule::async_statement
3093 | Rule::timeout_statement
3094 | Rule::remote_statement
3095 | Rule::async_statement_block => {
3096 let step_kind = parse_structural_command_with_lower(ctx, elem, lctx)?;
3097 steps.push(Step {
3098 guard: pending_guard.take(),
3099 kind: step_kind,
3100 scope_enter: 0,
3101 scope_exit: 0,
3102 });
3103 }
3104 Rule::type_statement => {
3105 return Err(ParseError::structural(
3106 KEYWORD_TYPE,
3107 "TYPE must appear at top level".to_string(),
3108 ctx,
3109 ));
3110 }
3111 Rule::guard_block => {
3112 let mut guard_pair = None;
3113 let mut inner_block = None;
3114 for inner in elem.into_inner() {
3115 match inner.as_rule() {
3116 Rule::guard_line => guard_pair = Some(inner),
3117 Rule::block => inner_block = Some(inner),
3118 _ => {}
3119 }
3120 }
3121 if let (Some(gp), Some(bp)) = (guard_pair, inner_block) {
3122 let guard_expr = parse_guard_line(ctx, gp)?;
3123 let mut inner_steps = parse_block_elements_with_lower(ctx, bp, lctx)?;
3124 for step in &mut inner_steps {
3125 let inner = step.guard.take();
3126 step.guard = Some(match (pending_guard.clone(), inner) {
3127 (Some(pending), Some(existing)) => {
3128 GuardExpr::all(vec![pending, existing])
3129 }
3130 (Some(pending), None) => {
3131 GuardExpr::all(vec![pending, guard_expr.clone()])
3132 }
3133 (None, Some(existing)) => existing,
3134 (None, None) => guard_expr.clone(),
3135 });
3136 }
3137 pending_guard = None;
3140 steps.extend(inner_steps);
3141 }
3142 }
3143 Rule::instruction | Rule::instruction_inner => {
3144 let kind = lower_instruction_pair(ctx, elem, lctx)?;
3145 steps.push(Step {
3146 guard: pending_guard.take(),
3147 kind,
3148 scope_enter: 0,
3149 scope_exit: 0,
3150 });
3151 }
3152 Rule::run_exec_statement | Rule::run_exec_inner => {
3153 let kind = lower_run_exec_pair(ctx, elem, lctx)?;
3154 steps.push(Step {
3155 guard: pending_guard.take(),
3156 kind,
3157 scope_enter: 0,
3158 scope_exit: 0,
3159 });
3160 }
3161 Rule::with_io_command => {
3162 let step_kind = parse_structural_command_with_lower(ctx, elem, lctx)?;
3163 steps.push(Step {
3164 guard: pending_guard.take(),
3165 kind: step_kind,
3166 scope_enter: 0,
3167 scope_exit: 0,
3168 });
3169 }
3170 Rule::inherit_env_command => {
3171 let step_kind = parse_structural_command_with_lower(ctx, elem, lctx)?;
3172 steps.push(Step {
3173 guard: pending_guard.take(),
3174 kind: step_kind,
3175 scope_enter: 0,
3176 scope_exit: 0,
3177 });
3178 }
3179 Rule::import_statement => {
3180 lower_import_statement(ctx, elem, lctx)?;
3184 }
3185 Rule::export_statement => {
3186 return Err(ParseError::validation(
3187 KEYWORD_EXPORT,
3188 "`EXPORT` is reserved for future script-module support and cannot be used yet."
3189 .to_string(),
3190 ctx,
3191 ));
3192 }
3193 _ => {} }
3195 }
3196 if pending_guard.is_some() {
3197 return Err(ParseError::structural(
3198 "guard",
3199 "guard declared immediately before '}' without a command".to_string(),
3200 ctx,
3201 ));
3202 }
3203 lctx.exit_scope();
3204 Ok(steps)
3205}
3206
3207fn parse_argument(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Vec<Arg>> {
3208 let inners: Vec<_> = pair.into_inner().collect();
3209 let mut groups: Vec<Vec<Pair<Rule>>> = vec![Vec::new()];
3215 for fragment in inners {
3216 let glued = fragment.as_rule() == Rule::expr
3217 && fragment.as_str().ends_with(|c: char| c.is_whitespace());
3218 groups
3219 .last_mut()
3220 .expect("argument always holds a group")
3221 .push(fragment);
3222 if glued {
3223 groups.push(Vec::new());
3224 }
3225 }
3226 let mut args = Vec::new();
3227 for group in groups {
3228 if group.is_empty() {
3229 continue;
3230 }
3231 if group.len() == 1 && group[0].as_rule() == Rule::expr {
3233 args.push(Arg::Expr(parse_expr(
3234 ctx,
3235 lctx,
3236 group.into_iter().next().expect("group holds one pair"),
3237 )?));
3238 continue;
3239 }
3240 if group.len() == 1 && group[0].as_rule() == Rule::string_literal {
3242 args.push(Arg::String(parse_fragments(&group)?, true));
3243 continue;
3244 }
3245 args.push(Arg::String(parse_fragments(&group)?, false));
3246 }
3247 Ok(args)
3248}
3249
3250fn parse_quoted_string(pair: Pair<Rule>) -> ParseResult<String> {
3251 let s = pair.as_str();
3252 let content = &s[1..s.len() - 1];
3253 Ok(content.to_string())
3255}
3256
3257fn parse_fragments(parts: &[Pair<Rule>]) -> ParseResult<String> {
3261 if parts.len() == 1 && parts[0].as_rule() == Rule::string_literal {
3263 let s = parts[0].as_str();
3264 return Ok(s[1..s.len() - 1].to_string());
3265 }
3266
3267 let mut body = String::new();
3268 let mut last_end = None;
3269 for part in parts {
3270 let span = part.as_span();
3271 if let Some(end) = last_end
3272 && span.start() > end
3273 {
3274 body.push(' ');
3275 }
3276 match part.as_rule() {
3277 Rule::string_literal => {
3278 let s = part.as_str();
3279 let unquoted = &s[1..s.len() - 1];
3280 body.push_str(unquoted);
3281 }
3282 Rule::templated_arg | Rule::unquoted_arg => {
3283 body.push_str(part.as_str());
3284 }
3285 Rule::expr => body.push_str(part.as_str()),
3286 _ => {}
3287 }
3288 last_end = Some(span.end());
3289 }
3290 Ok(body)
3291}
3292
3293fn parse_guard_line(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3294 let span = refine_span(ctx, &pair);
3295 for inner in pair.into_inner() {
3296 if inner.as_rule() == Rule::guard_expr {
3297 return parse_guard_expr(ctx, inner);
3298 }
3299 }
3300 Err(ParseError::structural(
3301 "guard",
3302 "guard line missing expression".to_string(),
3303 &span,
3304 ))
3305}
3306
3307fn parse_io_binding(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<IoBinding> {
3308 let span = refine_span(ctx, &pair);
3309 let mut stream = None;
3310 let mut pipe = None;
3311 for inner in pair.into_inner() {
3312 match inner.as_rule() {
3313 Rule::io_stream => stream = Some(parse_io_stream(inner.as_str())),
3314 Rule::pipe_binding => pipe = Some(parse_pipe_binding(ctx, inner)?),
3315 _ => {}
3316 }
3317 }
3318 let stream = stream.ok_or_else(|| {
3319 ParseError::structural("with_io", "missing IO stream in WITH_IO".to_string(), &span)
3320 })?;
3321 Ok(IoBinding { stream, pipe })
3322}
3323
3324fn parse_io_stream(text: &str) -> IoStream {
3325 match text {
3326 "stdin" => IoStream::Stdin,
3327 "stdout" => IoStream::Stdout,
3328 "stderr" => IoStream::Stderr,
3329 _ => unreachable!("parser produced invalid io_stream token"),
3330 }
3331}
3332
3333fn parse_pipe_binding(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<PipeTarget> {
3334 let span = refine_span(ctx, &pair);
3335 for inner in pair.into_inner() {
3336 if inner.as_rule() == Rule::dollar_ident {
3337 return Ok(PipeTarget::Var(parse_dollar_ident(inner)));
3338 }
3339 }
3340 Err(ParseError::structural(
3341 "with_io",
3342 "missing pipe identifier in WITH_IO binding".to_string(),
3343 &span,
3344 ))
3345}
3346
3347fn parse_guard_expr(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3348 let span = refine_span(ctx, &pair);
3349 match pair.as_rule() {
3350 Rule::guard_expr => {
3351 let next = pair.into_inner().next().ok_or_else(|| {
3352 ParseError::structural("guard", "guard expression missing body".to_string(), &span)
3353 })?;
3354 parse_guard_expr(ctx, next)
3355 }
3356 Rule::guard_seq => parse_guard_seq(ctx, pair),
3357 Rule::guard_factor => parse_guard_factor(ctx, pair),
3358 Rule::guard_not => {
3359 Err(ParseError::structural(
3361 "guard",
3362 "guard_not should not create a pair".to_string(),
3363 &span,
3364 ))
3365 }
3366 Rule::guard_primary => parse_guard_primary(ctx, pair),
3367 Rule::guard_group => parse_guard_group(ctx, pair),
3368 Rule::guard_any_call => parse_guard_any_call(ctx, pair),
3369 Rule::guard_all_call => parse_guard_all_call(ctx, pair),
3370 Rule::not_call => parse_not_call(ctx, pair),
3371 Rule::guard_term => parse_guard_term(ctx, pair),
3372 _ => Err(ParseError::structural(
3373 "guard",
3374 format!("unexpected guard expression rule: {:?}", pair.as_rule()),
3375 &span,
3376 )),
3377 }
3378}
3379
3380fn parse_guard_seq(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3381 let span = refine_span(ctx, &pair);
3382 let mut exprs = Vec::new();
3383 for inner in pair.into_inner() {
3384 if inner.as_rule() == Rule::guard_factor {
3385 exprs.push(parse_guard_factor(ctx, inner)?);
3386 }
3387 }
3388 match exprs.len() {
3389 0 => Err(ParseError::structural(
3390 "guard",
3391 "guard list requires at least one entry".to_string(),
3392 &span,
3393 )),
3394 1 => Ok(exprs.pop().unwrap()),
3395 _ => Ok(GuardExpr::all(exprs)),
3396 }
3397}
3398
3399fn parse_guard_factor(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3400 let span = refine_span(ctx, &pair);
3401 let inner = pair.into_inner().next().ok_or_else(|| {
3402 ParseError::structural(
3403 "guard",
3404 "guard factor missing expression".to_string(),
3405 &span,
3406 )
3407 })?;
3408 parse_guard_expr(ctx, inner)
3409}
3410
3411fn parse_not_call(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3412 let span = refine_span(ctx, &pair);
3413 for inner in pair.into_inner() {
3414 if inner.as_rule() == Rule::guard_expr {
3415 return parse_guard_expr(ctx, inner).map(|e| GuardExpr::Not(Box::new(e)));
3416 }
3417 }
3418 Err(ParseError::structural(
3419 "guard",
3420 "not() missing expression".to_string(),
3421 &span,
3422 ))
3423}
3424
3425fn parse_guard_primary(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3426 let span = refine_span(ctx, &pair);
3427 match pair.as_rule() {
3428 Rule::guard_primary => {
3429 let inner = pair.into_inner().next().ok_or_else(|| {
3430 ParseError::structural("guard", "guard primary missing body".to_string(), &span)
3431 })?;
3432 parse_guard_primary(ctx, inner)
3433 }
3434 Rule::guard_group => parse_guard_group(ctx, pair),
3435 Rule::guard_any_call => parse_guard_any_call(ctx, pair),
3436 Rule::guard_all_call => parse_guard_all_call(ctx, pair),
3437 Rule::not_call => parse_not_call(ctx, pair),
3438 Rule::guard_term => parse_guard_term(ctx, pair),
3439 _ => Err(ParseError::structural(
3440 "guard",
3441 format!("unexpected guard primary rule: {:?}", pair.as_rule()),
3442 &span,
3443 )),
3444 }
3445}
3446
3447fn parse_guard_group(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3448 let span = refine_span(ctx, &pair);
3449 for inner in pair.into_inner() {
3450 if inner.as_rule() == Rule::guard_expr {
3451 return parse_guard_expr(ctx, inner);
3452 }
3453 }
3454 Err(ParseError::structural(
3455 "guard",
3456 "grouped guard missing expression".to_string(),
3457 &span,
3458 ))
3459}
3460
3461fn parse_guard_any_call(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3462 let span = refine_span(ctx, &pair);
3463 let mut args = Vec::new();
3464 for inner in pair.into_inner() {
3465 if inner.as_rule() == Rule::guard_expr_list {
3466 args = parse_guard_expr_list(ctx, inner)?;
3467 }
3468 }
3469 if args.len() < 2 {
3470 return Err(ParseError::structural(
3471 "guard",
3472 "any(...) requires at least two guard expressions".to_string(),
3473 &span,
3474 ));
3475 }
3476 Ok(GuardExpr::or(args))
3477}
3478
3479fn parse_guard_all_call(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3480 let span = refine_span(ctx, &pair);
3481 let mut args = Vec::new();
3482 for inner in pair.into_inner() {
3483 if inner.as_rule() == Rule::guard_expr_list {
3484 args = parse_guard_expr_list(ctx, inner)?;
3485 }
3486 }
3487 if args.is_empty() {
3488 return Err(ParseError::structural(
3489 "guard",
3490 "all(...) requires at least one guard expression".to_string(),
3491 &span,
3492 ));
3493 }
3494 Ok(GuardExpr::all(args))
3495}
3496
3497fn parse_guard_expr_list(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Vec<GuardExpr>> {
3498 let mut exprs = Vec::new();
3499 for inner in pair.into_inner() {
3500 if inner.as_rule() == Rule::guard_expr {
3501 push_guard_or_args_from_expr(ctx, inner, &mut exprs)?;
3502 }
3503 }
3504 Ok(exprs)
3505}
3506
3507fn push_guard_or_args_from_expr(
3508 ctx: &SpanContext,
3509 expr_pair: Pair<Rule>,
3510 exprs: &mut Vec<GuardExpr>,
3511) -> ParseResult<()> {
3512 if let Some(seq_pair) = expr_pair
3513 .clone()
3514 .into_inner()
3515 .find(|inner| inner.as_rule() == Rule::guard_seq)
3516 {
3517 let factors: Vec<Pair<Rule>> = seq_pair
3518 .into_inner()
3519 .filter(|inner| inner.as_rule() == Rule::guard_factor)
3520 .collect();
3521 if factors.len() > 1 {
3522 for factor in factors {
3523 exprs.push(parse_guard_factor(ctx, factor)?);
3524 }
3525 return Ok(());
3526 }
3527 }
3528 exprs.push(parse_guard_expr(ctx, expr_pair)?);
3529 Ok(())
3530}
3531
3532fn parse_guard_term(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<GuardExpr> {
3533 let span = refine_span(ctx, &pair);
3534 for inner in pair.into_inner() {
3535 match inner.as_rule() {
3536 Rule::inject_guard => {
3537 return Ok(GuardExpr::Predicate(Guard::Attr {
3538 ns: Ns::Env,
3539 key: Some(inner.as_str().to_string()),
3540 val: None,
3541 }));
3542 }
3543 Rule::eq_guard => {
3544 return Ok(GuardExpr::Predicate(parse_func_guard(inner)?));
3545 }
3546 Rule::neq_guard => {
3547 let guard = parse_func_guard(inner)?;
3548 return Ok(GuardExpr::Not(Box::new(GuardExpr::Predicate(guard))));
3549 }
3550 Rule::bool_guard => {
3551 let val = inner
3552 .into_inner()
3553 .find(|p| p.as_rule() == Rule::bool_value)
3554 .expect("grammar invariant violated: bool_guard missing bool_value")
3555 .as_str()
3556 .to_string();
3557 return Ok(GuardExpr::Predicate(Guard::Attr {
3558 ns: Ns::Bool,
3559 key: None,
3560 val: Some(val),
3561 }));
3562 }
3563 Rule::env_guard => {
3564 return Ok(GuardExpr::Predicate(parse_env_guard(inner)?));
3565 }
3566 Rule::ns_guard => {
3567 return parse_ns_guard(inner, &span);
3568 }
3569 _ => {}
3570 }
3571 }
3572 Err(ParseError::structural(
3573 "guard",
3574 "missing guard predicate".to_string(),
3575 &span,
3576 ))
3577}
3578
3579fn parse_func_guard(pair: Pair<Rule>) -> ParseResult<Guard> {
3580 let mut key = String::new();
3581 let mut value = String::new();
3582 let mut saw_env_prefix = false;
3583 for inner in pair.into_inner() {
3584 match inner.as_rule() {
3585 Rule::env_prefix => saw_env_prefix = true,
3586 Rule::env_key if saw_env_prefix => {
3587 key = inner.as_str().trim().to_string();
3588 }
3589 Rule::bare_guard_value | Rule::quoted_string => {
3590 value = unquote(inner.as_str().trim()).to_string();
3591 }
3592 _ => {}
3593 }
3594 }
3595 Ok(Guard::Attr {
3596 ns: Ns::Env,
3597 key: Some(key),
3598 val: Some(value),
3599 })
3600}
3601
3602fn unquote(s: &str) -> &str {
3603 s.strip_prefix('"')
3604 .and_then(|s| s.strip_suffix('"'))
3605 .or_else(|| s.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
3606 .unwrap_or(s)
3607}
3608
3609fn parse_env_guard(pair: Pair<Rule>) -> ParseResult<Guard> {
3610 let mut key = String::new();
3611 for inner in pair.into_inner() {
3612 if inner.as_rule() == Rule::env_key {
3613 key = inner.as_str().trim().to_string();
3614 }
3615 }
3616 Ok(Guard::Attr {
3617 ns: Ns::Env,
3618 key: Some(key),
3619 val: None,
3620 })
3621}
3622
3623fn parse_ns_guard(pair: Pair<Rule>, span: &SpanContext) -> ParseResult<GuardExpr> {
3631 fn os(value: &str) -> GuardExpr {
3632 GuardExpr::Predicate(Guard::Attr {
3633 ns: Ns::Os,
3634 key: None,
3635 val: Some(value.to_string()),
3636 })
3637 }
3638 let mut ns = String::new();
3639 let mut val = String::new();
3640 for inner in pair.into_inner() {
3641 match inner.as_rule() {
3642 Rule::ns_name => ns = inner.as_str().trim().to_string(),
3643 Rule::ns_value => val = inner.as_str().trim().to_string(),
3644 _ => {}
3645 }
3646 }
3647 match ns.as_str() {
3648 "family" => match val.as_str() {
3649 "unix" => Ok(GuardExpr::or(vec![os("macos"), os("linux")])),
3650 "windows" => Ok(os("windows")),
3651 _ => Err(ParseError::structural(
3652 "guard",
3653 format!("unknown family '{val}'; known values: unix, windows"),
3654 span,
3655 )),
3656 },
3657 "os" => match val.as_str() {
3658 "macos" | "linux" | "windows" => Ok(os(val.as_str())),
3659 _ => Err(ParseError::structural(
3660 "guard",
3661 format!("unknown os '{val}'; known values: macos, linux, windows"),
3662 span,
3663 )),
3664 },
3665 "arch" => {
3666 if ARCH_VALUES.contains(&val.as_str()) {
3667 Ok(GuardExpr::Predicate(Guard::Attr {
3668 ns: Ns::Arch,
3669 key: None,
3670 val: Some(val),
3671 }))
3672 } else {
3673 Err(ParseError::structural(
3674 "guard",
3675 format!(
3676 "unknown arch '{val}'; known values: {}",
3677 ARCH_VALUES.join(", ")
3678 ),
3679 span,
3680 ))
3681 }
3682 }
3683 _ => Err(ParseError::structural(
3684 "guard",
3685 format!(
3686 "unknown guard namespace '{ns}'; known namespaces: family, os, arch, env, bool"
3687 ),
3688 span,
3689 )),
3690 }
3691}
3692
3693fn parse_dollar_ident(pair: Pair<Rule>) -> String {
3694 let s = pair.as_str();
3696 s.strip_prefix('$').unwrap_or(s).to_string()
3697}
3698
3699use crate::ast::{ArithOp, CompareOp, LogicalOp, Value};
3700
3701fn parse_expr(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3702 let span = refine_span(ctx, &pair);
3703 let expr = parse_expr_inner(ctx, lctx, pair)?;
3704 if matches!(expr, Expr::UnsignedIntBoundary(_)) {
3705 return Err(ParseError::structural(
3706 "expr",
3707 "integer overflow: 9223372036854775808 exceeds i64::MAX".to_string(),
3708 &span,
3709 ));
3710 }
3711 Ok(expr)
3712}
3713
3714fn parse_expr_inner(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3715 let span = refine_span(ctx, &pair);
3716 let inner = pair.into_inner().next().unwrap();
3717 match inner.as_rule() {
3718 Rule::expr_logical_or => parse_expr_logical_or(ctx, lctx, inner),
3719 _ => Err(ParseError::structural(
3720 "expr",
3721 format!("unexpected expr rule: {:?}", inner.as_rule()),
3722 &span,
3723 )),
3724 }
3725}
3726
3727fn parse_expr_logical_or(
3728 ctx: &SpanContext,
3729 lctx: &LowerCtx,
3730 pair: Pair<Rule>,
3731) -> ParseResult<Expr> {
3732 let span = refine_span(ctx, &pair);
3733 let mut inner = pair.into_inner();
3734 let mut left = parse_expr_logical_and(ctx, lctx, inner.next().unwrap())?;
3735 while let Some(op_pair) = inner.next() {
3736 let op = match op_pair.as_rule() {
3737 Rule::or_op => LogicalOp::Or,
3738 _ => {
3739 return Err(ParseError::structural(
3740 "expr",
3741 format!("unexpected operator in logical-or: {:?}", op_pair.as_rule()),
3742 &span,
3743 ));
3744 }
3745 };
3746 let right = parse_expr_logical_and(ctx, lctx, inner.next().unwrap())?;
3747 left = Expr::Logical {
3748 op,
3749 left: Box::new(left),
3750 right: Box::new(right),
3751 };
3752 }
3753 Ok(left)
3754}
3755
3756fn parse_expr_logical_and(
3757 ctx: &SpanContext,
3758 lctx: &LowerCtx,
3759 pair: Pair<Rule>,
3760) -> ParseResult<Expr> {
3761 let span = refine_span(ctx, &pair);
3762 let mut inner = pair.into_inner();
3763 let mut left = parse_expr_comparison(ctx, lctx, inner.next().unwrap())?;
3764 while let Some(op_pair) = inner.next() {
3765 let op = match op_pair.as_rule() {
3766 Rule::and_op => LogicalOp::And,
3767 _ => {
3768 return Err(ParseError::structural(
3769 "expr",
3770 format!(
3771 "unexpected operator in logical-and: {:?}",
3772 op_pair.as_rule()
3773 ),
3774 &span,
3775 ));
3776 }
3777 };
3778 let right = parse_expr_comparison(ctx, lctx, inner.next().unwrap())?;
3779 reject_boundary(ctx, &left)?;
3780 reject_boundary(ctx, &right)?;
3781 left = Expr::Logical {
3782 op,
3783 left: Box::new(left),
3784 right: Box::new(right),
3785 };
3786 }
3787 Ok(left)
3788}
3789
3790fn parse_expr_comparison(
3791 ctx: &SpanContext,
3792 lctx: &LowerCtx,
3793 pair: Pair<Rule>,
3794) -> ParseResult<Expr> {
3795 let span = refine_span(ctx, &pair);
3796 let mut inner = pair.into_inner();
3797 let left = parse_expr_ordering(ctx, lctx, inner.next().unwrap())?;
3798 if let Some(op_pair) = inner.next() {
3799 let op = match op_pair.as_rule() {
3800 Rule::eq_op => CompareOp::Eq,
3801 Rule::neq_op => CompareOp::Ne,
3802 _ => {
3803 return Err(ParseError::structural(
3804 "expr",
3805 format!("unexpected comparison operator: {:?}", op_pair.as_rule()),
3806 &span,
3807 ));
3808 }
3809 };
3810 let right = parse_expr_ordering(ctx, lctx, inner.next().unwrap())?;
3811 return make_compare(ctx, op, left, right);
3812 }
3813 Ok(left)
3814}
3815
3816fn parse_expr_ordering(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3817 let span = refine_span(ctx, &pair);
3818 let mut inner = pair.into_inner();
3819 let left = parse_expr_add_sub(ctx, lctx, inner.next().unwrap())?;
3820 if let Some(op_pair) = inner.next() {
3821 let op = match op_pair.as_rule() {
3822 Rule::lt_op => CompareOp::Lt,
3823 Rule::le_op => CompareOp::Le,
3824 Rule::gt_op => CompareOp::Gt,
3825 Rule::ge_op => CompareOp::Ge,
3826 _ => {
3827 return Err(ParseError::structural(
3828 "expr",
3829 format!("unexpected ordering operator: {:?}", op_pair.as_rule()),
3830 &span,
3831 ));
3832 }
3833 };
3834 let right = parse_expr_add_sub(ctx, lctx, inner.next().unwrap())?;
3835 return make_compare(ctx, op, left, right);
3836 }
3837 Ok(left)
3838}
3839
3840fn parse_expr_add_sub(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3841 let span = refine_span(ctx, &pair);
3842 let mut inner = pair.into_inner();
3843 let mut left = parse_expr_mul_div(ctx, lctx, inner.next().unwrap())?;
3844 while let Some(op_pair) = inner.next() {
3845 let op = match op_pair.as_rule() {
3846 Rule::plus_op => ArithOp::Add,
3847 Rule::minus_op => ArithOp::Sub,
3848 _ => {
3849 return Err(ParseError::structural(
3850 "expr",
3851 format!("unexpected additive operator: {:?}", op_pair.as_rule()),
3852 &span,
3853 ));
3854 }
3855 };
3856 let right = parse_expr_mul_div(ctx, lctx, inner.next().unwrap())?;
3857 left = make_arith(ctx, op, left, right)?;
3858 }
3859 Ok(left)
3860}
3861
3862fn parse_expr_mul_div(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3863 let span = refine_span(ctx, &pair);
3864 let mut inner = pair.into_inner();
3865 let mut left = parse_expr_unary(ctx, lctx, inner.next().unwrap())?;
3866 while let Some(op_pair) = inner.next() {
3867 let op = match op_pair.as_rule() {
3868 Rule::star_op => ArithOp::Mul,
3869 Rule::slash_op => ArithOp::Div,
3870 _ => {
3871 return Err(ParseError::structural(
3872 "expr",
3873 format!(
3874 "unexpected multiplicative operator: {:?}",
3875 op_pair.as_rule()
3876 ),
3877 &span,
3878 ));
3879 }
3880 };
3881 let right = parse_expr_unary(ctx, lctx, inner.next().unwrap())?;
3882 left = make_arith(ctx, op, left, right)?;
3883 }
3884 Ok(left)
3885}
3886
3887fn parse_expr_unary(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
3888 let span = refine_span(ctx, &pair);
3889 let mut prefixes = Vec::new();
3890 let mut atom = None;
3891 for inner in pair.into_inner() {
3892 match inner.as_rule() {
3893 Rule::not_op => prefixes.push(false),
3894 Rule::neg_op => prefixes.push(true),
3895 Rule::expr_atom => atom = Some(parse_expr_atom(ctx, lctx, inner)?),
3896 _ => {
3897 return Err(ParseError::structural(
3898 "expr",
3899 format!("unexpected unary operand rule: {:?}", inner.as_rule()),
3900 &span,
3901 ));
3902 }
3903 }
3904 }
3905 let mut expr = atom.ok_or_else(|| {
3906 ParseError::structural(
3907 "expr",
3908 "'!'/'-' requires an expression operand".to_string(),
3909 &span,
3910 )
3911 })?;
3912 for is_neg in prefixes.into_iter().rev() {
3914 if is_neg {
3915 expr = apply_unary_neg(ctx, expr)?;
3916 } else {
3917 reject_boundary(ctx, &expr)?;
3918 expr = Expr::Not(Box::new(expr));
3919 }
3920 }
3921 Ok(expr)
3922}
3923
3924fn reject_boundary(ctx: &SpanContext, expr: &Expr) -> ParseResult<()> {
3927 if matches!(expr, Expr::UnsignedIntBoundary(_)) {
3928 return Err(ParseError::structural(
3929 "expr",
3930 "integer overflow: 9223372036854775808 exceeds i64::MAX".to_string(),
3931 ctx,
3932 ));
3933 }
3934 Ok(())
3935}
3936
3937fn apply_unary_neg(ctx: &SpanContext, expr: Expr) -> ParseResult<Expr> {
3940 match expr {
3941 Expr::Literal(v) => match (v.as_i64(), v.as_f64()) {
3942 (Some(n), _) => match n.checked_neg() {
3943 Some(neg) => Ok(Expr::Literal(Value::int(neg))),
3944 None => Ok(Expr::CompiledMath(vec![MathOp::PushConst(v), MathOp::Neg])),
3945 },
3946 (None, Some(f)) => Ok(Expr::Literal(Value::float(-f))),
3947 (None, None) => {
3948 let other = Expr::Literal(v);
3949 if let Some(mut ops) = expr_to_rpn(&other) {
3950 ops.push(MathOp::Neg);
3951 Ok(Expr::CompiledMath(ops))
3952 } else {
3953 Ok(Expr::Arithmetic {
3956 op: ArithOp::Sub,
3957 left: Box::new(Expr::Literal(Value::int(0))),
3958 right: Box::new(other),
3959 })
3960 }
3961 }
3962 },
3963 Expr::UnsignedIntBoundary(n) => {
3964 if n == i64::MAX as u64 + 1 {
3965 Ok(Expr::Literal(Value::int(i64::MIN)))
3966 } else {
3967 Err(ParseError::structural(
3968 "expr",
3969 format!("integer overflow: {} exceeds i64::MAX", n),
3970 ctx,
3971 ))
3972 }
3973 }
3974 other => {
3975 if let Some(mut ops) = expr_to_rpn(&other) {
3976 ops.push(MathOp::Neg);
3977 Ok(Expr::CompiledMath(ops))
3978 } else {
3979 Ok(Expr::Arithmetic {
3982 op: ArithOp::Sub,
3983 left: Box::new(Expr::Literal(Value::int(0))),
3984 right: Box::new(other),
3985 })
3986 }
3987 }
3988 }
3989}
3990
3991fn try_fold_arith(op: ArithOp, left: &Expr, right: &Expr) -> Option<Expr> {
3996 let (Expr::Literal(lv), Expr::Literal(rv)) = (left, right) else {
3997 return None;
3998 };
3999 fold_arith_values(op, lv, rv).map(Expr::Literal)
4000}
4001
4002fn fold_arith_values(op: ArithOp, left: &Value, right: &Value) -> Option<Value> {
4003 match (left.as_i64(), right.as_i64()) {
4004 (Some(a), Some(b)) => {
4005 let v = match op {
4006 ArithOp::Add => a.checked_add(b)?,
4007 ArithOp::Sub => a.checked_sub(b)?,
4008 ArithOp::Mul => a.checked_mul(b)?,
4009 ArithOp::Div => a.checked_div(b)?,
4010 };
4011 Some(Value::int(v))
4012 }
4013 _ => {
4014 let (af, bf) = (as_f64(left)?, as_f64(right)?);
4015 fold_float(op, af, bf)
4016 }
4017 }
4018}
4019
4020fn fold_float(op: ArithOp, a: f64, b: f64) -> Option<Value> {
4021 if !a.is_finite() || !b.is_finite() {
4022 return None;
4023 }
4024 let v = match op {
4025 ArithOp::Add => a + b,
4026 ArithOp::Sub => a - b,
4027 ArithOp::Mul => a * b,
4028 ArithOp::Div => {
4029 if b == 0.0 {
4030 return None;
4031 }
4032 a / b
4033 }
4034 };
4035 if v.is_finite() {
4036 Some(Value::float(v))
4037 } else {
4038 None
4039 }
4040}
4041
4042fn try_fold_compare(op: CompareOp, left: &Expr, right: &Expr) -> Option<Expr> {
4043 let (Expr::Literal(lv), Expr::Literal(rv)) = (left, right) else {
4044 return None;
4045 };
4046 match (lv.as_i64(), rv.as_i64()) {
4047 (Some(a), Some(b)) => {
4048 let r = match op {
4049 CompareOp::Eq => a == b,
4050 CompareOp::Ne => a != b,
4051 CompareOp::Lt => a < b,
4052 CompareOp::Le => a <= b,
4053 CompareOp::Gt => a > b,
4054 CompareOp::Ge => a >= b,
4055 };
4056 Some(Expr::Literal(Value::bool(r)))
4057 }
4058 _ => {
4059 if let (Some(a), Some(b)) = (lv.as_bool(), rv.as_bool()) {
4060 return match op {
4061 CompareOp::Eq => Some(Expr::Literal(Value::bool(a == b))),
4062 CompareOp::Ne => Some(Expr::Literal(Value::bool(a != b))),
4063 _ => None,
4064 };
4065 }
4066 let (af, bf) = (as_f64(lv)?, as_f64(rv)?);
4067 let r = match op {
4068 CompareOp::Eq => af == bf,
4069 CompareOp::Ne => af != bf,
4070 CompareOp::Lt => af < bf,
4071 CompareOp::Le => af <= bf,
4072 CompareOp::Gt => af > bf,
4073 CompareOp::Ge => af >= bf,
4074 };
4075 Some(Expr::Literal(Value::bool(r)))
4076 }
4077 }
4078}
4079
4080fn as_f64(v: &Value) -> Option<f64> {
4081 if let Some(n) = v.as_i64() {
4082 return Some(n as f64);
4083 }
4084 match v.as_f64() {
4085 Some(f) if f.is_finite() => Some(f),
4086 _ => None,
4087 }
4088}
4089
4090fn make_arith(ctx: &SpanContext, op: ArithOp, left: Expr, right: Expr) -> ParseResult<Expr> {
4091 reject_boundary(ctx, &left)?;
4092 reject_boundary(ctx, &right)?;
4093 if let Some(folded) = try_fold_arith(op, &left, &right) {
4094 return Ok(folded);
4095 }
4096 if let (Some(mut lops), Some(mut rops)) = (expr_to_rpn(&left), expr_to_rpn(&right)) {
4097 lops.append(&mut rops);
4098 lops.push(match op {
4099 ArithOp::Add => MathOp::Add,
4100 ArithOp::Sub => MathOp::Sub,
4101 ArithOp::Mul => MathOp::Mul,
4102 ArithOp::Div => MathOp::Div,
4103 });
4104 return Ok(Expr::CompiledMath(lops));
4105 }
4106 Ok(Expr::Arithmetic {
4107 op,
4108 left: Box::new(left),
4109 right: Box::new(right),
4110 })
4111}
4112
4113fn make_compare(ctx: &SpanContext, op: CompareOp, left: Expr, right: Expr) -> ParseResult<Expr> {
4114 reject_boundary(ctx, &left)?;
4115 reject_boundary(ctx, &right)?;
4116 if let Some(folded) = try_fold_compare(op, &left, &right) {
4117 return Ok(folded);
4118 }
4119 if let (Some(mut lops), Some(mut rops)) = (expr_to_rpn(&left), expr_to_rpn(&right)) {
4120 lops.append(&mut rops);
4121 lops.push(match op {
4122 CompareOp::Eq => MathOp::Eq,
4123 CompareOp::Ne => MathOp::Ne,
4124 CompareOp::Lt => MathOp::Lt,
4125 CompareOp::Le => MathOp::Le,
4126 CompareOp::Gt => MathOp::Gt,
4127 CompareOp::Ge => MathOp::Ge,
4128 });
4129 return Ok(Expr::CompiledMath(lops));
4130 }
4131 Ok(Expr::Compare {
4132 op,
4133 left: Box::new(left),
4134 right: Box::new(right),
4135 })
4136}
4137
4138fn expr_to_rpn(expr: &Expr) -> Option<Vec<MathOp>> {
4142 match expr {
4143 Expr::Literal(v) => Some(vec![MathOp::PushConst(v.clone())]),
4144 Expr::Var(name) => Some(vec![MathOp::LoadVar(name.clone())]),
4145 Expr::Env(key) => Some(vec![MathOp::LoadEnv(key.clone())]),
4146 Expr::KeyPath { base, keys } => Some(vec![MathOp::LoadKeyPath {
4147 base: base.clone(),
4148 keys: keys.clone(),
4149 }]),
4150 Expr::Call { name, args } => {
4151 let mut ops = Vec::new();
4152 for arg in args {
4153 ops.extend(expr_to_rpn(arg)?);
4154 }
4155 ops.push(MathOp::Call {
4156 name: name.clone(),
4157 arity: args.len(),
4158 });
4159 Some(ops)
4160 }
4161 Expr::Inspect(var) => Some(vec![MathOp::Inspect(var.clone())]),
4162 Expr::Arithmetic { op, left, right } => {
4163 let mut ops = expr_to_rpn(left)?;
4164 ops.extend(expr_to_rpn(right)?);
4165 ops.push(match op {
4166 ArithOp::Add => MathOp::Add,
4167 ArithOp::Sub => MathOp::Sub,
4168 ArithOp::Mul => MathOp::Mul,
4169 ArithOp::Div => MathOp::Div,
4170 });
4171 Some(ops)
4172 }
4173 Expr::Compare { op, left, right } => {
4174 let mut ops = expr_to_rpn(left)?;
4175 ops.extend(expr_to_rpn(right)?);
4176 ops.push(match op {
4177 CompareOp::Eq => MathOp::Eq,
4178 CompareOp::Ne => MathOp::Ne,
4179 CompareOp::Lt => MathOp::Lt,
4180 CompareOp::Le => MathOp::Le,
4181 CompareOp::Gt => MathOp::Gt,
4182 CompareOp::Ge => MathOp::Ge,
4183 });
4184 Some(ops)
4185 }
4186 Expr::CompiledMath(ops) => Some(ops.clone()),
4187 Expr::FreshPipe => None,
4188 Expr::Not(_) | Expr::Logical { .. } | Expr::List(_) | Expr::Map(_) | Expr::Block(_) => None,
4189 Expr::UnsignedIntBoundary(_) => None,
4190 }
4191}
4192
4193fn parse_expr_atom(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
4194 let span = refine_span(ctx, &pair);
4195 let inner = pair.into_inner().next().unwrap();
4196 match inner.as_rule() {
4197 Rule::parenthesized_expr => parse_expr_inner(ctx, lctx, inner.into_inner().next().unwrap()),
4198 Rule::func_call => parse_func_call(ctx, lctx, inner),
4199 Rule::key_path => parse_key_path(ctx, inner),
4200 Rule::variable => {
4201 let name = inner.as_str();
4202 let name = name.strip_prefix('$').unwrap_or(name).to_string();
4203 Ok(Expr::Var(name))
4204 }
4205 Rule::env_read => parse_env_read(ctx, inner).map(Expr::Env),
4206 Rule::list_literal => parse_list_literal(ctx, lctx, inner),
4207 Rule::map_literal => parse_map_literal(ctx, lctx, inner),
4208 Rule::block => Ok(Expr::Block(parse_block_elements_with_lower(
4209 ctx, inner, lctx,
4210 )?)),
4211 Rule::string_literal | Rule::quoted_string => {
4212 let s = parse_quoted_string(inner)?;
4213 Ok(Expr::Literal(Value::string(s)))
4214 }
4215 Rule::numeric_literal => parse_numeric_literal(ctx, inner),
4216 Rule::bare_word => {
4217 let s = inner.as_str().to_string();
4218 match s.as_str() {
4219 "true" => Ok(Expr::Literal(Value::bool(true))),
4220 "false" => Ok(Expr::Literal(Value::bool(false))),
4221 _ => Ok(Expr::Literal(Value::string(s))),
4222 }
4223 }
4224 _ => Err(ParseError::structural(
4225 "expr",
4226 format!("unexpected expression atom rule: {:?}", inner.as_rule()),
4227 &span,
4228 )),
4229 }
4230}
4231
4232fn parse_numeric_literal(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Expr> {
4237 let span = refine_span(ctx, &pair);
4238 let text = pair.as_str();
4239 if text.contains('.') {
4240 let parsed: f64 = text.parse().map_err(|_| {
4241 ParseError::structural("expr", format!("invalid float literal {text:?}"), &span)
4242 })?;
4243 if !parsed.is_finite() {
4244 return Err(ParseError::structural(
4245 "expr",
4246 format!("invalid float literal {text:?}"),
4247 &span,
4248 ));
4249 }
4250 return Ok(Expr::Literal(Value::float(parsed)));
4251 }
4252 let digits: u64 = text.parse().map_err(|_| {
4253 ParseError::structural(
4254 "expr",
4255 format!("integer overflow: {text:?} exceeds i64::MAX"),
4256 &span,
4257 )
4258 })?;
4259 if digits <= i64::MAX as u64 {
4260 Ok(Expr::Literal(Value::int(digits as i64)))
4261 } else if digits == i64::MAX as u64 + 1 {
4262 Ok(Expr::UnsignedIntBoundary(digits))
4263 } else {
4264 Err(ParseError::structural(
4265 "expr",
4266 format!("integer overflow: {text:?} exceeds i64::MAX"),
4267 &span,
4268 ))
4269 }
4270}
4271
4272fn parse_env_read(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<String> {
4273 let span = refine_span(ctx, &pair);
4274 for inner in pair.into_inner() {
4275 if inner.as_rule() == Rule::env_read_key {
4276 return Ok(inner.as_str().trim().to_string());
4277 }
4278 }
4279 Err(ParseError::structural(
4280 "expr",
4281 "env read requires a key: env:KEY".to_string(),
4282 &span,
4283 ))
4284}
4285
4286fn parse_key_path(ctx: &SpanContext, pair: Pair<Rule>) -> ParseResult<Expr> {
4287 let span = refine_span(ctx, &pair);
4288 let mut base = None;
4289 let mut keys = Vec::new();
4290 for inner in pair.into_inner() {
4291 match inner.as_rule() {
4292 Rule::ident => {
4293 if base.is_none() {
4294 base = Some(inner.as_str().to_string());
4295 }
4296 }
4297 Rule::key_path_segment => {
4298 keys.push(inner.as_str().to_string());
4299 }
4300 _ => {}
4301 }
4302 }
4303 Ok(Expr::KeyPath {
4304 base: base.ok_or_else(|| {
4305 ParseError::structural(
4306 "expr",
4307 "key path requires a base identifier".to_string(),
4308 &span,
4309 )
4310 })?,
4311 keys,
4312 })
4313}
4314
4315fn parse_func_call(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
4316 let span = refine_span(ctx, &pair);
4317 let mut name = None;
4318 let mut args = Vec::new();
4319 for inner in pair.into_inner() {
4320 match inner.as_rule() {
4321 Rule::qualified_func_name => {
4322 name = Some(inner.as_str().to_string());
4323 }
4324 Rule::expr => {
4325 let arg = parse_expr_inner(ctx, lctx, inner)?;
4326 reject_boundary(ctx, &arg)?;
4327 args.push(arg);
4328 }
4329 _ => {}
4330 }
4331 }
4332 let name = name.ok_or_else(|| {
4333 ParseError::structural("expr", "function call requires a name".to_string(), &span)
4334 })?;
4335 if name == KEYWORD_INSPECT {
4340 let [arg] = args.as_slice() else {
4341 return Err(ParseError::structural(
4342 "expr",
4343 "INSPECT requires exactly one argument: INSPECT($var)".to_string(),
4344 &span,
4345 ));
4346 };
4347 if let Expr::Var(var) = arg {
4348 return Ok(Expr::Inspect(var.clone()));
4349 }
4350 return Err(ParseError::structural(
4351 "expr",
4352 format!("INSPECT requires a $variable argument, found {arg:?}"),
4353 &span,
4354 ));
4355 }
4356 let qualified = lctx.resolve_call(&span, &name)?;
4357 Ok(Expr::Call {
4358 name: qualified,
4359 args,
4360 })
4361}
4362
4363fn parse_list_literal(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
4364 let mut items = Vec::new();
4365 for inner in pair.into_inner() {
4366 if inner.as_rule() == Rule::expr {
4367 let item = parse_expr_inner(ctx, lctx, inner)?;
4368 reject_boundary(ctx, &item)?;
4369 items.push(item);
4370 }
4371 }
4372 Ok(Expr::List(items))
4373}
4374
4375fn parse_map_literal(ctx: &SpanContext, lctx: &LowerCtx, pair: Pair<Rule>) -> ParseResult<Expr> {
4376 let span = refine_span(ctx, &pair);
4377 let mut entries = Vec::new();
4378 for inner in pair.into_inner() {
4379 if inner.as_rule() == Rule::map_entry {
4380 let mut key = String::new();
4381 let mut value = None;
4382 for entry_inner in inner.into_inner() {
4383 match entry_inner.as_rule() {
4384 Rule::quoted_string => {
4385 key = parse_quoted_string(entry_inner)?;
4386 }
4387 Rule::bare_word => {
4388 key = entry_inner.as_str().to_string();
4389 }
4390 Rule::expr => {
4391 let val = parse_expr_inner(ctx, lctx, entry_inner)?;
4392 reject_boundary(ctx, &val)?;
4393 value = Some(val);
4394 }
4395 _ => {}
4396 }
4397 }
4398 let val = value.ok_or_else(|| {
4399 ParseError::structural("expr", "map entry missing value".to_string(), &span)
4400 })?;
4401 entries.push((key, val));
4402 }
4403 }
4404 Ok(Expr::Map(entries))
4405}