1use super::super::*;
9use super::semantic;
10use crate::kernel::{Position, Span, SpannedForm};
11use crate::lang::data::{OrderedMap, Vector};
12
13pub const INTERPRETER_LIVE_SNAPSHOT_SCHEMA: &str = "hal.interpreter-live-snapshot/0-alpha";
14pub const INTERPRETER_LIVE_BOUNDARY_SCHEMA: &str = "hal.interpreter-live-boundary/0-alpha";
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct EvalObservationLimits {
18 pub bindings: usize,
19 pub display_chars: usize,
20}
21
22impl Default for EvalObservationLimits {
23 fn default() -> Self {
24 Self {
25 bindings: 64,
26 display_chars: 160,
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum EvalObservationStatus {
33 Running,
34 Paused,
35 Suspended,
36 Returned,
37 Failed,
38 Cancelled,
39}
40
41impl EvalObservationStatus {
42 pub const fn as_keyword(self) -> &'static str {
43 match self {
44 Self::Running => "running",
45 Self::Paused => "paused",
46 Self::Suspended => "suspended",
47 Self::Returned => "returned",
48 Self::Failed => "failed",
49 Self::Cancelled => "cancelled",
50 }
51 }
52
53 pub const fn is_terminal(self) -> bool {
54 matches!(self, Self::Returned | Self::Failed | Self::Cancelled)
55 }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum EvalObservedBoundaryKind {
60 Semantic,
61 Continue,
62 Suspend,
63 Resume,
64 Return,
65 Fail,
66 Noop,
67}
68
69impl EvalObservedBoundaryKind {
70 pub const fn as_keyword(self) -> &'static str {
71 match self {
72 Self::Semantic => "evaluation/semantic",
73 Self::Continue => "evaluation/continue",
74 Self::Suspend => "evaluation/suspend",
75 Self::Resume => "evaluation/resume",
76 Self::Return => "evaluation/return",
77 Self::Fail => "evaluation/fail",
78 Self::Noop => "evaluation/noop",
79 }
80 }
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct EvalValueSnapshot {
85 pub kind: &'static str,
86 pub display: String,
87 pub truncated: bool,
88 pub redacted: bool,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct EvalBindingSnapshot {
93 pub name: String,
94 pub value: EvalValueSnapshot,
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct EvalErrorSnapshot {
99 pub message: String,
100 pub truncated: bool,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct EvalPendingSnapshot {
105 pub state: &'static str,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct EvalPositionSnapshot {
110 pub offset: usize,
111 pub line: usize,
112 pub column: usize,
113}
114
115#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct EvalSourceSpanSnapshot {
117 pub start: EvalPositionSnapshot,
118 pub end: EvalPositionSnapshot,
119}
120
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct EvalFocusSnapshot {
123 pub form: String,
124 pub form_truncated: bool,
125 pub form_kind: &'static str,
126 pub path: Option<Vec<usize>>,
127 pub span: Option<EvalSourceSpanSnapshot>,
128 pub source_candidates: usize,
129 pub ambiguous: bool,
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct EvalFrameSnapshot {
134 pub kind: &'static str,
135 pub binding_count: usize,
136 pub bindings: Vec<EvalBindingSnapshot>,
137 pub bindings_omitted: usize,
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct EvalSemanticCallSnapshot {
142 pub name: String,
143 pub arity: usize,
144 pub arguments: Vec<EvalValueSnapshot>,
145 pub arguments_omitted: usize,
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct EvalSemanticEffectSnapshot {
150 pub target: String,
151 pub before: Option<EvalValueSnapshot>,
152 pub after: EvalValueSnapshot,
153}
154
155#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct EvalSemanticErrorSnapshot {
157 pub category: &'static str,
158 pub message: String,
159 pub truncated: bool,
160 pub caught: bool,
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
164pub struct EvalSemanticSnapshot {
165 pub sequence: usize,
166 pub rule: &'static str,
167 pub focus: EvalFocusSnapshot,
168 pub result: Option<EvalValueSnapshot>,
169 pub call: Option<EvalSemanticCallSnapshot>,
170 pub effect: Option<EvalSemanticEffectSnapshot>,
171 pub error: Option<EvalSemanticErrorSnapshot>,
172 pub frames: Vec<EvalFrameSnapshot>,
173}
174
175#[derive(Clone, Debug, PartialEq, Eq)]
176pub struct EvalObservationSnapshot {
177 pub schema: &'static str,
178 pub source_id: String,
179 pub status: EvalObservationStatus,
180 pub paused: bool,
181 pub binding_count: usize,
182 pub bindings: Vec<EvalBindingSnapshot>,
183 pub bindings_omitted: usize,
184 pub semantic_pending: usize,
185 pub semantic: Option<EvalSemanticSnapshot>,
186 pub pending: Option<EvalPendingSnapshot>,
187 pub result: Option<EvalValueSnapshot>,
188 pub error: Option<EvalErrorSnapshot>,
189}
190
191impl EvalObservationSnapshot {
192 pub fn to_value(&self) -> Value {
193 object([
194 ("schema", string(self.schema)),
195 ("sourceId", string(&self.source_id)),
196 ("status", string(self.status.as_keyword())),
197 ("paused", Value::Bool(self.paused)),
198 ("bindingCount", integer(self.binding_count)),
199 ("bindings", vector(self.bindings.iter().map(binding_value))),
200 ("bindingsOmitted", integer(self.bindings_omitted)),
201 ("semanticPending", integer(self.semantic_pending)),
202 (
203 "semantic",
204 optional_value(self.semantic.as_ref().map(semantic_value)),
205 ),
206 (
207 "pending",
208 optional_value(self.pending.as_ref().map(pending_value)),
209 ),
210 (
211 "result",
212 optional_value(self.result.as_ref().map(value_snapshot_value)),
213 ),
214 (
215 "error",
216 optional_value(self.error.as_ref().map(error_value)),
217 ),
218 ])
219 }
220}
221
222#[derive(Clone, Debug, PartialEq, Eq)]
223pub struct EvalObservedBoundary {
224 pub schema: &'static str,
225 pub kind: EvalObservedBoundaryKind,
226 pub before: EvalObservationSnapshot,
227 pub after: EvalObservationSnapshot,
228}
229
230impl EvalObservedBoundary {
231 pub fn to_value(&self) -> Value {
232 object([
233 ("schema", string(self.schema)),
234 ("kind", string(self.kind.as_keyword())),
235 ("before", self.before.to_value()),
236 ("after", self.after.to_value()),
237 ])
238 }
239}
240
241impl EvalFiber {
242 pub fn snapshot_observed_value(&self, source_id: impl Into<String>) -> Value {
244 self.snapshot_observed(source_id, EvalObservationLimits::default())
245 .to_value()
246 }
247
248 pub fn snapshot_observed_value_with_limits(
250 &self,
251 source_id: impl Into<String>,
252 binding_limit: usize,
253 display_chars: usize,
254 ) -> Value {
255 self.snapshot_observed(
256 source_id,
257 EvalObservationLimits {
258 bindings: binding_limit,
259 display_chars,
260 },
261 )
262 .to_value()
263 }
264
265 pub fn step_observed_value(&mut self, source_id: impl Into<String>) -> Value {
267 self.step_observed_snapshot(source_id, EvalObservationLimits::default())
268 .to_value()
269 }
270
271 pub fn step_observed_value_with_limits(
273 &mut self,
274 source_id: impl Into<String>,
275 binding_limit: usize,
276 display_chars: usize,
277 ) -> Value {
278 self.step_observed_snapshot(
279 source_id,
280 EvalObservationLimits {
281 bindings: binding_limit,
282 display_chars,
283 },
284 )
285 .to_value()
286 }
287
288 pub fn resume_observed_value(
290 &mut self,
291 state: PromiseState,
292 source_id: impl Into<String>,
293 ) -> Value {
294 self.resume_observed_snapshot(state, source_id, EvalObservationLimits::default())
295 .to_value()
296 }
297
298 pub fn resume_observed_value_with_limits(
300 &mut self,
301 state: PromiseState,
302 source_id: impl Into<String>,
303 binding_limit: usize,
304 display_chars: usize,
305 ) -> Value {
306 self.resume_observed_snapshot(
307 state,
308 source_id,
309 EvalObservationLimits {
310 bindings: binding_limit,
311 display_chars,
312 },
313 )
314 .to_value()
315 }
316
317 pub(crate) fn snapshot_observed(
319 &self,
320 source_id: impl Into<String>,
321 limits: EvalObservationLimits,
322 ) -> EvalObservationSnapshot {
323 let source_id = source_id.into();
324 let status = observation_status(self);
325 let (binding_count, bindings, bindings_omitted) = {
326 let environment = self.env.borrow();
327 binding_projection(&environment, limits)
328 };
329 let semantic_pending = semantic::pending_count(&self.env);
330 let semantic = semantic_snapshot(self, limits);
331 let pending = self.pending.as_ref().map(|promise| EvalPendingSnapshot {
332 state: promise_state_keyword(&promise.state()),
333 });
334 let result = match &self.state {
335 EvalFiberState::Completed(value) => Some(value_snapshot(value, limits.display_chars)),
336 _ => None,
337 };
338 let error = match &self.state {
339 EvalFiberState::Failed(message) => {
340 let (message, truncated) = bounded_text(message, limits.display_chars);
341 Some(EvalErrorSnapshot { message, truncated })
342 }
343 _ => None,
344 };
345
346 EvalObservationSnapshot {
347 schema: INTERPRETER_LIVE_SNAPSHOT_SCHEMA,
348 source_id,
349 status,
350 paused: self.observed_paused(),
351 binding_count,
352 bindings,
353 bindings_omitted,
354 semantic_pending,
355 semantic,
356 pending,
357 result,
358 error,
359 }
360 }
361
362 pub(crate) fn step_observed_snapshot(
364 &mut self,
365 source_id: impl Into<String>,
366 limits: EvalObservationLimits,
367 ) -> EvalObservedBoundary {
368 let source_id = source_id.into();
369 let before = self.snapshot_observed(source_id.clone(), limits);
370 self.step_observed();
371 let after = self.snapshot_observed(source_id, limits);
372 EvalObservedBoundary {
373 schema: INTERPRETER_LIVE_BOUNDARY_SCHEMA,
374 kind: boundary_kind(&before, &after, false),
375 before,
376 after,
377 }
378 }
379
380 pub(crate) fn resume_observed_snapshot(
382 &mut self,
383 state: PromiseState,
384 source_id: impl Into<String>,
385 limits: EvalObservationLimits,
386 ) -> EvalObservedBoundary {
387 let source_id = source_id.into();
388 let before = self.snapshot_observed(source_id.clone(), limits);
389 self.resume_observed(state);
390 let after = self.snapshot_observed(source_id, limits);
391 EvalObservedBoundary {
392 schema: INTERPRETER_LIVE_BOUNDARY_SCHEMA,
393 kind: boundary_kind(&before, &after, true),
394 before,
395 after,
396 }
397 }
398}
399
400fn observation_status(fiber: &EvalFiber) -> EvalObservationStatus {
401 match &fiber.state {
402 EvalFiberState::Running if fiber.observed_paused() => EvalObservationStatus::Paused,
403 EvalFiberState::Running => EvalObservationStatus::Running,
404 EvalFiberState::Suspended => EvalObservationStatus::Suspended,
405 EvalFiberState::Completed(_) => EvalObservationStatus::Returned,
406 EvalFiberState::Failed(_) => EvalObservationStatus::Failed,
407 EvalFiberState::Cancelled => EvalObservationStatus::Cancelled,
408 }
409}
410
411fn boundary_kind(
412 before: &EvalObservationSnapshot,
413 after: &EvalObservationSnapshot,
414 resumed: bool,
415) -> EvalObservedBoundaryKind {
416 let before_sequence = before.semantic.as_ref().map(|semantic| semantic.sequence);
417 let after_sequence = after.semantic.as_ref().map(|semantic| semantic.sequence);
418 let semantic_advanced = before_sequence != after_sequence;
419 if semantic_advanced && before.status == after.status {
420 return EvalObservedBoundaryKind::Semantic;
421 }
422 match after.status {
423 EvalObservationStatus::Suspended => EvalObservedBoundaryKind::Suspend,
424 EvalObservationStatus::Returned => EvalObservedBoundaryKind::Return,
425 EvalObservationStatus::Failed => EvalObservedBoundaryKind::Fail,
426 EvalObservationStatus::Cancelled => EvalObservedBoundaryKind::Noop,
427 EvalObservationStatus::Running | EvalObservationStatus::Paused if resumed => {
428 EvalObservedBoundaryKind::Resume
429 }
430 EvalObservationStatus::Running | EvalObservationStatus::Paused => {
431 if before.status.is_terminal() {
432 EvalObservedBoundaryKind::Noop
433 } else {
434 EvalObservedBoundaryKind::Continue
435 }
436 }
437 }
438}
439
440fn binding_projection(
441 environment: &HashMap<String, Value>,
442 limits: EvalObservationLimits,
443) -> (usize, Vec<EvalBindingSnapshot>, usize) {
444 let mut bindings = environment
445 .iter()
446 .map(|(name, value)| EvalBindingSnapshot {
447 name: name.clone(),
448 value: value_snapshot(value, limits.display_chars),
449 })
450 .collect::<Vec<_>>();
451 bindings.sort_by(|left, right| left.name.cmp(&right.name));
452 let binding_count = bindings.len();
453 bindings.truncate(limits.bindings);
454 let bindings_omitted = binding_count.saturating_sub(bindings.len());
455 (binding_count, bindings, bindings_omitted)
456}
457
458fn frame_snapshot(
459 kind: &'static str,
460 environment: &HashMap<String, Value>,
461 limits: EvalObservationLimits,
462) -> EvalFrameSnapshot {
463 let (binding_count, bindings, bindings_omitted) = binding_projection(environment, limits);
464 EvalFrameSnapshot {
465 kind,
466 binding_count,
467 bindings,
468 bindings_omitted,
469 }
470}
471
472fn semantic_snapshot(
473 fiber: &EvalFiber,
474 limits: EvalObservationLimits,
475) -> Option<EvalSemanticSnapshot> {
476 let boundary = semantic::current_boundary(&fiber.env)?;
477 let source_forms = semantic::source_forms(&fiber.env);
478 let focus = focus_snapshot(
479 &boundary.form,
480 source_forms.as_deref().map(Vec::as_slice),
481 limits.display_chars,
482 );
483 let current = frame_snapshot("current", &boundary.environment, limits);
484 let session = {
485 let environment = fiber.env.borrow();
486 frame_snapshot("session", &environment, limits)
487 };
488 let (result, call, effect, error) = match &boundary.payload {
489 semantic::EvalSemanticPayload::Result(value) => (
490 Some(value_snapshot(value, limits.display_chars)),
491 None,
492 None,
493 None,
494 ),
495 semantic::EvalSemanticPayload::Call { name, arguments } => {
496 let arity = arguments.len();
497 let retained = arguments
498 .iter()
499 .take(limits.bindings)
500 .map(|value| value_snapshot(value, limits.display_chars))
501 .collect::<Vec<_>>();
502 (
503 None,
504 Some(EvalSemanticCallSnapshot {
505 name: name.clone(),
506 arity,
507 arguments_omitted: arity.saturating_sub(retained.len()),
508 arguments: retained,
509 }),
510 None,
511 None,
512 )
513 }
514 semantic::EvalSemanticPayload::Effect {
515 target,
516 before,
517 after,
518 } => (
519 None,
520 None,
521 Some(EvalSemanticEffectSnapshot {
522 target: target.clone(),
523 before: before
524 .as_ref()
525 .map(|value| value_snapshot(value, limits.display_chars)),
526 after: value_snapshot(after, limits.display_chars),
527 }),
528 None,
529 ),
530 semantic::EvalSemanticPayload::Error { message, caught } => {
531 let (message, truncated) = bounded_text(message, limits.display_chars);
532 (
533 None,
534 None,
535 None,
536 Some(EvalSemanticErrorSnapshot {
537 category: normalized_error_category(&message),
538 message,
539 truncated,
540 caught: *caught,
541 }),
542 )
543 }
544 };
545 Some(EvalSemanticSnapshot {
546 sequence: boundary.sequence,
547 rule: boundary.rule.as_keyword(),
548 focus,
549 result,
550 call,
551 effect,
552 error,
553 frames: vec![current, session],
554 })
555}
556
557fn normalized_error_category(message: &str) -> &'static str {
558 let message = message.to_ascii_lowercase();
559 if message.contains("division by zero")
560 || message.contains("divide by zero")
561 || message.contains("/ by zero")
562 {
563 "division by zero"
564 } else if message.contains("expects numbers")
565 || message.contains("expects two numbers")
566 || message.contains("expected a number")
567 || message.contains("expected numeric")
568 {
569 "expects numbers"
570 } else if message.contains("unbound symbol") || message.contains("unbound var") {
571 "unbound symbol"
572 } else if message.contains("recur") {
573 "recur"
574 } else if message.contains("unsupported") {
575 "unsupported form"
576 } else {
577 "runtime"
578 }
579}
580
581#[derive(Clone)]
582struct SourceMatch {
583 path: Vec<usize>,
584 span: Span,
585}
586
587fn focus_snapshot(
588 form: &Form,
589 source_forms: Option<&[SpannedForm]>,
590 display_chars: usize,
591) -> EvalFocusSnapshot {
592 let matches = source_forms
593 .map(|forms| source_matches(forms, form))
594 .unwrap_or_default();
595 let source_candidates = matches.len();
596 let unique = source_candidates == 1;
597 let (path, span) = if unique {
598 let matched = matches.into_iter().next().expect("one source match");
599 (Some(matched.path), Some(span_snapshot(&matched.span)))
600 } else {
601 (None, None)
602 };
603 let form_kind = form_kind(form);
604 let (form, form_truncated) = bounded_text(&form.to_string(), display_chars);
605 EvalFocusSnapshot {
606 form,
607 form_truncated,
608 form_kind,
609 path,
610 span,
611 source_candidates,
612 ambiguous: source_candidates > 1,
613 }
614}
615
616fn form_kind(form: &Form) -> &'static str {
617 match form {
618 Form::Symbol(_) => "symbol",
619 Form::List(values) => match values.first() {
620 Some(Form::Symbol(name)) if SYNC_SPECIAL_FORMS.contains(&name.as_str()) => {
621 "special-form"
622 }
623 _ => "call",
624 },
625 Form::Map(_) | Form::Set(_) | Form::Vector(_) => "collection",
626 Form::Metadata(_, _) => "metadata",
627 Form::Tagged(_, _) => "tagged",
628 _ => "literal",
629 }
630}
631
632fn source_matches(forms: &[SpannedForm], target: &Form) -> Vec<SourceMatch> {
633 let mut output = Vec::new();
634 collect_source_matches(forms, target, &[], &mut output);
635 output
636}
637
638fn collect_source_matches(
639 forms: &[SpannedForm],
640 target: &Form,
641 prefix: &[usize],
642 output: &mut Vec<SourceMatch>,
643) {
644 for (index, form) in forms.iter().enumerate() {
645 let mut path = prefix.to_vec();
646 path.push(index);
647 if &form.form == target {
648 output.push(SourceMatch {
649 path: path.clone(),
650 span: form.span.clone(),
651 });
652 }
653 collect_source_matches(&form.children, target, &path, output);
654 }
655}
656
657fn span_snapshot(span: &Span) -> EvalSourceSpanSnapshot {
658 EvalSourceSpanSnapshot {
659 start: position_snapshot(span.start),
660 end: position_snapshot(span.end),
661 }
662}
663
664fn position_snapshot(position: Position) -> EvalPositionSnapshot {
665 EvalPositionSnapshot {
666 offset: position.offset,
667 line: position.line,
668 column: position.column,
669 }
670}
671
672fn value_snapshot(value: &Value, display_chars: usize) -> EvalValueSnapshot {
673 let kind = value_kind(value);
674 let (display, redacted) = safe_display(value);
675 let (display, truncated) = bounded_text(&display, display_chars);
676 EvalValueSnapshot {
677 kind,
678 display,
679 truncated,
680 redacted,
681 }
682}
683
684fn value_kind(value: &Value) -> &'static str {
685 match value {
686 Value::Number(_) => "long",
687 Value::BigInteger(_) if crate::numeric::is_long_value(value) => "long",
688 Value::BigInteger(_) => "bigint",
689 Value::Float(_) => "float",
690 Value::Character(_) => "character",
691 Value::Bool(_) => "boolean",
692 Value::String(_) => "string",
693 Value::Keyword(_) => "keyword",
694 Value::Symbol(_) => "symbol",
695 Value::Bytes(_) => "bytes",
696 Value::Promise(_) => "promise",
697 Value::Function(_) => "function",
698 Value::Var(_) => "var",
699 Value::Extension(_) => "extension",
700 Value::Coroutine(_) => "coroutine",
701 Value::Iterator(_) => "iterator",
702 Value::Nil => "nil",
703 _ => "value",
704 }
705}
706
707fn safe_display(value: &Value) -> (String, bool) {
708 match value {
709 Value::Promise(promise) => (
710 format!("<promise {}>", promise_state_keyword(&promise.state())),
711 true,
712 ),
713 Value::Function(_) => ("<function>".into(), true),
714 Value::Coroutine(_) => ("<coroutine>".into(), true),
715 Value::Iterator(_) => ("<iterator>".into(), true),
716 Value::Extension(extension) => (
717 format!("<extension {}/{}>", extension.provider, extension.type_name),
718 true,
719 ),
720 Value::ByteBuffer(_) => ("<byte-buffer>".into(), true),
721 Value::Array(_) => ("<array>".into(), true),
722 Value::Object(_) => ("<object>".into(), true),
723 Value::MutableCollection(_) => ("<mutable-collection>".into(), true),
724 Value::Mutable(_) => ("<mutable>".into(), true),
725 _ => (value.display(), false),
726 }
727}
728
729fn promise_state_keyword(state: &PromiseState) -> &'static str {
730 match state {
731 PromiseState::Pending => "pending",
732 PromiseState::Fulfilled(_) => "fulfilled",
733 PromiseState::Rejected(_) => "rejected",
734 }
735}
736
737fn bounded_text(value: &str, limit: usize) -> (String, bool) {
738 let mut characters = value.chars();
739 let mut retained = characters.by_ref().take(limit).collect::<String>();
740 let truncated = characters.next().is_some();
741 if truncated {
742 retained.push('…');
743 }
744 (retained, truncated)
745}
746
747fn semantic_value(semantic: &EvalSemanticSnapshot) -> Value {
748 object([
749 ("sequence", integer(semantic.sequence)),
750 ("rule", string(semantic.rule)),
751 ("focus", focus_value(&semantic.focus)),
752 (
753 "result",
754 optional_value(semantic.result.as_ref().map(value_snapshot_value)),
755 ),
756 (
757 "call",
758 optional_value(semantic.call.as_ref().map(semantic_call_value)),
759 ),
760 (
761 "effect",
762 optional_value(semantic.effect.as_ref().map(semantic_effect_value)),
763 ),
764 (
765 "error",
766 optional_value(semantic.error.as_ref().map(semantic_error_value)),
767 ),
768 ("frames", vector(semantic.frames.iter().map(frame_value))),
769 ])
770}
771
772fn semantic_call_value(call: &EvalSemanticCallSnapshot) -> Value {
773 object([
774 ("name", string(&call.name)),
775 ("arity", integer(call.arity)),
776 (
777 "arguments",
778 vector(call.arguments.iter().map(value_snapshot_value)),
779 ),
780 ("argumentsOmitted", integer(call.arguments_omitted)),
781 ])
782}
783
784fn semantic_effect_value(effect: &EvalSemanticEffectSnapshot) -> Value {
785 object([
786 ("target", string(&effect.target)),
787 (
788 "before",
789 optional_value(effect.before.as_ref().map(value_snapshot_value)),
790 ),
791 ("after", value_snapshot_value(&effect.after)),
792 ])
793}
794
795fn semantic_error_value(error: &EvalSemanticErrorSnapshot) -> Value {
796 object([
797 ("category", string(error.category)),
798 ("message", string(&error.message)),
799 ("truncated", Value::Bool(error.truncated)),
800 ("caught", Value::Bool(error.caught)),
801 ])
802}
803
804fn focus_value(focus: &EvalFocusSnapshot) -> Value {
805 object([
806 ("form", string(&focus.form)),
807 ("formTruncated", Value::Bool(focus.form_truncated)),
808 ("formKind", string(focus.form_kind)),
809 (
810 "path",
811 optional_value(
812 focus
813 .path
814 .as_ref()
815 .map(|path| vector(path.iter().copied().map(integer))),
816 ),
817 ),
818 (
819 "span",
820 optional_value(focus.span.as_ref().map(source_span_value)),
821 ),
822 ("sourceCandidates", integer(focus.source_candidates)),
823 ("ambiguous", Value::Bool(focus.ambiguous)),
824 ])
825}
826
827fn source_span_value(span: &EvalSourceSpanSnapshot) -> Value {
828 object([
829 ("start", position_value(&span.start)),
830 ("end", position_value(&span.end)),
831 ])
832}
833
834fn position_value(position: &EvalPositionSnapshot) -> Value {
835 object([
836 ("offset", integer(position.offset)),
837 ("line", integer(position.line)),
838 ("column", integer(position.column)),
839 ])
840}
841
842fn frame_value(frame: &EvalFrameSnapshot) -> Value {
843 object([
844 ("kind", string(frame.kind)),
845 ("bindingCount", integer(frame.binding_count)),
846 ("bindings", vector(frame.bindings.iter().map(binding_value))),
847 ("bindingsOmitted", integer(frame.bindings_omitted)),
848 ])
849}
850
851fn binding_value(binding: &EvalBindingSnapshot) -> Value {
852 object([
853 ("name", string(&binding.name)),
854 ("value", value_snapshot_value(&binding.value)),
855 ])
856}
857
858fn value_snapshot_value(value: &EvalValueSnapshot) -> Value {
859 object([
860 ("kind", string(value.kind)),
861 ("display", string(&value.display)),
862 ("truncated", Value::Bool(value.truncated)),
863 ("redacted", Value::Bool(value.redacted)),
864 ])
865}
866
867fn pending_value(pending: &EvalPendingSnapshot) -> Value {
868 object([("state", string(pending.state))])
869}
870
871fn error_value(error: &EvalErrorSnapshot) -> Value {
872 object([
873 ("message", string(&error.message)),
874 ("truncated", Value::Bool(error.truncated)),
875 ])
876}
877
878fn object<const N: usize>(fields: [(&str, Value); N]) -> Value {
879 Value::OrderedMap(Box::new(OrderedMap::from_iter(
880 fields
881 .into_iter()
882 .map(|(key, value)| (Value::String(key.into()), value)),
883 )))
884}
885
886fn vector(values: impl IntoIterator<Item = Value>) -> Value {
887 Value::Vector(Vector::from_iter(values))
888}
889
890fn string(value: impl Into<String>) -> Value {
891 Value::String(value.into())
892}
893
894fn integer(value: usize) -> Value {
895 Value::Number(i64::try_from(value).unwrap_or(i64::MAX))
896}
897
898fn optional_value(value: Option<Value>) -> Value {
899 value.unwrap_or(Value::Nil)
900}
901
902#[cfg(test)]
903mod tests {
904 use super::*;
905
906 #[test]
907 fn snapshots_sort_bound_and_redact_environment_bindings() {
908 let mut environment = HashMap::new();
909 environment.insert("zeta".into(), Value::Number(3));
910 environment.insert("alpha".into(), Value::String("abcdefgh".into()));
911 environment.insert(
912 "extension".into(),
913 Value::Extension(ExtensionValue {
914 provider: "demo".into(),
915 type_name: "socket".into(),
916 handle: 999,
917 }),
918 );
919 let fiber = EvalFiber::start_observed("nil", environment).unwrap();
920 let snapshot = fiber.snapshot_observed(
921 "fixture/snapshot.hal",
922 EvalObservationLimits {
923 bindings: 2,
924 display_chars: 4,
925 },
926 );
927
928 assert_eq!(snapshot.status, EvalObservationStatus::Paused);
929 assert_eq!(snapshot.binding_count, 3);
930 assert_eq!(snapshot.bindings_omitted, 1);
931 assert_eq!(snapshot.bindings[0].name, "alpha");
932 assert_eq!(snapshot.bindings[1].name, "extension");
933 assert!(snapshot.bindings[0].value.truncated);
934 assert!(snapshot.bindings[1].value.redacted);
935 assert!(!snapshot.bindings[1].value.display.contains("999"));
936 let json = crate::json::write(&snapshot.to_value()).unwrap();
937 assert!(json.contains("hal.interpreter-live-snapshot/0-alpha"));
938 assert!(!json.contains("999"));
939 }
940
941 #[test]
942 fn live_boundaries_project_before_after_state_and_terminal_result() {
943 let limits = EvalObservationLimits::default();
944 let mut fiber = EvalFiber::start_observed("(+ 19 23)", HashMap::new()).unwrap();
945 let first = fiber.step_observed_snapshot("fixture/add.hal", limits);
946 assert_eq!(first.kind, EvalObservedBoundaryKind::Semantic);
947 assert_eq!(first.before.status, EvalObservationStatus::Paused);
948 assert_eq!(first.after.status, EvalObservationStatus::Paused);
949
950 let mut returned = fiber.step_observed_snapshot("fixture/add.hal", limits);
951 while returned.after.status == EvalObservationStatus::Paused {
952 returned = fiber.step_observed_snapshot("fixture/add.hal", limits);
953 }
954 assert_eq!(returned.kind, EvalObservedBoundaryKind::Return);
955 assert_eq!(returned.after.status, EvalObservationStatus::Returned);
956 assert_eq!(
957 returned
958 .after
959 .result
960 .as_ref()
961 .map(|value| value.display.as_str()),
962 Some("42")
963 );
964 let json = crate::json::write(&returned.to_value()).unwrap();
965 assert!(json.contains("evaluation/return"));
966 assert!(json.contains("\"display\":\"42\""));
967 }
968
969 #[test]
970 fn promise_boundaries_expose_state_without_identity_or_automatic_drain() {
971 let promise = Promise::new();
972 let mut environment = HashMap::new();
973 environment.insert("pending-value".into(), Value::Promise(promise.clone()));
974 let limits = EvalObservationLimits::default();
975 let mut fiber =
976 EvalFiber::start_observed("(Coroutine/await pending-value)", environment).unwrap();
977
978 while matches!(fiber.state(), EvalFiberState::Running) {
979 fiber.step_observed_snapshot("fixture/await.hal", limits);
980 }
981 let suspended = fiber.snapshot_observed("fixture/await.hal", limits);
982 assert_eq!(suspended.status, EvalObservationStatus::Suspended);
983 assert_eq!(
984 suspended.pending.as_ref().map(|pending| pending.state),
985 Some("pending")
986 );
987
988 promise.resolve(Value::Number(42));
989 let resumed = fiber.resume_observed_snapshot(promise.state(), "fixture/await.hal", limits);
990 assert_eq!(resumed.kind, EvalObservedBoundaryKind::Resume);
991 assert_eq!(resumed.after.status, EvalObservationStatus::Paused);
992 assert!(resumed.after.pending.is_none());
993 let json = crate::json::write(&resumed.to_value()).unwrap();
994 assert!(!json.contains("identity"));
995 }
996
997 fn collect_semantics(source: &str) -> Vec<EvalSemanticSnapshot> {
998 let mut fiber = EvalFiber::start_observed(source, HashMap::new()).unwrap();
999 let mut output = Vec::new();
1000 let mut sequence = 0;
1001 loop {
1002 let snapshot =
1003 fiber.snapshot_observed("fixture/semantic.hal", EvalObservationLimits::default());
1004 if !matches!(fiber.state(), EvalFiberState::Running) && snapshot.semantic_pending == 0 {
1005 break;
1006 }
1007 let boundary = fiber
1008 .step_observed_snapshot("fixture/semantic.hal", EvalObservationLimits::default());
1009 if let Some(semantic) = boundary.after.semantic {
1010 if semantic.sequence > sequence {
1011 sequence = semantic.sequence;
1012 output.push(semantic);
1013 }
1014 }
1015 assert!(sequence < 128, "semantic evaluation did not terminate");
1016 }
1017 output
1018 }
1019
1020 #[test]
1021 fn nested_calls_retain_actual_result_form_path_and_span() {
1022 let semantics = collect_semantics("(+ 1 (* 2 3))");
1023 let multiply = semantics
1024 .iter()
1025 .find(|semantic| {
1026 semantic.focus.form == "(* 2 3)"
1027 && semantic
1028 .result
1029 .as_ref()
1030 .is_some_and(|result| result.display == "6")
1031 })
1032 .expect("inner multiply return boundary");
1033 assert_eq!(
1034 multiply
1035 .result
1036 .as_ref()
1037 .map(|result| result.display.as_str()),
1038 Some("6")
1039 );
1040 assert_eq!(multiply.focus.form_kind, "call");
1041 assert_eq!(multiply.focus.path.as_deref(), Some(&[0, 2][..]));
1042 assert_eq!(multiply.focus.source_candidates, 1);
1043 assert_eq!(
1044 multiply
1045 .focus
1046 .span
1047 .as_ref()
1048 .map(|span| (span.start.offset, span.end.offset)),
1049 Some((5, 12))
1050 );
1051
1052 let outer = semantics
1053 .iter()
1054 .find(|semantic| {
1055 semantic.focus.form == "(+ 1 (* 2 3))"
1056 && semantic
1057 .result
1058 .as_ref()
1059 .is_some_and(|result| result.display == "7")
1060 })
1061 .expect("outer addition boundary");
1062 assert_eq!(outer.focus.path.as_deref(), Some(&[0][..]));
1063 }
1064
1065 #[test]
1066 fn lexical_boundary_captures_binding_before_scope_restoration() {
1067 let semantics = collect_semantics("(let [x 41] (+ x 1))");
1068 let resolved = semantics
1069 .iter()
1070 .find(|semantic| {
1071 semantic.focus.form == "x"
1072 && semantic
1073 .result
1074 .as_ref()
1075 .is_some_and(|result| result.display == "41")
1076 })
1077 .expect("resolved lexical symbol boundary");
1078 let current = resolved
1079 .frames
1080 .iter()
1081 .find(|frame| frame.kind == "current")
1082 .expect("current lexical frame");
1083 let x = current
1084 .bindings
1085 .iter()
1086 .find(|binding| binding.name == "x")
1087 .expect("captured x binding");
1088 assert_eq!(x.value.display, "41");
1089 }
1090
1091 #[test]
1092 fn duplicate_source_forms_are_explicitly_ambiguous() {
1093 let semantics = collect_semantics("(+ 1 1)");
1094 let literal = semantics
1095 .iter()
1096 .find(|semantic| semantic.focus.form == "1")
1097 .expect("literal boundary");
1098 assert_eq!(literal.focus.source_candidates, 2);
1099 assert!(literal.focus.ambiguous);
1100 assert!(literal.focus.path.is_none());
1101 assert!(literal.focus.span.is_none());
1102 }
1103
1104 #[test]
1105 fn call_entry_is_published_before_the_matching_return() {
1106 let semantics = collect_semantics("(+ 1 (* 2 3))");
1107 let enter = semantics
1108 .iter()
1109 .position(|semantic| semantic.rule == "call/enter" && semantic.focus.form == "(* 2 3)")
1110 .expect("inner call entry");
1111 let returned = semantics
1112 .iter()
1113 .position(|semantic| {
1114 semantic.rule == "value/return"
1115 && semantic.focus.form == "(* 2 3)"
1116 && semantic
1117 .result
1118 .as_ref()
1119 .is_some_and(|result| result.display == "6")
1120 })
1121 .expect("inner call return");
1122 assert!(enter < returned);
1123 let call = semantics[enter].call.as_ref().expect("call payload");
1124 assert_eq!(call.arity, 2);
1125 assert_eq!(
1126 call.arguments
1127 .iter()
1128 .map(|argument| argument.display.as_str())
1129 .collect::<Vec<_>>(),
1130 vec!["2", "3"]
1131 );
1132 }
1133
1134 #[test]
1135 fn var_mutations_are_explicit_ordered_effects() {
1136 let semantics = collect_semantics("(do (def counter 1) (set! counter 42) counter)");
1137 let define = semantics
1138 .iter()
1139 .find(|semantic| semantic.rule == "effect/var-define")
1140 .expect("definition effect");
1141 let define_effect = define.effect.as_ref().expect("definition payload");
1142 assert_eq!(define_effect.after.display, "1");
1143
1144 let set = semantics
1145 .iter()
1146 .find(|semantic| semantic.rule == "effect/var-set")
1147 .expect("set effect");
1148 let set_effect = set.effect.as_ref().expect("set payload");
1149 assert_eq!(
1150 set_effect
1151 .before
1152 .as_ref()
1153 .map(|value| value.display.as_str()),
1154 Some("1")
1155 );
1156 assert_eq!(set_effect.after.display, "42");
1157 assert!(define.sequence < set.sequence);
1158 }
1159
1160 #[test]
1161 fn raised_errors_and_selected_catches_are_explicit() {
1162 let semantics = collect_semantics("(try (/ 1 0) (catch Exception error 42))");
1163 let raised = semantics
1164 .iter()
1165 .find(|semantic| semantic.rule == "error/raise")
1166 .expect("raise event");
1167 let raised_error = raised.error.as_ref().expect("raise payload");
1168 assert_eq!(raised_error.category, "division by zero");
1169 assert!(!raised_error.caught);
1170 assert_eq!(raised.focus.form, "(/ 1 0)");
1171
1172 let caught = semantics
1173 .iter()
1174 .find(|semantic| semantic.rule == "error/catch")
1175 .expect("catch event");
1176 assert!(caught.error.as_ref().is_some_and(|error| error.caught));
1177 assert!(raised.sequence < caught.sequence);
1178 }
1179}