Skip to main content

miden_core/events/
sys_events.rs

1use core::fmt;
2
3use super::{EventId, EventName};
4
5// SYSTEM EVENTS
6// ================================================================================================
7
8/// Defines a set of host-side actions which can be initiated from the VM.
9///
10/// Most actions update or query one of the three advice-provider components: Merkle store, advice
11/// stack, or advice map. Deferred-DAG actions update host-side deferred state, and evaluation may
12/// also push canonical node data to the advice stack.
13///
14/// All actions, except for `MerkleNodeMerge`, `Ext2Inv` and `UpdateMerkleNode` can be invoked
15/// directly from Miden assembly via dedicated instructions.
16///
17/// System event IDs are derived from blake3-hashing their names (prefixed with "sys::").
18///
19/// The enum variant order matches the indices in SYSTEM_EVENT_LOOKUP, allowing efficient const
20/// lookup via `to_event_id()`. The discriminants are implicitly 0, 1, 2, ... `COUNT - 1`.
21#[derive(Copy, Clone, Debug, Eq, PartialEq)]
22#[repr(u8)]
23pub enum SystemEvent {
24    // MERKLE STORE EVENTS
25    // --------------------------------------------------------------------------------------------
26    /// Creates a new Merkle tree in the advice provider by combining Merkle trees with the
27    /// specified roots. The root of the new tree is defined as `Hash(LEFT_ROOT, RIGHT_ROOT)`.
28    ///
29    /// Inputs:
30    ///   Operand stack: [LEFT_ROOT, RIGHT_ROOT, ...]
31    ///   Merkle store: {LEFT_ROOT, RIGHT_ROOT}
32    ///
33    /// Outputs:
34    ///   Operand stack: [LEFT_ROOT, RIGHT_ROOT, ...]
35    ///   Merkle store: {LEFT_ROOT, RIGHT_ROOT, hash(LEFT_ROOT, RIGHT_ROOT)}
36    ///
37    /// After the operation, both the original trees and the new tree remains in the advice
38    /// provider (i.e., the input trees are not removed).
39    MerkleNodeMerge,
40
41    // ADVICE STACK SYSTEM EVENTS
42    // --------------------------------------------------------------------------------------------
43    /// Pushes a node of the Merkle tree specified by the values on the top of the operand stack
44    /// onto the advice stack in structural order for consumption by `AdvPopW`.
45    ///
46    /// Inputs:
47    ///   Operand stack: [depth, index, TREE_ROOT, ...]
48    ///   Advice stack: [...]
49    ///   Merkle store: {TREE_ROOT<-NODE}
50    ///
51    /// Outputs:
52    ///   Operand stack: [depth, index, TREE_ROOT, ...]
53    ///   Advice stack: [NODE, ...]
54    ///   Merkle store: {TREE_ROOT<-NODE}
55    MerkleNodeToStack,
56
57    /// Pushes a list of field elements onto the advice stack. The list is looked up in the advice
58    /// map using the specified word from the operand stack as the key.
59    ///
60    /// Inputs:
61    ///   Operand stack: [KEY, ...]
62    ///   Advice stack: [...]
63    ///   Advice map: {KEY: values}
64    ///
65    /// Outputs:
66    ///   Operand stack: [KEY, ...]
67    ///   Advice stack: [values, ...]
68    ///   Advice map: {KEY: values}
69    MapValueToStack,
70
71    /// Pushes the number of elements in a list of field elements onto the advice stack. The list is
72    /// looked up in the advice map using the specified word from the operand stack as the key.
73    ///
74    /// Inputs:
75    ///   Operand stack: [KEY, ...]
76    ///   Advice stack: [...]
77    ///   Advice map: {KEY: values}
78    ///
79    /// Outputs:
80    ///   Operand stack: [KEY, ...]
81    ///   Advice stack: [values.len(), ...]
82    ///   Advice map: {KEY: values}
83    MapValueCountToStack,
84
85    /// Pushes a list of field elements onto the advice stack, along with the number of elements in
86    /// that list. The list is looked up in the advice map using the word at the top of the operand
87    /// stack as the key.
88    ///
89    /// Notice that the resulting elements list is not padded.
90    ///
91    /// Inputs:
92    ///   Operand stack: [KEY, ...]
93    ///   Advice stack: [...]
94    ///   Advice map: {KEY: values}
95    ///
96    /// Outputs:
97    ///   Operand stack: [KEY, ...]
98    ///   Advice stack: [num_values, values, ...]
99    ///   Advice map: {KEY: values}
100    MapValueToStackN0,
101
102    /// Pushes a padded list of field elements onto the advice stack, along with the number of
103    /// elements in that list. The list is looked up in the advice map using the word at the top of
104    /// the operand stack as the key.
105    ///
106    /// Notice that the elements list obtained from the advice map will be padded with zeros,
107    /// increasing its length to the next multiple of 4.
108    ///
109    /// Inputs:
110    ///   Operand stack: [KEY, ...]
111    ///   Advice stack: [...]
112    ///   Advice map: {KEY: values}
113    ///
114    /// Outputs:
115    ///   Operand stack: [KEY, ...]
116    ///   Advice stack: [num_values, values, padding, ...]
117    ///   Advice map: {KEY: values}
118    MapValueToStackN4,
119
120    /// Pushes a padded list of field elements onto the advice stack, along with the number of
121    /// elements in that list. The list is looked up in the advice map using the word at the top of
122    /// the operand stack as the key.
123    ///
124    /// Notice that the elements list obtained from the advice map will be padded with zeros,
125    /// increasing its length to the next multiple of 8.
126    ///
127    /// Inputs:
128    ///   Operand stack: [KEY, ...]
129    ///   Advice stack: [...]
130    ///   Advice map: {KEY: values}
131    ///
132    /// Outputs:
133    ///   Operand stack: [KEY, ...]
134    ///   Advice stack: [num_values, values, padding, ...]
135    ///   Advice map: {KEY: values}
136    MapValueToStackN8,
137
138    /// Pushes a flag onto the advice stack whether advice map has an entry with specified key.
139    ///
140    /// If the advice map has the entry with the key equal to the key placed at the top of the
141    /// operand stack, `1` will be pushed to the advice stack and `0` otherwise.
142    ///
143    /// Inputs:
144    ///   Operand stack: [KEY, ...]
145    ///   Advice stack:  [...]
146    ///
147    /// Outputs:
148    ///   Operand stack: [KEY, ...]
149    ///   Advice stack:  [has_mapkey, ...]
150    HasMapKey,
151
152    /// Given an element in a quadratic extension field on the top of the stack (i.e., a0, b1),
153    /// computes its multiplicative inverse and push the result onto the advice stack.
154    ///
155    /// Inputs:
156    ///   Operand stack: [a1, a0, ...]
157    ///   Advice stack: [...]
158    ///
159    /// Outputs:
160    ///   Operand stack: [a1, a0, ...]
161    ///   Advice stack: [b0, b1...]
162    ///
163    /// Where (b0, b1) is the multiplicative inverse of the extension field element (a0, a1) at the
164    /// top of the stack.
165    Ext2Inv,
166
167    /// Pushes the number of the leading zeros of the top stack element onto the advice stack.
168    ///
169    /// Inputs:
170    ///   Operand stack: [n, ...]
171    ///   Advice stack: [...]
172    ///
173    /// Outputs:
174    ///   Operand stack: [n, ...]
175    ///   Advice stack: [leading_zeros, ...]
176    U32Clz,
177
178    /// Pushes the number of the trailing zeros of the top stack element onto the advice stack.
179    ///
180    /// Inputs:
181    ///   Operand stack: [n, ...]
182    ///   Advice stack: [...]
183    ///
184    /// Outputs:
185    ///   Operand stack: [n, ...]
186    ///   Advice stack: [trailing_zeros, ...]
187    U32Ctz,
188
189    /// Pushes the number of the leading ones of the top stack element onto the advice stack.
190    ///
191    /// Inputs:
192    ///   Operand stack: [n, ...]
193    ///   Advice stack: [...]
194    ///
195    /// Outputs:
196    ///   Operand stack: [n, ...]
197    ///   Advice stack: [leading_ones, ...]
198    U32Clo,
199
200    /// Pushes the number of the trailing ones of the top stack element onto the advice stack.
201    ///
202    /// Inputs:
203    ///   Operand stack: [n, ...]
204    ///   Advice stack: [...]
205    ///
206    /// Outputs:
207    ///   Operand stack: [n, ...]
208    ///   Advice stack: [trailing_ones, ...]
209    U32Cto,
210
211    /// Pushes the base 2 logarithm of the top stack element, rounded down.
212    /// Inputs:
213    ///   Operand stack: [n, ...]
214    ///   Advice stack: [...]
215    ///
216    /// Outputs:
217    ///   Operand stack: [n, ...]
218    ///   Advice stack: [ilog2(n), ...]
219    ILog2,
220
221    // ADVICE MAP SYSTEM EVENTS
222    // --------------------------------------------------------------------------------------------
223    /// Reads words from memory at the specified range and inserts them into the advice map under
224    /// the key `KEY` located at the top of the stack.
225    ///
226    /// Inputs:
227    ///   Operand stack: [KEY, start_addr, end_addr, ...]
228    ///   Advice map: {...}
229    ///
230    /// Outputs:
231    ///   Operand stack: [KEY, start_addr, end_addr, ...]
232    ///   Advice map: {KEY: values}
233    ///
234    /// Where `values` are the elements located in memory[start_addr..end_addr].
235    MemToMap,
236
237    /// Reads two words from the operand stack and inserts them into the advice map under
238    /// `hdword(A, B)`.
239    ///
240    /// Inputs:
241    ///   Operand stack: [A, B, ...]
242    ///   Advice map: {...}
243    ///
244    /// Outputs:
245    ///   Operand stack: [A, B, ...]
246    ///   Advice map: {KEY: [a0, a1, a2, a3, b0, b1, b2, b3]}
247    ///
248    /// KEY is the generic Eidos hash of the eight Felts in `A || B`.
249    HdwordToMap,
250
251    /// Reads four words from the operand stack and inserts them into the advice map under the key
252    /// defined by the hash of these words.
253    ///
254    /// Inputs:
255    ///   Operand stack: [A, B, C, D, ...]
256    ///   Advice map: {...}
257    ///
258    /// Outputs:
259    ///   Operand stack: [A, B, C, D, ...]
260    ///   Advice map: {KEY: [A, B, C, D]} (16 elements)
261    ///
262    /// Where:
263    /// - KEY is computed as the canonical Eidos hash of `[A, B, C, D]`, using two 8-Felt
264    ///   compression blocks with the full 16-Felt length bound into the initial chaining value.
265    HqwordToMap,
266
267    /// Reads three words from the operand stack and inserts the top two words into the advice map
268    /// under the key `Eidos::compress(C, A || B)`.
269    ///
270    /// Inputs:
271    ///   Operand stack: [A, B, C, ...]
272    ///   Advice map: {...}
273    ///
274    /// Outputs:
275    ///   Operand stack: [A, B, C, ...]
276    ///   Advice map: {KEY: [a0, a1, a2, a3, b0, b1, b2, b3]}
277    ///
278    /// In particular, using the framed initial CV for `(domain_tag = d, param0 = 8)` produces
279    /// `Eidos::hash_elements_in_domain(A || B, d)`.
280    CompressToMap,
281
282    // DEFERRED-DAG SYSTEM EVENTS
283    // --------------------------------------------------------------------------------------------
284    /// Registers and eagerly evaluates a deferred node whose full payload is on the operand stack.
285    ///
286    /// `CV` is one word containing the framed initial Eidos chaining value.
287    /// `PAYLOAD_LO || PAYLOAD_HI` is eight field elements:
288    /// either one [`crate::deferred::DataChunk`], two child digests (`lhs || rhs`) for a
289    /// precompile-owned join, or one `lhs || rhs` pair for a pair-list node. The installed registry
290    /// recovers the semantic
291    /// [`crate::deferred::EidosFrame`] from `CV` and decodes it via
292    /// [`crate::deferred::DeferredState::decode`]. Framework-owned AND and `TRUE` nodes are not
293    /// accepted by this generic event; `log_deferred` constructs rolling AND nodes. Frames that
294    /// semantically require more data chunks or pairs are rejected during precompile-specific
295    /// evaluation. Registration is performed by
296    /// [`crate::deferred::DeferredState::register`], so semantic failures surface immediately.
297    ///
298    /// This event does not push advice or return the node digest. The stack arguments are visible
299    /// in the VM execution trace, but the host-side registration is not constrained by the event.
300    /// Assembly code that later relies on the digest must compute it inside the VM from the same
301    /// `CV` and payload.
302    ///
303    /// Inputs:
304    ///   Operand stack: [event_id, CV, PAYLOAD_LO, PAYLOAD_HI, ...]
305    ///
306    /// Outputs:
307    ///   Operand stack:  unchanged
308    ///   Advice stack:   unchanged
309    ///   Deferred state: node registered and semantically evaluated
310    DeferredRegister,
311
312    /// Evaluates a registered deferred node and pushes its canonical frame and payload as advice.
313    ///
314    /// `NODE_DIGEST` is one word (4 field elements) and must already be registered in deferred
315    /// state. The handler evaluates it with [`crate::deferred::DeferredState::evaluate_digest`],
316    /// fetches the canonical node, and pushes its frame followed by its payload to the advice
317    /// stack.
318    ///
319    /// The frame is emitted first in advice-pop order so `adv_pushw adv_pushw adv_pushw` leaves
320    /// `[PAYLOAD_LO, PAYLOAD_HI, FRAME, ...]` on the operand stack for a single 8-felt payload.
321    /// Data payloads push two words per 8-felt chunk in advice order `HIGH, LOW`, preserving
322    /// canonical chunk order. Join payloads use the same two-word LIFO convention, leaving
323    /// `[lhs, rhs, FRAME, ...]`. `TRUE` pushes one zero word. These felts are unbound host hints.
324    /// Before proof-relevant use, assembly code must relate them with VM instructions to values
325    /// established independently of that advice.
326    ///
327    /// Inputs:
328    ///   Operand stack: [event_id, NODE_DIGEST, ...]
329    ///
330    /// Outputs:
331    ///   Operand stack: unchanged
332    ///   Advice stack:  canonical frame, then canonical payload words for `adv_pushw` LIFO
333    /// consumption
334    DeferredEvaluate,
335
336    /// Evaluates a registered deferred node and pushes only its canonical frame as advice.
337    ///
338    /// `NODE_DIGEST` is one word (4 field elements) and must already be registered in deferred
339    /// state. `TRUE` pushes one zero word. The returned frame is an unbound host hint; before
340    /// proof-relevant use, assembly code must relate it with VM instructions to a value established
341    /// independently of that advice.
342    ///
343    /// Inputs:
344    ///   Operand stack: [event_id, NODE_DIGEST, ...]
345    ///
346    /// Outputs:
347    ///   Operand stack: unchanged
348    ///   Advice stack:  canonical frame only
349    DeferredEvaluateFrame,
350
351    /// Evaluates a registered deferred node and pushes only its canonical payload as advice.
352    ///
353    /// Data payloads push two words per 8-felt chunk in advice order `HIGH, LOW` so
354    /// `adv_pushw adv_pushw` leaves `[LOW, HIGH, ...]` on the operand stack for that chunk. Chunks
355    /// are emitted in canonical chunk order. Join payloads use the same two-word LIFO convention,
356    /// leaving `[lhs, rhs, ...]` after two `adv_pushw`s. `TRUE` pushes no advice. These felts are
357    /// unbound host hints. Before proof-relevant use, assembly code must relate them with VM
358    /// instructions to values established independently of that advice.
359    ///
360    /// Inputs:
361    ///   Operand stack: [event_id, NODE_DIGEST, ...]
362    ///
363    /// Outputs:
364    ///   Operand stack: unchanged
365    ///   Advice stack:  canonical payload only, word-ordered for `adv_pushw` LIFO consumption
366    DeferredEvaluatePayload,
367
368    /// Registers and eagerly evaluates a memory-backed deferred node.
369    ///
370    /// `CV` is one word containing the framed initial Eidos chaining value. The installed registry
371    /// recovers its semantic frame to determine the memory-backed payload shape. The stack-supplied
372    /// `ptr` and `n_chunks` are visible in the VM
373    /// execution trace and select the range `[ptr, ptr + 8 * n_chunks)`. The host reads `n_chunks`
374    /// 8-felt [`crate::deferred::DataChunk`] values from that range, but this event adds no AIR
375    /// constraint tying the registered contents to those memory cells.
376    ///
377    /// The registered deferred-CHUNKS domain denotes framework-owned opaque data, while other data
378    /// frames remain precompile-owned. Pair-list frames interpret chunks as `lhs || rhs` pairs.
379    /// Precompile-owned join frames require `n_chunks == 1` and interpret the single chunk as
380    /// `lhs || rhs`. Framework-owned AND and `TRUE` nodes are not accepted by this generic event;
381    /// `log_deferred` constructs rolling AND nodes. The handler performs a cheap budget pre-check
382    /// before allocating or reading memory, then delegates registration to
383    /// [`crate::deferred::DeferredState::register`].
384    ///
385    /// This event does not push advice or return the node digest. A program that relies on the
386    /// registered node must compute its digest with VM instructions from the same `CV` and ordered
387    /// chunk sequence. The `register_mem` MASM wrapper does this with the canonical Eidos deferred
388    /// framing: the frame determines the initial chaining word, then payload chunks are compressed
389    /// in order. If the event and the VM hash different chunk
390    /// sequences, the VM-computed digest does not identify the host-registered node and cannot bind
391    /// that registration into a proof-relevant deferred claim.
392    ///
393    /// Inputs:
394    ///   Operand stack: [event_id, n_chunks, CV, ptr, ...]
395    ///
396    /// Outputs:
397    ///   Operand stack:  unchanged
398    ///   Advice stack:   unchanged
399    ///   Deferred state: node registered and semantically evaluated
400    DeferredRegisterData,
401
402    /// Exports a registered true assertion's witness into the advice map.
403    ///
404    /// The selected node may be unlogged, but cannot be literal TRUE. Its original digest is
405    /// both the witness root and map key; any existing value is replaced with the computed witness.
406    /// The value is `[byte_len, packed_u32_le(witness_bytes)...]`. Only the final u32 limb is
407    /// zero-padded, and `byte_len` excludes padding.
408    ///
409    /// Both stacks and the deferred root are preserved. Errors leave existing advice unchanged.
410    /// Consumers must validate the witness and bind its root to an independently established
411    /// expected digest.
412    ///
413    /// Inputs:
414    ///   Operand stack: [event_id, NODE_DIGEST, ...]
415    ///
416    /// Outputs:
417    ///   Operand stack: unchanged
418    ///   Advice stack: unchanged
419    ///   Advice map: {NODE_DIGEST: [byte_len, packed_u32_le(witness_bytes)...]}
420    DeferredExportWitness,
421
422    // NON-MUTATING SYSTEM EVENTS
423    // --------------------------------------------------------------------------------------------
424    /// Signals an optional, read-only trace event to the host.
425    ///
426    /// Assembly programs emit trace events with `trace`, `trace.CONST`, or `trace.event("...")`.
427    /// When the underlying `emit` observes this system event ID at stack position 0, the VM
428    /// forwards the user trace event ID at stack position 1 to the host's trace handler. The
429    /// immediate form lowers to `push.<user_trace_id> push.<sys::trace_event> emit drop drop`.
430    /// Trace handlers can observe the processor state, but cannot mutate VM state or the advice
431    /// provider. If no handler is registered for the user trace event ID, the event is a no-op.
432    ///
433    /// Hosts are expected not to raise an error if they encounter a `user_trace_id` for which no
434    /// trace handler is registered.
435    ///
436    /// Inputs:
437    ///   Operand stack: [sys::trace_event, user_trace_id, ...]
438    ///
439    /// Outputs:
440    ///   Operand stack: unchanged
441    ///   Advice provider: unchanged
442    TraceEvent,
443}
444
445impl SystemEvent {
446    /// Attempts to convert an EventId into a SystemEvent by looking it up in the const table.
447    ///
448    /// Returns `Some(SystemEvent)` if the ID matches a known system event, `None` otherwise.
449    /// This uses a const lookup table with hardcoded EventIds, avoiding runtime hash computation.
450    pub const fn from_event_id(event_id: EventId) -> Option<Self> {
451        let lookup = Self::LOOKUP;
452        let mut i = 0;
453        while i < lookup.len() {
454            if lookup[i].id.as_u64() == event_id.as_u64() {
455                return Some(lookup[i].event);
456            }
457            i += 1;
458        }
459        None
460    }
461
462    /// Attempts to convert a name into a SystemEvent by looking it up in the const table.
463    ///
464    /// Returns `Some(SystemEvent)` if the name matches a known system event, `None` otherwise.
465    /// This uses const string comparison against the lookup table.
466    pub const fn from_name(name: &str) -> Option<Self> {
467        let lookup = Self::LOOKUP;
468        let mut i = 0;
469        while i < lookup.len() {
470            if str_eq(name, lookup[i].name) {
471                return Some(lookup[i].event);
472            }
473            i += 1;
474        }
475        None
476    }
477
478    /// Returns the human-readable name of this system event as an [`EventName`].
479    ///
480    /// System event names are prefixed with `sys::` to distinguish them from user-defined events.
481    pub const fn event_name(&self) -> EventName {
482        EventName::new(Self::LOOKUP[*self as usize].name)
483    }
484
485    /// Returns the [`EventId`] for this system event.
486    ///
487    /// The ID is looked up from the const LOOKUP table using the enum's discriminant
488    /// as the index. The discriminants are explicitly set to match the array indices.
489    pub const fn event_id(&self) -> EventId {
490        Self::LOOKUP[*self as usize].id
491    }
492
493    /// Returns an array of all system event variants.
494    pub const fn all() -> [Self; Self::COUNT] {
495        [
496            Self::MerkleNodeMerge,
497            Self::MerkleNodeToStack,
498            Self::MapValueToStack,
499            Self::MapValueCountToStack,
500            Self::MapValueToStackN0,
501            Self::MapValueToStackN4,
502            Self::MapValueToStackN8,
503            Self::HasMapKey,
504            Self::Ext2Inv,
505            Self::U32Clz,
506            Self::U32Ctz,
507            Self::U32Clo,
508            Self::U32Cto,
509            Self::ILog2,
510            Self::MemToMap,
511            Self::HdwordToMap,
512            Self::HqwordToMap,
513            Self::CompressToMap,
514            Self::DeferredRegister,
515            Self::DeferredEvaluate,
516            Self::DeferredEvaluateFrame,
517            Self::DeferredEvaluatePayload,
518            Self::DeferredRegisterData,
519            Self::DeferredExportWitness,
520            Self::TraceEvent,
521        ]
522    }
523}
524
525impl From<SystemEvent> for EventName {
526    fn from(system_event: SystemEvent) -> Self {
527        system_event.event_name()
528    }
529}
530
531impl crate::prettier::PrettyPrint for SystemEvent {
532    fn render(&self) -> crate::prettier::Document {
533        crate::prettier::display(self)
534    }
535}
536
537impl fmt::Display for SystemEvent {
538    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539        const PREFIX_LEN: usize = "sys::".len();
540
541        let (_prefix, rest) = Self::LOOKUP[*self as usize].name.split_at(PREFIX_LEN);
542        write!(f, "{rest}")
543    }
544}
545
546// LOOKUP TABLE
547// ================================================================================================
548
549/// An entry in the system event lookup table, containing all metadata for a system event.
550#[derive(Copy, Clone, Debug)]
551pub(crate) struct SystemEventEntry {
552    /// The unique event ID (hash of the name)
553    pub id: EventId,
554    /// The system event variant
555    pub event: SystemEvent,
556    /// The full event name string (e.g., "sys::merkle_node_merge")
557    pub name: &'static str,
558}
559
560impl SystemEvent {
561    /// The total number of system events.
562    pub const COUNT: usize = 25;
563
564    /// Lookup table mapping system events to their metadata.
565    ///
566    /// The enum variant order matches the indices in this table, allowing efficient const
567    /// lookup via array indexing using discriminants.
568    const LOOKUP: [SystemEventEntry; Self::COUNT] = [
569        SystemEventEntry {
570            id: EventId::from_u64(7243907139105902342),
571            event: SystemEvent::MerkleNodeMerge,
572            name: "sys::merkle_node_merge",
573        },
574        SystemEventEntry {
575            id: EventId::from_u64(6873007751276594108),
576            event: SystemEvent::MerkleNodeToStack,
577            name: "sys::merkle_node_to_stack",
578        },
579        SystemEventEntry {
580            id: EventId::from_u64(17843484659000820118),
581            event: SystemEvent::MapValueToStack,
582            name: "sys::map_value_to_stack",
583        },
584        SystemEventEntry {
585            id: EventId::from_u64(3470274154276391308),
586            event: SystemEvent::MapValueCountToStack,
587            name: "sys::map_value_count_to_stack",
588        },
589        SystemEventEntry {
590            id: EventId::from_u64(11775886982554463322),
591            event: SystemEvent::MapValueToStackN0,
592            name: "sys::map_value_to_stack_n_0",
593        },
594        SystemEventEntry {
595            id: EventId::from_u64(3443305460233942990),
596            event: SystemEvent::MapValueToStackN4,
597            name: "sys::map_value_to_stack_n_4",
598        },
599        SystemEventEntry {
600            id: EventId::from_u64(1741586542981559489),
601            event: SystemEvent::MapValueToStackN8,
602            name: "sys::map_value_to_stack_n_8",
603        },
604        SystemEventEntry {
605            id: EventId::from_u64(5642583036089175977),
606            event: SystemEvent::HasMapKey,
607            name: "sys::has_map_key",
608        },
609        SystemEventEntry {
610            id: EventId::from_u64(9660728691489438960),
611            event: SystemEvent::Ext2Inv,
612            name: "sys::ext2_inv",
613        },
614        SystemEventEntry {
615            id: EventId::from_u64(1503707361178382932),
616            event: SystemEvent::U32Clz,
617            name: "sys::u32_clz",
618        },
619        SystemEventEntry {
620            id: EventId::from_u64(10656887096526143429),
621            event: SystemEvent::U32Ctz,
622            name: "sys::u32_ctz",
623        },
624        SystemEventEntry {
625            id: EventId::from_u64(12846584985739176048),
626            event: SystemEvent::U32Clo,
627            name: "sys::u32_clo",
628        },
629        SystemEventEntry {
630            id: EventId::from_u64(6773574803673468616),
631            event: SystemEvent::U32Cto,
632            name: "sys::u32_cto",
633        },
634        SystemEventEntry {
635            id: EventId::from_u64(7444351342957461231),
636            event: SystemEvent::ILog2,
637            name: "sys::ilog2",
638        },
639        SystemEventEntry {
640            id: EventId::from_u64(5768534446586058686),
641            event: SystemEvent::MemToMap,
642            name: "sys::mem_to_map",
643        },
644        SystemEventEntry {
645            id: EventId::from_u64(5988159172915333521),
646            event: SystemEvent::HdwordToMap,
647            name: "sys::hdword_to_map",
648        },
649        SystemEventEntry {
650            id: EventId::from_u64(11723176702659679401),
651            event: SystemEvent::HqwordToMap,
652            name: "sys::hqword_to_map",
653        },
654        SystemEventEntry {
655            // This string is part of the event identity and is hashed into compiled programs.
656            // Changing it requires a MAST format transition even though the assembly instruction is
657            // spelled `adv.insert_compress`.
658            id: EventId::from_u64(454105713963103935),
659            event: SystemEvent::CompressToMap,
660            name: "sys::bcompress_to_map",
661        },
662        SystemEventEntry {
663            id: EventId::from_u64(2036565939244996992),
664            event: SystemEvent::DeferredRegister,
665            name: "sys::adv::register_deferred",
666        },
667        SystemEventEntry {
668            id: EventId::from_u64(2795079927114754645),
669            event: SystemEvent::DeferredEvaluate,
670            name: "sys::adv::evaluate_deferred",
671        },
672        SystemEventEntry {
673            id: EventId::from_u64(7236367774590045862),
674            event: SystemEvent::DeferredEvaluateFrame,
675            name: "sys::adv::evaluate_deferred_frame",
676        },
677        SystemEventEntry {
678            id: EventId::from_u64(8091749904895009326),
679            event: SystemEvent::DeferredEvaluatePayload,
680            name: "sys::adv::evaluate_deferred_payload",
681        },
682        SystemEventEntry {
683            id: EventId::from_u64(13021247594355482329),
684            event: SystemEvent::DeferredRegisterData,
685            name: "sys::adv::register_deferred_data",
686        },
687        SystemEventEntry {
688            id: EventId::from_u64(15159456528524262446),
689            event: SystemEvent::DeferredExportWitness,
690            name: "sys::adv::export_deferred_witness",
691        },
692        SystemEventEntry {
693            id: EventId::from_u64(1768618069850226410),
694            event: SystemEvent::TraceEvent,
695            name: "sys::trace_event",
696        },
697    ];
698}
699
700// HELPERS
701// ================================================================================================
702
703/// Const-compatible string equality check.
704const fn str_eq(a: &str, b: &str) -> bool {
705    let a_bytes = a.as_bytes();
706    let b_bytes = b.as_bytes();
707
708    if a_bytes.len() != b_bytes.len() {
709        return false;
710    }
711
712    let mut i = 0;
713    while i < a_bytes.len() {
714        if a_bytes[i] != b_bytes[i] {
715            return false;
716        }
717        i += 1;
718    }
719    true
720}
721
722#[cfg(test)]
723mod test {
724    use super::*;
725
726    #[test]
727    fn test_system_events() {
728        // Comprehensive test verifying consistency between SystemEvent::all() and
729        // SystemEvent::LOOKUP. This ensures all() and LOOKUP are in sync, lookup table has
730        // correct IDs/names, and all variants are covered.
731
732        // Verify lengths match COUNT
733        assert_eq!(SystemEvent::all().len(), SystemEvent::COUNT);
734        assert_eq!(SystemEvent::LOOKUP.len(), SystemEvent::COUNT);
735
736        // Iterate through both all() and LOOKUP together, checking all invariants
737        for (i, (event, entry)) in
738            SystemEvent::all().iter().zip(SystemEvent::LOOKUP.iter()).enumerate()
739        {
740            // Verify LOOKUP entry matches the event at the same index
741            assert_eq!(
742                entry.event, *event,
743                "LOOKUP[{}].event ({:?}) doesn't match all()[{}] ({:?})",
744                i, entry.event, i, event
745            );
746
747            // Verify LOOKUP entry ID matches enum lookup.
748            let looked_up_id = event.event_id();
749            assert_eq!(
750                entry.id,
751                looked_up_id,
752                "LOOKUP[{}].id is EventId::from_u64({}), but {:?}.event_id() returns EventId::from_u64({})",
753                i,
754                entry.id.as_u64(),
755                event,
756                looked_up_id.as_u64()
757            );
758
759            // Verify name has correct "sys::" prefix
760            assert!(
761                entry.name.starts_with("sys::"),
762                "SystemEvent name should start with 'sys::': {}",
763                entry.name
764            );
765            assert_eq!(
766                entry.id,
767                EventId::from_name(entry.name),
768                "LOOKUP entry for {} has a stale hardcoded event ID",
769                entry.name
770            );
771
772            // Verify from_event_id lookup works
773            let looked_up =
774                SystemEvent::from_event_id(entry.id).expect("SystemEvent should be found by ID");
775            assert_eq!(looked_up, *event);
776
777            // Verify from_name lookup works
778            let looked_up_by_name =
779                SystemEvent::from_name(entry.name).expect("SystemEvent should be found by name");
780            assert_eq!(looked_up_by_name, *event);
781
782            // Verify EventName conversion works
783            let event_name = event.event_name();
784            assert_eq!(event_name.as_str(), entry.name);
785            assert!(SystemEvent::from_name(event_name.as_str()).is_some());
786            let event_name_from_into: EventName = (*event).into();
787            assert_eq!(event_name_from_into.as_str(), entry.name);
788            assert!(SystemEvent::from_name(event_name_from_into.as_str()).is_some());
789
790            // Exhaustive match to ensure compile-time error when adding new variants
791            match event {
792                SystemEvent::MerkleNodeMerge
793                | SystemEvent::MerkleNodeToStack
794                | SystemEvent::MapValueToStack
795                | SystemEvent::MapValueCountToStack
796                | SystemEvent::MapValueToStackN0
797                | SystemEvent::MapValueToStackN4
798                | SystemEvent::MapValueToStackN8
799                | SystemEvent::HasMapKey
800                | SystemEvent::Ext2Inv
801                | SystemEvent::U32Clz
802                | SystemEvent::U32Ctz
803                | SystemEvent::U32Clo
804                | SystemEvent::U32Cto
805                | SystemEvent::ILog2
806                | SystemEvent::MemToMap
807                | SystemEvent::HdwordToMap
808                | SystemEvent::HqwordToMap
809                | SystemEvent::CompressToMap
810                | SystemEvent::DeferredRegister
811                | SystemEvent::DeferredEvaluate
812                | SystemEvent::DeferredEvaluateFrame
813                | SystemEvent::DeferredEvaluatePayload
814                | SystemEvent::DeferredRegisterData
815                | SystemEvent::DeferredExportWitness
816                | SystemEvent::TraceEvent => {},
817            }
818        }
819    }
820
821    #[test]
822    fn compress_to_map_preserves_its_wire_identity() {
823        assert_eq!(SystemEvent::CompressToMap.event_id(), EventId::from_u64(454105713963103935));
824        assert_eq!(SystemEvent::CompressToMap.event_name().as_str(), "sys::bcompress_to_map");
825    }
826}