OP_HALT = 1 now. This commit also adjusts the error checking in
inst_read_bytecode.
The main reasoning behind this is when other platforms or applications
target the AVM: whenever a new opcode may be added, the actual binary
for OP_HALT changes (as a result of how C enums work).
Say your application targets commit alpha of AVM. OP_HALT is, say,
98. In commit beta, AVM is updated with a new opcode so OP_HALT is
changed to 99 (due to the new opcode being placed before OP_HALT). If
your application builds a binary for AVM version alpha and AVM version
beta is used instead, OP_HALT will be interpreted as another
instruction, which can lead to undefined behaviour.
This can be hard to debug, so here I've made the decision to try and
not place new opcodes in between old ones; new ones will always be
placed *before* NUMBER_OF_OPCODES.
No longer relying on darr_t or anything other than the C runtime and
aliases. This means it should be *even easier* to target this via FFI
from other languages without having to initialise my custom made
structures! Furthermore I've removed any form of allocation in the
library so FFI callers don't need to manage memory in any way.
Instead we rely on the caller allocating the correct amount of memory
for the functions to work, with basic error handling if that doesn't
happen.
In the case of inst_read_bytecode, error reporting occurs by making
the return of a function an integer. If the integer is positive it is
the number of bytes read from the buffer. If negative it flags a
possible error, which is a member of read_err_t.
prog_read_bytecode has been split into two functions: prog_read_header
and prog_read_instructions. prog_read_instructions works under the
assumption that the program's header has been filled, e.g. via
prog_read_header. prog_read_header returns 0 if there's not enough
space in the buffer or if the start_address is greater than the count.
prog_read_instructions returns a custom structure which contains an
byte position as well as an error enum, allowing for finer error
reporting.
In the case of inst_write_bytecode via the assumption that the caller
allocated the correct memory there is no need for error reporting.
For prog_write_bytecode if an error occurs due to
In the case of inst_read_bytecode we return the number
Due to reordering I need to have two macros for checking if an opcode
is of a type. If the type is signed then the upper bound must be
OP_<type>_LONG whereas if it is unsigned then the upper bound must be
OP_<type>_WORD.
This "header" is now embedded directly into the struct. The semantic
of a header never really matters in the actual runtime anyway, it's
only for bytecode (de)serialising.
This means if I write the new assembler in another language I only
need to FFI this header rather than all the functions as well which
may not be as useful.
Have to define _DEFAULT_SOURCE before you can use the endian
conversion functions. As most standard library headers use
features.h, and _DEFAULT_SOURCE must be defined before features.h is
included, we have to include base.h before other headers.
Essentially a "program header", followed by a count, followed by
instructions.
Provides a stronger format for bytecode files and allows for better
bounds checking on instructions.
Essentially you may "call" an absolute program address, which pushes
the current address onto the call stack. CALL_STACK does the same
thing but the absolute program address is taken from the data stack.
RET pops an address off the call stack then jumps to that address.
Essentially they use the stack for their one and only operand. This
allows user level control, in particular it allows for loops to work
correctly while using these operands.
This will allow for more library level code to be written. For
example, say you wanted to write a generic byte level reversal
algorithm for dynamically sized allocations. Getting the size of the
allocation would be fundamental to this operation.
One may allocate any number of (bytes|hwords|words), set or get some
index from allocated memory, and delete heap memory.
The idea is that all the relevant datums will be on the stack, so no
register usage. This means no instructions should use register space
at all (other than POP, which I'm debating about currently). Register
space is purely for users.
Thankfully multiplication, like addition, is the same under 2s
complement as it is for unsigned numbers. So I just need to implement
those versions to be fine.
A negative number under 2s complement can never be equal to its
positive as the top bit *must* be on. If two numbers are equivalent
bit-by-bit then they are equal for both signed and unsigned numbers.
As it has no dependencies on vm specifically, and it's more necessary
for any vendors who wish to target the virtual machine, it makes more
sense for inst to be a lib module rather than a vm module.