miden-processor 0.22.1

Miden VM processor
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
use alloc::sync::Arc;

use miden_air::trace::{RowIndex, chiplets::hasher::STATE_WIDTH, decoder::NUM_USER_OP_HELPERS};
use miden_core::{
    Felt, Word, ZERO,
    crypto::merkle::MerklePath,
    field::{BasedVectorSpace, Field, QuadFelt},
    mast::{MastForest, MastNodeId},
};

use crate::{
    ContextId,
    continuation_stack::{Continuation, ContinuationStack},
    trace::{chiplets::CircuitEvaluation, utils::split_u32_into_u16},
};

// TRACER TRAIT
// ================================================================================================

/// A trait for tracing the execution of a processor.
///
/// Allows for recording different aspects of the processor's execution. For example, the
/// [`crate::FastProcessor::execute_for_trace`] execution mode needs to build a
/// [`crate::fast::execution_tracer::TraceGenerationContext`] which records information necessary to
/// build the trace at each clock cycle.
///
/// A useful mental model to differentiate between the processor and the tracer is:
/// - Processor: maintains and mutates the state of the VM components (system, stack, memory, etc)
///   as execution progresses
/// - Tracer: records auxiliary information *derived from* the processor state
///
/// # Methods overview
///
/// The main methods of this trait are [`start_clock_cycle`] and [`finalize_clock_cycle`], which are
/// called at the start and end of each clock cycle, respectively. These methods are the only
/// methods for which a reference to the processor is passed in, due to the fact that the state that
/// the processor is in at that point is completely unambiguous (i.e. either before all mutations
/// for the clock cycle, or after all mutations except the clock register for the clock cycle).
///
/// We refer to all other methods as "in-cycle methods". These methods are called at some point
/// during the execution of the clock cycle, with the following guarantee:
///
/// *At most one in-cycle method is called between [`start_clock_cycle`] and
/// [`finalize_clock_cycle`] for each clock cycle.*
///
/// This guarantee is designed to prevent implementations from relying on undocumented ordering
/// between in-cycle methods, which may be subject to change in future versions of the processor.
///
/// Note 1: The only exception to the above rule is the method [`record_mast_forest_resolution`],
/// which is called at the point of resolving a MAST forest (i.e. when execution encounters an
/// external node or a dyn node with an unknown callee). This method is special in that we expect
/// implementations to simply record the resolved MAST forest, without performing any other logic
/// that depends on the timing of the call.
///
/// Note 2: The in-cycle methods are included in the trait for performance reasons; technically,
/// implementations *could* only use [`start_clock_cycle`] and [`finalize_clock_cycle`] to record
/// any auxiliary information they need by accessing the processor state before or after each clock
/// cycle. However, experiments show that this requires redoing too much work in the tracer already
/// performed by the processor, which degrades performance.
pub trait Tracer {
    type Processor;

    /// Signals the start of a new clock cycle, guaranteed to be called at the start of the clock
    /// cycle, before any mutations to the processor state is made. For example, it is safe to
    /// access the processor's stack and memory state as they are before executing the operation at
    /// the current clock cycle.
    ///
    /// `continuation` represents what is to be executed at the beginning of this clock cycle, while
    /// `continuation_stack` represents whatever comes after execution `continuation`.
    ///
    /// The following continuations do not occur at the start of a clock cycle, and hence will never
    /// be passed to this method:
    /// - Continuation::FinishExternal: because external nodes are resolved before starting a clock
    ///   cycle,
    /// - Continuation::EnterForest: because entering a new forest does not consume a clock cycle,
    /// - Continuation::AfterExitDecorators and Continuation::AfterExitDecoratorsBasicBlock: because
    ///   after-exit decorators are executed at the end of an `END` operation; never at the start of
    ///   a clock cycle
    ///
    /// Additionally, [miden_core::mast::ExternalNode] nodes are guaranteed to be resolved before
    /// this method is called.
    fn start_clock_cycle(
        &mut self,
        processor: &Self::Processor,
        continuation: Continuation,
        continuation_stack: &ContinuationStack,
        current_forest: &Arc<MastForest>,
    );

