Skip to main content

hara_native/fiber/coroutine/
snapshot.rs

1//! Portable, bounded projections of the live production evaluator fiber.
2//!
3//! These snapshots observe the retained CPS continuation introduced by the
4//! live-fiber seam. They contain only owned scalar and string data: executable
5//! values, promises, continuations, mutable cells, and host handles remain
6//! owned by [`EvalFiber`].
7
8use 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    /// Returns a bounded JSON-safe document with default observation limits.
243    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    /// Returns a bounded JSON-safe document without exposing runtime handles.
249    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    /// Executes one production continuation and returns before/after evidence.
266    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    /// Executes one production continuation with caller-selected evidence bounds.
272    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    /// Applies one real promise settlement and returns before/after evidence.
289    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    /// Applies one promise settlement with caller-selected evidence bounds.
299    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    /// Projects the current evaluator state without exposing executable values.
318    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    /// Executes one live evaluator boundary and returns bounded before/after state.
363    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    /// Applies one promise settlement and returns the resulting live boundary.
381    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}