Replies: 9 comments 11 replies
-
Hi, I'd be interested in working on this. I have some rough ideas on how to tackle this task and have already started to hack around in the vm related code a bit (mainly If it's okay for me to work on this task, should I create a draft PR with the beginnings of my implementation attempt or should we discuss direction/implementation details here first? |
Beta Was this translation helpful? Give feedback.
-
PR is up: #242 |
Beta Was this translation helpful? Give feedback.
-
As discussed on Matrix, here's an overview of the current status together with some context, ideas and an attempt at a roadmap. To make discussion via replies easier, I tried splitting all of it up at thematic breaks First, some overall goals and non-goals Goals:
Non-goals:
|
Beta Was this translation helpful? Give feedback.
-
Some concepts and terminology. Locations and memoryThe meaning and function of a Atomic value types are:
Background: I came up with this set of types by looking at what kind of values the original VM instructions operated on Complex value types are:
A value is also either managed or unmanaged. Managed values are:
At the physical layer, values are stored flat in contiguous memory. A A Pseudo code of type VmType = object
size: int # The size-in-bytes of the value (including all sub-values, if any)
alignment: int # The alignment requirement of the value
case kind: ValueKind # The kind of value type
of vkInt, vkFloat, vkSet, vkString, vkNimNode: # These don't need extra information
of vkSeq:
seqElemStride: int # The amount of bytes between two consecutive elements
seqElemType: VmType # The vm type of `T` in `seq[T]`
of vkPtr, vkRef:
targetType: VmType # The vm type `T` in `ptr T`
of vkObject:
fields: seq[tuple[offset: int, typ: VmType]] # The fields of an `object`/`tuple` type
of vkArray:
elemCount: int # The number of elements in the array
elemStride: int # The amount of bytes between two consecutive elements
elemType: VmType # The vm type of `T` in `array[..., T]` Variant objects are currently not accounted for. I still need to come up with a solution on how to handle them. The following nim type: type Obj = object
x: int32
y: int16
z: int32
a: array[2, bool] would be represented as a let i32T = VmType(kind: vkInt, size: 4, alignment: 4)
let i16T = VmType(kind: vkInt: size: 2, alignemnt: 2)
let aT = VmType(kind: vkArray, elemCount: 2, elemStride: 1,
elemType: VmType(kind: vkInt, size: 1, alignment: 1)
)
result = VmType(kind: vkObject, size: 4 + 4 + 4 + 2, # padding must be inserted after `y`
alignemnt: 4,
fields: @[(0, i32T), (4, i16T), (8, i32T), (12, aT)]
) The in-memory layout would be: # 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E
# [ x: int32 ][y: int16] - - - - -[ z: int32 ][(0)][(1)]
# ^ ^ ^
# | |____a___|
# |_________________________________Obj________________________________| A location is required to be accessed via a handle of the same, super or sub-type (as described in the manual). Unlike the C/C++ backend, this in enforced in the VM.
Future direction: Managed types that are only considered managed due to them containing either The Future direction: Overload the An untyped memory region refers to the memory region occupied by elements of a Values can have their in-memory representation copied via The following code is illegal and will fail during run-time: var a, b: int32
template sub(x): cast[pointer](cast[int](addr x) + 2)
copyMem(sub(a), sub(b), 2) When the destination is a typed location, the source must be a typed location with the same type as the destination. Due the overall architecture here, it is easy to enforce this at runtime too. When the destination is untyped, the source can be either typed or untyped. The requirements here are more strict than those of the previous iteration in the PR. This also makes them somewhat less complex, which, I think, is a good thing. RegistersA register holds one of the following:
While ints, floats and
To de-reference pointers, they have to be first loaded into a register. The rationale here is to speed up repeated de-referencing of an unchanging pointer, while also making memory safety easier. When memory is deallocated (either via
Future direction: Track the id of the owning memory region for addresses. Only invalidate address and handle registers that contain references into the freed region. |
Beta Was this translation helpful? Give feedback.
-
Idea: Restructuring the execution loopRight now, all of the execution logic is tangled up into one big function (
The basic flow is like this: Instruction execution at the lowest level should not need to know about things like: program counter, stack, (guest) exception handling. It should simply execute the instruction fed to it and return an expectation on what should happen next, along with some data. Expectations are:
The instruction execution stage doesn't know what do with these, it just reports them back to the one who invoked it. During normal operation of the VM, this would be some form of director/orchestrator/supervisor (can't think of better name right now). It would then examine the execution result and act accordingly. It could, for example: return in-VM control flow back to the calling guest function (in case of return), run code generation for a new function (in case of call), find and go to the closest encompassing finally block (in case of raise), quit VM execution. The instruction execution stage may only read and modify VM heap memory and the registers that are operands to the executed instruction. Things like file-system I/O and echoing should be done in VM callbacks. Splitting up the execution into multiple physical components could be done in several ways. Probably the most simple one, would be to just put them into their own procs ( Some cool things all of this would enable (or make a whole lot easier):
All of this is just an idea, nothing has to be done the way described here. |
Beta Was this translation helpful? Give feedback.
-
Debugger/DebuggingMy overall idea regarding debugging would be, to only provide a low-level framework in the VM. The debuggers' implementation would live outside the VM's code. Things the framework/API should provide/allow:
Stepping, breakpoints, watchpoints and similar things are all implemented in the debugger via the aforementioned APIs |
Beta Was this translation helpful? Give feedback.
-
Current stateThis describes my local changes. I haven't pushed them yet, so as to not make review of my PR any harder. Overview of what I got so far:
DetailsIn order to not throw too much working parts away at once, I tried to not change instruction semantics in ways that would require large adjustments or structural changes in Only one new instruction was added. It is needed as a temporary measure to keep Values are stored flat in memory (described in one of the posts above) and read/modified via handles and A handle is simply a To match the expectations of At the end of execution, the result of a VM invocation is unmarshalled from it's atom based representation back to it's What is missing (to restore previous functionality):
At this point, bootstrapping should work again and all test should be green What is left to get it to a stable state:
Other things:
Proposed roadmap (ordered)Near term
After that (These are all meant as single PRs)
Splitting up the As an alternative, I thought that it might be a good idea to create a temporary branch, where, after an initial review round, the big PR is merged. The remaining items in the near-term bracket could then be done as smaller standalone PRs (easier to review) against the temporary branch, without the hard requirement of each PR being perfectly stable/finished. When finished, the temporary branch would be rebased/fixed/squashed and then merged into |
Beta Was this translation helpful? Give feedback.
-
Error handlingWhere and what kind of errors can happen in the VM?
The current situation
Callers of Guest exception handling is heavily centered around Improvement proposalNote that this proposal doesn't aim to "fix" all issues with VM error handling at once. Some fixes only make sense after further progress on the general VM overhaul happened (e.g. new General problems:
The edges to consider are:
My proposal:For errors that can't be handled inside Since the execution engine code has a call depth of 1 for the most part, it makes sense to do error reporting via In addition, replace all Guest input validation issues in callbacks are reported either via an Errors during both callback execution and codegen abort VM execution with a failure, propagating the report upwards. If VM execution or codegen happened in a context where a result (both value and If VM execution or codegen happened in a context where no result is expected, turn the failure into a I'd propose re-using While the error handling situation in regards to the VM would still not be perfect, it should be a bit better after the changes proposed here. Thoughts? |
Beta Was this translation helpful? Give feedback.
-
Idea: Instruction Set ExtensionThere is a current push to move things into callbacks via Loose descriptions:
The dividing line in the above is pretty fuzzy, admittedly. Looking at all the metaprogramming related opcodes and the code within Improvement ProposalI think a slightly different facility that's more inspired by "instruction set extension" might be a better way to go. Perhaps # compiler/vm/vm.nim, assuming no parameterized TCtx
template makeExecute*(extRawExecuteSetup: untyped, extRawExecute: untyped): untyped =
proc rawExecute(c: var TCtx, pc: var int, tos: var StackFrameIndex): YieldReason =
rawExecuteSetup(c, pc, tos) # template that does the pre-while loop setup
extRawExecuteSetup(c, pc, tos)
while true:
let instr = c.code[pc]
let ra = instr.regA
if instr.isExtended:
# extRawExecute(c, pc, tos, instr, ra)
else:
# rawExecute's core while loop
c.profiler.leave(c)
inc pc
proc execute(c: var TCtx, thread: var VmThread): YieldReason =
var
pc = thread.pc
sframe = thread.frame
try:
result = rawExecute(c, pc, sframe, extRawExecuteSetup, extRawExecute)
# ... rest of code is mostly the same The above is a very rough sketch, still need to sort out all the bind/import issues. But effectively one could declare an extended VM this way, the template shenanigans would be simliar to There would be a few major VM instances I imagine:
|
Beta Was this translation helpful? Give feedback.
-
Any structure that is not
int
-based orfloat
-based is stored in the VM usingPNode
.This is needlessly wasteful, given that the nodes are meant to construct the syntax tree of any given program and contains metadata that is completely unused by the VM.
With the move to DoD AST brewing in #139 (experimental implementation in #144) which will specialize the AST further to fit its purpose for codegen, it makes little sense to keep using
PNode
for the VM and add unnecessary constraints to the new AST design.Since the VM is fairly self-contained, I'd propose that we move VM data presentation away from
PNode
before proceeding further on the DoD AST effort.Any help in this endeavor would be appreciated!
Beta Was this translation helpful? Give feedback.
All reactions