    /// Signals the end of a clock cycle, guaranteed to be called before incrementing the system
    /// clock, and after all mutations to the processor state have been applied.
    ///
    /// Implementations should use this method to finalize any tracing information related to the
    /// just-completed clock cycle.
    ///
    /// The `current_forest` parameter is guaranteed to be the same as the one passed to
    /// [Tracer::start_clock_cycle] for the same clock cycle.
    fn finalize_clock_cycle(
        &mut self,
        processor: &Self::Processor,
        op_helper_registers: OperationHelperRegisters,
        current_forest: &Arc<MastForest>,
    );

    // MAST FOREST RESOLUTION
    // --------------------------------------------------------------------------------------------

    /// Records and replays the resolutions of [crate::host::Host::get_mast_forest], either due to
    /// an [miden_core::mast::ExternalNode] or a [miden_core::mast::DynNode] for which the callee
    /// digest is not found in the current MAST forest.
    ///
    /// Note that when execution encounters a [miden_core::mast::ExternalNode], the external node
    /// gets resolved to the MAST node it refers to in the new MAST forest, without consuming the
    /// clock cycle (or writing anything to the trace). Hence, a clock cycle where execution
    /// encounters an external node effectively has 2 nodes associated with it.
    /// [Tracer::start_clock_cycle] is called on the resolved node (i.e. *not* the external node).
    /// This method is called on the external node before it is resolved, and hence is guaranteed to
    /// be called before [Tracer::start_clock_cycle] for clock cycles involving an external node.
    fn record_mast_forest_resolution(&mut self, _node_id: MastNodeId, _forest: &Arc<MastForest>) {}

    // IN-CYCLE METHODS
    // --------------------------------------------------------------------------------------------

    /// Records the result of a call to `Hasher::permute()`.
    ///
    /// Called by: `HPERM`, `LOG_PRECOMPILE`.
    fn record_hasher_permute(
        &mut self,
        _input_state: [Felt; STATE_WIDTH],
        _output_state: [Felt; STATE_WIDTH],
    ) {
    }

    /// Records the result of a call to `Hasher::build_merkle_root()`.
    ///
    /// The `path` is an `Option` to support environments where the `Hasher` is not present, such as
    /// in the context of parallel trace generation.
    ///
    /// Called by: `MPVERIFY`.
    fn record_hasher_build_merkle_root(
        &mut self,
        _node: Word,
        _path: Option<&MerklePath>,
        _index: Felt,
        _output_root: Word,
    ) {
    }

    /// Records the result of a call to `Hasher::update_merkle_root()`.
    ///
    /// The `path` is an `Option` to support environments where the `Hasher` is not present, such as
    /// in the context of parallel trace generation.
    ///
    /// Called by: `MRUPDATE`.
    fn record_hasher_update_merkle_root(
        &mut self,
        _old_value: Word,
        _new_value: Word,
        _path: Option<&MerklePath>,
        _index: Felt,
        _old_root: Word,
        _new_root: Word,
    ) {
    }

    /// Records the element read from memory at the given address.
    ///
    /// Called by: `MLOAD`.
    fn record_memory_read_element(
        &mut self,
        _element: Felt,
        _addr: Felt,
        _ctx: ContextId,
        _clk: RowIndex,
    ) {
    }

    /// Records the word read from memory at the given address.
    ///
    /// Called by: `MLOADW`, `HORNER_EVAL_EXT`, `DYN`.
    fn record_memory_read_word(
        &mut self,
        _word: Word,
        _addr: Felt,
        _ctx: ContextId,
        _clk: RowIndex,
    ) {
    }

    /// Records the element written to memory at the given address.
    ///
    /// Called by: `MSTORE`, `CALL` (non-syscall, for FMP initialization).
    fn record_memory_write_element(
        &mut self,
        _element: Felt,
        _addr: Felt,
        _ctx: ContextId,
        _clk: RowIndex,
    ) {
    }

    /// Records the word written to memory at the given address.
    ///
    /// Called by: `MSTOREW`.
    fn record_memory_write_word(
        &mut self,
        _word: Word,
        _addr: Felt,
        _ctx: ContextId,
        _clk: RowIndex,
    ) {
    }

    /// Records two element reads at the given addresses.
    ///
    /// Called by: `HORNER_EVAL_BASE`.
    fn record_memory_read_element_pair(
        &mut self,
        _element_0: Felt,
        _addr_0: Felt,
        _element_1: Felt,
        _addr_1: Felt,
        _ctx: ContextId,
        _clk: RowIndex,
    ) {
    }

