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