12 KiB
TODOs
- Rework heap to use one allocation
- Deal with TODOs
- WAIT Better documentation
[0%] - WAIT Standard library
- Completed
TODO Rework heap to use one allocation
The current approach for the heap is so:
- Per call to
malloc, allocate a newpage_tstructure by requesting memory from the operating system - Append the pointer to the
page_tto a dynamic array of pointers
In the worst case, per allocation call by the user the runtime must request memory twice from the operating system. For small scale allocations of a few bytes this is especially wasteful. Furthermore the actual heap usage of a program can seem unpredictable for a user of the virtual machine, particularly in cases where the dynamic array of pointers must resize to append a new allocation.
I propose that the runtime has one massive allocation done at init
time for a sufficiently large buffer of bytes (call it B) which we
use as the underlying memory for the heap.
TODO Deal with TODOs
There is a large variety of TODOs about errors. Let's fix them!
./vm/runtime.c:228: // TODO: Figure out a way to ensure the ordering of OP_PRINT_* is ./vm/runtime.c:578:// TODO: rename this to something more appropriate ./vm/runtime.c:625:// TODO: rename this to something more appropriate ./vm/runtime.c:641:// TODO: rename this to something more appropriate ./vm/runtime.c:655:// TODO: rename this to something more appropriate ./lib/heap.c:59: // TODO: When does this fragmentation become a performance ./lib/base.c:19: // TODO: is there a faster way of doing this? ./lib/base.c:25: // TODO: is there a faster way of doing this? ./lib/base.c:32: // TODO: is there a faster way of doing this?
WAIT Better documentation [0%] DOC
TODO
Comment coverage [0%]
WIP Lib [75%]
DONE lib/base.h
DONE lib/darr.h
DONE lib/heap.h
TODO lib/inst.h
TODO
VM [0%]
TODO vm/runtime.h
TODO vm/struct.h
TODO vm/main.c
TODO Specification
WAIT Standard library VM
I should start considering this and how a user may use it. Should it be an option in the VM and/or assembler binaries (i.e. a flag) or something the user has to specify in their source files?
Something to consider is static and dynamic "linking" i.e.:
-
Static linking: assembler inserts all used library definitions into the bytecode output directly
-
We could insert all of it at the start of the bytecode file, and with Start points this won't interfere with user code
- 2023-11-03: Finishing the Start point feature has made these features more tenable. A program header which is compiled and interpreted in bytecode works wonders.
- Furthermore library code will have fixed program addresses (always at the start) so we'll know at start of assembler runtime where to resolve standard library subroutine calls
- Virtual machine needs no changes to do this
-
WAIT Consider dynamic Linking
-
Dynamic linking: virtual machine has fixed program storage for library code (a ROM), and assembler makes jump references specifically for this program storage
- When assembling subroutine calls, just need to put references to this library storage (some kind of shared state between VM and assembler to know what these references are)
- VM needs to manage a ROM of some kind for library code
- How do we ensure assembled links to subroutine calls don't conflict with user code jumps?
What follows is a possible dynamic linking strategy. It requires quite a few moving parts:
The address operand of every program control instruction (CALL,
JUMP, JUMP.IF) has a specific encoding if the standard library is
dynamically linked:
- If the most significant bit is 0, the remaining 63 bits encode an absolute address within the program
-
Otherwise, the address encodes a standard library subroutine. The bits within the address follow this schema:
- The next 30 bits represent the specific module where the subroutine is defined (over 1.07 billion possible library values)
- The remaining 33 bits (4 bytes + 1 bit) encode the absolute program address in the bytecode of that specific module for the start of the subroutine (over 8.60 billion values)
The assembler will automatically encode this based on "%USE" calls and the name of the subroutines called. On the virtual machine, there is a storage location (similar to the ROM of real machines) which stores the bytecode for modules of the standard library, indexed by the module number. This means, on deserialising the address into the proper components, the VM can refer to the module bytecode then jump to the correct address.
2023-11-09: I'll need a way to run library code in the current program system in the runtime. It currently doesn't support jumps or work in programs outside of the main one unfortunately. Any proper work done in this area requires some proper refactoring.
2023-11-09: Constants or inline macros need to be reconfigured for this to work: at parse time, we work out the inlines directly which means compiling bytecode with "standard library" macros will not work as they won't be in the token stream. Either we don't allow preprocessor work in the standard library at all (which is bad cos we can't then set standard limits or other useful things) or we insert them into the registries at parse time for use in program parsing (which not only requires assembler refactoring to figure out what libraries are used (to pull definitions from) but also requires making macros "recognisable" in bytecode because they're essentially invisible).
2024-04-15: Perhaps we could insert the linking information into the program header?
- A table which states the load order of certain modules would allow the runtime to selectively spin up and properly delegate module jumps to the right bytecode
Completed
DONE Write a label/jump system ASM
Essentially a user should be able to write arbitrary labels (maybe
through label x or x: syntax) which can be referred to by jump.
It'll purely be on the assembler side as a processing step, where the emitted bytecode purely refers to absolute addresses; the VM should just be dealing with absolute addresses here.
DONE Allow relative addresses in jumps ASM
As requested, a special syntax for relative address jumps. Sometimes it's a bit nicer than a label.
DONE Calling and returning control flow :VM: ASM
When writing library code we won't know the addresses of where callers are jumping from. However, most library functions want to return control flow back to where the user had called them: we want the code to act almost like an inline function.
There are two ways I can think of achieving this:
-
Some extra syntax around labels (something like
@inline <label>:) which tells the assembly processor to inline the label when a "jump" to that label is given- This requires no changes to the VM, which keeps it simple, but a major change to the assembler to be able to inline code. However, the work on writing a label system and relative addresses should provide some insight into how this could be possible.
-
A call stack and two new syntactic constructs
callandretwhich work like so:- When
call <label>is encountered, the next program address is pushed onto the call stack and control flow is set to the label - During execution of the
<label>, when aretis encountered, pop an address off the call stack and set control flow to that address - This simulates the notion of "calling" and "returning from" a function in classical languages, but requires more machinery on the VM side.
- When
2024-04-15: The latter option was chosen, though the former has been implemented through Constants.
DONE Start points ASM VM
In standard assembly you can write
global _start
_start:
...
and that means the label _start is the point the program should
start from. This means the user can define other code anywhere in the
program and specify something similar to "main" in C programs.
Proposed syntax:
init <label>
2024-04-15: Used the same syntax as standard assembly, with the
conceit that multiple global's may be present but only the last one
has an effect.
DONE Constants
Essentially a directive which assigns some literal to a symbol as a constant. Something like
%const(n) 20 %end
Then, during my program I could use it like so
...
push.word $n
print.word
The preprocessor should convert this to the equivalent code of
...
push.word 20
print.word
2023-11-04: You could even put full program instructions for a constant potentially
%const(print-1)
push.word 1
print.word
%end
which when referred to (by $print-1) would insert the bytecode given
inline.
DONE Rigid endian LIB
Say a program is compiled on a little endian machine. The resultant bytecode file, as a result of using C's internal functions, will use little endian.
This file, when distributed to other computers, will not work on those that use big endian.
This is a massive problem; I would like bytecode compiled on one
computer to work on any other one. Therefore we have to enforce big
endian. This refactor is limited to only LIB as a result of only the
convert_* functions being used in the runtime to convert between
byte buffers (usually read from the bytecode file directly or from
memory to use in the stack).
2024-04-09: Found the hto_e functions under endian.h that provide
both way host to specific endian conversion of shorts, half words and
words. This will make it super simple to just convert.
2024-04-15: Found it better to implement the functions myself as
endian.h is not particularly portable.
DONE Import another file
Say I have two "asm" files: a.asm and b.asm.
global main
main:
push.word 1
push.word 1
push.word 1
sub.word
sub.word
call b-println
halt
b-println:
print.word
push.byte '\n'
print.char
ret
How would one assemble this? We've got two files, with a.asm
depending on b.asm for the symbol b-println. It's obvious they
need to be assembled "together" to make something that could work. A
possible "correct" program would be having the file b.asm completely
included into a.asm, such that compiling a.asm would lead to
classical symbol resolution without much hassle. As a feature, this
would be best placed in the preprocessor as symbol resolution occurs
in the third stage of parsing (process_presults), whereas the
preprocessor is always the first stage.
That would be a very simple way of solving the static vs dynamic linking problem: just include the files you actually need. Even the standard library would be fine and not require any additional work. Let's see how this would work.
DONE Do not request for more memory in registers
The stack is a fixed size object allocated at the start of a program and inserted onto the VM. The VM cannot request more memory for the stack if it runs out, but this also ensures a very strict upper bound on stack memory usage which can be profiled easily. Furthermore, the code that interacts with the stack can use the strict sizing as an invariant to simplify implementation (e.g. pushing to the stack when the stack is full will trap the program). Also the stack cannot be used to OOM attack the virtual machine.
Registers are currently dynamic arrays. Say 8 word registers are
allocated at init time. If a user requests a 9th word register,
memory is requested from the operating system to increase register
space. This is unacceptable from both a profiling and an attack point
of view; it would be trivial to write a program which forced the
runtime to request ridiculous amounts of memory from the operating
system (for example, by mov.word <very large number>).
Registers should not be infinite; a standardised size (with a compile time option to alter it) ensures the benefits stated above for the stack.