    /// Records two consecutive word reads (a "dword" read) starting at the given address.
    ///
    /// Called by: `MSTREAM`.
    fn record_memory_read_dword(
        &mut self,
        _words: [Word; 2],
        _addr: Felt,
        _ctx: ContextId,
        _clk: RowIndex,
    ) {
    }

    /// Records a `DYNCALL`'s memory operations: reading the callee hash from memory and writing
    /// the initial FMP value to the new context's memory.
    ///
    /// Called by: `DYNCALL`.
    fn record_dyncall_memory(
        &mut self,
        _callee_hash: Word,
        _read_addr: Felt,
        _read_ctx: ContextId,
        _fmp_ctx: ContextId,
        _clk: RowIndex,
    ) {
    }

    /// Records a `CRYPTO_STREAM` operation: reading 2 plaintext words from source memory and
    /// writing 2 ciphertext words to destination memory.
    ///
    /// Called by: `CRYPTO_STREAM`.
    fn record_crypto_stream(
        &mut self,
        _plaintext: [Word; 2],
        _src_addr: Felt,
        _ciphertext: [Word; 2],
        _dst_addr: Felt,
        _ctx: ContextId,
        _clk: RowIndex,
    ) {
    }

    /// Records a `PIPE` operation: popping a dword from the advice stack and writing it to two
    /// consecutive memory words.
    ///
    /// Called by: `PIPE`.
    fn record_pipe(&mut self, _words: [Word; 2], _addr: Felt, _ctx: ContextId, _clk: RowIndex) {}

    /// Records the value returned by a [crate::host::advice::AdviceProvider::pop_stack] operation.
    ///
    /// Called by: `ADVPOP`.
    fn record_advice_pop_stack(&mut self, _value: Felt) {}

    /// Records the value returned by a [crate::host::advice::AdviceProvider::pop_stack_word]
    /// operation.
    ///
    /// Called by: `ADVPOPW`.
    fn record_advice_pop_stack_word(&mut self, _word: Word) {}

    /// Records the operands of a u32and operation.
    ///
    /// Called by: `U32AND`.
    fn record_u32and(&mut self, _a: Felt, _b: Felt) {}

    /// Records the operands of a u32xor operation.
    ///
    /// Called by: `U32XOR`.
    fn record_u32xor(&mut self, _a: Felt, _b: Felt) {}

    /// Records the high and low 32-bit limbs of the result of a u32 operation for the purposes of
    /// the range checker. This is expected to result in four 16-bit range checks.
    ///
    /// Called by: `U32SPLIT`, `U32ADD`, `U32ADD3`, `U32SUB`, `U32MUL`, `U32MADD`, `U32DIV`,
    /// `U32ASSERT2`.
    fn record_u32_range_checks(&mut self, _clk: RowIndex, _u32_lo: Felt, _u32_hi: Felt) {}

    /// Records the procedure hash of a syscall.
    ///
    /// Called by: `SYSCALL`.
    fn record_kernel_proc_access(&mut self, _proc_hash: Word) {}

    /// Records the evaluation of a circuit.
    ///
    /// Called by: `EVAL_CIRCUIT`.
    fn record_circuit_evaluation(&mut self, _circuit_evaluation: CircuitEvaluation) {}
}

// OPERATION HELPER REGISTERS
// ================================================================================================

