SWITCH the way you use conditionals!

NOTE: This article is part of the series of posts about coding in assembly. Please check out the entire series for more fun!

Captain's log day 2

-- You've been burned from the previous day, but your motivation stays intact.

Last time we saw how to use the conditional if statement in assembly and what the bytecode did behind the curtains, but you may have seen that when we use an if statement in assembly there is a downside: We don't have an else statement :persevere:

But don't worry, Assembly comes to the rescue with the switch statement! :drum:

In this article we will be discussing how to make use of the switch statement to cover multiple conditional cases that determine the flow of the code to be executed.

Episode VI – Return of the Code

Let's use the example from day 1 and slightly change its desired functionality. If we wanted a different non-zero value in the output when the input is higher than 26 (0x1a), we could make use of the if-else statement in Solidity combined with the less than operator <:

function solidityIfElse(uint256 input) public pure returns (uint256 output) {
    if (input < 0x1a) {
        output = 0x1;
    } else {
        output = 0x2;
    }
}

But remember that we don't have the else statement in assembly, so instead we can use the switch statement:

function assemblySwitch(uint256 input) public pure returns (uint256 output) {
    assembly {
        switch lt(input, 0x1a)
        case 0x1 {
            output := 0x1
        }
        default {
            output := 0x2
        }
    }
}

As you can see, we compare the input against the 0x1a value using the lt(x,y) instruction. This instruction outputs a one when x is strictly less than y and a zero in all other cases. We have split the results into two cases: when input is a one and when it's not. This last case would be the default.

Analogously, there is a gt(x,y) instruction, which is the reciprocal instruction that outputs a one when x is strictly greater than y and a zero in all other cases.

The switch statement reads the input and classifies it among the defined options. Although in this situation we only have a binary problem, the beauty of the switch statement is that we can define multiple cases above the default one - which handles all the situations that don't match with the other conditions - and perform operations based on each case.

For our purposes, the possible options are a one and a zero - the output from the lt instruction, but, for example, it’s possible that an integer variable goes from 0 to 9, and when the variable is a 2, 6, or 7 the code emits an event but stays quiet otherwise.

One thing to notice - contrary to the gas consumption of the assembly code that we've seen in the past - is that the switch function consumes more gas in the assembly case than when we used the Solidity version. So you shouldn't extrapolate the idea that writing the code in assembly will always be cheaper. I'll repeat it again: Focus on the code readability and not in the gas optimization.

If you didn't drink any coffee yet, now is the time. I'll wait...

Episode V – The Bytecode Strikes Back

Ready? Caffeinated? Great, because we are going to compare what the opcodes do in each scenario. This was extracted from the Remix debug tool, and as you've learned from Ale's posts, the most important instructions of the Solidity solidityIfElse function are:

# Opcode Value Case
182 PUSH1 00
184 PUSH1 1A
186 DUP3
187 LT
188 ISZERO
189 PUSH1 C7
191 JUMPI D--->
192 PUSH1 01
194 SWAP1
195 POP
196 PUSH1 CC
198 JUMP 1--->
199 JUMPDEST <---D
200 PUSH1 02
202 SWAP1
203 POP
204 JUMPDEST <---1
205 SWAP2
206 SWAP1
207 POP
208 JUMP R--->

If you are sweating, believe in yourself. YOU CAN DO IT!

Again, some magic has happened and we start with our input right on top of the stack.

We start PUSHing two values to the stack, a 0x0 and a 0x1A, and we duplicate the input value on top of the stack. After that, we use our known LT instruction with the first two elements in the stack which would be the input and the 0x1A. For those that are new here and don't know how the LT works, I recommend you go to my previous post first =)

LT would decide if the first element is less than the second one, and leave the result on top of the stack. After that, the ISZERO instruction in 188 would mirror the result (0x0 -> 0x1 or 0x1 -> 0x0), and from here, we have another crossroads. If the input is less than 0x1A, LT would output a 0x1 in line 187, resulting in a 0x0 after line 188; however, if the input is greater than or equal to 0x1A, we would have a 0x0 after 187 but a 0x1 after 188.

– Why the heck are you trying to make me dizzy?   - You may ask, but all of this is the evil intention of the compiler, not mine!

Fasten your seatbelts, because after the PUSH in line 189, which defines the destination line 199 (0xC7), we are ready to JUMP through space and time!

yep, that's a wormhole

... or not...

wormhole_dis

In line 191, we have a JUMPI instruction, and as you may recall, it's a conditional JUMP that will happen only if the second element in the stack is a one. So, now we REALLY REALLY have to divide the problem into two paths.

