Prologue: | This document provides a more in-depth explanation of the semantics of the WGC virtual machine. The WGC is not a clock-based machine. Each instruction takes as long as it takes to evaluate, and then the next is run. There is no pipelining or instruction-level parallelism; the code is executed exactly as written sequentially. All caching is handled by the host machine. Assembler Syntax: Statements: | The assembler is line-oriented. Each line is either blank or a statement. As an exception, cond may appear on its own line immediately preceeding what it affects. A statement may take any of these forms: [;] [;] [;] [;] A nomacro-operation is: [cond [] [line-break]] [] An operation is: [] [] . [] macro ([]) proc [] end extern proc [] A label_decl is: @: global $: label $$: A data-reservation is: ""[%] $""[%] %""[%] % $% %% An argument is either a register, a literal number, a character, a string, or a symbol. Registers, numbers, and strings may be preceded with a *, which denotes an indirect lookup. Values: | Registers are denoted as [name], and the list of registers is detailed below. Numbers are decimal by default, however a case-insensitive base suffix may be given. H denotes hexadecimal, B denotes binary, O or C denotes octal. In the case of hexadecimal, if the leading digit is not 0-9, a leading 0 must be prepended to remove ambiguity. Unlike in C, this does not cause it to be octal. Prefixing an integer by a negative sign performs a 2's complement on it. Digits may be grouped by inserting commas or colons. Such separators must have at least one digit on either side, but are not otherwise checked for sanity. Numbers may not be broken up by spaces. Floating point numbers are identified by the presence of a decimal point or by the letter E followed by an exponent, or P for hex floating point literals. Numbers may also be written as parenthesised constant expressions, using conventional notation. Every math operation for which there is an instruction in the VM is allowed. (Specifically, those listed in the imath, fmath, bitwise, or ick sections of the instruction list, except for rand or frand). In order to avoid creating novel syntax, any operation that cannot be directly expressed in C is written as a function with the same name as the instruction. Constant registers may also be used in constant expressions, with either the R or F addressing modes. Thus, (64 + (7<<3 | shcr(15, 1))) equals 8000,0000,0000,007Fh Characters are simply numbers with an alternate representation. A character is surrounded by single quotes. Any Unicode BMP character is accepted; its value is its code point. A literal single quote is written as '''. A register may be offset by a number, which is written as the register plus '+' or '-' plus the number, in any accepted format, except for floating-point numbers. Note that this, like numbers (literal or expression) may only be used as an input parameter. Thus, [gp0]+100 evaluates to the current value of [gp0] plus 100. Note that, for a read operand, register, small immediate, and register offset are all actually the same mode. That is, [zero]+0, [zero], and 0 are all assembled identically, and 100 and [zero]+100 are also identical. A value may also be %P or %H, which explicitly indicates that the top of either the stack or the hstack, respectively, is to be used as the operand. A read operand is popped, while a write operand is pushed. In order to use near pointers, the [offset] register is provided. Once it is set with the high-order bits of an address, it can be used with the syntax %, where is any of a register-name, a small integer value, or a register offset by a small integer value. Thus, arguably the most complex addressing mode supported is offset-register-adjusted. This looks like %[gp0]+4, which is equivalent to ([index]<<20)|(([gp0]+4])&20), however that expression is not of constant value and cannot be used as an operand to an instruction. Strings are pointers to a block of character data. The first word of a string contains a 32-bit (unsigned) length, followed by 4 UTF-8 code units. Subsequent words contain 8 code units each. Thus, in some contexts, a string must be prefixed with * to dereference the pointer. Strings can be decorated in various ways, which influences how they are stored in the binary. They are detailed in the section on directives. Note that all instructions' operands are executed right-to-left, which is mostly important for instructions popping multiple words from the stack. Reads are executed before writes in all cases, independent of instruction operand order. Architecture: Memory model: | The WGC has a flat memory space with 32-bit addressing. A memory word is 64 bits, however unaligned access on an octet basis is supported by special instructions. Thus, at some loss of efficiency, one can use 35-bit pointers and 8-bit memory words. 32 bits is enough to address 4 GiW, which is 32 GiB. Memory layout: :| Memory is organized into 4096 segments of 1048576 words. That is, 2**12 segments of 2**20 words, for a total address space of 32 bits. Each page has certain associated permissions. The memory layout at program initialization is: Scratch region: Size: 1 segment (1MiW / 8 MiB) Perms: Read/Write Purpose: | This region comprises those words which can be accessed by the *S addressing mode. Thus, this region is efficiently accessible by any code at any time. As an exception, the first 512 words are inaccessible and will signal a segfault on access; this is so that memory address 0 can be used as a null pointer. Therefore, 1048064 words are accessible. Data region: Size: N segments (N MiW / N*8 MiB) Perms: Read Purpose: | Holds read-only data, such as strings or constants. User code region: Size: N segments (N MiW / N*8 MiB) Perms: Read/Write* Purpose: | Holds the user-written code. The code pointer starts at the beginning of this region. *: the write permission can be toggled for this region with the system calls @noselfmodify and @doselfmodify. Library region: Size: N segments (N MiW / N*8 MiB) Perms: Read Purpose: | Holds non-privileged OS/standard library code, such as memcmp. This region is not affected by the self-modification controls, and is never modifiable. Otherwise, it can be considered an extension of the user code region. Call stack region: Size: 1 segment Perms: None Purpose: | Holds the call stack, which is inaccessible to user code. The call stack holds past instruction pointers and frame pointers. With 1 MiW allocated, and each frame using one word, a stack depth of just over 1 million is possible. If this is exceeded, an interrupt will be raised on the offending transfer. Stack region: Size: Perms: Read/Write Purpose: | Holds the data stack and high stack. The data stack begins at the beginning of this region, and the high stack begins at the end. Heap region: Size: explicitly set Perms: Read/Write Purpose: | Heap aka free store. Unless requested by a directive, this region has zero size. If it does exist, its space is taken out of the hstack portion of the stack region. Unlike the other regions, this region has word size-granularity, because it is technically part of the hstack. It is referred to as a region nonetheless for convenience. When multiple assembly inputs request heaps, their sizes are added together such that the overall program has a single heap. OS region: Size: N segments (N MiW / N*8 MiB) Perms: Privileged Purpose: | Holds OS code. Can only be transferred to with the systransfer instruction. Instructions: | Bit format: 1bits skipnext 4bits condition code 9bits opcode* 5bits addr1 5bits addr2 20bits data 1 20bits data 2 Addressing modes: # total: 30 -> 5 bits Read-Writable: R: syntax: '' desc: | Use the named register. Technically, this is 25 distinct addressing modes, and is just the special case of F with offset zero. *: syntax: '* followed by any readable mode.' desc: | Any input addressing mode may be used to derive a pointer, which is then dereferenced to obtain the actual argument. This is called "indirection", and is marked in this document by placing a * before the name(s) of the addressing modes allowed. Only instructions in the "general memory access" or "device control" sections of this manual can use indirect memory accesses. Mode *{M...} applies * to everything inside it. Mode *[M...] is an abbreviation of M...,*{M...}. Writable: P: syntax: blank, or %P if explicit. desc: | Pushed to the stack. H: syntax: blank, or %H if explicit. desc: | Pushed to the hstack. Non-writable: P: syntax: blank, or %P if explicit. %P+ for adjusted desc: | Popped from the stack. Adjustment is as in mode F. H: syntax: blank, or %H if explicit. %H+ for adjusted desc: | Popped from the hstack. Adjustment is as in mode F. F: syntax: '+' desc: | Add number to the named register's value after reading it. The register is not mutated; the addition happens "in flight". S: syntax: signed number which fits within 20 bits desc: | Store a value inside of the instruction word, allowing for efficient small constants. Technically, this is [zero]+number. O: syntax: either [] or +[] desc: | Use the small immediate value bitor $index<<20. Other: D: syntax: blank (no argument is given) desc: | Use a designated default value which is specified by the instruction. Often, this is [jump] or %P, but see the documentation of the particular instruction. Note that this is different from implicit or conceptual arguments, which are not actually represented in the machine code at all. Register categories: Constant registers: | The value is "hardwired" and, while modifying operations are permitted, they have no effect. Read/write unprivileged. Immutable registers: | The value is dependent on the state of the program, but directly modifying their value is not allowed. Instead, there are special-purpose instructions which can alter their values in specific, valid ways. Read unprivileged, write restricted. Mutable registers: | These registers are entirely user-controlled. They may be used as general-purpose registers, although some are given special meaning in the ABI. Read/write unprivileged. Memory-mapped registers: | These registers allow for treating special memory regions as registers, and have the same access control as the backing memory. Hidden registers: | These registers are not directly accessible by user code at all, although certain unprivileged instructions are allowed to make use of their values in specific, valid ways. Read/write restricted. Registers: # total: 26 Constant registers: # 5 zero: size: 64 bits access: R/W value: 0 (constant) max: size: 64 bits access: R/W value: 0:FFFF:FFFF:FFFF:FFFFh (constant) fzero: size: 64 bits access: R/W value: floating-point +0 (constant) finf: size: 64 bits access: R/W value: floating-point +inf (constant) err: size: 32 bits access: R value: Holds a pointer to the beginning of the error handler pointer region. See documentation on interrupts. Immutable registers: # 9 [stackptr, SP]: size: 32 bits access: R value: address of the top of the stack [frameptr, FP]: size: 32 bits access: R value: Value of SP at time of last transfer [hstackptr, HSP]: size: 32 bits access: R value: address of the top of the hstack [codeptr, IP]: size: 32 bits access: R value: address of the current instruction flag: size: 16 bits access: R value: | various status bits which are set or read by certain instructions. Each bit has a corresponding macro to extract it and push it to the stack, which is the bit name prefixed by 'f_'. For instance, to push the overflow bit, you write f_overflow control: size: 16 bits access: R value: | Holds control bits which influence how the processor behaves. [lastmemaccess, LMA]: size: 32 bits access: R value: Last memory address accessed. Not set by all instructions. ccnt: size: 64 bits access: R value: A 64-bit count of instructions run since last reset. clock: size: 64 bits access: R value: Holds the current Unix timestamp. 64-bit count of seconds. clock_frac: size: 20 bits access: R value: | Holds the microseconds value of the current time as of when the instruction began processing. Maximum value of 999,999. Mutable registers: # 6 gp0: size: 64 bits access: R/W value: general-purpose gp1: size: 64 bits access: R/W value: general-purpose arg: size: 64 bits access: R/W value: | Holds number of arguments passed for gencall convention. Usually safe to use as a general-purpose register. counter: size: 64 bits access: R/W value: | Used implicitly as length parameter for mcpy and mset instructions. Usually safe to use as a general-purpose register. jump: size: 64 bits access: R/W value: | Destination of jump or transfer. Note that this register is clobbered in several cases; using it as a general-purpose register is unadvised. index: size: 12 bits access: R/W value: relocation base for addressing mode O. - Memory-mapped registers: # 4 [stackval, SV]: size: 64 bits access: R/W value: holds *[SP] [prevstackval, PSV]: size: 64 bits access: R/W value: holds *[SP#-1] [hstackval, HSV]: size: 64 bits access: R/W value: holds *[HSP] LMV: size: 64 bits access: depends value: holds *[LMA] Hidden registers: # 2 [callstackptr, CSP]: size: 32 bits access: none value: address of top of call stack [callstackval, CSV]: size: 64 bits access: none value: holds *[CSP], the most recent return address interval: size: 32 bits access: R value: counts down from last value set; interrupts when it reaches zero. CIC: size: 8 bits access: none value: holds the interrupt number to raise when the interval counter reaches 0. timer: size: 32 bits access: R/W value: | Time at which the next timer goes off, in microseconds. TIC: size: 8 bits access: none value: holds the interrupt number to raise upon next timer fire. Flags register: Bit: 0: reserved (always set) 1: condition: If the last test was successful. 2: zero: If the result of the last arithmetic or bitwise operation was zero. 3: sign: sign bit of the result of the last arithmetic or bitwise operation. 4: parity: If the number of bits in the result of the last arithmetic or bitwise operation was odd. 5: overflow: If the last arithmetic operation overflowed. 6: carry: If the last arithmetic operation carried out. 7: underflow: If the last FP operation underflowed. 8: saturation: If the last arithmetic operation saturated. 9: domain: If the last FP operation failed due to a domain error. A: divzero: Last FP operation performed a division by zero and fexcept is unset. B: range: One of the operands to the last FP operation was out of range. C: D: E: !condition: always holds the opposite of condition F: reserved (always unset) Control register: Bit: 0-1: intl, inth: Interrupt priority mask. 2: fexcept: If FP errors should raise interrupts or only set flags. 3: 4: direction: Direction to copy for mcpy instruction. 0=incrementing, 1=decrementing. 5: 6: 7: sup: If running in supervisor mode. Instructions: # total: 85 (condense imath, fmath) -> 7 bits # 102 (condense fmath) -> 7 bits # 151 (no condensing) -> 8 bits General memory access: # total: 26 set: args: #4 dest: '*[R,P,H],*{F,S,O}' val: '*[R,S,O,F,P,H]' effect: | Sets (dest) to (val). (Mostly) equivalent to push (val) pop (dest) Because P and H are legal modes for both arguments, push, pop, peek, the hstack versions of each, and 1-element move instructions can all be implemented as aliases of set. Sets [LMA] in the following circumstances: dest is indirect, val is not: [LMA] = dest address dest and val are indirect: [LMA] = dest address val is indirect, dest is not: [LMA] = val address neither is indirect: [LMA] unchanged Note: set %P %P is a nop. This is important for understanding some macros. Alias instructions: push (v): set %P v pop (d): set d %P peek (d): set d [SV] drop: set [zero] %P over: set %P [PSV] hpush (v): set %H v hpop (d): set d %H hpeek (d): set d [HSV] hdrop: set [zero] %H swap: args: #4 A: '*[R],*{F,S,O,P,H}' B: '*[R],*{F,S,O,P,H}' effect: | Swaps the values of A and B. Both arguments must be readable and writable. Swapping the same thing twice is a nop. Does not set [LMA]. Note that swap with no arguments is treated as an intrinsic, and will be assembled as swap [SV] [PSV]. To prevent surprising behavior, swap with one argument is illegal. xchg: args: A: '*[R],*{F,S,O,P,H}' B: '*[R,S,O,F,P,H]' (implicit) dest: 'P' effect: | Pushes A to the stack, and then assigns B to it. This operation is equivalent to push A set A B load_ua: args: #2 src: '*[R,S,O,F,P,H]' c_o: R,F,S,O,P,H (implicit) dest: P effect: | Reads an unaligned partial word starting at src + offset/8 and extending count octets and pushes it. The read word is packed to the right. Note: the c_o argument is a bitfield with the format (offset&111b | count-1<<3) offset is the number of leading octets to ignore. count is the number of octets to read in total. Never sets [LMA]. load_ua_se: args: #2 src: '*[R,S,O,F,P,H]' c_o: R,F,S,O,P,H (implicit) dest: P effect: | Reads an unaligned partial word starting at src + offset/8 and extending count octets and pushes it. The read word is packed to the right and sign-extended. Note: the c_o argument is a bitfield with the format (offset&111b | count-1<<3) offset is the number of leading octets to ignore. count is the number of octets to read in total. Never sets [LMA]. store_ua: args: #2 (implicit) src: P dest: '*[R,S,O,F,P,H]' c_o: R,F,S,O,P,H effect: | Left-truncates P to count octets and writes it to the unaligned address dest + offset/8. Because dest is an address, it is an input parameter. Unwritten leading or trailing bytes are unchanged. Note: the c_o argument is a bitfield with the format (offset&111b | count-1<<3) offset is the number of leading octets to ignore. count is the number of octets to read in total. Never sets [LMA]. mcpy: args: #4 dest: '*[R,S,O,F,P,H]' src: '*[R,S,O,F,P,H]' (implicit) len: D([counter]) effect: | Copies len words from *src to *dest. Does not set LMA. mset: args: #4 dest: '*[R,S,O,F,P,H]' val: '*[R,S,O,F,P,H]' (implicit) len: D([counter]) effect: | Sets len words starting at *dest to val. Does not set LMA. dereference: args: #4 ptr: '*[R,S,O,F,P,H]' inds: '*[R,S,O,F,P,H],D(1)' effect: | Perform a multiply-indirect memory access. Specifically: dereference ptr inds times, then discard the result (but set [LMA]). The final address, which [LMA] is set to, is not actually accessed, and need not be accessible. inds=1 corresponds to a single indirection, and is the default. Stack instructions: # 4 mpush: args: len: R,F,S,O,P,H effect: | Reserves (len) words on the stack without initializing them. Equivalent to subtracting len from [SP]. mpop: args: len: R,F,S,O,P,H effect: | Discard the top len words on the stack. Equivalent to adding len to [SP]. macro reserve: args: len: R,F,S,O,P effect: | Pushes (len) 0s to the stack. len cannot be %H for implementation reasons. impl: | ; handle %P/%H as arg hpush len hpush [counter] swap *[HSP] *[HSP]+1 set [counter] %H hpush [SP] mpush [counter] mset %H [zero] hpop [counter] rot: args: n: R,F,S,O,P,H p: R,F,S,O,P,H stack: depends on arguments effect: | Rotates top (n) values on the stack by (p) places Alias instructions: rot31: rot 3 1 ; -> (a b c -- c a b) rot32: rot 3 2 ; -> (a b c -- b c a) rev: args: n: R,F,S,O,P,H stack: depends on arguments effect: | Reverses top (n) stack values. e.g.: rev 3 ; -> (a b c -- c b a) High stack instructions: # total: 3 mhpush: args: len: R,F,S,O,P,H effect: | Reserves (len) words on the hstack without initializing them. Equivalent to subtracting len from [HSP]. mhpop: args: len: R,F,S,O,P,H effect: | Discard the top len words on the hstack. Equivalent to adding len to [HSP]. macro hreserve: args: len: R,F,S,O,P effect: | Pushes (len) 0s to the hstack. impl: | ; handle %P as arg push [counter] ; save the old [counter] push len set [counter] [SV] ; because the hstack grows down, the mset goes after the memory is reserved. mhpush [counter] mset [HSP] [zero] ; clear len arg pop [zero] pop [counter] ; restore the old [counter] save: args: effect: | Pushes all mutable registers to the hstack. restore: args: effect: | Pops all mutable registers from to the hstack. This instruction perfectly reverses save. macro movesh: args: len: R,F,S,O effect: | Moves (len) words from stack to hstack. The stack pointer is decremented by (len) but the words are not cleared. This is essentially a limited mcpy with special stack behavior; the words transferred to the hstack will be popped in reverse order as they were on the stack. Uses global storage @__move__: @%3 impl: | set *@__move__ len mhpush *@__move__ set *@__move__+1 [HSP] ; dest ; get beginning of source range mpop *@__move__ set *@__move__+2 [SP] ; src mpush *@__move__ ; [counter] is implicitly used by mcpy swap [counter] *@__move__ mcpy *@__move__+1 *@__move__+2 swap [counter] *@__move__ macro movehs: args: len: R,F,S,O effect: | Moves (len) words from hstack to stack. The hstack pointer is incremented by (len) but the words are not cleared. This is essentially a limited mcpy with special stack behavior; the words transferred to the stack will be popped in reverse order as they were on the hstack. Uses global storage @__move__: @%3 impl: | set *@__move__ len set *@__move__+1 [SP] ; dest mpush *@__move__ set *@__move__+2 [HSP] ; src ; note that the source pointer points to the low end ; of the source range, while the dest pointer ; (because it was set before reserving) points to the ; low end of the destination range ; [counter] is implicitly used by mcpy swap [counter] *@__move__ mcpy *@__move__+1 *@__move__+2 swap [counter] *@__move__ Serial device control: # total: read_word: args: device: R,S,F,P,H dest: '*[R,P,H],*{F,S,O}' effect: | Read one word from device and store to dest. read_blck: args: device: R,S,F,P,H len: 'R,S,O,F,P,H' effect: | read len words onto stack read_ch: args: device: R,S,F,P,H dest: '*[R,P,H],*{F,S,O}' effect: | Read one character from device and store to dest. write_word: args: device: R,S,F,P,H val: '*[R,S,O,F,P,H]' effect: | Write val to device. write_str: args: device: R,S,F,P,H str: '*[R,S,O,F,P,H]' effect: | write length-prefixed string pointed to by str. write_blck: args: device: R,S,F,P,H len: 'R,S,O,I,F,P,H' effect: | Write the top len words of the stack to device, in memory order (not pop order). write_ch: args: device: R,S,F,P,H val: '*[R,S,O,F,P,H]' effect: | Write val&255 to dest. poll_in: args: device: R,S,F,P,H effect: | pushes the number of available words of input on a device. output: (p)=[stackptr] ; With no argument, use the stack pointer to output a single word. Integer math (imath) instructions: # total: 3, 20 fma: args: (implicit) A: P B: R,S,P,F C: R,S,P,F stack: (a b c -- (a*b)+c) effect: | Pushes (signed) A*B+C. Flags are set based on the overall result. ufma: args: (implicit) A: P B: R,S,P,F C: R,S,P,F stack: (a b c -- (a*b)+c) effect: | Pushes (unsigned) A*B+C. Flags are set based on the overall result. imath: args: mode: S op: S (if present) (implicit) operand1: P operand2: R,S,P,F (implicit) output: P effect: | Performs an integer math operation on the elements on the stack. In order to reduce the number of opcodes needed, the specific operation to perform is pushed into the small immediate 1 section of the instruction word. If an additional explicit argument is provided, then that argument shall be treated as the second operand of the specified operation. 0-operand instructions: # total: 1 random: op: 0h effect: | Push a single word of pseudorandom data onto the stack. The random data source is not in software. 1-operand instructions: # total: 1 abs: op: 10h stack: (x -- abs(x)) effect: | Pops a value from the stack, negates it if negative, and pushes it. If x is INT_MIN, the result is INT_MIN and the overflow flag is set. 2-operand instructions: # total: 16 add: op: 40h stack: (a b -- a+b) effect: | Add without carry. Overflow causes saturation and the relevant flags are set. addc: op: 42h stack: (a b -- a+b+carry) effect: | Add with carry. Overflow causes saturation and the relevant flags are set. sub: op: 44h stack: (a b -- a-b) effect: | Subtract without carry. Overflow causes saturation and the relevant flags are set. subc: op: 46h stack: (a b -- a + bitnot(b) + C) effect: | Subtract with carry. Overflow causes saturation and the relevant flags are set. mul: Signed multiplication, with saturation. op: 48h stack: (a b -- a*b) effect: | Pushes (signed) A*B. Overflow causes saturation and the relevant flags are set. div: op: 4Ah stack: (a b -- a/b a%b) effect: | Signed integer division. Pushes (signed) A/B and A%B. If B is zero, then the trap @__div_by_zero__ (10h) is triggered. idiv: op: 4Ch stack: (a b -- a/b) effect: | Pushes (signed) A/B. If B is zero, then the trap @__div_by_zero__ (10h) is triggered. mod: op: 4Eh stack: (a b -- a%b) effect: | Pushes (signed) A%B. If B is zero, then the trap @__div_by_zero__ (10h) is triggered. wadd: op: 41h stack: (a b -- a*b) effect: | Pushes A+B. Overflow wraps, and the relevant flags are set. waddc: op: 42h stack: (a b -- a+b) effect: | Pushes A+B+carry. Overflow wraps, and the relevant flags are set. wsub: op: 45h stack: (a b -- a*b) effect: | Pushes A-B. Overflow wraps, and the relevant flags are set. wsubb: op: 46h stack: (a b -- a-b) effect: | Pushes A-(B+carry). Overflow wraps, and the relevant flags are set. umul: op: 49h stack: (a b -- a*b) effect: | Pushes (unsigned) A*B. udiv: op: 4Bh stack: (a b -- a/b a%b) effect: | Pushes (unsigned) A/B and A%B. If B is zero, then the trap @__div_by_zero__ (10h) is triggered. uidiv: op: 4Dh stack: (a b -- a/b) effect: | Pushes (unsigned) A/B. If B is zero, then the trap @__div_by_zero__ (10h) is triggered. umod: op: 4Fh stack: (a b -- a%b) effect: | Pushes (unsigned) A%B. If B is zero, then the trap @__div_by_zero__ (10h) is triggered. Floating-point math (fmath) instructions: # total: 1, 50 fmath: args: mode: S op: S (if present) (implicit) operands: P (implicit) output: P effect: | Performs a floating point math operation on the elements on the stack. In order to reduce the number of opcodes needed, the specific operation to perform is pushed into the small immediate 1 section of the instruction word. If an additional explicit argument is provided, then that argument shall be treated as the second operand of the specified operation. Note that there are no instructions for multiplying mixed floats and ints, so itof must be used on every integer operand. 1-operand instructions: # total: 35 itof: effect: | Converts its sole operand to floating-point format. ftoi: effect: | Truncates its operand and converts to integer. Results out of range saturate and set the saturation flag. fabs: fsqrt: fcbrt: frcp: ; (a -- (1/a)) fneg: ; (a -- (-a)) fceil: ffloor: ftrunc: fround: fiszero: fisnormal: fissubnormal: fisinf: fisnan: fsign: fsin: ; (a -- sin(a)) fcos: ftan: fasin: facos: fatan: fsinh: fcosh: ftanh: fasinh: facosh: fatanh: fexp: fexp2: fexpm1: fln: flb: flg: flnp1: 2-operand instructions: # total: 14 fadd: fsub: fmul: fdiv: fpow: fcmpgt: fcmpge: fcmplt: fcmple: fcmpne: fcmpeq: fcmpuo: ; unordered fatan2: ; (a b -- atan2(a, b)) frand: ; (a b -- rand_in_range(a, b)) 3-operand instructions: # total: 1 ffma: ; FP "coprocessor" control: ; based on value of (op), performs a floating point operation on the values in the stack fmath (op) ; Alias instructions ; conversions itof ftoi ; simple math fadd fsub fmul fdiv ; (a b -- (a/b)) fpow ffma ; (a b c -- ((a*b)+c)) # 8 ; floating-point comparisons (like integer ones) fcmpgt fcmpge fcmplt fcmple fcmpne fcmpeq ; single-operand math fabs fsqrt # 16 fcbrt frcp ; (a -- (1/a)) fneg ; (a -- (-a)) ; rounding/nearest integer fceil ffloor ftrunc fround ; floating point classifications fiszero # 24 fisnormal fissubnormal fisinf fisnan fsign ; trigonometric functions fsin ; (a -- sin(a)) fcos ftan # 32 fasin facos fatan ; Binary trigonometric fatan2 ; (a b -- atan2(a, b)) ; hyperbolic functions fsinh fcosh ftanh fasinh # 40 facosh fatanh ; exponential functions fexp fexp2 fexpm1 ; logarithms: natural, binary, decimal fln flb flg # 48 flnp1 ; random float frand ; (a b -- rand_in_range(a, b)) # 50 Bitwise instructions: # total: 12 # NOTE: Flags are set as in integer math operations shll: args: A: P,R,F B: P,R,S,F stack: (a b -- a*(2^b)) effect: | Logical left shift. Pops two values, A and B, and pushes A<<(B&63). Equivalent to multiplication of A by 2^B. If set bits are shifted off the end, the overflow and carry flags are set. If the result is zero, the zero flag is set. In any case, the parity bit is set based on the result. shlr: args: A: P,R,F B: P,R,S,F stack: (a b -- a/(2^b)) effect: | Logical right shift. Pops two values, A and B, and pushes A>>(B&63). Equivalent to division of A by 2^B. If set bits are shifted off the end, the carry bit is set. If the result is zero, the zero flag is set. In any case, the parity bit is set based on the result. shal: args: A: P,R,F B: P,R,S,F stack: (a b -- a*(2^b)) effect: | Note that logical and arithmetic left shifts are identical; this instruction is provided only for symmetry. Arithmetic left shift. Pops two values, A and B, and pushes A<<(B&63). Equivalent to multiplication of A by 2^B. Flags are set as in shll. shar: args: A: P,R,F B: P,R,S,F stack: (a b -- a/(2^b)) effect: | Arithmetic right shift. Pops two values, A and A, and pushes A>>(B&63), with the modification that instead of shifting in zeros on the left, the leftmost (aka sign) bit is duplicated instead. Flags are set as in shlr. shcl: args: A: P,R,F B: P,R,S,F stack: (a b -- a*(2^b)) effect: | Circular left shift. Pops two values, A and B, and left shifts A by B places, with bits falling off the left side being pushed in on the right. If the result is zero, the zero flag is set. In any case, the parity bit is set based on the result. shcr: args: A: P,R,F B: P,R,S,F stack: (a b -- a*(2**b)) effect: | Circular right shift. Pops two values, A and B, and right shifts A by B places, with bits falling off the right side being pushed in on the left. If the result is zero, the zero flag is set. In any case, the parity bit is set based on the result. bitand: args: A: P,R,F B: P,R,S,F stack: (a b -- a & b) effect: | Pushes the bitwise and of the operands. If the result is zero, the zero flag is set. In any case, the parity bit is set based on the result. bitor: args: A: P,R,F B: P,R,S,F stack: (a b -- a | b) effect: | Pushes the bitwise or of the operands. Flags are set as in bitand. bitxor: args: A: P,R,F B: P,R,S,F stack: (a b -- a ^ b) effect: | Pushes the bitwise xor of the operands. Flags are set as in bitand. bitnot: args: A: P,R,F stack: (a -- ~a) effect: | Negates all bits in the operand. Flags are set as in bitand. popcnt: args: A: P,R,F effect: | Pushes the number of set bits in the operand. Flags are set as in bitand. clz: args: A: P,R,F effect: | Pushes the number of leading zero bits in the operand. Flags are set as in bitand. C-INTERCAL (ick) operations: # total: 5 mingle: args: A: P,R,F B: P,R,S,F effect: | Interleave the bits of the lower halfwords of the operands. If either operands' upper halfword is not zero, set overflow flag. See ops.wgc for a partial implementation without using mingle. Set flags as in a shift operation. Alias instructions: - interleave select: args: A: P,R,F B: P,R,S,F effect: | Perform a packed bitmask. All selected bits are packed to the right in the result. That is, if a single bit is selected, the result is in the ones place, and not in the same place as it started. See ops.wgc for a partial implementation without using select. Set flags as in a bitwise and operation. iand: args: A: P,R,F effect: | Unary bitwise and. Equivalent to (A & shcr(A,1)). It is also equivalent to dup shcr %P 1 bitand but faster. Flags are set as in the corresponding binary operation. ior: args: A: P,R,F effect: | Unary bitwise or. Equivalent to (A | shcr(A,1)). Flags are set as in the corresponding binary operation. ixor: args: A: P,R,F effect: | Unary bitwise xor. Equivalent to (A ^ shcr(A,1)). Flags are set as in the corresponding binary operation. Logical and comparison operations: # total: 18 and: args: A: P B: P stack: (a b -- ) a&&b -> f_cond effect: | Tests if both A and B are nonzero. or: args: A: P B: P stack: (a b -- ) a||b -> f_cond effect: | Tests if either A or B or both are nonzero. xor: args: A: P B: P stack: (a b -- ) bool(a) ^ bool(b) -> f_cond effect: | Tests if one but not both of A and B are nonzero. cmp: args: A: P B: P effect: | Compares A and B, signedly. If: A < B: pushes -1 A = B: pushes 0 A > B: pushes 1 ucmp: effect: like cmp, but unsigned. sucmp: args: A: P B: P effect: | Compares A and B, with signed A and unsigned B. If: A < 0, pushes -1 B > SIGNED_MAX: pushes 1 A < B: pushes -1 A = B: pushes 0 A > B: pushes 1 uscmp: effect: like -1 * sucmp(B, A) cmpgt: args: A: P B: P stack: (a b -- ) a>b -> f_cond effect: | Tests if (signed) A > B cmpge: args: A: P B: P stack: (a b -- ) a>=b -> f_cond effect: | Tests if (signed) A >= B cmplt: args: A: P B: P stack: (a b -- ) a f_cond effect: | Tests if (signed) A < B cmple: args: A: P B: P stack: (a b -- ) a<=b -> f_cond effect: | Tests if (signed) A <= B ucmpgt: args: A: P B: P stack: (a b -- ) a>b -> f_cond effect: | Tests if (unsigned) A > B ucmpge: args: A: P B: P stack: (a b -- ) a>=b -> f_cond effect: | Tests if (unsigned) A >= B ucmplt: args: A: P B: P stack: (a b -- ) a f_cond effect: | Tests if (unsigned) A < B ucmple: args: A: P B: P stack: (a b -- ) a<=b -> f_cond effect: | Tests if (unsigned) A <= B cmpeq: args: A: P B: P stack: (a b -- ) a==b -> f_cond effect: | Tests if A == B cmpne: args: A: P B: P stack: (a b -- ) a!=b -> f_cond effect: | Tests if A != B popbool: args: A: P effect: | Set cond is A is nonzero, unset it otherwise. pushbool: args: A: P effect: | Pushes condition flag to stack as either 1 or 0. Equivalent to select [flag] 1 ; cond is the lowest bit of flag test: args: A: P effect: | Sets condition flags 1, 2, and 3 as if by a math operation producing A. not: effect: | Inverts cond flag. true: effect: | Assert true. Logically equivalent to set f_cond 1 false: effect: | Assert false. Logically equivalent to set f_cond 0 Control flow instructions: # total: 11 jmp: args: J: R,F,S,O,D([jump]) effect: | Jumps to address J. Does not save a return address. Signals traps for illegal jumps: Memory not readable: @__perm_no_read__ Memory marked noexecute: @__perm_no_exec__ Memory marked for privileged execution: @__perm_denied__ J < 16: @__null_deref__ Memory marked writeable while noselfmodify is active: @__exec_nsm_v__ Note that all of these signals are raised any time such memory is attempted to be executed. Note that jmp [CP] is an infinite loop. reljmp: args: J: R,F,S,O,D([jump]) effect: | Jumps J addresses forward (or backwards if negative). In effect, similar to jmp [CP]+1 however it works for addressing modes that do not yield constants. Note that reljmp 1 is a no-op. intrinsic cond: args: F: (4 bit number, or a mnemonic, default 1) effect: | Only execute the next instruction if the specified status flag is set. intrinsic skip: effect: | Unconditionally skip the next instruction (after decoding; will not execute immediate data for a following two-byte instruction). Eqivalent to cond 0Fh or, if that would be invalid, reljmp 2 transfer: args: J: D([jump]),R,F effect: | Function call: Pushes [IP]+1 and [FP] to call stack. Sets [FP] to [SP]. Jumps to function in [jump]. If an argument is provided, this becomes an intrinsic, which assembles as set [jump] J transfer return: effect: | Return to just after last transfer. pops [FP] from call stack pops [IP] from call stack (this is effectively a jump) halt: effect: | Stops execution normally. If no timers are active, the virtual machine will shut down, rather than hang, in a manner equivalent to syscall @shutdown with argument 0. intrinsic err: effect: | Stops execution abnormally: value of [arg] is return code. This immediately terminates the virtual machine, no outstanding timers are fired. Assembled as: set [jump] @shutdown systransfer trap: effect: | Raises interrupt [jump]. handle: effect: | Clears the interrupt flags. If the interrupt flags are already clear, this instruction traps with interrupt @__handle_none__. handle_quiet: effect: | Clears the interrupt flags. If the interrupt flags are already clear, this instruction has no effect. int_us: args: Time: P,R,F,S IClass: P,R,F,S,D(@__timer_fired__) effect: | Raises an interrupt of type IClass in Time microseconds. Equivalent to: push [arg] push [gp0] systransfer @hightime # can assume that current time is not negative umul [arg] 1,000,000 add [gp0] add Time pop [timer] pop [gp0] pop [arg] except much faster. int_c: args: C: P,R,F,S IClass: P,R,F,S,D(@__ccnt_fired__) effect: | Raises an interrupt of type IClass after C cycles. intrinsic wait_us: args: Time: P,R,F,S effect: | sleeps for approximately Time microseconds. Expands to: int_us Time @__resume_exec__ halt A preceeding cond will assemble to cond (cflag) reljmp 3 so as to skip the halt as well. systransfer: args: Call: D([jump]),R,F effect: | Perform a system call based on the value of [jump] Miscellaneous operations: # total: 3 intrinsic adjust: args: R: R N: R,S,F effect: | Add n to register r in place. Expands to push R add N pop R That is, a saturating add is used and flags are set accordingly. If N is a constant small expression, this expands to set [R] [R]+N xchg_cc: args: dest: R,P,H val: P,H,R,S,F effect: | Replaces the value of the cycle counter [ccnt] with val, and assign the old value to dest. Logically equivalent to set dest [ccnt] set [ccnt] val aliases: reset_cc (val): xchg_cc [zero] val explode: args: N: S effect: | Split top of stack into N (power of two in range 2-64) segments, big-endian, data in low bits collapse: args: N: S effect: | Join together top (n) (same as above) words' low bits into one word nop: args: A: '*[R,S,O,F,P,H],D(0)' B: '*[R,S,O,F,P,H],D(0)' effect: | Instruction that does absolutely nothing. Despite allowing indirected and stack arguments, nop never has any side-effects at all. nop %P *0 does not perform a pop or dereference the null pointer. Unless the first operand is a large immediate value, the assembler completely ignores both arguments and compiles to the simple nop instruction. If the first argument is a large immediate value, it assembles to set [zero] A Other nops include: set [zero] (anything) ; or [max] set (anything) (the same thing) rot 0 0 ; or rot 1 1 or etc reverse 0 ; or reverse 1 however, most other nops will be slightly slower than the dedicated one, and will actually access their arguments.