/// Captures the auxiliary values produced by an operation that are needed to populate the
/// "user operation helper" columns of the execution trace.
///
/// Most operations do not require helper registers and use [`Empty`](Self::Empty). For those
/// that do, the variant records the minimal set of values from which
/// [`to_user_op_helpers`](Self::to_user_op_helpers) can derive the full
/// `[Felt; NUM_USER_OP_HELPERS]` array written into the trace.
#[derive(Debug, Clone)]
pub enum OperationHelperRegisters {
    /// Helper for the `EQ` operation, which pops two stack elements and pushes ONE if they are
    /// equal, ZERO otherwise.
    ///
    /// - `stack_second`: the element at stack position 1 (i.e. the second element) *before* the
    ///   operation.
    /// - `stack_first`: the element at stack position 0 (i.e. the top element) *before* the
    ///   operation.
    ///
    /// The helper register is set to `(stack_first - stack_second)^{-1}` when the values differ,
    /// or ZERO when they are equal.
    Eq { stack_second: Felt, stack_first: Felt },
    /// Helper for the `U32SPLIT` operation, which splits a field element into its low and high
    /// 32-bit limbs.
    ///
    /// - `lo`: the low 32-bit limb of the top stack element.
    /// - `hi`: the high 32-bit limb of the top stack element.
    ///
    /// The helper registers are populated with the four 16-bit limbs of `lo` and `hi` (used for
    /// range checking), plus the overflow flag `(u32::MAX - hi)^{-1}`.
    U32Split { lo: Felt, hi: Felt },
    /// Helper for the `EQZ` operation, which pushes ONE if the top element is zero, ZERO
    /// otherwise.
    ///
    /// - `top`: the value at the top of the stack *before* the operation.
    ///
    /// The helper register is set to `top^{-1}` when `top` is nonzero, or ZERO when `top` is
    /// zero.
    Eqz { top: Felt },
    /// Helper for the `EXPACC` operation, which performs a single step of binary exponentiation.
    ///
    /// - `acc_update_val`: the value by which the accumulator is multiplied in this step. Equals
    ///   `base` when the least-significant bit of the exponent is 1, or ONE otherwise.
    Expacc { acc_update_val: Felt },
    /// Helper for the `FRI_EXT2FOLD4` operation, which folds 4 query values during a FRI layer
    /// reduction.
    ///
    /// - `ev`: evaluation point `alpha / x`, where `alpha` is the verifier challenge and `x` is the
    ///   domain point.
    /// - `es`: `ev^2`, i.e. `(alpha / x)^2`.
    /// - `x`: the domain point, computed as `poe * tau_factor`.
    /// - `x_inv`: the multiplicative inverse of `x`.
    FriExt2Fold4 {
        ev: QuadFelt,
        es: QuadFelt,
        x: Felt,
        x_inv: Felt,
    },
    /// Helper for the `U32ADD` operation, which adds two u32 values and splits the result into
    /// a 32-bit sum and a carry bit.
    ///
    /// - `sum`: the low 32-bit part of `a + b`.
    /// - `carry`: the high 32-bit part of `a + b` (0 or 1).
    ///
    /// The helper registers hold the four 16-bit limbs of `sum` and `carry`.
    U32Add { sum: Felt, carry: Felt },
    /// Helper for the `U32ADD3` operation, which adds three u32 values and splits the result
    /// into a 32-bit sum and a carry.
    ///
    /// - `sum`: the low 32-bit part of `a + b + c`.
    /// - `carry`: the high 32-bit part of `a + b + c`.
    ///
    /// The helper registers hold the four 16-bit limbs of `sum` and `carry`.
    U32Add3 { sum: Felt, carry: Felt },
    /// Helper for the `U32SUB` operation, which computes `a - b` for two u32 values and pushes
    /// the difference and a borrow flag.
    ///
    /// - `second_new`: the 32-bit difference `a - b` (truncated to 32 bits, i.e. the value placed
    ///   at stack position 1 after the operation).
    ///
    /// The helper registers hold the two 16-bit limbs of `second_new`.
    U32Sub { second_new: Felt },
    /// Helper for the `U32MUL` operation, which multiplies two u32 values and splits the result
    /// into low and high 32-bit limbs.
    ///
    /// - `lo`: the low 32-bit limb of `a * b`.
    /// - `hi`: the high 32-bit limb of `a * b`.
    ///
    /// The helper registers hold the four 16-bit limbs of `lo` and `hi`, plus the overflow flag
    /// `(u32::MAX - hi)^{-1}`.
    U32Mul { lo: Felt, hi: Felt },
    /// Helper for the `U32MADD` operation, which computes `a * b + c` for three u32 values and
    /// splits the result into low and high 32-bit limbs.
    ///
    /// - `lo`: the low 32-bit limb of `a * b + c`.
    /// - `hi`: the high 32-bit limb of `a * b + c`.
    ///
    /// The helper registers hold the four 16-bit limbs of `lo` and `hi`, plus the overflow flag
    /// `(u32::MAX - hi)^{-1}`.
    U32Madd { lo: Felt, hi: Felt },
    /// Helper for the `U32DIV` operation, which divides `a` by `b` and pushes the quotient and
    /// remainder.
    ///
    /// - `lo`: `numerator - quotient`, used to range-check that `quotient <= numerator`.
    /// - `hi`: `denominator - remainder - 1`, used to range-check that `remainder < denominator`.
    ///
    /// The helper registers hold the four 16-bit limbs of `lo` and `hi`.
    U32Div { lo: Felt, hi: Felt },
    /// Helper for the `U32ASSERT2` operation, which asserts that the top two stack elements are
    /// valid u32 values.
    ///
    /// - `first`: the element at stack position 0 (top).
    /// - `second`: the element at stack position 1.
    ///
    /// The helper registers hold the four 16-bit limbs of `second` and `first` (used for range
    /// checking).
    U32Assert2 { first: Felt, second: Felt },
    /// Helper for the `HPERM` operation, which applies a Poseidon2 permutation to the top 12
    /// stack elements.
    ///
    /// - `addr`: the address in the hasher chiplet where the permutation is recorded.
    HPerm { addr: Felt },
    /// Helper for Merkle path operations (`MPVERIFY` and `MRUPDATE`), which verify or update a
    /// node in a Merkle tree.
    ///
    /// - `addr`: the address in the hasher chiplet where the Merkle path computation is recorded.
    MerklePath { addr: Felt },
    /// Helper for the `HORNER_EVAL_BASE` operation, which performs 8 steps of Horner evaluation
    /// on a polynomial with base-field coefficients.
    ///
    /// - `alpha`: the evaluation point, read from memory.
    /// - `tmp0`: Level 1 intermediate result: `(acc * alpha + c0) * alpha + c1`.
    /// - `tmp1`: Level 2 intermediate result: `((tmp0 * alpha + c2) * alpha + c3) * alpha + c4`.
    HornerEvalBase {
        alpha: QuadFelt,
        tmp0: QuadFelt,
        tmp1: QuadFelt,
    },
    /// Helper for the `HORNER_EVAL_EXT` operation, which performs 4 steps of Horner evaluation
    /// on a polynomial with extension-field coefficients.
    ///
    /// - `alpha`: the evaluation point, read from memory.
    /// - `k0`, `k1`: auxiliary values read from the same memory word as `alpha` (elements 2 and 3
    ///   of the word).
    /// - `acc_tmp`: the intermediate accumulator after processing the first 2 (highest-degree)
    ///   coefficients: `(acc * alpha + s[0]) * alpha + s[1]`.
    HornerEvalExt {
        alpha: QuadFelt,
        k0: Felt,
        k1: Felt,
        acc_tmp: QuadFelt,
    },
    /// Helper for the `LOG_PRECOMPILE` operation, which absorbs `TAG` and `COMM` into the
    /// precompile sponge via a Poseidon2 permutation.
    ///
    /// - `addr`: the address in the hasher chiplet where the permutation is recorded.
    /// - `cap_prev`: the previous sponge capacity word, provided non-deterministically and used as
    ///   the capacity input to the permutation.
    LogPrecompile { addr: Felt, cap_prev: Word },
    /// No helper registers are needed for this operation. All helper columns are set to ZERO.
    Empty,
}

