What if?¶
Choose your path.
Our assembler gives us a lot of convenience for testing features of our VM. So let us start doing interesting stuff with it. We do have support for jumps already, but as it is now, save of an endless loop, there is absolutely no reason to do it, yet. All our programs run their predetermined way. If you look again at label.lva
, you can see that none of those goto
s introduce any dynamic. We could just ditch them and reorder the rest. It would do the same, only more efficient. They simple tangle up our linear code, without removing its linearity.
Today we will introduce branches to our VM. A branch is a point in a program from which there are multiple possible paths to take. Two paths, normally. Which of those paths is takes is decided at runtime by looking at the state of the program. For us that means that we look at the value on top of the stack. How does it work?
Conditional jump¶
We already introduced the goto
operation. What we will add now, works exactly the same way, but only if a certain condition is met. And, yes, we will call that operation if. But if what? How about if equal?
So we get the new opname ifeq
, that pops a value from the stack and only executes its jump when that value is equal. Equal to what, you want to know? How about if it is equal to zero. If you want to compare it to a different number, it is easy to subtract that number from your value before you compare it to zero, and you achieve what you need.
New operations¶
We will introduce multiple if-operations. Six, to be precise.
And we add another operation, while we add it: dup
src/op.rs | |
---|---|
This one simply duplicates the value on top of the stack, so that there will be another copy of it on top of it. We will use that often when testing values with an if
, if we still need the value after testing it. The if
will consume the top most value.
Extending the assembler¶
We add the parsing handlers for our new instructions:
And that is all we need to change on our assembler. The way we have written it, it is easy to introduce new operations, when they share the same syntax in assembly and in bytecode as existing ones.
Adjust the VM¶
First, we add the handler for the dup
. Just pop a value and push it back, twice. Easy.
src/vm.rs | |
---|---|
And now, the if*
-handlers. They are similar to the goto
-handler, just with an if
added.
And that is all the code we have to change. Our VM can now execute conditional jumps. Now we can do some serious programming!
A for-loop¶
Can't wait to use an if in program:
pgm/loop.lva | |
---|---|
And execute it:
kratenko@jotun:~/git/lovem$ cargo run --bin lovas -- -r pgm/loop.lva --print --trace
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/lovas -r pgm/loop.lva --print --trace`
Pgm { name: "pgm/loop.lva", text: [2, 3, 2, 1, 17, 3, 37, 255, 249, 1, 255] }
VM { stack: [], pc: 0, op_cnt: 0, trace: true, watermark: 0 }
Executing op 0x02
VM { stack: [3], pc: 2, op_cnt: 1, trace: true, watermark: 1 }
Executing op 0x02
VM { stack: [3, 1], pc: 4, op_cnt: 2, trace: true, watermark: 2 }
Executing op 0x11
VM { stack: [2], pc: 5, op_cnt: 3, trace: true, watermark: 2 }
Executing op 0x03
VM { stack: [2, 2], pc: 6, op_cnt: 4, trace: true, watermark: 2 }
Executing op 0x25
Jump from 9 by -7
VM { stack: [2], pc: 2, op_cnt: 5, trace: true, watermark: 2 }
Executing op 0x02
VM { stack: [2, 1], pc: 4, op_cnt: 6, trace: true, watermark: 2 }
Executing op 0x11
VM { stack: [1], pc: 5, op_cnt: 7, trace: true, watermark: 2 }
Executing op 0x03
VM { stack: [1, 1], pc: 6, op_cnt: 8, trace: true, watermark: 2 }
Executing op 0x25
Jump from 9 by -7
VM { stack: [1], pc: 2, op_cnt: 9, trace: true, watermark: 2 }
Executing op 0x02
VM { stack: [1, 1], pc: 4, op_cnt: 10, trace: true, watermark: 2 }
Executing op 0x11
VM { stack: [0], pc: 5, op_cnt: 11, trace: true, watermark: 2 }
Executing op 0x03
VM { stack: [0, 0], pc: 6, op_cnt: 12, trace: true, watermark: 2 }
Executing op 0x25
VM { stack: [0], pc: 9, op_cnt: 13, trace: true, watermark: 2 }
Executing op 0x01
VM { stack: [], pc: 10, op_cnt: 14, trace: true, watermark: 2 }
Terminated!
VM { stack: [], pc: 11, op_cnt: 15, trace: true, watermark: 2 }
Terminated.
Runtime=100.972µs
op_cnt=15, pc=11, stack-depth=0, watermark=2
Nice! This is basically a for-loop. Granted, it does not do anything but loop, but you can see how the program counts down from 3
to 0
and after the third time it reaches line 8, it stops jumping back to loop:
and advances to the end.
We can increase the number in line 3, and the number of runs increase with it. If we change it to 200
, we get this (I ditched the --trace
for this).
kratenko@jotun:~/git/lovem$ cargo run --bin lovas -- -r pgm/loop.lva --print
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/lovas -r pgm/loop.lva --print`
Pgm { name: "pgm/loop.lva", text: [2, 200, 2, 1, 17, 3, 37, 255, 249, 1, 255] }
Terminated.
Runtime=128.709µs
op_cnt=803, pc=11, stack-depth=0, watermark=2
More than 800 operations with only 10 lines of code. Shall we cranc it up to a million?
kratenko@jotun:~/git/lovem$ cargo run --bin lovas -- -r pgm/loop.lva --print
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/lovas -r pgm/loop.lva --print`
Pgm { name: "pgm/loop.lva", text: [2, 100, 2, 100, 18, 2, 100, 18, 2, 1, 17, 3, 37, 255, 249, 1, 255] }
Terminated.
Runtime=564.184652ms
op_cnt=4000007, pc=17, stack-depth=0, watermark=2
Takes about have a second to execute, over 4000000 operations where executed. And the stack never held more than 2 values, as you can see by the watermark. We are programming!
Homework¶
Wait a second! Our only way of getting values on the stack is push_u8
. That can only push a u8
, so only values 0
- 255
. How did I push that 1000000
there?
The source code for this post can be found under the tag v0.0.11-journey
.
- v0.0.11-journey source code
- v0.0.11-journey release
- v0.0.11-journey.zip
- v0.0.11-journey.tar.gz
git checkout v0.0.11-journey