Skip to main content

hara_native/instrumentation/
model.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3pub const INSTRUMENTATION_PROTOCOL: &str = "hara.instrumentation/0-alpha";
4pub const INSTRUMENTATION_EVENT_SCHEMA: &str = "hara.instrumentation.event/0-alpha";
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub enum InstrumentMode {
8    Passive,
9    Control,
10    Transform,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub enum TargetKind {
15    Interpreter,
16    Hbc,
17    WholeWasm,
18}
19
20impl TargetKind {
21    pub const fn as_str(self) -> &'static str {
22        match self {
23            Self::Interpreter => "interpreter",
24            Self::Hbc => "hbc",
25            Self::WholeWasm => "whole-wasm",
26        }
27    }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct RuntimeBackend(String);
32
33impl RuntimeBackend {
34    pub fn new(value: impl Into<String>) -> Result<Self, &'static str> {
35        let value = value.into();
36        if value.trim().is_empty() {
37            return Err("runtime backend must be non-empty");
38        }
39        Ok(Self(value))
40    }
41
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub enum Capability {
49    EventSemanticBoundary,
50    EventInstruction,
51    EventCall,
52    EventException,
53    EventEffect,
54    EventSuspension,
55    EventLifecycle,
56    InspectSourceLocation,
57    InspectCurrentFrame,
58    InspectFrames,
59    InspectLocals,
60    InspectStack,
61    InspectValuePreview,
62    InspectSnapshot,
63    ControlPause,
64    ControlSingleStep,
65    ControlResume,
66    ControlSettle,
67    ControlTerminate,
68    TransformHalc,
69    TransformHbc,
70    RetransformHalc,
71    RetransformHbc,
72}
73
74impl Capability {
75    pub const fn is_control(self) -> bool {
76        matches!(
77            self,
78            Self::ControlPause
79                | Self::ControlSingleStep
80                | Self::ControlResume
81                | Self::ControlSettle
82                | Self::ControlTerminate
83        )
84    }
85
86    pub const fn is_transform(self) -> bool {
87        matches!(
88            self,
89            Self::TransformHalc | Self::TransformHbc | Self::RetransformHalc | Self::RetransformHbc
90        )
91    }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
95#[repr(u8)]
96pub enum EventKind {
97    SemanticBoundary = 0,
98    InstructionExecute = 1,
99    CallEnter = 2,
100    CallReturn = 3,
101    ExceptionRaise = 4,
102    ExceptionUnwind = 5,
103    VarSet = 6,
104    FieldSet = 7,
105    PromiseSuspend = 8,
106    PromiseResume = 9,
107    MachineSuspend = 10,
108    MachineResume = 11,
109    ExecutionTerminal = 12,
110    ProtocolCall = 13,
111}
112
113impl EventKind {
114    pub const fn required_capability(self) -> Capability {
115        match self {
116            Self::SemanticBoundary => Capability::EventSemanticBoundary,
117            Self::InstructionExecute => Capability::EventInstruction,
118            Self::CallEnter | Self::CallReturn => Capability::EventCall,
119            Self::ExceptionRaise | Self::ExceptionUnwind => Capability::EventException,
120            Self::VarSet | Self::FieldSet => Capability::EventEffect,
121            Self::PromiseSuspend
122            | Self::PromiseResume
123            | Self::MachineSuspend
124            | Self::MachineResume => Capability::EventSuspension,
125            Self::ExecutionTerminal => Capability::EventLifecycle,
126            Self::ProtocolCall => Capability::EventSemanticBoundary,
127        }
128    }
129
130    pub const fn supports_target(self, target: TargetKind) -> bool {
131        match target {
132            TargetKind::Interpreter => matches!(
133                self,
134                Self::SemanticBoundary
135                    | Self::CallEnter
136                    | Self::CallReturn
137                    | Self::ExceptionRaise
138                    | Self::VarSet
139                    | Self::FieldSet
140                    | Self::PromiseSuspend
141                    | Self::PromiseResume
142                    | Self::ExecutionTerminal
143            ),
144            TargetKind::Hbc => matches!(
145                self,
146                Self::InstructionExecute
147                    | Self::CallEnter
148                    | Self::CallReturn
149                    | Self::ExceptionUnwind
150                    | Self::MachineSuspend
151                    | Self::MachineResume
152                    | Self::ExecutionTerminal
153            ),
154            TargetKind::WholeWasm => matches!(self, Self::ProtocolCall | Self::ExecutionTerminal),
155        }
156    }
157
158    const fn bit(self) -> u64 {
159        1_u64 << (self as u8)
160    }
161}
162
163#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
164pub struct EventMask(u64);
165
166impl EventMask {
167    pub const fn empty() -> Self {
168        Self(0)
169    }
170
171    pub const fn is_empty(self) -> bool {
172        self.0 == 0
173    }
174
175    pub const fn contains(self, event: EventKind) -> bool {
176        self.0 & event.bit() != 0
177    }
178
179    pub fn insert(&mut self, event: EventKind) {
180        self.0 |= event.bit();
181    }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub struct ProjectionLimits {
186    pub max_items: usize,
187    pub max_depth: usize,
188    pub max_bytes: usize,
189}
190
191impl Default for ProjectionLimits {
192    fn default() -> Self {
193        Self {
194            max_items: 256,
195            max_depth: 16,
196            max_bytes: 64 * 1024,
197        }
198    }
199}
200
201impl ProjectionLimits {
202    pub const fn is_bounded(self) -> bool {
203        self.max_items > 0 && self.max_depth > 0 && self.max_bytes > 0
204    }
205}
206
207#[derive(Debug, Clone, Default, PartialEq, Eq)]
208pub struct ProjectionRequest {
209    pub source_location: bool,
210    pub current_frame: Option<ProjectionLimits>,
211    pub frames: Option<ProjectionLimits>,
212    pub locals: Option<ProjectionLimits>,
213    pub stack: Option<ProjectionLimits>,
214    pub value_preview: Option<ProjectionLimits>,
215    pub machine_snapshot: Option<ProjectionLimits>,
216}
217
218impl ProjectionRequest {
219    pub fn is_bounded(&self) -> bool {
220        [
221            self.current_frame,
222            self.frames,
223            self.locals,
224            self.stack,
225            self.value_preview,
226            self.machine_snapshot,
227        ]
228        .into_iter()
229        .flatten()
230        .all(ProjectionLimits::is_bounded)
231    }
232
233    pub fn required_capabilities(&self) -> BTreeSet<Capability> {
234        let mut required = BTreeSet::new();
235        if self.source_location {
236            required.insert(Capability::InspectSourceLocation);
237        }
238        for (projection, capability) in [
239            (self.current_frame, Capability::InspectCurrentFrame),
240            (self.frames, Capability::InspectFrames),
241            (self.locals, Capability::InspectLocals),
242            (self.stack, Capability::InspectStack),
243            (self.value_preview, Capability::InspectValuePreview),
244            (self.machine_snapshot, Capability::InspectSnapshot),
245        ] {
246            if projection.is_some() {
247                required.insert(capability);
248            }
249        }
250        required
251    }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub enum EventDelivery {
256    Callback,
257    Queue { capacity: usize },
258}
259
260impl Default for EventDelivery {
261    fn default() -> Self {
262        Self::Queue { capacity: 256 }
263    }
264}
265
266impl EventDelivery {
267    pub const fn is_bounded(&self) -> bool {
268        match self {
269            Self::Callback => true,
270            Self::Queue { capacity } => *capacity > 0,
271        }
272    }
273}
274
275#[derive(Debug, Clone, Default, PartialEq, Eq)]
276pub struct InstrumentFilter {
277    pub session_id: Option<String>,
278    pub target_ids: BTreeSet<String>,
279    pub target_kinds: BTreeSet<TargetKind>,
280    pub backends: BTreeSet<RuntimeBackend>,
281}
282
283impl InstrumentFilter {
284    pub fn matches(&self, target: &TargetDescriptor) -> bool {
285        self.session_id
286            .as_ref()
287            .map_or(true, |session| session == &target.session_id)
288            && (self.target_ids.is_empty() || self.target_ids.contains(&target.target_id))
289            && (self.target_kinds.is_empty() || self.target_kinds.contains(&target.kind))
290            && (self.backends.is_empty() || self.backends.contains(&target.backend))
291    }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct InstrumentRegistration {
296    pub instrument_id: String,
297    pub session_id: String,
298    pub mode: InstrumentMode,
299    pub capabilities: BTreeSet<Capability>,
300    pub events: BTreeSet<EventKind>,
301    pub filter: InstrumentFilter,
302    pub projection: ProjectionRequest,
303    pub delivery: EventDelivery,
304}
305
306impl InstrumentRegistration {
307    pub fn validate(&self) -> Result<(), &'static str> {
308        if self.instrument_id.trim().is_empty() {
309            return Err("instrument id must be non-empty");
310        }
311        if self.session_id.trim().is_empty() {
312            return Err("instrument session id must be non-empty");
313        }
314        if !self.projection.is_bounded() {
315            return Err("instrument projections must be bounded");
316        }
317        if !self.delivery.is_bounded() {
318            return Err("queued event delivery must have positive capacity");
319        }
320        if self
321            .events
322            .iter()
323            .any(|event| !self.capabilities.contains(&event.required_capability()))
324        {
325            return Err("event subscriptions require their event capability");
326        }
327        if !self
328            .projection
329            .required_capabilities()
330            .is_subset(&self.capabilities)
331        {
332            return Err("instrument projections require their inspection capability");
333        }
334        if self
335            .filter
336            .session_id
337            .as_ref()
338            .is_some_and(|session_id| session_id.trim().is_empty())
339            || self
340                .filter
341                .target_ids
342                .iter()
343                .any(|target_id| target_id.trim().is_empty())
344        {
345            return Err("instrument filters cannot contain empty ids");
346        }
347        if self.mode != InstrumentMode::Control
348            && self
349                .capabilities
350                .iter()
351                .copied()
352                .any(Capability::is_control)
353        {
354            return Err("only control instruments can request control capabilities");
355        }
356        if self.mode != InstrumentMode::Transform
357            && self
358                .capabilities
359                .iter()
360                .copied()
361                .any(Capability::is_transform)
362        {
363            return Err("only transform instruments can request transform capabilities");
364        }
365        Ok(())
366    }
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct TargetDescriptor {
371    pub target_id: String,
372    pub session_id: String,
373    pub kind: TargetKind,
374    pub backend: RuntimeBackend,
375    pub capabilities: BTreeSet<Capability>,
376}
377
378impl TargetDescriptor {
379    pub fn validate(&self) -> Result<(), &'static str> {
380        if self.target_id.trim().is_empty() {
381            return Err("target id must be non-empty");
382        }
383        if self.session_id.trim().is_empty() {
384            return Err("target session id must be non-empty");
385        }
386        Ok(())
387    }
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
391pub struct InstrumentHandle {
392    instrument_id: String,
393    generation: u64,
394}
395
396impl InstrumentHandle {
397    pub(crate) fn new(instrument_id: String, generation: u64) -> Self {
398        Self {
399            instrument_id,
400            generation,
401        }
402    }
403
404    pub fn instrument_id(&self) -> &str {
405        &self.instrument_id
406    }
407
408    pub const fn generation(&self) -> u64 {
409        self.generation
410    }
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
414pub struct TargetHandle {
415    target_id: String,
416    generation: u64,
417}
418
419impl TargetHandle {
420    pub(crate) fn new(target_id: String, generation: u64) -> Self {
421        Self {
422            target_id,
423            generation,
424        }
425    }
426
427    pub fn target_id(&self) -> &str {
428        &self.target_id
429    }
430
431    pub const fn generation(&self) -> u64 {
432        self.generation
433    }
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub enum EventPhase {
438    Live,
439    Replay,
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443pub enum InstrumentDirective {
444    Continue,
445    Suspend,
446    StepNext,
447    Terminate,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq)]
451pub struct SourceSpan {
452    pub start: usize,
453    pub end: usize,
454}
455
456#[derive(Debug, Clone, Default, PartialEq, Eq)]
457pub struct EventLocation {
458    pub source_id: Option<String>,
459    pub form_path: Option<Vec<usize>>,
460    pub span: Option<SourceSpan>,
461    pub function: Option<String>,
462    pub instruction_pointer: Option<usize>,
463}
464
465#[derive(Debug, Clone, PartialEq, Eq)]
466pub struct EventEnvelope<D = BTreeMap<String, String>> {
467    pub schema: String,
468    pub protocol: String,
469    pub instrument_id: String,
470    pub runtime: RuntimeBackend,
471    pub session_id: String,
472    pub target_id: String,
473    pub target_kind: TargetKind,
474    pub generation: u64,
475    pub sequence: u64,
476    pub phase: EventPhase,
477    pub event: EventKind,
478    pub location: Option<EventLocation>,
479    pub data: D,
480}
481
482impl<D> EventEnvelope<D> {
483    pub fn validate(&self) -> Result<(), &'static str> {
484        if self.schema != INSTRUMENTATION_EVENT_SCHEMA {
485            return Err("unsupported instrumentation event schema");
486        }
487        if self.protocol != INSTRUMENTATION_PROTOCOL {
488            return Err("unsupported instrumentation protocol");
489        }
490        if self.instrument_id.trim().is_empty()
491            || self.session_id.trim().is_empty()
492            || self.target_id.trim().is_empty()
493        {
494            return Err("instrument, session, and target ids must be non-empty");
495        }
496        if !self.event.supports_target(self.target_kind) {
497            return Err("event kind is not supported by target kind");
498        }
499        if let Some(location) = &self.location {
500            if self.target_kind == TargetKind::Interpreter && location.instruction_pointer.is_some()
501            {
502                return Err("interpreter events cannot claim an instruction pointer");
503            }
504            if self.target_kind == TargetKind::Hbc
505                && self.event == EventKind::InstructionExecute
506                && location.form_path.is_some()
507            {
508                return Err("HBC instruction events cannot claim an AST form path");
509            }
510            if location
511                .span
512                .as_ref()
513                .is_some_and(|span| span.end < span.start)
514            {
515                return Err("event source span end precedes its start");
516            }
517        }
518        Ok(())
519    }
520}