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

But don’t worry, Assembly comes to the rescue with the switch statement!

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 PUSH ing 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!

… or not…

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!

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 PUSH ed 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).

There, we start PUSH ing 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 RETURN s 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 - 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 if s and switch es.

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