triton-vm icon indicating copy to clipboard operation
triton-vm copied to clipboard

replace `skiz` by `if_then_call`

Open jan-ferdinand opened this issue 3 years ago • 3 comments

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.

jan-ferdinand avatar Oct 12 '22 13:10 jan-ferdinand

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

jan-ferdinand avatar Oct 12 '22 15:10 jan-ferdinand

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 % &

aszepieniec avatar Oct 13 '22 10:10 aszepieniec

Very elegant. This warrants a more thorough analysis of the costs of the two instructions skiz and if_then_call.

jan-ferdinand avatar Oct 13 '22 18:10 jan-ferdinand

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.

sshine avatar Jan 02 '23 10:01 sshine

A limitation of calling to a conditional branch is that the branch cannot return all the way back.

I don't follow. Please elaborate.

aszepieniec avatar Jan 04 '23 21:01 aszepieniec

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.

sshine avatar Jan 04 '23 22:01 sshine

There seems to be no immediate need to replace instruction skiz.

jan-ferdinand avatar Jan 12 '23 15:01 jan-ferdinand