-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspecification.let
More file actions
85 lines (61 loc) · 3.28 KB
/
specification.let
File metadata and controls
85 lines (61 loc) · 3.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
= Specification (version 0.1.0)
== Bytecode
Single-byte instruction words appear in the following format:
`1 b a a o o o o`
If the least-significant-bit is not a 1, it is interpreted as a "halt"
instruction, stopping the machine. This way, uninitialized values in ROM
(zeroes, by default) will halt execution.
If bit `b` is 1, the opcode's arguments are popped, and then a single-byte
boolean flag is popped afterwards. If that flag byte is zero, the opcode's
arguments are restored to the stack (minus the flag byte), and the opcode is
skipped.
`a` is the variable argument byte counter, taking values from 1 to 4.
`o` refers to the opcode, one of 16 stack-manipulation instructions
=== Opcodes
> A note on stack notation:
We use FORTH-like stack notation for each opcode, like `( a b -- c d )`, where
`b` is on the top of the stack *before* the operation, and `d` is on the top
*after* the operation.
Seeing a variable followed by a `*`, like `n*`, means that this stack value can
be a variable amount of bytes depending on the `aa` bits of the opcode (1-4
bytes).
Seeing a variable followed by a `W` (rather than a `*`) means that this stack
value is a word (by default, 3 bytes, but this can be redefined with a C macro
if you include the `okvm.h` library and make your own emulator).
Finally, seeing a variable in stack notation followed by a 1, 2, 3, or 4, means
that this stack value is that number of bytes.
So, here are the opcodes:
- `add` ( a* b* -- a+b* ) add 2 stack values
- `and` ( a* b* -- a&b* ) bitwise-and two values
- `xor` ( a* b* -- a^b* ) xor two values
- `shf` ( n* s1 -- m* ) logically shift a value- high nibble is left shift, low nibble is right shift, with right shift occurring first
- `swp` ( a* b* -- b* a* ) swap top 2 values
- `cmp` ( a* b* -- cmp1 ) cmp = 0 if equal, 1 if a > b, 255 if a < b
- `str` ( data* addrW -- ) store data in RAM at addr
- `lod` ( addrW -- data* ) fetch data from RAM at addr
- `dup` ( n* -- n* n* ) duplicate top value
- `drp` ( n* -- ) discard top value
- `psh` ( data* -- ) push data to return stack
- `pop` ( -- data* ) pop data from return stack
- `jmp` ( dest* -- ) jump to dest
- `lit` ( -- val* ) push immediate bytes
- `fet` ( addrW -- data* ) fetch data from ROM at addr
- `nop` ( -- ) no operation, regardless of `a` flag (although `b` flag still works)
=== The "int" opcode
`int` pops a byte off of the stack and interprets it as a device port- basically
a magic address between 0x00 and 0xff that refers to a specific device. When
this port is called upon with an `int` instruction, it calls a C function that is
registered to that port at VM startup- if no such function exists, the VM panics.
At most 16 device functions can be registered to the VM at a time.
Device functions are C functions that have the following signature:
```
uint8_t device_function(uint8_t* ram, uint8_t* rom);
```
Devices can only manipulate the VM's RAM and ROM. After the device is triggered,
the `int` instruction pushes the return value of this function, which can be used
like a status code.
You can call multiple devices with a single `int` instruction, using the `aa`
bytecode flags.
== Note on memory and the stack
Integers are stored in *big-endian* order in RAM and ROM, and multi-byte
integers on the stack have their low (most significant) byte closest to the top.