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}