triton-vm
triton-vm copied to clipboard
replace `skiz` by `if_then_call`
As discussed in this issue, implementing a proper “if then else” is not currently very straightforward using Triton assembly. The new instruction if_then_call alleviates the problem.
For instruction if_then_call, note the difference between the potential offsets added to instruction pointer ip and jump origin o. Specifically, if the top of the stack a is 0, the instruction pointer ip is incremented by 2, skipping the instruction's argument d. However, if the top of the stack a is not 0, the return address put on the jump stack is ip+4. This skips not only the instruction's argument d, but also the next two instructions when returning. Together, this mechanic allows following instruction if_then_call + d with instruction call + e to serve as the “else” branch. If no “else” branch is desired, instruction if_then_else + d can be followed with two instructions nop. Of course, instruction if_then_call can be followed by any instruction. The appropriate care should be taken to return only to where it makes sense.
We might drop instruction skiz from the ISA, but the combination of skiz recurse return is beautiful.
Expressing skiz using if_then_call:
push 0 eq
if_then_call dummy-label
instruction_to_maybe_skip
nop_or_argument
dummy-label: return
Program length: 8. Execution cycle count: 4 or 5.
Note that the emulated skiz, unlike the currently implemented version, does not pop the top of the stack. Instead, the top of the stack is modified. The new value is 1 if the old value was 0, and 0 otherwise
$$\mathsf{st0}' = \begin{cases}1, &\text{ if }\mathsf{st0}= 0 \ 0,&\text{ otherwise}\end{cases}$$
The technical reason is that any one instruction may change at most one memory pointer. Since if_then_call might change the jump stack pointer jsp, the length of the operational stack cannot change at the same time, because that would modify osp.
Of course, depending on instruction_to_maybe_skip, the top of the stack might have been popped. For example, somewhat preserving the beauty I see in skiz recurse return:
push 0 eq
if_then_call dummy-label
pop recurse
return
dummy-label: pop return
I thought about emulating if-then-else with skiz, and came up with a way to do that. If I understand correctly, the limitation was that the two branches could not leave the stack in an arbitrary state. This construction alleviates that limitation, with no change to the instruction set or arithmetization.
dup0
skiz
call if-wrapper
call else-wrapper
if-wrapper:
pop
call if-branch
push 1
return
else-wrapper:
push 0 eq # logical not
skiz
call else-branch
return
To verify the execution, see the following table:
| instruction | stack | stack |
|---|---|---|
dup0 |
_ 0 |
_ 1 |
skiz |
_ 0 0 |
_ 1 1 |
call if-wrapper |
_ 1 |
|
pop |
_ 1 |
|
call if-branch |
_ |
|
... return |
& |
|
push 1 |
& |
|
return |
& 1 |
|
call else-wrapper |
_ 0 |
& 1 |
push 0 eq |
_ 0 |
& 1 |
skiz |
_ 1 |
& 0 |
call else-branch |
_ |
|
... return |
% |
|
return |
% |
& |
Very elegant. This warrants a more thorough analysis of the costs of the two instructions skiz and if_then_call.
A variation of this if-then-else wrapper that I derived is like this:
// Before: _ c
// After: _ ...
some-routine:
push 1 // -> _ c 1
swap1 // -> _ 1 c
skiz // -> _ 1
call then-branch
skiz // -> _
call else-branch
...
return
// Before: _ 1
// After: _ ... 0
then-branch:
pop
...
push 0
return
// Before: _
// After: _ ...
else-branch:
...
return
I.e., start by pushing a 1 that will make the else-skiz trigger, and in the then-branch change that 1 to a 0.
A limitation of calling to a conditional branch is that the branch cannot return all the way back.
A limitation of calling to a conditional branch is that the branch cannot return all the way back.
I don't follow. Please elaborate.
A limitation of calling to a conditional branch is that the branch cannot return all the way back.
I don't follow. Please elaborate.
What I meant was just: The return instruction can only jump one call back, and conditionals with bodies of more than one instruction must likely call.
So if you want to leave a function entirely from within a skiz call branch, you can leave a boolean on the stack (similar to the if-then-else construction) and skiz return immediately after returning from the branch, e.g.:
// Before: _ c
// After: _ (...x|...y)
foo:
push 0 // _ c 0
swap1 // _ 0 c
skiz // _ 0
call foo-then // _ ...x 1
skiz // _ ...x
return
...y // _ ...y
return // leave 'foo'
// Before: _ 0
// After: _ ...x 1
foo-then:
pop // _
...x // _ ...x
push 1 // _ ...x 1
return // return to 'foo', does not leave 'foo'
For nested conditionals where the inner branch leaves the function, applying this trick once for each nesting is necessary.
There seems to be no immediate need to replace instruction skiz.