impl OperationHelperRegisters {
    pub fn to_user_op_helpers(&self) -> [Felt; NUM_USER_OP_HELPERS] {
        match self {
            Self::Eq { stack_second, stack_first } => {
                let h0 = if stack_second == stack_first {
                    ZERO
                } else {
                    (*stack_first - *stack_second).inverse()
                };

                [h0, ZERO, ZERO, ZERO, ZERO, ZERO]
            },
            Self::U32Split { lo, hi } => {
                let (t1, t0) = split_u32_into_u16(lo.as_canonical_u64());
                let (t3, t2) = split_u32_into_u16(hi.as_canonical_u64());
                let m = (Felt::from_u32(u32::MAX) - *hi).try_inverse().unwrap_or(ZERO);

                [
                    Felt::from_u16(t0),
                    Felt::from_u16(t1),
                    Felt::from_u16(t2),
                    Felt::from_u16(t3),
                    m,
                    ZERO,
                ]
            },
            Self::Eqz { top } => {
                let h0 = top.try_inverse().unwrap_or(ZERO);

                [h0, ZERO, ZERO, ZERO, ZERO, ZERO]
            },
            Self::Expacc { acc_update_val } => [*acc_update_val, ZERO, ZERO, ZERO, ZERO, ZERO],
            Self::FriExt2Fold4 { ev, es, x, x_inv } => {
                let ev_felts = ev.as_basis_coefficients_slice();
                let es_felts = es.as_basis_coefficients_slice();

                [ev_felts[0], ev_felts[1], es_felts[0], es_felts[1], *x, *x_inv]
            },
            Self::U32Add { sum, carry } => {
                let (t1, t0) = split_u32_into_u16(sum.as_canonical_u64());
                let (t3, t2) = split_u32_into_u16(carry.as_canonical_u64());

                [
                    Felt::from_u16(t0),
                    Felt::from_u16(t1),
                    Felt::from_u16(t2),
                    Felt::from_u16(t3),
                    ZERO,
                    ZERO,
                ]
            },
            Self::U32Add3 { sum, carry } => {
                let (t1, t0) = split_u32_into_u16(sum.as_canonical_u64());
                let (t3, t2) = split_u32_into_u16(carry.as_canonical_u64());

                [
                    Felt::from_u16(t0),
                    Felt::from_u16(t1),
                    Felt::from_u16(t2),
                    Felt::from_u16(t3),
                    ZERO,
                    ZERO,
                ]
            },
            Self::U32Sub { second_new } => {
                let (t1, t0) = split_u32_into_u16(second_new.as_canonical_u64());

                [Felt::from_u16(t0), Felt::from_u16(t1), ZERO, ZERO, ZERO, ZERO]
            },
            Self::U32Mul { lo, hi } => {
                let (t1, t0) = split_u32_into_u16(lo.as_canonical_u64());
                let (t3, t2) = split_u32_into_u16(hi.as_canonical_u64());
                let m = (Felt::from_u32(u32::MAX) - *hi).try_inverse().unwrap_or(ZERO);

                [
                    Felt::from_u16(t0),
                    Felt::from_u16(t1),
                    Felt::from_u16(t2),
                    Felt::from_u16(t3),
                    m,
                    ZERO,
                ]
            },
            Self::U32Madd { lo, hi } => {
                let (t1, t0) = split_u32_into_u16(lo.as_canonical_u64());
                let (t3, t2) = split_u32_into_u16(hi.as_canonical_u64());
                let m = (Felt::from_u32(u32::MAX) - *hi).try_inverse().unwrap_or(ZERO);

                [
                    Felt::from_u16(t0),
                    Felt::from_u16(t1),
                    Felt::from_u16(t2),
                    Felt::from_u16(t3),
                    m,
                    ZERO,
                ]
            },
            Self::U32Div { lo, hi } => {
                let (t1, t0) = split_u32_into_u16(lo.as_canonical_u64());
                let (t3, t2) = split_u32_into_u16(hi.as_canonical_u64());

                [
                    Felt::from_u16(t0),
                    Felt::from_u16(t1),
                    Felt::from_u16(t2),
                    Felt::from_u16(t3),
                    ZERO,
                    ZERO,
                ]
            },
            Self::U32Assert2 { first, second } => {
                let (t1, t0) = split_u32_into_u16(second.as_canonical_u64());
                let (t3, t2) = split_u32_into_u16(first.as_canonical_u64());

                [
                    Felt::from_u16(t0),
                    Felt::from_u16(t1),
                    Felt::from_u16(t2),
                    Felt::from_u16(t3),
                    ZERO,
                    ZERO,
                ]
            },
            Self::HPerm { addr } => [*addr, ZERO, ZERO, ZERO, ZERO, ZERO],
            Self::MerklePath { addr } => [*addr, ZERO, ZERO, ZERO, ZERO, ZERO],
            Self::HornerEvalBase { alpha, tmp0, tmp1 } => [
                alpha.as_basis_coefficients_slice()[0],
                alpha.as_basis_coefficients_slice()[1],
                tmp1.as_basis_coefficients_slice()[0],
                tmp1.as_basis_coefficients_slice()[1],
                tmp0.as_basis_coefficients_slice()[0],
                tmp0.as_basis_coefficients_slice()[1],
            ],
            Self::HornerEvalExt { alpha, k0, k1, acc_tmp } => [
                alpha.as_basis_coefficients_slice()[0],
                alpha.as_basis_coefficients_slice()[1],
                *k0,
                *k1,
                acc_tmp.as_basis_coefficients_slice()[0],
                acc_tmp.as_basis_coefficients_slice()[1],
            ],
            Self::LogPrecompile { addr, cap_prev } => {
                [*addr, cap_prev[0], cap_prev[1], cap_prev[2], cap_prev[3], ZERO]
            },
            Self::Empty => [ZERO; NUM_USER_OP_HELPERS],
        }
    }
}