Memory operations used operands to encode positional/size arguments,
with stack variants in case the user wanted to programmatically do so.
In most large scale cases I don't see the non-stack variants being
used; many cases require using the stack for additions and
subtractions to create values such as indexes or sizes. Therefore,
it's better to be stack-first.
One counter point is inline optimisation of code at runtime: if an
compile-time-known object is pushed then immediately used in an
operation, we can instead encode the value directly into an operand
based instruction which will speed up execution time because it's
slower to pop the value off the stack than have it available as part
of the instruction.
Little Endian ordering is now ensured for stack based and register
based operations e.g. PUSH now pushes datums in LE ordering onto the
stack, POP pops data on the stack, converting from LE ordering to host
ordering.
More functions in the runtime are now macro defined, so there's less
code while still maintaining the lookup table.
--------------------------------------------------------------------------------
What LE ordering means for actual source code is that I utilise the
convert_*_to_* functions for PUSH and POP. All other data oriented
instructions are completely internal to the system and any data
storage must be in Little Endian. So MOV, PUSH-REGISTER and DUP can
all directly memcpy the data around the system without needing to
consider endian at all.
Macros were a necessity after I felt the redefinition of push for 4
different data types was too much. Most functions are essentially
copies for each datatype. Lisp macros would make this so easy :(
struct.c is used for the general construction, inspection and deletion
of the virtual machine structure (vm_t). runtime defines the
execution routines, error enum, lookup tables, etc.
Firstly abuse OPCODE_DATA_TYPE along with integer arithmetic to do a
POP_ROUTINE table lookup (no more ugly conditionals). Then make a
format string table which we can lookup using the same data type.
Same method as when simplifying OP_POP's implementation: use the
lookup table along with OPCODE_DATA_TYPE abuse. In this case I made a
lookup table called OP_POP for this method to work, but it wasn't difficult.
OP_PUSH: Increment program pointer iff no error from PUSH_ROUTINE call
STACK_ROUTINES: Same as OP_PUSH
RET: Pop off the call stack iff the jump to the address was good.
This simple fix made the routine for OP_POP not require an additional
dispatch step on top of the conditional due to OPCODE_DATA_TYPE,
instead using data_type_t as a map from an opcode's base type to a
specific type.
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.
Though practically this would work, as the storage for the half word is
not limited in any way, nevertheless it isn't syntactically right and
it's better to fix now.
When an error occurred, because prog->ptr was incremented beforehand
the trace would show the next instruction as the culprit rather than
the actual instruction. This commit fixes that by incrementing the
program if and only if the command was run successfully.
Very barebones, essentially a simple refactor.
I need to introduce a feature to append to a program as well, but as
it's a flexible structure it will likely have to be functional.
Happened because we weren't printing all relevant words due to
naturally flooring the result of division. Here I ceil the division
to ensure we get the maximal number of words necessary.
Very easy, they just pop a word then defer to their normal versions.
This is probably the best case where a macro works directly so I
didn't even need to write a function form for them first. One thing
is that then names for the macros are quite bad right now, probably
need renaming.
Pretty simple, required some new types of errors. Obviously there's
space for a macro for the differing instruction implementations, but
these things need to be tested a bit more before I can do that.
As PUSH_REGISTER and MOV have the same signature of taking a word as
input, DUP may as well be part of it.
This leads to a larger discussion about how signatures of functions
matter: I may need to do a cleanup at some point.
word register 0 refers to the first 8 bytes of the dynamic array.
Hence the used counter should be at least 8 bytes. This deals with
those issues. Also print more useful information in
vm_print_registers (how many byte|hword|word registers are currently
in use, how many are available).