Prologue: | This document provides a more in-depth explanation of the semantics of the WGC virtual machine. The virtual CPU is relatively simple: it executes instructions in-order, with no pipelining or instruction-level parallelism. There is no clock for instruction timing, rather each instruction simply takes as long as it needs before the next one is executed. Despite having a 64-bit word-size, the address space is only 32 bits wide. The WGC is an impure stack machine. That means that *most* operations use the data stack, however there are registers and random access to memory is permitted. Additionally, there is a second stack, which is very helpful for certain algorithms, and it provides a place to put data that won't be needed for a while. Additionally, but inaccessibly, there is a call stack which contains return addresses, which means that they do not clutter the data stack, which especially helps when calling functions and passing arguments on the stack. Assembler Syntax: Statements: | The assembler is line-oriented. Each line is either blank or a statement. A statement may take any of these forms: [;] [;] [;] [;] A nomacro-operation is: [cond []] [] 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 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 '''. 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. 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. 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. 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. Note that .heap N trivially expands to: @__heap__: %N%N or "global __heap__ is an array of size N where the first word is initalized to N" Library region: Size: N segments (N MiW / N*8 MiB) Perms: Read Purpose: | Holds non-privileged OS/standard library code, such as memcmp. 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. :| Data segment: 4 pages, 16k words. Perms::read 0-15: reserved (attempting to access them is a segfault) 16-31: Special input register alias addresses 32-1055: Input register alias addresses 1056-2047: symbols (1-2W each; 1-8 or 9-16 characters) 2048-16383: strings/data ROM Code/scratch segment: 32 pages, 128k words Perms::read | Perms::write | Perms::exec Call stack segment: 2 pages, 8k words Perms::read | (Perms::write * selfmod) | Perms::priv Data stack segment: 214 pages, 856k words memory[38] = MMUentry(Perms::read | Perms::write); High data stack: grows downward toward data stack memory[251] = MMUentry(Perms::read | Perms::write); Library code segment: 4 pages, 16k words memory[252] = MMUentry(Perms::read | Perms::exec); memory[253] = MMUentry(Perms::read | Perms::exec); memory[254] = MMUentry(Perms::read | Perms::exec); memory[255] = MMUentry(Perms::read | Perms::exec); Instructions: | Bit format: 8bits opcode 4bits condition code 6bits addressing mode 1 6bits addressing mode 2 20bits data 1 20bits data 2 Addressing modes: # total: 29 * 2 = 58 -> 6 bits P: syntax: blank, or %P if explicit. desc: | Popped from or pushed to the stack (depending on if it is an in or out argument.) H: syntax: blank, or %H if explicit desc: | Popped from or pushed to the hstack (depending on if it is an in or out argument.) R: syntax: '[register]' desc: | Use the named register. Technically, this is 25 distinct addressing modes, and is just the special case of F with offset zero. F: syntax: '[register#number]' desc: | Add number to the named register's value after reading it. The register is not mutated; the addition happens "in flight". number must be a 20-bit constant expression. S: syntax: number which fits within 20 bits desc: | Store a value inside of the instruction word, allowing for efficient small constants. O: syntax: [number] desc: Use the small immediate value plus [index]<<20. I: syntax: number larger than 20 bits; or a number prefixed by %. desc: | Store a value in the word immediately following the instruction word. Note that no instruction may use mode I for two different arguments; i.e. an instruction may not be 3 words long. 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. *: syntax: '* followed by any of the above syntaxes.' desc: | Any other addressing mode may be used to derive a pointer, which is then accessed 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. 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: 25 Constant registers: # 6 zero: size: 64 bits access: R/W value: 0 (constant) one: size: 64 bits access: R/W value: 1 (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: # 7 [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 {cond, zero, carry, overflow, saturation, sign, fdomain, fovfl, funfl, sup, intl, inth, parity, priv} 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. 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 counter: size: 64 bits access: R/W value: general-purpose 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 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: F: 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 memcpy instruction. 0=incrementing, 1=decrementing. 5: 6: 7: sup: If running in supervisor mode. Instructions: # total: 81, 146 -> 7, 8 bits Stack instructions: # 7 push: args: val: '*[R,S,O,I,F,P,H]' stack: ( -- val) effect: | Pushes (val) to the stack. If the val arg is indirect, sets [LMA]. Alias instructions: dup: push [SV] ; -> (a -- a a) over: push [PSV] ; -> (a b -- a b a) pop: args: dest: '*[R,F,P,H]' stack: (a -- ) a -> (dest) effect: | Pops the stack and stores the result in (dest) Alternatively, it can be understood conceptually as peek (dest) drop If the dest arg is indirect, sets [LMA]. Alias instructions: drop: pop [zero] ; -> (a -- ) peek: args: dest: '*[R,F]' stack: (a -- a) a -> (dest) effect: | Stores the value on top of the stack in (dest) non-destructively. If the dest arg is indirect, sets [LMA]. fast_reserve: args: len: R,S,P,F effect: | Reserves (len) words on the stack without initializing them. Behaves as if adjust [SP] len reserve: args: len: R,S,P,F effect: | Pushes (len) 0s to the stack. rot: args: n: R,S,F p: R,S,F 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) reverse: args: n: R,S,F stack: depends on arguments effect: | Reverses top (n) stack values. e.g.: reverse 3 ; -> (a b c -- c b a) High stack instructions: # total: 9 hpush: args: val: '*[R,S,I,O,F]' hstack: ( -- val) effect: | Pushes (val) to the hstack. If the val arg is indirect, sets [LMA]. hpop: args: dest: '*[R,F]' hstack: (a -- ) a -> (dest) effect: | Pops the hstack and stores the result in (dest) Alternatively, it can be understood as hpeek (dest) drop If the dest arg is indirect, sets [LMA]. hpeek: args: dest: '*[R,F]' hstack: (a -- a) a -> (dest) effect: | Stores the value on top of the hstack in (dest) non-destructively. If the dest arg is indirect, sets [LMA]. 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. movesh: args: len: R,S,F 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 memcpy with special stack behavior; the words transferred to the hstack will be popped in reverse order as they were on the stack. movehs: args: len: R,S,F 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 memcpy with special stack behavior; the words transferred to the stack will be popped in reverse order as they were on the hstack. hreserve: args: len: R,S,P,F effect: | Pushes (len) 0s to the hstack. fast_hreserve: args: len: R,S,P,F effect: | Reserves (len) words on the hstack without initializing them. Serial device control: # total: 2? input: output: (p)=[stackptr] ; With no argument, use the stack pointer to output a single word. General memory access: # total: 7 set: args: dest: '*[R,F,P,H]' val: '*[R,S,O,I,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, technically 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 exchange: args: A: '*[R,F,P,H]' B: '*[R,F,P,H]' effect: | Exchanges the values of A and B. Both arguments must be readable and writable. Does not set [LMA]. Alias instructions: swap: exchange [SV] [PSV] ; -> (a b -- b a) load_ua: args: src: '*[R,S,O,I,F]' c_o: R,S (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: src: '*[R,S,O,I,F]' c_o: R,S (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: (implicit) src: P dest: '*[R,S,O,I,F]' c_o: R,S effect: | Left-truncates P to count octets and writes it to the unaligned address dest + offset/8. 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]. memcpy: args: src: '*[R,S,O,F,P,H]' dest: '*[R,S,O,I,F,P,H]' (implicit) len: D([counter]) effect: | Copies len words from *src to *dest. Does not set LMA. dereference: args: ptr: '*[R,S,O,I,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. Integer math (imath) instructions: # total: 3, 20 fma: args: (implicit) A: P B: S,R,P C: S,R,P 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: S,R,P C: S,R,P 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) operands: P (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 negate: op: 10h stack: (x -- -x) effect: | Negates the top of the stack. abs: op: 11h 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 xor 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. 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 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: 12 jmp: args: J: R,F,S,O,I,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#J] however it works for addressing modes that do not yield constants. Note that reljmp 1 is a no-op. skip: effect: | Unconditionally skip the next instruction (after decoding; will not execute immediate data for a following two-byte instruction). In effect, executes reljmp (1+size of next instruction) transfer: args: J: D([jump]),R,F,I effect: | Function call: Pushes [IP#1] and [FP] to call stack. Sets [FP] to [SP]. Jumps to function in [jump]. 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, shutting down the virtual machine. Equivalent to syscall @shutdown 0 err: effect: | Stops execution abnormally: value of [arg] is return code. Equivalent to syscall @shutdown [arg] 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. wait_us: args: Time: P,R,F,S effect: | sleeps for approximately Time microseconds. systransfer: args: Call: D([jump]),R,F,I effect: | Perform a system call based on the value of [jump] Miscellaneous operations: # total: 4 adjust: args: R: R N: R,S,F effect: | Add n to register r in place. Behavior exactly mimics push R push N add pop R That is, a saturating add is used and flags are set accordingly. 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,I,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. Other nops include: set [zero] (anything) ; or [one] 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.