1pub mod bifs;
2pub mod buffer;
3pub mod context;
4mod pty;
5
6use std::collections::HashMap;
7use std::time::Instant;
8
9use crate::cancel::CancelToken;
10use regex::Regex;
11use regex::RegexBuilder;
12
13use crate::RuntimeContext;
14use crate::observe::structured::EventSeq;
15use crate::observe::structured::FnCallKind;
16use crate::observe::structured::MatchContext;
17use crate::observe::structured::SpanId;
18use crate::observe::structured::SpanKind;
19use crate::observe::structured::StructuredLogBuilder;
20use crate::report::result::Cancellation;
21use crate::report::result::ExecError;
22use crate::report::result::Failure;
23use crate::report::result::FailureContext;
24
25const BUFFER_TAIL_BYTES: usize = 4096;
29use crate::vm::buffer::FailPatternHit;
30use crate::vm::buffer::MultiMatchHit;
31use crate::vm::buffer::PatternSlot;
32use crate::vm::buffer::regex_error_summary;
33use crate::vm::context::Captures;
34use crate::vm::context::ExecutionContext;
35use crate::vm::context::FailPattern;
36use crate::vm::pty::PtyShell;
37use relux_core::diagnostics::IrSpan;
38use relux_ir::IrCallExpr;
39use relux_ir::IrExpr;
40use relux_ir::IrFn;
41use relux_ir::IrInterpolation;
42use relux_ir::IrMultiMatchPattern;
43use relux_ir::IrPureFn;
44use relux_ir::IrShellStmt;
45use relux_ir::IrTimeout;
46use relux_ir::Tables;
47
48#[derive(Debug)]
50struct MultiSlot {
51 slot: PatternSlot,
52 source: String,
55 is_regex: bool,
56 hit: Option<MultiMatchHit>,
58 done: bool,
60 buffer_seq: Option<EventSeq>,
63}
64
65pub struct Vm {
68 pty: PtyShell,
69 ctx: ExecutionContext,
70 tables: Tables,
71 pub log: StructuredLogBuilder,
72 shell_prompt: String,
73 pub(crate) cancel: CancelToken,
74 flaky_timeout_multiplier: f64,
75 terminated: bool,
76 shell_marker: String,
80}
81
82impl Vm {
83 pub async fn new(
84 shell_name: String,
85 shell_marker: String,
86 ctx: ExecutionContext,
87 rt_ctx: &RuntimeContext,
88 block_span: IrSpan,
89 ) -> Result<Self, ExecError> {
90 let shell_command = rt_ctx.shell.command.to_string();
91 let shell_prompt = rt_ctx.shell.prompt.to_string();
92
93 let log = rt_ctx.log.clone();
94 let pty = PtyShell::spawn(
95 &shell_command,
96 ctx.process_env(),
97 log.clone(),
98 shell_name.clone(),
99 shell_marker.clone(),
100 )
101 .map_err(|e| Failure::Runtime {
102 message: format!("failed to spawn shell: {e}"),
103 span: block_span.clone(),
104 shell: Some(shell_name.clone()),
105 context: FailureContext::pre_vm_with_span(ctx.current_span()),
106 })?;
107
108 let cancel = rt_ctx.cancel.clone();
109 let span = ctx.current_span();
110
111 let mut vm = Self {
112 pty,
113 ctx,
114 tables: rt_ctx.tables.clone(),
115 log: log.clone(),
116 shell_prompt,
117 cancel,
118 flaky_timeout_multiplier: rt_ctx.flaky_timeout_multiplier,
119 terminated: false,
120 shell_marker: shell_marker.clone(),
121 };
122
123 log.emit_shell_spawn(span, &shell_name, &shell_marker, &shell_command, None);
124
125 vm.pty
126 .init_prompt(
127 &vm.shell_prompt,
128 vm.ctx
129 .timeout()
130 .adjusted_duration_with_flaky(vm.flaky_timeout_multiplier),
131 )
132 .await
133 .map_err(|_| Failure::Runtime {
134 message: "shell did not produce prompt during init".to_string(),
135 span: block_span.clone(),
136 shell: Some(shell_name),
137 context: FailureContext::pre_vm_with_span(vm.ctx.current_span()),
138 })?;
139
140 let ready_shell = vm.ctx.current_name();
141 vm.log
142 .emit_shell_ready(span, &ready_shell, &shell_marker, None);
143
144 Ok(vm)
145 }
146
147 pub fn current_name(&self) -> String {
149 self.ctx.current_name()
150 }
151
152 pub fn shell_marker(&self) -> &str {
154 &self.shell_marker
155 }
156
157 pub fn reset_for_export(
159 &mut self,
160 new_scope: context::Scope,
161 parent_alias: Option<String>,
162 parent_effect_name: Option<String>,
163 shell_local_name: String,
164 ) {
165 self.ctx.reset_for_export(
166 new_scope,
167 parent_alias,
168 parent_effect_name,
169 shell_local_name,
170 );
171 }
172
173 pub fn shell_prompt(&self) -> &str {
174 &self.shell_prompt
175 }
176
177 pub fn set_block_span(&mut self, span: SpanId) {
180 self.ctx.set_block_span(span);
181 }
182
183 pub async fn exec_stmts(&mut self, stmts: &[IrShellStmt]) -> Result<String, ExecError> {
184 let mut last = String::new();
185 for stmt in stmts {
186 if self.cancel.is_cancelled() {
187 return Err(self.observed_cancel(None).await);
188 }
189 last = self.exec_stmt(stmt).await?;
190 }
191 Ok(last)
192 }
193
194 pub(crate) async fn observed_cancel(&self, span: Option<IrSpan>) -> ExecError {
198 let context = self.capture_failure_context().await;
199 let reason = self
200 .cancel
201 .reason()
202 .expect("production cancels always carry a reason");
203 let shell = self.ctx.current_name();
204 self.log.emit_cancelled(
205 self.current_span(),
206 Some(&shell),
207 Some(&self.shell_marker),
208 &reason,
209 );
210 let _ = span;
211 ExecError::Cancelled(Cancellation { reason, context })
212 }
213
214 fn current_span(&self) -> SpanId {
215 self.ctx.current_span()
216 }
217
218 pub(crate) async fn capture_failure_context(&self) -> FailureContext {
224 let call_stack = self.log.resolve_stack(self.ctx.current_span());
225 self.capture_failure_context_with_stack(call_stack).await
226 }
227
228 pub(crate) async fn capture_failure_context_with_stack(
235 &self,
236 call_stack: Vec<crate::observe::structured::failure::StackFrame>,
237 ) -> FailureContext {
238 FailureContext::Vm {
239 span: self.ctx.current_span(),
240 event_seq: self.log.current_seq(),
241 call_stack,
242 buffer_tail: self.pty.output_buf.snapshot_tail(BUFFER_TAIL_BYTES).await,
243 vars_in_scope: self.ctx.snapshot_user_vars().await,
244 }
245 }
246
247 async fn render_interp(&mut self, expr: &IrInterpolation, location: Option<&IrSpan>) -> String {
252 let guard = self.ctx.scope.vars().lock().await;
253 let (scopes, env) = self.ctx.interp_chain(&guard);
254 let captures = self.ctx.current_captures_map();
255 let rendered = relux_ir::evaluator::render_interpolation(expr, &scopes, env, captures);
256 drop(guard);
257 if rendered.emitted {
258 let shell = self.ctx.current_name();
259 self.log.emit_interpolation(
260 self.current_span(),
261 Some(&shell),
262 Some(&self.shell_marker),
263 &rendered.template,
264 &rendered.result,
265 &rendered.bindings,
266 location,
267 );
268 }
269 rendered.result
270 }
271
272 pub async fn exec_stmt(&mut self, stmt: &IrShellStmt) -> Result<String, ExecError> {
273 use relux_ir::IrNode;
274 let span = stmt.span().clone();
275 self.check_fail(span.clone()).await?;
276 match stmt {
277 IrShellStmt::Comment { .. } => Ok(String::new()),
278 IrShellStmt::FailRegex {
279 pattern,
280 span: ir_span,
281 } => {
282 let pat = self.render_interp(pattern, Some(&span)).await;
283 let shell = self.ctx.current_name();
284 self.log.emit_fail_pattern_set(
285 self.current_span(),
286 &shell,
287 &self.shell_marker,
288 &pat,
289 true,
290 Some(&span),
291 );
292 let re = match RegexBuilder::new(&pat).multi_line(true).crlf(true).build() {
293 Ok(re) => re,
294 Err(e) => {
295 let context = self.capture_failure_context().await;
296 return Err(Failure::Runtime {
297 message: format!("invalid fail regex: {}", regex_error_summary(&e)),
298 span: ir_span.clone(),
299 shell: Some(self.ctx.current_name().to_string()),
300 context,
301 }
302 .into());
303 }
304 };
305 let fp = Some(FailPattern::Regex(re));
306 self.ctx.set_fail_pattern(fp);
307 self.check_fail(span).await?;
308 Ok(String::new())
309 }
310 IrShellStmt::FailLiteral { pattern, .. } => {
311 let pat = self.render_interp(pattern, Some(&span)).await;
312 let shell = self.ctx.current_name();
313 self.log.emit_fail_pattern_set(
314 self.current_span(),
315 &shell,
316 &self.shell_marker,
317 &pat,
318 false,
319 Some(&span),
320 );
321 let fp = Some(FailPattern::Literal(pat));
322 self.ctx.set_fail_pattern(fp);
323 self.check_fail(span).await?;
324 Ok(String::new())
325 }
326 IrShellStmt::ClearFailPattern { .. } => {
327 let shell = self.ctx.current_name();
328 self.log.emit_fail_pattern_cleared(
329 self.current_span(),
330 &shell,
331 &self.shell_marker,
332 Some(&span),
333 );
334 self.ctx.set_fail_pattern(None);
335 Ok(String::new())
336 }
337 IrShellStmt::Timeout { timeout, .. } => {
338 let previous = self.ctx.timeout().clone();
339 self.ctx.set_timeout(timeout.clone());
340 let shell = self.ctx.current_name();
341 self.log.emit_timeout_set(
342 self.current_span(),
343 &shell,
344 &self.shell_marker,
345 self.ctx.timeout(),
346 &previous,
347 Some(&span),
348 );
349 Ok(String::new())
350 }
351 IrShellStmt::Let { stmt: let_stmt, .. } => {
352 let value = if let Some(expr) = let_stmt.value() {
353 self.eval_expr(expr).await?
354 } else {
355 String::new()
356 };
357 let shell = self.ctx.current_name();
358 self.log.emit_var_let(
359 self.current_span(),
360 Some(&shell),
361 Some(&self.shell_marker),
362 let_stmt.name().name(),
363 &value,
364 Some(&span),
365 );
366 self.ctx
367 .let_insert(let_stmt.name().name().to_string(), value.clone());
368 Ok(value)
369 }
370 IrShellStmt::Assign { stmt: assign, .. } => {
371 let value = self.eval_expr(assign.value()).await?;
372 let Some(previous) = self.ctx.assign(assign.name().name(), value.clone()).await
373 else {
374 let context = self.capture_failure_context().await;
375 return Err(Failure::Runtime {
376 message: format!(
377 "assignment to undeclared variable `{}`",
378 assign.name().name()
379 ),
380 span: assign.name().span().clone(),
381 shell: Some(self.ctx.current_name().to_string()),
382 context,
383 }
384 .into());
385 };
386 let shell = self.ctx.current_name();
387 self.log.emit_var_assign(
388 self.current_span(),
389 &shell,
390 &self.shell_marker,
391 assign.name().name(),
392 &value,
393 &previous,
394 Some(&span),
395 );
396 Ok(value)
397 }
398 IrShellStmt::Expr { expr, .. } => self.eval_expr(expr).await,
399 IrShellStmt::Send { payload, .. } => {
400 let data = self.render_interp(payload, Some(&span)).await;
401 let shell = self.ctx.current_name();
402 self.log.emit_send(
403 self.current_span(),
404 &shell,
405 &self.shell_marker,
406 &data,
407 Some(&span),
408 );
409 self.send_bytes(format!("{data}\n").as_bytes(), span.clone())
410 .await?;
411 Ok(data)
412 }
413 IrShellStmt::SendRaw { payload, .. } => {
414 let data = self.render_interp(payload, Some(&span)).await;
415 let shell = self.ctx.current_name();
416 self.log.emit_send(
417 self.current_span(),
418 &shell,
419 &self.shell_marker,
420 &data,
421 Some(&span),
422 );
423 self.send_bytes(data.as_bytes(), span.clone()).await?;
424 Ok(data)
425 }
426 IrShellStmt::MatchLiteral { pattern, .. } => {
427 let timeout = self.ctx.timeout().clone();
428 let pat = self.render_interp(pattern, Some(&span)).await;
429 let shell = self.ctx.current_name();
430 self.log.emit_match_start(
431 self.current_span(),
432 &shell,
433 &self.shell_marker,
434 &pat,
435 false,
436 &timeout,
437 Some(&span),
438 );
439 let match_start = Instant::now();
440 let (mat, buffer_seq) = self
441 .wait_consume_literal(&pat, &timeout, span.clone())
442 .await?;
443 let shell = self.ctx.current_name();
444 self.log.emit_match_done_record(
445 self.current_span(),
446 &shell,
447 &self.shell_marker,
448 &mat.value.0,
449 match_start.elapsed(),
450 None,
451 buffer_seq,
452 Some(&span),
453 );
454 Ok(pat)
455 }
456 IrShellStmt::MatchRegex { pattern, .. } => {
457 let timeout = self.ctx.timeout().clone();
458 let pat = self.render_interp(pattern, Some(&span)).await;
459 let re = match RegexBuilder::new(&pat).multi_line(true).crlf(true).build() {
460 Ok(re) => re,
461 Err(e) => {
462 let context = self.capture_failure_context().await;
463 return Err(Failure::Runtime {
464 message: format!("invalid regex: {}", regex_error_summary(&e)),
465 span: pattern.span().clone(),
466 shell: Some(self.ctx.current_name().to_string()),
467 context,
468 }
469 .into());
470 }
471 };
472 let shell = self.ctx.current_name();
473 self.log.emit_match_start(
474 self.current_span(),
475 &shell,
476 &self.shell_marker,
477 &pat,
478 true,
479 &timeout,
480 Some(&span),
481 );
482 let match_start = Instant::now();
483 let (mat, buffer_seq) = self
484 .wait_consume_regex(&pat, &re, &timeout, span.clone())
485 .await?;
486 let full = mat.value.0.get("0").cloned().unwrap_or_default();
487 let captures = mat.value.0.clone();
488 let shell = self.ctx.current_name();
489 self.log.emit_match_done_record(
490 self.current_span(),
491 &shell,
492 &self.shell_marker,
493 &full,
494 match_start.elapsed(),
495 Some(captures.clone()),
496 buffer_seq,
497 Some(&span),
498 );
499 self.set_captures_from_map(captures);
500 Ok(full)
501 }
502 IrShellStmt::PureMatch {
503 lhs,
504 pattern,
505 is_regex,
506 ..
507 } => {
508 let value = self.eval_expr(lhs).await?;
509 let pat = self.render_interp(pattern, Some(&span)).await;
510 let outcome = {
511 let mut sink = crate::observe::structured::log_sink::LogSink::new_in_shell(
512 &self.log,
513 self.current_span(),
514 self.ctx.current_name(),
515 self.shell_marker.clone(),
516 );
517 relux_ir::eval_pure_match(&mut sink, &value, &pat, *is_regex, &span)
518 };
519 match outcome {
520 Ok(Some(hit)) => {
521 let matched = hit.matched_text;
522 if *is_regex {
523 self.set_captures_from_map(hit.captures);
524 }
525 Ok(matched)
526 }
527 Ok(None) => {
528 let context = self.capture_failure_context().await;
529 let match_context = match self.ctx.current_fn_name() {
530 Some(name) => MatchContext::Fn {
531 name: name.to_string(),
532 },
533 None => MatchContext::Shell {
534 name: self.ctx.current_name(),
535 },
536 };
537 Err(Failure::PureMatch {
538 value,
539 pattern: pat,
540 is_regex: *is_regex,
541 span: span.clone(),
542 match_context,
543 context,
544 }
545 .into())
546 }
547 Err(e) => {
548 let context = self.capture_failure_context().await;
549 Err(Failure::Runtime {
550 message: crate::report::result::invalid_regex_message(&e.reason),
551 span: span.clone(),
552 shell: Some(self.ctx.current_name().to_string()),
553 context,
554 }
555 .into())
556 }
557 }
558 }
559 IrShellStmt::TimedMatchLiteral {
560 timeout, pattern, ..
561 } => {
562 let pat = self.render_interp(pattern, Some(&span)).await;
563 let shell = self.ctx.current_name();
564 self.log.emit_match_start(
565 self.current_span(),
566 &shell,
567 &self.shell_marker,
568 &pat,
569 false,
570 timeout,
571 Some(&span),
572 );
573 let match_start = Instant::now();
574 let (mat, buffer_seq) = self
575 .wait_consume_literal(&pat, timeout, span.clone())
576 .await?;
577 let shell = self.ctx.current_name();
578 self.log.emit_match_done_record(
579 self.current_span(),
580 &shell,
581 &self.shell_marker,
582 &mat.value.0,
583 match_start.elapsed(),
584 None,
585 buffer_seq,
586 Some(&span),
587 );
588 Ok(pat)
589 }
590 IrShellStmt::TimedMatchRegex {
591 timeout, pattern, ..
592 } => {
593 let pat = self.render_interp(pattern, Some(&span)).await;
594 let re = match RegexBuilder::new(&pat).multi_line(true).crlf(true).build() {
595 Ok(re) => re,
596 Err(e) => {
597 let context = self.capture_failure_context().await;
598 return Err(Failure::Runtime {
599 message: format!("invalid regex: {}", regex_error_summary(&e)),
600 span: pattern.span().clone(),
601 shell: Some(self.ctx.current_name().to_string()),
602 context,
603 }
604 .into());
605 }
606 };
607 let shell = self.ctx.current_name();
608 self.log.emit_match_start(
609 self.current_span(),
610 &shell,
611 &self.shell_marker,
612 &pat,
613 true,
614 timeout,
615 Some(&span),
616 );
617 let match_start = Instant::now();
618 let (mat, buffer_seq) = self
619 .wait_consume_regex(&pat, &re, timeout, span.clone())
620 .await?;
621 let full = mat.value.0.get("0").cloned().unwrap_or_default();
622 let captures = mat.value.0.clone();
623 let shell = self.ctx.current_name();
624 self.log.emit_match_done_record(
625 self.current_span(),
626 &shell,
627 &self.shell_marker,
628 &full,
629 match_start.elapsed(),
630 Some(captures.clone()),
631 buffer_seq,
632 Some(&span),
633 );
634 self.set_captures_from_map(captures);
635 Ok(full)
636 }
637 IrShellStmt::MultiMatch {
638 timeout, patterns, ..
639 } => self.exec_multimatch(patterns, timeout.as_ref(), span).await,
640 IrShellStmt::BufferReset { .. } => {
641 let _consumed = self.pty.output_buf.clear().await;
644 Ok(String::new())
645 }
646 }
647 }
648
649 async fn exec_multimatch(
650 &mut self,
651 patterns: &[IrMultiMatchPattern],
652 timeout: Option<&IrTimeout>,
653 span: IrSpan,
654 ) -> Result<String, ExecError> {
655 use relux_ir::IrNode;
656 let effective = timeout
657 .cloned()
658 .unwrap_or_else(|| self.ctx.timeout().clone());
659
660 let mut compiled: Vec<MultiSlot> = Vec::with_capacity(patterns.len());
662 for ir_pat in patterns {
663 let resolved = self.render_interp(ir_pat.pattern(), Some(&span)).await;
664 let slot = if ir_pat.is_regex() {
665 let re = match RegexBuilder::new(&resolved)
666 .multi_line(true)
667 .crlf(true)
668 .build()
669 {
670 Ok(re) => re,
671 Err(e) => {
672 let context = self.capture_failure_context().await;
673 return Err(Failure::Runtime {
674 message: format!("invalid regex: {}", regex_error_summary(&e)),
675 span: ir_pat.pattern().span().clone(),
676 shell: Some(self.ctx.current_name().to_string()),
677 context,
678 }
679 .into());
680 }
681 };
682 PatternSlot::regex(resolved.clone(), re)
683 } else {
684 PatternSlot::literal(resolved.clone())
685 };
686 compiled.push(MultiSlot {
687 slot,
688 source: resolved.clone(),
689 is_regex: ir_pat.is_regex(),
690 hit: None,
691 done: false,
692 buffer_seq: None,
693 });
694 }
695
696 let shell = self.ctx.current_name();
698 let parent_span = self.current_span();
699 let mm_guard = self
700 .log
701 .open_multimatch_span(parent_span, &shell, Some(&span));
702 let mm_span_id = mm_guard.id();
703 self.ctx.push_span(mm_span_id);
704
705 let pattern_meta: Vec<crate::observe::structured::MultiMatchPattern> = compiled
707 .iter()
708 .map(|m| crate::observe::structured::MultiMatchPattern {
709 pattern: m.source.clone(),
710 is_regex: m.is_regex,
711 })
712 .collect();
713 self.log.emit_multimatch_start(
714 mm_span_id,
715 &shell,
716 &self.shell_marker,
717 &pattern_meta,
718 &effective,
719 Some(&span),
720 );
721
722 let block_entry = self.pty.output_buf.base_offset().await;
724 let block_start = Instant::now();
725
726 let outcome = self
729 .wait_multimatch(
730 &mut compiled,
731 block_entry,
732 block_start,
733 &effective,
734 &span,
735 mm_span_id,
736 )
737 .await;
738
739 self.ctx.pop_span();
741 drop(mm_guard);
742
743 outcome.map(|()| String::new())
744 }
745
746 async fn wait_multimatch(
747 &self,
748 slots: &mut [MultiSlot],
749 block_entry: usize,
750 block_start: Instant,
751 timeout: &IrTimeout,
752 span: &IrSpan,
753 mm_span_id: SpanId,
754 ) -> Result<(), ExecError> {
755 let dur = timeout.adjusted_duration_with_flaky(self.flaky_timeout_multiplier);
756 let shell = self.ctx.current_name();
757
758 let fut = async {
759 loop {
760 let notified = self.pty.output_buf.notify.notified();
761
762 let fail_pat = self.ctx.fail_pattern();
764 if let Some(hit) = self.pty.output_buf.check_fail_pattern(fail_pat).await {
765 return Err(self.make_fail_pattern_error(hit, span.clone()).await);
766 }
767
768 let mut active_idx: Vec<usize> = Vec::with_capacity(slots.len());
771 let mut active_slots: Vec<PatternSlot> = Vec::with_capacity(slots.len());
772 for (i, s) in slots.iter().enumerate() {
773 if s.hit.is_none() {
774 active_idx.push(i);
775 active_slots.push(s.slot.clone());
776 }
777 }
778 let hits = self
779 .pty
780 .output_buf
781 .multimatch_scan(&mut active_slots, block_entry)
782 .await;
783
784 let mut max_end: Option<(usize, EventSeq)> = None;
787 for (k, hit_opt) in hits.into_iter().enumerate() {
788 let Some(hit) = hit_opt else { continue };
789 let i = active_idx[k];
790 let buffer_seq = self.pty.output_buf.push_multimatch_matched_event(
791 hit.before.clone(),
792 hit.matched_text.clone(),
793 hit.after.clone(),
794 );
795 let end_abs = hit.end_abs;
796 slots[i].hit = Some(hit);
797 slots[i].buffer_seq = Some(buffer_seq);
798 slots[i].done = true;
799 let elapsed = block_start.elapsed();
800 self.log.emit_multimatch_pattern_done(
801 mm_span_id,
802 &shell,
803 &self.shell_marker,
804 i,
805 elapsed,
806 buffer_seq,
807 Some(span),
808 );
809 match max_end {
810 Some((cur, _)) if end_abs <= cur => {}
811 _ => max_end = Some((end_abs, buffer_seq)),
812 }
813 }
814 for s in slots.iter() {
817 if let (Some(h), Some(seq)) = (s.hit.as_ref(), s.buffer_seq) {
818 match max_end {
819 Some((cur, _)) if h.end_abs <= cur => {}
820 _ => max_end = Some((h.end_abs, seq)),
821 }
822 }
823 }
824
825 if slots.iter().all(|s| s.hit.is_some()) {
827 let (final_end, advance_seq) =
828 max_end.expect("all slots matched -> max_end set");
829 self.pty.output_buf.drain_to(final_end).await;
830 self.log.emit_multimatch_done(
831 mm_span_id,
832 &shell,
833 &self.shell_marker,
834 advance_seq,
835 Some(span),
836 );
837 return Ok::<(), ExecError>(());
838 }
839
840 tokio::select! {
842 _ = notified => {}
843 _ = self.cancel.cancelled() => {
844 return Err(self.observed_cancel(Some(span.clone())).await);
845 }
846 }
847 }
848 };
849
850 match tokio::time::timeout(dur, fut).await {
851 Ok(result) => result,
852 Err(_) => {
853 let unmatched: Vec<usize> = slots
854 .iter()
855 .enumerate()
856 .filter(|(_, s)| s.hit.is_none())
857 .map(|(i, _)| i)
858 .collect();
859 let matched: Vec<usize> = slots
860 .iter()
861 .enumerate()
862 .filter(|(_, s)| s.hit.is_some())
863 .map(|(i, _)| i)
864 .collect();
865 self.log.emit_multimatch_timeout(
866 mm_span_id,
867 &shell,
868 &self.shell_marker,
869 &unmatched,
870 Some(span),
871 );
872 let pattern_meta: Vec<crate::observe::structured::MultiMatchPattern> = slots
873 .iter()
874 .map(|m| crate::observe::structured::MultiMatchPattern {
875 pattern: m.source.clone(),
876 is_regex: m.is_regex,
877 })
878 .collect();
879 let context = self.capture_failure_context().await;
880 Err(Failure::MultiMatch {
881 shell: self.ctx.current_name().to_string(),
882 patterns: pattern_meta,
883 matched,
884 span: span.clone(),
885 effective: Box::new(timeout.clone()),
886 context,
887 }
888 .into())
889 }
890 }
891 }
892
893 fn set_captures_from_map(&mut self, map: HashMap<String, String>) {
894 let mut caps = Captures::new();
895 for (k, v) in map {
896 caps.set(k, v);
897 }
898 self.ctx.set_captures(caps);
899 }
900
901 #[async_recursion::async_recursion]
902 async fn eval_expr(&mut self, expr: &IrExpr) -> Result<String, ExecError> {
903 use relux_ir::IrNode;
904 let span = expr.span().clone();
905 self.check_fail(span.clone()).await?;
906 match expr {
907 IrExpr::String { value, .. } => {
908 let result = self.render_interp(value, Some(&span)).await;
909 let shell = self.ctx.current_name();
910 self.log.emit_string_eval(
911 self.current_span(),
912 &shell,
913 &self.shell_marker,
914 &result,
915 Some(&span),
916 );
917 Ok(result)
918 }
919 IrExpr::Var { name, .. } => Ok(self.ctx.lookup(name).await.unwrap_or_default()),
920 IrExpr::QualifiedVar {
921 qualifier, name, ..
922 } => {
923 let qualified = format!("{qualifier}.{name}");
924 Ok(self.ctx.lookup(&qualified).await.unwrap_or_default())
925 }
926 IrExpr::CaptureRef { index, .. } => Ok(self.ctx.capture(*index).unwrap_or_default()),
927 IrExpr::Call { call, .. } => self.eval_call(call, &span).await,
928 }
929 }
930
931 async fn eval_call(&mut self, call: &IrCallExpr, span: &IrSpan) -> Result<String, ExecError> {
932 let fn_id = call.resolved().clone();
933 let fn_name = call.name().name().to_string();
934
935 let mut evaluated_args = Vec::with_capacity(call.args().len());
937 for arg in call.args() {
938 evaluated_args.push(self.eval_expr(arg).await?);
939 }
940
941 if let Some(result) = self.tables.fns.get(&fn_id) {
943 let ir_fn = match result.as_ref() {
944 Ok(f) => f,
945 Err(e) => {
946 let context = self.capture_failure_context().await;
947 return Err(Failure::Runtime {
948 message: format!("function resolution failed: {e:?}"),
949 span: span.clone(),
950 shell: Some(self.ctx.current_name().to_string()),
951 context,
952 }
953 .into());
954 }
955 };
956 match ir_fn {
957 IrFn::UserDefined { params, body, .. } => {
958 let params = params.clone();
959 let body = body.clone();
960 let named_args: Vec<(String, String)> = params
961 .iter()
962 .zip(evaluated_args.iter())
963 .map(|(p, v)| (p.name().to_string(), v.clone()))
964 .collect();
965 let parent_span = self.current_span();
966 let fn_guard = self.log.open_span(
967 SpanKind::FnCall {
968 name: fn_name.clone(),
969 args: named_args.clone(),
970 result: None,
971 callee_kind: FnCallKind::User,
972 is_pure: false,
973 },
974 Some(parent_span),
975 Some(span),
976 );
977 self.ctx.push_span(fn_guard.id());
978 self.ctx
979 .push_call(fn_name.clone(), named_args.into_iter().collect());
980 self.log.push_fn_enter(&fn_name);
981 let mut last = String::new();
982 for stmt in &body {
983 match self.exec_stmt(stmt).await {
984 Ok(v) => last = v,
985 Err(e) => {
986 self.ctx.pop_call();
987 self.ctx.pop_span();
988 self.log.push_fn_exit();
989 return Err(e);
990 }
991 }
992 }
993 self.ctx.pop_call();
994 self.ctx.pop_span();
995 self.log.set_fn_call_result(fn_guard.id(), &last);
996 self.log.push_fn_exit();
997 return Ok(last);
998 }
999 IrFn::Builtin { name, arity } => {
1000 if let Some(bif) = bifs::lookup_impure(name, *arity) {
1002 let positional_args: Vec<(String, String)> = evaluated_args
1003 .iter()
1004 .enumerate()
1005 .map(|(i, v)| (format!("${i}"), v.clone()))
1006 .collect();
1007 let parent_span = self.current_span();
1008 let fn_guard = self.log.open_span(
1009 SpanKind::FnCall {
1010 name: fn_name.clone(),
1011 args: positional_args,
1012 result: None,
1013 callee_kind: FnCallKind::Bif,
1014 is_pure: false,
1015 },
1016 Some(parent_span),
1017 Some(span),
1018 );
1019 self.ctx.push_span(fn_guard.id());
1020 self.log.push_fn_enter(&fn_name);
1021 let result = bif.call(self, evaluated_args, span).await;
1022 self.ctx.pop_span();
1023 if let Ok(ref v) = result {
1024 self.log.set_fn_call_result(fn_guard.id(), v);
1025 }
1026 self.log.push_fn_exit();
1027 return result;
1028 }
1029 }
1030 }
1031 }
1032
1033 if let Some(result) = self.tables.pure_fns.get(&fn_id) {
1035 let ir_fn = match result.as_ref() {
1036 Ok(f) => f,
1037 Err(e) => {
1038 let context = self.capture_failure_context().await;
1039 return Err(Failure::Runtime {
1040 message: format!("pure function resolution failed: {e:?}"),
1041 span: span.clone(),
1042 shell: Some(self.ctx.current_name().to_string()),
1043 context,
1044 }
1045 .into());
1046 }
1047 };
1048 let named_args: Vec<(String, String)> = match ir_fn {
1049 IrPureFn::UserDefined { params, .. } => params
1050 .iter()
1051 .zip(evaluated_args.iter())
1052 .map(|(p, v)| (p.name().to_string(), v.clone()))
1053 .collect(),
1054 IrPureFn::Builtin { .. } => evaluated_args
1055 .iter()
1056 .enumerate()
1057 .map(|(i, v)| (format!("${i}"), v.clone()))
1058 .collect(),
1059 };
1060 let callee_kind = match ir_fn {
1061 IrPureFn::UserDefined { .. } => FnCallKind::User,
1062 IrPureFn::Builtin { .. } => FnCallKind::Bif,
1063 };
1064 let parent_span = self.current_span();
1065 let fn_guard = self.log.open_span(
1066 SpanKind::FnCall {
1067 name: fn_name.clone(),
1068 args: named_args,
1069 result: None,
1070 callee_kind,
1071 is_pure: true,
1072 },
1073 Some(parent_span),
1074 Some(span),
1075 );
1076 self.ctx.push_span(fn_guard.id());
1077 self.log.push_fn_enter(&fn_name);
1078 let mut sink = crate::observe::structured::log_sink::LogSink::new_in_shell(
1079 &self.log,
1080 fn_guard.id(),
1081 self.ctx.current_name(),
1082 self.shell_marker.clone(),
1083 );
1084 let return_value = match relux_ir::evaluator::eval_pure_fn(
1085 ir_fn,
1086 evaluated_args,
1087 &self.ctx.env,
1088 &self.tables.pure_fns,
1089 &mut sink,
1090 ) {
1091 Ok(v) => v,
1092 Err(err) => {
1093 let call_stack = match sink.deepest_open_span() {
1101 Some(leaf) => self.log.resolve_stack(leaf),
1102 None => self.log.resolve_stack(self.current_span()),
1103 };
1104 let match_context = MatchContext::Fn {
1107 name: call_stack
1108 .last()
1109 .and_then(|f| f.name.clone())
1110 .unwrap_or_else(|| self.ctx.current_name()),
1111 };
1112 drop(sink);
1116 let context = self.capture_failure_context_with_stack(call_stack).await;
1117 self.ctx.pop_span();
1118 self.log.push_fn_exit();
1119 return Err(Failure::from_pure_eval(err, match_context, context).into());
1120 }
1121 };
1122 self.ctx.pop_span();
1123 self.log.set_fn_call_result(fn_guard.id(), &return_value);
1124 self.log.push_fn_exit();
1125 return Ok(return_value);
1126 }
1127
1128 let context = self.capture_failure_context().await;
1129 Err(Failure::Runtime {
1130 message: format!(
1131 "undefined function `{}` with arity {}",
1132 fn_name,
1133 call.args().len()
1134 ),
1135 span: span.clone(),
1136 shell: Some(self.ctx.current_name().to_string()),
1137 context,
1138 }
1139 .into())
1140 }
1141
1142 pub async fn match_literal(
1145 &mut self,
1146 pattern: &str,
1147 span: &IrSpan,
1148 ) -> Result<String, ExecError> {
1149 let shell = self.ctx.current_name();
1150 let timeout = self.ctx.timeout().clone();
1151 self.log.emit_match_start(
1152 self.current_span(),
1153 &shell,
1154 &self.shell_marker,
1155 pattern,
1156 false,
1157 &timeout,
1158 Some(span),
1159 );
1160 let match_start = Instant::now();
1161 let (mat, buffer_seq) = self
1162 .wait_consume_literal(pattern, &timeout, span.clone())
1163 .await?;
1164 let shell = self.ctx.current_name();
1165 self.log.emit_match_done_record(
1166 self.current_span(),
1167 &shell,
1168 &self.shell_marker,
1169 &mat.value.0,
1170 match_start.elapsed(),
1171 None,
1172 buffer_seq,
1173 Some(span),
1174 );
1175 Ok(pattern.to_string())
1176 }
1177
1178 pub async fn send_line(&mut self, line: &str, span: &IrSpan) -> Result<(), ExecError> {
1179 let shell = self.ctx.current_name();
1180 self.log.emit_send(
1181 self.current_span(),
1182 &shell,
1183 &self.shell_marker,
1184 line,
1185 Some(span),
1186 );
1187 self.send_bytes(format!("{line}\n").as_bytes(), span.clone())
1188 .await?;
1189 Ok(())
1190 }
1191
1192 pub async fn send_raw(&mut self, data: &[u8], span: &IrSpan) -> Result<(), ExecError> {
1193 let display = data
1194 .iter()
1195 .map(|b| format!("\\x{b:02x}"))
1196 .collect::<String>();
1197 let shell = self.ctx.current_name();
1198 self.log.emit_send(
1199 self.current_span(),
1200 &shell,
1201 &self.shell_marker,
1202 &display,
1203 Some(span),
1204 );
1205 self.send_bytes(data, span.clone()).await?;
1206 Ok(())
1207 }
1208
1209 async fn wait_consume_literal(
1212 &self,
1213 pattern: &str,
1214 timeout: &IrTimeout,
1215 span: IrSpan,
1216 ) -> Result<(buffer::Match<buffer::LiteralMatch>, EventSeq), ExecError> {
1217 let dur = timeout.adjusted_duration_with_flaky(self.flaky_timeout_multiplier);
1218 let fut = async {
1219 loop {
1220 let notified = self.pty.output_buf.notify.notified();
1221 let fail_pat = self.ctx.fail_pattern();
1222 match self
1223 .pty
1224 .output_buf
1225 .fail_check_consume_literal(pattern, fail_pat)
1226 .await
1227 {
1228 Err(hit) => {
1229 return Err(self.make_fail_pattern_error(hit, span.clone()).await);
1230 }
1231 Ok(Some(result)) => {
1232 return Ok::<(buffer::Match<buffer::LiteralMatch>, EventSeq), ExecError>(
1233 result,
1234 );
1235 }
1236 Ok(None) => {}
1237 }
1238 tokio::select! {
1239 _ = notified => {}
1240 _ = self.cancel.cancelled() => {
1241 return Err(self.observed_cancel(Some(span.clone())).await);
1242 }
1243 }
1244 }
1245 };
1246
1247 match tokio::time::timeout(dur, fut).await {
1248 Ok(result) => result,
1249 Err(_) => {
1250 let shell = self.ctx.current_name();
1251 self.log.emit_timeout(
1252 self.current_span(),
1253 &shell,
1254 &self.shell_marker,
1255 pattern,
1256 timeout,
1257 Some(&span),
1258 );
1259 let context = self.capture_failure_context().await;
1260 Err(Failure::MatchTimeout {
1261 pattern: pattern.to_string(),
1262 span,
1263 shell: self.ctx.current_name().to_string(),
1264 effective: Box::new(timeout.clone()),
1265 context,
1266 }
1267 .into())
1268 }
1269 }
1270 }
1271
1272 async fn wait_consume_regex(
1273 &self,
1274 pattern: &str,
1275 re: &Regex,
1276 timeout: &IrTimeout,
1277 span: IrSpan,
1278 ) -> Result<(buffer::Match<buffer::RegexMatch>, EventSeq), ExecError> {
1279 let dur = timeout.adjusted_duration_with_flaky(self.flaky_timeout_multiplier);
1280 let fut = async {
1281 loop {
1282 let notified = self.pty.output_buf.notify.notified();
1283 let fail_pat = self.ctx.fail_pattern();
1284 match self
1285 .pty
1286 .output_buf
1287 .fail_check_consume_regex(re, fail_pat)
1288 .await
1289 {
1290 Err(hit) => {
1291 return Err(self.make_fail_pattern_error(hit, span.clone()).await);
1292 }
1293 Ok(Some(result)) => {
1294 return Ok::<(buffer::Match<buffer::RegexMatch>, EventSeq), ExecError>(
1295 result,
1296 );
1297 }
1298 Ok(None) => {}
1299 }
1300 tokio::select! {
1301 _ = notified => {}
1302 _ = self.cancel.cancelled() => {
1303 return Err(self.observed_cancel(Some(span.clone())).await);
1304 }
1305 }
1306 }
1307 };
1308
1309 match tokio::time::timeout(dur, fut).await {
1310 Ok(result) => result,
1311 Err(_) => {
1312 let shell = self.ctx.current_name();
1313 self.log.emit_timeout(
1314 self.current_span(),
1315 &shell,
1316 &self.shell_marker,
1317 pattern,
1318 timeout,
1319 Some(&span),
1320 );
1321 let context = self.capture_failure_context().await;
1322 Err(Failure::MatchTimeout {
1323 pattern: pattern.to_string(),
1324 span,
1325 shell: self.ctx.current_name().to_string(),
1326 effective: Box::new(timeout.clone()),
1327 context,
1328 }
1329 .into())
1330 }
1331 }
1332 }
1333
1334 async fn check_fail(&self, span: IrSpan) -> Result<(), ExecError> {
1335 let fail_pat = self.ctx.fail_pattern();
1336 if let Some(hit) = self.pty.output_buf.check_fail_pattern(fail_pat).await {
1337 return Err(self.make_fail_pattern_error(hit, span).await);
1338 }
1339 Ok(())
1340 }
1341
1342 async fn make_fail_pattern_error(&self, hit: FailPatternHit, span: IrSpan) -> ExecError {
1343 let shell = self.ctx.current_name();
1344 self.log.emit_fail_pattern_triggered(
1345 self.current_span(),
1346 &shell,
1347 &self.shell_marker,
1348 &hit.pattern,
1349 hit.is_regex,
1350 &hit.matched_text,
1351 Some(&span),
1352 );
1353 let context = self.capture_failure_context().await;
1354 Failure::FailPatternMatched {
1355 pattern: hit.pattern,
1356 matched_line: hit.matched_text,
1357 span,
1358 shell: self.ctx.current_name().to_string(),
1359 context,
1360 }
1361 .into()
1362 }
1363
1364 async fn send_bytes(&mut self, data: &[u8], span: IrSpan) -> Result<(), ExecError> {
1365 match self.pty.send_bytes(data).await {
1366 Ok(()) => Ok(()),
1367 Err(e) => {
1368 let context = self.capture_failure_context().await;
1369 Err(Failure::ShellExited {
1370 shell: self.ctx.current_name().to_string(),
1371 exit_code: e.raw_os_error(),
1372 span,
1373 context,
1374 }
1375 .into())
1376 }
1377 }
1378 }
1379
1380 pub async fn shutdown(&mut self) {
1381 if self.terminated {
1387 return;
1388 }
1389 self.terminated = true;
1390 let shell = self.ctx.current_name();
1391 self.log
1392 .emit_shell_terminate(self.current_span(), &shell, &self.shell_marker, None);
1393 self.pty.shutdown().await;
1394 }
1395}