logicaffeine_system/obs.rs
1//! The observation channel for COMPILED (AOT-Rust) programs — Wave 13.
2//!
3//! Emitted-Rust programs compiled in the OBSERVED profile call these functions
4//! for `Note`/`Warn`/`Record`/`During`, so the compiled binary produces the same
5//! observation event stream the interpreter and VM do (the compile-side buffer
6//! lives in `logicaffeine_compile::observe`; this is its standalone-runtime twin,
7//! reachable from generated code via `use logicaffeine_system::*`). Events land
8//! in a thread-local buffer a host sink drains; when nothing is installed,
9//! emission is a cheap no-op, so the release (elided) profile — which never emits
10//! these calls at all — pays exactly nothing.
11
12use std::cell::RefCell;
13
14/// One observation event captured in a compiled program.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ObsEvent {
17 /// `"note"` / `"warn"` / `"record"` for a message, or `"span-enter"` /
18 /// `"span-exit"` for a `During` boundary.
19 pub kind: String,
20 /// The that-clause text (or the span label).
21 pub message: String,
22 /// Captured payloads (`with X and Y`), name → rendered value.
23 pub payloads: Vec<(String, String)>,
24}
25
26thread_local! {
27 static BUFFER: RefCell<Option<Vec<ObsEvent>>> = const { RefCell::new(None) };
28}
29
30/// Begin capturing observation events on this thread (a fresh, empty buffer).
31pub fn install() {
32 BUFFER.with(|b| *b.borrow_mut() = Some(Vec::new()));
33}
34
35/// Take and clear the captured events (empty if nothing was installed).
36pub fn drain() -> Vec<ObsEvent> {
37 BUFFER.with(|b| {
38 b.borrow_mut()
39 .as_mut()
40 .map(std::mem::take)
41 .unwrap_or_default()
42 })
43}
44
45/// Is a sink capturing on this thread? (Emission is a no-op otherwise.)
46pub fn capturing() -> bool {
47 BUFFER.with(|b| b.borrow().is_some())
48}
49
50fn push(kind: &str, message: &str, payloads: Vec<(String, String)>) {
51 BUFFER.with(|b| {
52 if let Some(buf) = b.borrow_mut().as_mut() {
53 buf.push(ObsEvent {
54 kind: kind.to_string(),
55 message: message.to_string(),
56 payloads,
57 });
58 }
59 });
60}
61
62/// Emit a `Note`/`Warn`/`Record` message with its captured payloads.
63pub fn record(kind: &str, message: &str, payloads: &[(&str, String)]) {
64 if !capturing() {
65 return;
66 }
67 let owned = payloads
68 .iter()
69 .map(|(n, v)| (n.to_string(), v.clone()))
70 .collect();
71 push(kind, message, owned);
72}
73
74/// Emit a `During` span entry.
75pub fn span_enter(label: &str) {
76 push("span-enter", label, Vec::new());
77}
78
79/// Emit a `During` span exit (always paired with its `span_enter`).
80pub fn span_exit(label: &str) {
81 push("span-exit", label, Vec::new());
82}