This is a suite of acceptance tests for the B4 virtual machine. There are B4 implementations in multiple languages, so this file helps to make sure they are all compatible and follow the spec.
This is a slightly messy process at the moment, as test runner is currently in another repository:
https://github.com/tangentstorm/learntris
See the test-b4-*
scripts in this repository for examples.
> %q
= %q : quit
: The '%q' command instructs b4 to quit.
:
: b4 should not produce any output unless
: explicitly instructed to do so.
> ?d %q
ds: [] # should be an empty array
= ?d : query the data stack
> ?c %q
cs: [] # should be an empty array
= ?c : query the call stack
> FF CC ?d %q
ds: [FF CC]
= hex numbers are pushed to data stack
: it should print in hex
> 'a 'A ' ?d %q
ds: [61 41 20]
= ascii syntax
: note lack of a second space after the '
: it should print in hex
> `@ `A `B `C `X `Y `Z ?d %q
ds: [0 4 8 C 60 64 68]
= control characters
: the 32 ascii control characters act as a dictionary.
: (^@ is written 00 and is a no-op)
: ^@ calls the word whose address is at address 0000
: (but the actual code emitted is =lb 00 cl= op, as bytecode 0 is a null op)
: ^A calls the word whose address is at address (4 * 1 = 0004)
: ^X calls the word whose address is at address (4 * ord('X')-ord('A') = 5C)
: But if we want handy way to refer to the address, we use `X
> `[ `\ `] `^ `_ ?d %q
ds: [6C 70 74 78 7C]
= non-alphabetic control characters
: there are 5 control characters after ^Z that use punctuation.
> 01 02 ad ?d %q
ds: [3]
= ad: add top two items on the stack
: result is pushed back to stack
> 03 03 ml ?d %q
ds: [9]
> 0A 05 sb ?d %q
ds: [5]
> 0A 05 dv ?d %q
ds: [2]
> 0A 05 md ?d
ds: [0]
> zp 0A 03 md ?d %q
ds: [1]
> 06 01 sh ?d %q
ds: [C]
> 12 35 an ?d %q
ds: [10]
> 12 35 or ?d %q
ds: [37]
> 12 35 xr ?d %q
ds: [27]
> 12 nt ?d %q
ds: [-13]
> AA BB eq CC CC eq ?d %q
ds: [0 -1]
> AA BB lt DD CC lt EE EE lt ?d %q
ds: [-1 0 0]
> 0A du ?d %q
ds: [A A]
> 0A ?d zp ?d %q
ds: [A]
ds: []
> 0A 0B sw ?d %q
ds: [B A]
> 0A 0B ov ?d %q
ds: [A B A]
> 0A dc ?d ?c
ds: []
cs: [A]
> cd ?d ?c %q
ds: [A]
cs: []
> ?100 %q
.. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. # 16 0 bytes
> ?100
.. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
> :100 00 AA BB CC
> ?100 %q
.. AA BB CC .. .. .. .. .. .. .. .. .. .. .. ..
wi
writes a 32-bit integer.
> %C
> AABBCCDD 0100 wi
> ?100 %q
DD CC BB AA .. .. .. .. .. .. .. .. .. .. .. .. # 16 0 bytes
wb
writes a single byte:
> %C
> AABBCCDD 0100 wb
> ?100 %q
DD .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. # 16 0 bytes
which of these should it be?
ds: [CC00BBAA] ds: [-33FF4456]
rb
reads a byte
> :100 AA BB 00 77
> ?100
AA BB .. +W .. .. .. .. .. .. .. .. .. .. .. ..
> 0100 ri ?d %q
ds: [7700BBAA]
> :100 AA BB CC 00
> ?100
AA BB CC .. .. .. .. .. .. .. .. .. .. .. .. ..
> 0100 ri ?d %q
ds: [CCBBAA]
> 12345678 !X ?d
ds: []
> @X ?d %q
ds: [12345678]
> :100 lb 12 !X @X c1 +X @X
> %s ?d
ds: [12]
> %s ?d ?X # !X
ds: []
00000012
> %s ?d ?X # @X
ds: [12]
00000012
> %s %s ?d ?X # c1 +X
ds: [12 12]
00000013
> %s ?d ?X # @X
ds: [12 12 13]
00000013
> %q
The “+” ops take a value off the stack and add it to a register, leaving the original value of the register. You can use this to treat the register as a cursor through a string or array of values.
> 11223344 !X
> 04 +X 02 +X @X ?d %q
ds: [11223344 11223348 1122334A]
> c0 c1 ?d %q
ds: [0 1]
> ?i %q
ip: 100
= ?i : query instruction pointer
: it should print in hex
> ?i %s ?i %q
ip: 100
ip: 101
= %s : step
: step and execute a no-op
> :100 lb AB
> ?100
lb AB .. .. .. .. .. .. .. .. .. .. .. .. .. ..
> ?d
ds: [] # it should not be on the stack YET
> %s ?d ?i %q
ds: [AB]
ip: 102
= lb: load byte
: lb loads a byte from memory at runtime.
: we never needed it before because our debug shell
: is pushing numbers directly to the stack
Hop is a small relative jump. It takes a signed 8-bit int as a parameter, and can thus move the instruction pointer forward up to 127 bytes, or backwards up to 128 bytes.
> :100 hp 05
> ?i %s ?i %q
ip: 100
ip: 105
> :100 hp 7F
> %s ?i %q
ip: 17F
here we set the high bit so it’s the same as negative 1. (but then that puts us at address 00FF, which is too small so we clamp to 0100 and then we have an infinite loop)
> :100 hp 80
> %s ?i %q
ip: 100
> :100 .. .. .. hp -3
> %s %s %s ?i %s ?i %q
ip: 103
ip: 100
> :100 hp -5
> %s ?i %q
ip: 100
This causes an infinite loop.
> :100 hp 00
> %s ?i %q
ip: 100
h0
is the same as hp
but conditional.
It pops a value off the data stack, and only hops if the value is 0.
We push 0 to the stack and then step, so we should jump to address $0123
> :100 h0 23
> 00 %s ?i %q
ip: 123
Here the hop is not taken, but we still hop over the argument.
> :100 h0 23
> 01 %s ?i %q
ip: 102
jm
is an unconditional jump to a 4-byte address.
> :100 jm 78 56 34 12
> %s ?i %q
ip: 12345678
cl
is the same as jm
but also pushes a return address to the control stack.
Note that the instruction pointer is incremented by 5 first, to skip over the cl
op itself, plus its 4-byte argument.
> :100 cl 78 56 34 12
> %s ?i ?c %q
ip: 12345678
cs: [105]
In general, rt
is used to return control from a called function.
The actual mechanic is a jump to an address popped from the control stack.
To simplify this test, we simply push the address we want to the control stack ourselves.
> :100 rt
> 1234 dc ?i ?c %s ?i ?c %q
ip: 100
cs: [1234]
ip: 1234
cs: []
Here’s a small catch for the “dynamic call” technique used in the previous test.
It only
comes into play when using the “calculator mode”.
Note that in the previous test, we used %s
to trigger a step. This
reads an instruction from ram[ip] and then causes the instruction pointer to increment.
If we had simply invoked rt
directly using the “calculator”, no “step” has occured, and so the address would be off by one.
In general, it probably just doesn’t make sense to use contrtol flow ops from the “calculator” outside of testing.
> 1234 dc ?i ?c rt ?i ?c %q
ip: 100
cs: [1234]
ip: 1233
cs: []
This is probably the most complicated operation.
It’s intended for loops where you do something a fixed number of times.
An integer counter is stored on the control stack. Every time nx
is run,
the counter is decremented. A hop is triggered when the result is
not zero, so that the loop continues until the countdown reaches 0.
On the step where it does reach zero, the counter is dropped.
In this test, we loop back to the starting address twice, and then proceed.
> :100 nx 00
> 2 dc
> ?c ?i
cs: [2]
ip: 100
> %s ?c ?i
cs: [1]
ip: 100
> %s ?c ?i
cs: []
ip: 102
> %s ?c ?i
cs: []
ip: 103
> %q
This should emit a single character.
While running in the b4i interpreter, it should buffer the output until the end of line is received.
> 'h 'e io 'i 'e io %q
hi
> ?E # by default, most registers are blank
00000000
> ?_ # but "here" pointer is set to $100=256
00000100
> :E lb 'e io rt # assemble "emit"
> ?E # now ^E should be assigned
00000100
> ?_ %q # and ^_ should reflect the 4 bytes we assembled
00000104
> :E lb 'e io rt
> 'o ^E 'k ^E
ok
> %q