Skip to main content
Version: Next (unstable)

Operand stack

Miden VM is a stack machine. The stack is a push-down stack of practically unlimited depth (in practical terms, the depth will never exceed 2322^{32}), but only the top 1616 items are directly accessible to the VM. Items on the stack are elements in a prime field with modulus 264−232+12^{64}-2^{32} + 1.

To keep the constraint system for the stack manageable, we impose the following rules:

  1. All operations executed on the VM can shift the stack by at most one item. That is, the end result of an operation must be that the stack shrinks by one item, grows by one item, or the number of items on the stack stays the same.
  2. Stack depth must always be greater than or equal to 1616. At the start of program execution, the stack is initialized with exactly 1616 input values, all of which could be 00's.
  3. By the end of program execution, exactly 1616 items must remain on the stack (again, all of them could be 00's). These items comprise the output of the program.

To ensure that managing stack depth does not impose significant burden, we adopt the following rule:

  • When the stack depth is 1616, removing additional items from the stack does not change its depth. To keep the depth at 1616, 00's are inserted into the deep end of the stack for each removed item.

Stack representation​

The VM allocates 1919 trace columns for the stack. The layout of the columns is illustrated below.

trace_layout

The meaning of the above columns is as follows:

  • s0...s15s_0 ... s_{15} are the columns representing the top 1616 slots of the stack.
  • Column b0b_0 contains the number of items on the stack (i.e., the stack depth). In the above picture, there are 16 items on the stacks, so b0=16b_0 = 16.
  • Column b1b_1 contains the address of the top row in the overflow table, which stores data beyond the directly accessible 1616 stack slots. A zero value indicates an empty overflow table.
  • Helper column h0h_0 is used to ensure that stack depth does not drop below 1616. Values in this column are set by the prover non-deterministically to 1b0−16\frac{1}{b_0-16} when b0≠16b_0 \neq 16, and to any other value otherwise.

Overflow table​

The overflow table stores operand-stack values beyond the 1616 directly accessible slots. It is a virtual table implemented by typed LogUp messages. An insertion contributes multiplicity +1+1 and a removal contributes multiplicity −1-1; lookup closure requires the two multisets to match.

The table itself can be thought of as having 3 columns as illustrated below.

overflow_table_layout

The meaning of the columns is as follows:

  • Column t0t_0 contains row address. Every address in the table must be unique.
  • Column t1t_1 contains the value that overflowed the stack.
  • Column t2t_2 contains the address of the row containing the value that overflowed the stack right before the value in the current row. For example, in the picture above, first value aa overflowed the stack, then bb overflowed the stack, and then value cc overflowed the stack. Thus, row with value bb points back to the row with value aa, and row with value cc points back to the row with value bb.

Each row is encoded as a domain-separated StackOverflowTable message with payload (t0,t1,t2)(t_0,t_1,t_2):

S(t0,t1,t2)=Pstack_overflow+t0+βt1+β2t2.S(t_0,t_1,t_2) = P_{stack\_overflow} + t_0 + \beta t_1 + \beta^2 t_2.

Here Pstack_overflowP_{stack\_overflow} is the fixed bus prefix for this relation. The table satisfies two invariants:

  • A row is removed only after it has been inserted.
  • Each insertion uses a distinct row address.

The overflow-table constraints assign the current VM clock as each inserted row's address and carry its predecessor through b1b_1; the shared LogUp argument binds each removal to the corresponding inserted row.

Right shift​

If an operation adds data to the stack, we say that the operation caused a right shift. For example, PUSH and DUP operations cause a right shift. Graphically, this looks like so:

stack_right_shift

Here, we pushed value v17v_{17} onto the stack. All other values on the stack are shifted by one slot to the right and the stack depth increases by 11. There is not enough space at the top of the stack for all 1717 values, thus, v1v_1 needs to be moved to the overflow table.

To do this, we need to rely on another column: clkclk. This is a system column which keeps track of the current VM cycle. The value in this column is simply incremented by 11 with every step.

The row we want to add to the overflow table is defined by tuple (clk,v1,0)(clk, v1, 0), and after it is added, the table would look like so:

stack_overflow_table_post_1_right_shift

The reason we use VM clock cycle as row address is that the clock cycle is guaranteed to be unique, and thus, the same row can not be added to the table twice.

Let's push another item onto the stack:

stack_overflow_push_2nd_item

Again, as we push v18v_{18} onto the stack, all items on the stack are shifted to the right, and now v2v_2 needs to be moved to the overflow table. The tuple we want to insert into the table now is (clk+1,v2,clk)(clk+1, v2, clk). After the operation, the overflow table will look like so:

stack_overflow_table_post_2_right_shift

Notice that t2t_2 for row which contains value v2v_2 points back to the row with address clkclk.

Overall, during a right shift we do the following:

  • Increment stack depth by 11.
  • Shift stack columns s0,...,s14s_0, ..., s_{14} right by 11 slot.
  • Add a row to the overflow table described by tuple (clk,s15,b1)(clk, s_{15}, b_1).
  • Set the next value of b1b_1 to the current value of clkclk.

Also, as mentioned previously, the prover sets values in h0h_0 non-deterministically to 1b0−16\frac{1}{b_0 - 16}.

Left shift​

If an operation removes an item from the stack, we say that the operation caused a left shift. For example, a DROP operation causes a left shift. Assuming the stack is in the state we left it at the end of the previous section, graphically, this looks like so:

stack_1st_left_shift

Overall, during the left shift we do the following:

  • When stack depth is greater than 1616:
    • Decrement stack depth by 11.
    • Shift stack columns s1,...,s15s_1, ..., s_{15} left by 11 slot.
    • Remove a row from the overflow table with t0t_0 equal to the current value of b1b_1.
    • Set the next value of s15s_{15} to the value in t1t_1 of the removed overflow table row.
    • Set the next value of b1b_1 to the value in t2t_2 of the removed overflow table row.
  • When the stack depth is equal to 1616:
    • Keep the stack depth the same.
    • Shift stack columns s1,...,s15s_1, ..., s_{15} left by 11 slot.
    • Set the value of s15s_{15} to 00.
    • Set the value to h0h_0 to 00 (or any other value).

If the stack depth becomes (or remains) 1616, the prover can set h0h_0 to any value (e.g., 00). But if the depth is greater than 1616 the prover sets h0h_0 to 1b0−16\frac{1}{b_0-16}.

AIR Constraints​

To simplify constraint descriptions, we'll assume that the VM exposes two binary flag values described below.

FlagDegreeDescription
fshrf_{shr}6Low-degree aggregate for operations which shift the stack right by one element.
fshlf_{shl}5Low-degree aggregate for ordinary one-element left shifts; DYNCALL is handled separately.

These flags are mutually exclusive. That is, if fshl=1f_{shl}=1, then fshr=0f_{shr}=0 and vice versa. However, both flags can be set to 00 simultaneously. How these flags are computed is described here.

We also use a combined call-entry flag fenterf_{enter} to denote entry into a new execution context. Here, fenter=fcall+fdyncall+fsyscallf_{enter} = f_{call} + f_{dyncall} + f_{syscall}. We use frestore=fend⋅h6f_{restore} = f_{end} \cdot h_6 for an END that consumes a caller-frame entry. The block-stack relation authenticates the restored depth, so caller-frame restoration is handled separately from the ordinary stack-depth rule below.

Stack overflow flag​

Additionally, we'll define a flag to indicate whether the overflow table contains values. This flag will be set to 00 when the overflow table is empty, and to 11 otherwise (i.e., when stack depth >16>16). This flag can be computed as follows:

fov=(b0−16)⋅h0 | degree=2f_{ov} = (b_0-16) \cdot h_0 \text{ | degree} = 2

To ensure that this flag is set correctly, we need to impose the following constraint:

(1−fov)⋅(b0−16)=0 | degree=3(1 - f_{ov}) \cdot (b_0 - 16) = 0 \text{ | degree} = 3

The above constraint can be satisfied only when either of the following holds:

  • b0=16b_0 = 16, in which case fovf_{ov} evaluates to 00, regardless of the value of h0h_0.
  • fov=1f_{ov} = 1, in which case b0b_0 cannot be equal to 1616 (and h0h_0 must be set to 1b0−16\frac{1}{b_0 - 16}).

Stack depth constraints​

To make sure stack depth column b0b_0 is updated correctly, we need to impose the following constraints:

ConditionConstraint__Description
fshr=1f_{shr}=1b0′=b0+1b'_0 = b_0 + 1A one-element right shift adds one row to the overflow table.
fshl=1f_{shl}=1
fov=1f_{ov}=1
b0′=b0−1b'_0 = b_0 - 1A one-element left shift removes one row when the overflow table is not empty.
fenter=1f_{enter}=1b0′=16b'_0 = 16On CALL/SYSCALL/DYNCALL entry, the stack depth resets to the accessible top 16 positions.
frestore=1f_{restore}=1from caller frameA caller-frame END restores the authenticated saved depth through the block-stack relation.
otherwiseb0′=b0b'_0 = b_0In all other cases, stack depth should not change.

For rows with fenter=frestore=0f_{enter}=f_{restore}=0, we can combine the shift constraints into a single expression as follows:

b0′−b0+fshl⋅fov−fshr=0 | degree=7b'_0 - b_0 + f_{shl} \cdot f_{ov} - f_{shr} = 0 \text{ | degree} = 7

On CALL/SYSCALL/DYNCALL entry, we instead enforce b0′=16b'_0 = 16 via a dedicated term. Caller-frame END depth updates are handled by the block-stack relation.

Overflow table constraints​

When the stack is shifted to the right, the message S(clk,s15,b1)S(clk,s_{15},b_1) is added to the overflow table with multiplicity +1+1.

When the stack is shifted to the left and the overflow table is non-empty, S(b1,s15′,b1′)S(b_1,s'_{15},b'_1) is removed with multiplicity −1-1. A DYNCALL with a non-empty overflow table instead removes S(b1,s15′,h5)S(b_1,s'_{15},h_5), because the restored predecessor is staged in decoder hasher register h5h_5 while b1′b'_1 is reset for the new context. Operations that do not add or remove an overflow entry make no StackOverflowTable interaction.

Let dpush=S(clk,s15,b1)d_{push}=S(clk,s_{15},b_1), dpop=S(b1,s15′,b1′)d_{pop}=S(b_1,s'_{15},b'_1), and ddyncall=S(b1,s15′,h5)d_{dyncall}=S(b_1,s'_{15},h_5) denote the message encodings above.

The row contribution to the LogUp sum is:

fshrdpush−fshlfovdpop−fdyncallfovddyncall.\frac{f_{shr}}{d_{push}} - \frac{f_{shl} f_{ov}}{d_{pop}} - \frac{f_{dyncall} f_{ov}}{d_{dyncall}}.

The compiled lookup constraint cross-multiplies these fractions and has degree 99.

On a left shift with non-empty overflow, the constraint forces the prover to set the next values of s15s_{15} and b1b_1 to values t1t_1 and t2t_2 of the row removed from the overflow table.

On a right shift, we also need to make sure that the next value of b1b_1 is set to the current value of clkclk. This can be done with the following constraint:

fshr⋅(b1′−clk)=0 | degree=7f_{shr} \cdot (b'_1 - clk) = 0 \text{ | degree} = 7

Entering a CALL, DYNCALL, or SYSCALL context starts with an empty overflow table, so we also enforce:

fenter⋅b1′=0f_{enter} \cdot b'_1 = 0

All other operations must preserve b1b_1, except for right shifts, non-empty left shifts, or caller-frame END rows. Those transitions update or restore b1b_1 through the overflow or block-stack relation. Define:

fupdate=fenter+frestore+fshr+fshl⋅fovf_{update} = f_{enter} + f_{restore} + f_{shr} + f_{shl} \cdot f_{ov}

The direct preservation constraint is:

(1−fupdate)⋅(b1′−b1)=0(1 - f_{update}) \cdot (b'_1 - b_1) = 0

When the overflow table is empty, every operation that left-shifts into position 15 must set s15′s'_{15} to zero. DYNCALL is excluded from fshlf_{shl} because call entry resets the ordinary depth and overflow-pointer columns, but it still performs this stack shift. Thus the local selector for this constraint is fbottom=fshl+fdyncallf_{bottom} = f_{shl} + f_{dyncall}, and we enforce:

fbottom⋅(1−fov)⋅s15′=0 | degree=8f_{bottom} \cdot (1 - f_{ov}) \cdot s_{15}' = 0 \text{ | degree} = 8

Boundary constraints​

In addition to the constraints described above, we also need to enforce the following boundary constraints:

  • b0=16b_0 = 16 at the first and at the last row of execution trace.
  • b1=0b_1 = 0 at the first and at the last row of execution trace.

The LogUp closure additionally requires every inserted overflow message to have a matching removal.