Skip to main content

hara_native/fiber/coroutine/
observation.rs

1//! Opt-in live stepping for the production CPS evaluator.
2//!
3//! The ordinary [`EvalFiber::start`] path still drains every trampoline
4//! continuation immediately. `start_observed` stores that same continuation
5//! inside the existing fiber and executes at most one `Step::Continue`
6//! boundary per `step_observed` call. Promise suspension keeps the real
7//! promise and resume closure; no journal replay or alternate evaluator is
8//! involved.
9
10use super::super::*;
11use super::semantic;
12use crate::instrumentation::{
13    EventKind, EventLocation, PortableProjection, ProducerEvent, ProjectionLimits,
14};
15use crate::kernel::{read_forms, SpannedForm};
16use std::collections::BTreeMap;
17
18impl EvalFiber {
19    /// Creates a live evaluator paused before the first production CPS step.
20    pub fn start_observed(source: &str, env: HashMap<String, Value>) -> Result<Self, String> {
21        let spanned = read_forms(source).map_err(|error| error.to_string())?;
22        let forms = spanned.iter().map(|form| form.form.clone()).collect();
23        Self::start_forms_observed_internal(forms, Some(Rc::new(spanned)), env)
24    }
25
26    /// Creates a live evaluator paused before evaluating forms without source spans.
27    pub fn start_forms_observed(
28        forms: Vec<Form>,
29        env: HashMap<String, Value>,
30    ) -> Result<Self, String> {
31        Self::start_forms_observed_internal(forms, None, env)
32    }
33
34    fn start_forms_observed_internal(
35        forms: Vec<Form>,
36        source_forms: Option<Rc<Vec<SpannedForm>>>,
37        env: HashMap<String, Value>,
38    ) -> Result<Self, String> {
39        let (namespace_registry, environment) = execution_context(env);
40        let env = Rc::new(RefCell::new(environment));
41        // The compatibility observer retains its historical full projection.
42        // Shared instrumentation changes these flags before every safepoint.
43        semantic::register_context(&env, source_forms, true, true);
44        let execution_env = env.clone();
45        let forms = Rc::new(forms);
46        let resume: Resume =
47            Box::new(move |_| forms_cps(forms, 0, Value::Nil, execution_env, Box::new(Step::Done)));
48        Ok(Self {
49            env,
50            namespace_registry,
51            pending: None,
52            resume: Some(resume),
53            state: EvalFiberState::Running,
54        })
55    }
56
57    /// Configures semantic capture for the next real CPS safepoint. Disabling
58    /// capture drops pending evidence but never changes the retained continuation.
59    pub(crate) fn configure_instrumentation_capture(
60        &self,
61        capture_events: bool,
62        capture_environment: bool,
63    ) {
64        semantic::configure_capture(&self.env, capture_events, capture_environment);
65    }
66
67    pub(crate) fn instrumentation_environment_clone_count(&self) -> u64 {
68        semantic::environment_clone_count(&self.env)
69    }
70
71    /// Returns the cheap identity/data for the currently published semantic
72    /// boundary without materializing source, frames, locals, or value displays.
73    pub(crate) fn instrumentation_event(&self) -> Option<(usize, ProducerEvent)> {
74        let boundary = semantic::current_boundary(&self.env)?;
75        let kind = match boundary.rule {
76            semantic::EvalSemanticRule::FormReturn | semantic::EvalSemanticRule::ValueReturn => {
77                EventKind::SemanticBoundary
78            }
79            semantic::EvalSemanticRule::CallEnter => EventKind::CallEnter,
80            semantic::EvalSemanticRule::CallReturn => EventKind::CallReturn,
81            semantic::EvalSemanticRule::VarDefine | semantic::EvalSemanticRule::VarSet => {
82                EventKind::VarSet
83            }
84            semantic::EvalSemanticRule::FieldSet => EventKind::FieldSet,
85            semantic::EvalSemanticRule::ErrorRaise | semantic::EvalSemanticRule::ErrorCatch => {
86                EventKind::ExceptionRaise
87            }
88        };
89        let mut event = ProducerEvent::live(kind).with_data("rule", boundary.rule.as_keyword());
90        match &boundary.payload {
91            semantic::EvalSemanticPayload::Result(value) => {
92                event = event.with_data("result/type", crate::core::portable_type_name(value));
93                if boundary.rule == semantic::EvalSemanticRule::CallReturn {
94                    if let Some(function) = &boundary.function {
95                        event = event.with_data("function", function);
96                    }
97                }
98            }
99            semantic::EvalSemanticPayload::Call { name, arguments } => {
100                event = event
101                    .with_data("function", name)
102                    .with_data("arguments/count", arguments.len().to_string());
103            }
104            semantic::EvalSemanticPayload::Effect {
105                target,
106                before,
107                after,
108            } => {
109                event = event
110                    .with_data("target", target)
111                    .with_data("before/present", before.is_some().to_string())
112                    .with_data("after/type", crate::core::portable_type_name(after));
113            }
114            semantic::EvalSemanticPayload::Error { message, caught } => {
115                event = event
116                    .with_data("caught", caught.to_string())
117                    .with_data("message", bounded_text(message, 1_024));
118            }
119        }
120        Some((boundary.sequence, event))
121    }
122
123    pub(crate) fn instrumentation_source_location(&self, source_id: &str) -> Option<EventLocation> {
124        let boundary = semantic::current_boundary(&self.env)?;
125        Some(EventLocation {
126            source_id: Some(source_id.into()),
127            function: boundary.function,
128            ..EventLocation::default()
129        })
130    }
131
132    pub(crate) fn instrumentation_current_frame(
133        &self,
134        limits: ProjectionLimits,
135    ) -> Option<PortableProjection> {
136        let boundary = semantic::current_boundary(&self.env)?;
137        Some(environment_projection(
138            "interpreter/current-frame",
139            &boundary.environment,
140            limits,
141        ))
142    }
143
144    pub(crate) fn instrumentation_frames(
145        &self,
146        limits: ProjectionLimits,
147    ) -> Option<PortableProjection> {
148        let boundary = semantic::current_boundary(&self.env)?;
149        let session = self.env.borrow();
150        let mut projection = PortableProjection::new("interpreter/frames")
151            .with_field("current/bindings", boundary.environment.len().to_string())
152            .with_field("session/bindings", session.len().to_string());
153        let current = environment_projection("current", &boundary.environment, limits);
154        let session = environment_projection("session", &session, limits);
155        for (name, value) in current.fields {
156            projection.fields.insert(format!("current/{name}"), value);
157        }
158        for (name, value) in session.fields {
159            projection.fields.insert(format!("session/{name}"), value);
160        }
161        Some(projection)
162    }
163
164    pub(crate) fn instrumentation_locals(
165        &self,
166        limits: ProjectionLimits,
167    ) -> Option<PortableProjection> {
168        let environment = self.env.borrow();
169        Some(environment_projection(
170            "interpreter/locals",
171            &environment,
172            limits,
173        ))
174    }
175
176    pub(crate) fn instrumentation_value_preview(
177        &self,
178        limits: ProjectionLimits,
179    ) -> Option<PortableProjection> {
180        let boundary = semantic::current_boundary(&self.env)?;
181        let display_chars = limits.max_bytes.min(16_384);
182        let projection = match &boundary.payload {
183            semantic::EvalSemanticPayload::Result(value) => {
184                let kind = if boundary.rule == semantic::EvalSemanticRule::CallReturn {
185                    "interpreter/call-return-preview"
186                } else {
187                    "interpreter/value-preview"
188                };
189                let mut projection = PortableProjection::new(kind)
190                    .with_field("kind", crate::core::portable_type_name(value))
191                    .with_field("display", bounded_text(&value.display(), display_chars));
192                if let Some(function) = &boundary.function {
193                    projection
194                        .fields
195                        .insert("function".into(), function.clone());
196                }
197                projection
198            }
199            semantic::EvalSemanticPayload::Call { name, arguments } => {
200                let mut projection = PortableProjection::new("interpreter/call-preview")
201                    .with_field("function", name)
202                    .with_field("arguments/count", arguments.len().to_string());
203                for (index, argument) in arguments.iter().take(limits.max_items).enumerate() {
204                    projection.fields.insert(
205                        format!("argument/{index}"),
206                        bounded_text(&argument.display(), display_chars),
207                    );
208                }
209                projection
210            }
211            semantic::EvalSemanticPayload::Effect {
212                target,
213                before,
214                after,
215            } => {
216                let mut projection = PortableProjection::new("interpreter/effect-preview")
217                    .with_field("target", target)
218                    .with_field("after", bounded_text(&after.display(), display_chars));
219                if let Some(before) = before {
220                    projection.fields.insert(
221                        "before".into(),
222                        bounded_text(&before.display(), display_chars),
223                    );
224                }
225                projection
226            }
227            semantic::EvalSemanticPayload::Error { message, caught } => {
228                PortableProjection::new("interpreter/error-preview")
229                    .with_field("caught", caught.to_string())
230                    .with_field("message", bounded_text(message, display_chars))
231            }
232        };
233        Some(projection)
234    }
235
236    pub(crate) fn instrumentation_snapshot(
237        &self,
238        limits: ProjectionLimits,
239    ) -> Option<PortableProjection> {
240        let mut projection = PortableProjection::new("interpreter/snapshot")
241            .with_field("state", instrumentation_state_keyword(&self.state))
242            .with_field(
243                "semantic/pending",
244                semantic::pending_count(&self.env).to_string(),
245            )
246            .with_field(
247                "environment/clones",
248                self.instrumentation_environment_clone_count().to_string(),
249            );
250        if let Some(promise) = &self.pending {
251            projection.fields.insert(
252                "promise/state".into(),
253                promise_state_keyword(&promise.state()).into(),
254            );
255        }
256        if let Some(locals) = self.instrumentation_locals(limits) {
257            for (name, value) in locals.fields {
258                projection.fields.insert(format!("locals/{name}"), value);
259            }
260        }
261        Some(projection)
262    }
263
264    /// Returns true while an observed fiber owns a retained CPS continuation.
265    pub fn observed_paused(&self) -> bool {
266        matches!(self.state, EvalFiberState::Running)
267            && self.pending.is_none()
268            && self.resume.is_some()
269    }
270
271    /// Returns the number of semantic boundaries retained for host publication.
272    pub fn observed_pending_boundaries(&self) -> usize {
273        semantic::pending_count(&self.env)
274    }
275
276    /// Executes at most one retained production continuation boundary.
277    pub fn step_observed(&mut self) -> EvalFiberState {
278        if semantic::advance_pending(&self.env) {
279            return self.state();
280        }
281        if !matches!(self.state, EvalFiberState::Running) {
282            return self.state();
283        }
284        let Some(resume) = self.resume.take() else {
285            self.state = EvalFiberState::Failed("observed evaluator continuation missing".into());
286            return self.state();
287        };
288        let step = with_namespace_registry(&self.namespace_registry, || {
289            semantic::with_active_context(&self.env, || resume(PromiseState::Pending))
290        });
291        self.accept_observed(step);
292        semantic::advance_pending(&self.env);
293        self.state()
294    }
295
296    /// Runs up to `boundary_limit` evaluator or queued semantic boundaries.
297    pub fn run_observed(&mut self, boundary_limit: usize) -> EvalFiberState {
298        for _ in 0..boundary_limit {
299            if !matches!(self.state, EvalFiberState::Running)
300                && semantic::pending_count(&self.env) == 0
301            {
302                break;
303            }
304            self.step_observed();
305        }
306        self.state()
307    }
308
309    /// Applies one real promise settlement without draining later boundaries.
310    pub fn resume_observed(&mut self, state: PromiseState) -> EvalFiberState {
311        if !matches!(self.state, EvalFiberState::Suspended) {
312            return self.state();
313        }
314        let Some(resume) = self.resume.take() else {
315            self.state = EvalFiberState::Failed("fiber continuation missing".into());
316            return self.state();
317        };
318        self.pending = None;
319        self.state = EvalFiberState::Running;
320        let step = with_namespace_registry(&self.namespace_registry, || {
321            semantic::with_active_context(&self.env, || resume(state))
322        });
323        self.accept_observed(step);
324        semantic::advance_pending(&self.env);
325        self.state()
326    }
327
328    fn accept_observed(&mut self, step: Step) {
329        match step {
330            Step::Continue(next) => {
331                self.resume = Some(Box::new(move |_| next()));
332                self.pending = None;
333                self.state = EvalFiberState::Running;
334            }
335            Step::Done(Ok(value)) => {
336                self.resume = None;
337                self.pending = None;
338                self.state = EvalFiberState::Completed(value);
339            }
340            Step::Done(Err(error)) => {
341                self.resume = None;
342                self.pending = None;
343                self.state = EvalFiberState::Failed(error);
344            }
345            Step::Wait(promise, resume) => {
346                self.pending = Some(promise);
347                self.resume = Some(resume);
348                self.state = EvalFiberState::Suspended;
349            }
350            Step::Yield(_, _) => {
351                self.resume = None;
352                self.pending = None;
353                self.state =
354                    EvalFiberState::Failed("coroutine/yield used outside of a coroutine".into());
355            }
356        }
357    }
358}
359
360impl Drop for EvalFiber {
361    fn drop(&mut self) {
362        semantic::remove_context(&self.env);
363    }
364}
365
366fn environment_projection(
367    kind: &str,
368    environment: &HashMap<String, Value>,
369    limits: ProjectionLimits,
370) -> PortableProjection {
371    let mut entries = environment.iter().collect::<Vec<_>>();
372    entries.sort_by(|left, right| left.0.cmp(right.0));
373    let retained = entries.len().min(limits.max_items);
374    let display_chars = limits.max_bytes.min(16_384);
375    let mut fields = BTreeMap::new();
376    for (name, value) in entries.into_iter().take(retained) {
377        fields.insert(
378            format!("binding/{name}"),
379            bounded_text(&value.display(), display_chars),
380        );
381    }
382    fields.insert("bindings/count".into(), environment.len().to_string());
383    fields.insert(
384        "bindings/omitted".into(),
385        environment.len().saturating_sub(retained).to_string(),
386    );
387    PortableProjection {
388        kind: kind.into(),
389        fields,
390    }
391}
392
393fn bounded_text(value: &str, limit: usize) -> String {
394    if value.chars().count() <= limit {
395        return value.into();
396    }
397    let mut output = value.chars().take(limit).collect::<String>();
398    output.push('…');
399    output
400}
401
402fn instrumentation_state_keyword(state: &EvalFiberState) -> &'static str {
403    match state {
404        EvalFiberState::Running => "running",
405        EvalFiberState::Suspended => "suspended",
406        EvalFiberState::Completed(_) => "returned",
407        EvalFiberState::Failed(_) => "failed",
408        EvalFiberState::Cancelled => "cancelled",
409    }
410}
411
412fn promise_state_keyword(state: &PromiseState) -> &'static str {
413    match state {
414        PromiseState::Pending => "pending",
415        PromiseState::Fulfilled(_) => "fulfilled",
416        PromiseState::Rejected(_) => "rejected",
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn live_fiber_starts_paused_and_executes_one_trampoline_at_a_time() {
426        let mut fiber =
427            EvalFiber::start_observed("(do 1 2 (+ 1 (* 2 3)))", HashMap::new()).unwrap();
428        assert_eq!(fiber.state(), EvalFiberState::Running);
429        assert!(fiber.observed_paused());
430
431        let first = fiber.run_observed(1);
432        assert_eq!(first, EvalFiberState::Running);
433        assert!(fiber.observed_paused());
434
435        let mut boundaries = 1;
436        while matches!(fiber.state(), EvalFiberState::Running) {
437            fiber.step_observed();
438            boundaries += 1;
439            assert!(boundaries < 64, "observed evaluation did not terminate");
440        }
441        assert!(boundaries > 2);
442        assert_eq!(fiber.state(), EvalFiberState::Completed(Value::Number(7)));
443    }
444
445    #[test]
446    fn promise_suspension_retains_the_real_promise_and_resume_continuation() {
447        let promise = Promise::new();
448        let mut env = HashMap::new();
449        env.insert("pending-value".into(), Value::Promise(promise.clone()));
450        let mut fiber = EvalFiber::start_observed("(Coroutine/await pending-value)", env).unwrap();
451
452        fiber.run_observed(16);
453        assert_eq!(fiber.state(), EvalFiberState::Suspended);
454        let retained = fiber.pending().expect("retained promise");
455        assert!(retained.same_identity(&promise));
456
457        assert!(promise.resolve(Value::Number(42)));
458        let resumed = fiber.resume_observed(promise.state());
459        assert_eq!(resumed, EvalFiberState::Running);
460        assert!(fiber.observed_paused());
461
462        let completed = fiber.run_observed(16);
463        assert_eq!(completed, EvalFiberState::Completed(Value::Number(42)));
464    }
465
466    #[test]
467    fn cancellation_discards_a_paused_live_continuation() {
468        let mut fiber = EvalFiber::start_observed("(do 1 2 3)", HashMap::new()).unwrap();
469        assert!(fiber.cancel());
470        assert_eq!(fiber.state(), EvalFiberState::Cancelled);
471        assert!(!fiber.observed_paused());
472        assert_eq!(fiber.step_observed(), EvalFiberState::Cancelled);
473        assert!(!fiber.cancel());
474    }
475
476    #[test]
477    fn ordinary_eval_fiber_remains_full_speed() {
478        let fiber = EvalFiber::start("(+ 19 23)", HashMap::new()).unwrap();
479        assert_eq!(fiber.state(), EvalFiberState::Completed(Value::Number(42)));
480    }
481
482    #[test]
483    fn disabled_instrumentation_capture_avoids_environment_clones() {
484        let mut fiber = EvalFiber::start_observed("(+ 19 23)", HashMap::new()).unwrap();
485        fiber.configure_instrumentation_capture(false, false);
486        fiber.run_observed(32);
487        assert_eq!(fiber.instrumentation_environment_clone_count(), 0);
488        assert_eq!(fiber.state(), EvalFiberState::Completed(Value::Number(42)));
489    }
490}