Let's assume that the input is less than 0x1A. In this case, we would have a zero as a second parameter for the JUMPI, so the JUMP wouldn't be made =(

Who wants to JUMP anyway? That's too mainstream...

Downstream, we are pushing a one into the stack, we POP an old zero, and we JUMP weeee =D to the line 204 (0xCC). There, we SWAP a couple of times in line 205 and 206 - to POP the input that we don't need anymore - and we JUMP again to the line 96 (0x60).

– What happens there?  We find a place in memory that's free, we save the output there, and we RETURN it. And when I say "we", it's actually the code, but it's too shy to go alone.

Easy peasy, right? Now, let's get into the other scenario.

We are back into line 191 just before the JUMPI, but this time, we have a one as a secondary parameter so... yes, you guessed it: hyperspace!

yep, that's a wormhole

Nevertheless, this trip is short because it leaves us at line 199 (0xC7) where we PUSH a 2 onto the stack (200) and POP a zero from it. But after that, we continue with the same path as before: We JUMP to line 96, and we RETURN the output.

– What's the difference then?  The output. In the first run, we were inside the body of the if, but in the second run, we went into the else body, the default one :o

– Wooow, so what happens then with the assemblySwitch function?  Eeeasy now. We are getting into that. The instructions that are relevant for the assemblySwitch function are the following:

# Opcode Value Case
210 PUSH1 00
212 PUSH1 1A
214 DUP3
215 LT
216 PUSH1 01
218 DUP2
219 EQ
220 PUSH1 E6
222 JUMPI 1--->
223 PUSH1 02
225 SWAP2
226 POP
227 PUSH1 EB
229 JUMP D--->
230 JUMPDEST <---1
231 PUSH1 01
233 SWAP2
234 POP
235 JUMPDEST <---D
236 POP
237 SWAP2
238 SWAP1
239 POP
240 JUMP R--->

Alrighty then, I reckon you have enough strength to continue with this last step, so no more coffee breaks.

As always, we start with the input in the stack and we PUSH a 0x0 and a 0x1A in lines 210-212 to compare our input with the 0x1A in line 215 using an LT instruction. That would give us a 0x1 or a 0x0 depending on the input (as before).

After that, a 0x1 is PUSHed to compare with the result from the LT (216). Why?  Because this is our first case in the switch statement. It checks if the value - in this case the result from the LT - corresponds with one of the defined cases. Amazing, right?

Let's imagine that we have a value that is less than 0x1A. In this case, LT would return a one and after the EQ in line 219 we would have a 0x1 as a second parameter for the JUMPI that is on line 222.

What does it mean? We are jumpiiiinng.
Where? To the line 230 (0xE6).

yep, that's a wormhole

There, we start PUSHing a one into the stack (231), and we POP away some garbage in 234, 236, and 239.

After that, we have a final JUMP to the part of the code that RETURNs the output.

For the other case, when the input is at least 0x1A, we go down another road.

The result from the LT in line 215 is going to be 0x0 so when it's compared to 0x1 with the EQ (219), the result will be 0x0, too, which means no JUMP in line 222.

But because we completed the comparisons of all the possible cases - a list with only one case :stuck_out_tongue: - we end up executing the default one. Here we PUSH a 0x2 as output, JUMP to the common garbage removal, and RETURN the value 0x2.

Episode IV – A New Conclusion

This example is trivial because we only had 2 possibilities, but the switch statement would allow me to list all the possible outcomes - each one of them with their respective execution code - and define a default case that covers all other situations.

We've learned that this statement sequentially checks all the cases, so some gas will be wasted iterating all possibilities. When none of the cases matched with the condition, we also saw that the default body execution code is the one that comes first. Compared with the if-else alternative in Solidity, the gas consumption is greater this time. This means that you shouldn't go to full assembly mode just to save some gas; use it only when it's needed =)

On a more global view, during these last 3 days, we've learned how certain instructions like eq, not, iszero, lt, and gt are used and also how we can make conditional statements in assembly using ifs and switches.

You should be proud of yourself, but remember that there are plenty more opcodes to have fun with, so stay tuned for the next chapter, or even better, why don't you write one? We'll be very happy to include it in the index :smiley:

5 Likes

Hi @andresbach,
Star Trek, Star Wars and potentially Stargate SG-1, plus assembly. :rocket:

Will need to reread slowly with more caffeine to spot all the references and let the assembly sink in.

1 Like

" … plus assembly." Haha :joy:

Glad you liked it @abcoathup!
Let’s see if you can find the theme of the next article :laughing:

1 Like