-
Notifications
You must be signed in to change notification settings - Fork 8
Tagha Instruction Set Architecture
Tagha is a little-endian, 64-bit stack & register based VM. Tagha employs both an operand stack and a callstack. Tagha utilizes 3 registers: osp
, csp
, and lr
.
osp
- Operand Stack Pointer.
csp
- Call Stack Pointer.
lr
- Link Register.
Through the opcodes & operand stack, the instruction set can allocate up to 256 registers per alloc
opcode, access up to 256 registers per register-based opcodes, and address up to 32,000+ register addresses.
Stops execution of a script/function.
does jack.
reduces the op-stack pointer by n * 8 bytes to create room for data.
increases the op-stack pointer by n * 8 bytes to destroy data. Inverse of alloc
.
copies an 8 byte immediate value to a register.
copies a source register's contents to a destination register.
"Load Register Address", gets the address of the op-stack pointer, adds an unsigned 2-byte offset multiplied by 8 (word size of Tagha) to it, and puts the resulting address in a register.
"Load Effective Address", performs address calculation of a source register and signed 2-byte offset to a destination register.
loads the address of a global variable into a register.
loads a function pointer to a register.
loads a byte value from a memory address (added with a signed 2-byte offset) into a register.
loads a short (2 byte) value from a memory address (added with a signed 2-byte offset) into a register.
loads a long (4 byte) value from a memory address (added with a signed 2-byte offset) into a register.
loads a long long (8 byte) value from a memory address (added with a signed 2-byte offset) into a register.
loads an unsigned byte value from a memory address (added with a signed 2-byte offset) into a register.
loads an unsigned short (2 byte) value from a memory address (added with a signed 2-byte offset) into a register.
loads an unsigned long (4 byte) value from a memory address (added with a signed 2-byte offset) into a register.
stores a byte value from a register into a memory address (added with a signed 2-byte offset).
stores a short (2 byte) value from a register into a memory address (added with a signed 2-byte offset).
stores a long (4 byte) value from a register into a memory address (added with a signed 2-byte offset).
stores a long long (8 byte) value from a register into a memory address (added with a signed 2-byte offset).
Adds the integer value of a source register to a destination register.
Subtracts the integer value of a source register to a destination register.
multiplies the integer value of a source register to a destination register.
divides the integer value of a source register to a destination register.
modulos the integer value of a source register to a destination register.
negates the integer value of a register.
same as add
but for floating point values. Math is done for the largest data width that's enabled (meaning if both doubles and floats are used, fadd
will perform on doubles, regardless whether floats are defined or not).
same as sub
but for floating point values. Math is done for the largest data width that's enabled (meaning if both doubles and floats are used, fsub
will perform on doubles, regardless whether floats are defined or not).
same as mul
but for floating point values. Math is done for the largest data width that's enabled (meaning if both doubles and floats are used, fmul
will perform on doubles, regardless whether floats are defined or not).
same as idiv
but for floating point values. Math is done for the largest data width that's enabled (meaning if both doubles and floats are used, fdiv
will perform on doubles, regardless whether floats are defined or not).
same as neg
but for floating point values. Math is done for the largest data width that's enabled (meaning if both doubles and floats are used, fneg
will perform on doubles, regardless whether floats are defined or not).
peforms bitwise AND of the integer value of a source register to a destination register.
Assembler can use and
& bit_and
naming of opcode.
peforms bitwise OR of the integer value of a source register to a destination register.
Assembler can use or
& bit_or
naming of opcode.
peforms bitwise XOR of the integer value of a source register to a destination register.
Assembler can use xor
& bit_xor
naming of opcode.
peforms logical leftward bit shift of the integer value of a source register to a destination register.
peforms logical rightward bit shift of the integer value of a source register to a destination register.
peforms arithmetic rightward bit shift of the integer value of a source register to a destination register.
peforms bitwise NOT to a register.
Assembler can use not
& bit_not
naming of opcode.
signed LESS-THAN comparison between two registers.
signed LESS-EQUAL comparison between two registers.
unsigned LESS-THAN comparison between two registers.
unsigned LESS-EQUAL comparison between two registers.
EQUALITY comparison between two registers.
same as ilt
but for floating point values. Math is done for the largest data width that's enabled (meaning if both doubles and floats are used, flt
will perform on doubles, regardless whether floats are defined or not).
same as ile
but for floating point values. Math is done for the largest data width that's enabled (meaning if both doubles and floats are used, fle
will perform on doubles, regardless whether floats are defined or not).
stores the conditional flag to a register.
converts a register's float32 value to float64 value. Does nothing if floats or doubles values aren't defined for tagha registers or floating point support isn't used at all.
converts a register's float64 value to float32 value. Does nothing if floats or doubles values aren't defined for tagha registers or floating point support isn't used at all.
converts a register's integer value to a float64, does nothing if doubles values aren't defined for registers or floating point support isn't used at all.
converts a register's integer value to a float32, does nothing if floats values aren't defined for registers or floating point support isn't used at all.
converts a register's float64 value to an int, does nothing if floats values aren't defined for registers or floating point support isn't used at all.
converts a register's float32 value to a int, does nothing if floats values aren't defined for registers or floating point support isn't used at all.
local instruction jump using a signed 8 byte immediate value.
Jump if Zero by a comparison result using a signed 8 byte immediate value.
Jump if Not Zero by a comparison result using a signed 8 byte immediate value.
pushes the link register to the call stack. Necessary if a function calls another.
pops a return address from the call stack to the link register.
jumps to a function using an unsigned 2 byte value as an index. can also call a native function.
jumps to a function using a function pointer. can also call a native function pointer.
loads the value of the link register to the program counter and resumes execution.
Vectors are basically packed registers that can be larger than 8 bytes but can be any width multiplied by an element size. For any vector opcode that takes a source and destination register, the destination register must be the same vector size or larger than the source register. In terms of technical operation, a vector register is basically an array.
When using a register as a vector, any element size less than word (64-bits) must be packed. For float-based vector operations, both float32_t
and float64_t
MUST BE DEFINED AND USEABLE, otherwise it's entirely a nop.
sets the operational width of vectors that are used for the vector opcodes.
sets the element size of vectors. Valid sizes are byte
, half
, long
, and word
. For float-based vector operations, only long
and word
are valid. If the element size is invalid, word
size is used.
copies a source register's contents as a vector (element size multiplied by vector width) to a destination register, destination registers is also used as a vector.
same as add
but source + destination registers are vectors.
same as sub
but source + destination registers are vectors.
same as mul
but source + destination registers are vectors.
same as div
but source + destination registers are vectors.
same as mod
but source + destination registers are vectors.
same as neg
but source register is a vector.
Same as vadd
but with floats.
Same as vsub
but with floats.
Same as vmul
but with floats.
Same as vdiv
but with floats.
Same as vneg
but with floats.
same as bit_and
but source + destination registers are vectors.
same as bit_or
but source + destination registers are vectors.
same as bit_xor
but source + destination registers are vectors.
same as shl
but source + destination registers are vectors.
same as shr
but source + destination registers are vectors.
same as shar
but source + destination registers are vectors.
same as bit_not
but source + destination registers are vectors.
same as cmp
but source + destination registers are vectors.
same as ilt
but source + destination registers are vectors.
same as ile
but source + destination registers are vectors.
same as ult
but source + destination registers are vectors. Not a "deus vult" joke.
same as ule
but source + destination registers are vectors.
same as flt
but source + destination registers are vectors.
same as fle
but source + destination registers are vectors.
All opcodes take up a single byte and additional bytes depending on whether they operate on registers, immediate values, or doing memory operations.
- | byte: opcode | 1 byte
- | byte: opcode | byte: register id / argument | 2 bytes
- | byte: opcode | byte: dest reg | byte: src reg | 3 bytes
- | byte: opcode | byte: dest reg | 2 bytes: imm | 4 bytes
- | byte: opcode | byte: dest reg | byte: src reg | 2 bytes: offset | 5 bytes
- | byte: opcode | 4 bytes: imm (immediate) value | 5 bytes
- | byte: opcode | byte: register id | 8 bytes: imm value | 10 bytes
If a bytecode function calls another bytecode function (even itself) and that bytecode function is not tail-call optimized, then it's REQUIRED to preserve the link register (using pushlr
) and restore it (using poplr
) at the end of the function's context. External function calls ALWAYS preserves and restores the link register so pushlr
and poplr
is not necessary for external calls.
All arguments must be placed in registers from r1
to rN
as needed for the amount of arguments.
It's suggested that r0
stores the number of arguments (or perhaps total byte size sum of all arguments) but not required.
In a different calling convention (called "Clobber Call"), a bytecode function may use r0
to hold the first argument and clobber r0
with the final return result.
For va_list
, it's required to use a register to store a pointer that will point to two values, a pointer to the array of arguments and a number of the arguments.
Here's an example in pseudo-ASM:
r6 = 10
r5 = 15
r4 = 20
r3 = 3
r2 = &r4
r1 = &r2
In a native function, r1
will be used as like union TaghaVal[2]
where [0]
will hold the array of arguments and [1]
holds the argument count.
All functions, bytecode & native, must allocate one extra register as r0
(top of stack) will be the return value register. r0
may be used as pleased by the compiler if the function returns nothing (void
).
Native functions clobber r0
regardless as all native wrappers must return data, even if the C(++) function returns void
.
r0
may optionally hold a copy of any of the other arguments passed to the function for simplifying code size.
Realistically, though Tagha allows functions to allocate & reduce the amount of available registers, it's also permissible to allocate a large amount of registers and use those same registers throughout the entire program.