1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
|
# Argument Passing
Every invocation in MoarVM consists of two pieces:
* A static Callsite descriptor (MVMCallsite), which incorporates the
number of arguments along with a set of flags about what is being
passed.
* A contiguous group of MVMRegister, which is a union type. This contains
the actual arguments being passed. The static descriptor indicates how
to process the registers. As they are registers, they really are just a
chunk of the register space in the caller, thus anything that wants the
args for a longer period of time must work to keep them.
## Memory management
Callsite descriptors are kept at a compilation unit level, and thus their
lifetimes are simply that of the compilation unit. (An alternative way would
be to keep a global store of these and intern them, fixing up all of the
references to them at bytecode loading time.)
The argument data itself is more subtle. A simple approach would be to
allocate the argument data array per call, but that's too much allocation
for the common case. Thus each callframe, along with its working space,
also allocates an amount of space equal to that required by the most
demanding callsite (that is, the one that implies the most storage). This
space is populated with arguments per call, and a pointer passed to the
invocation target.
This area of memory will clearly never be safe to consider beyond the point
that the callee yields control. Control is yielded by:
* Returning
* Yielding co-routine style
* Calling anything that will potentially do co-routine or continuation stuff
Typically, though, an argument processor immediately copies what was passed
into locals, lexicals and so forth. It may need to make its own copy of the
original arguments if:
* It wants to keep them around and present them "first class" (a bit like
Raku does with Captures, in order to do nextsame et al.)
* Part way through processing them, some other code needs to be run in order
to obtain default values, build up data structures, etc.
A language that doesn't need to worry about such matters will generally be
able to avoid any of the copying. A language like NQP will be able to avoid
it perhaps entirely, by handling default values after the initial binding.
A language like Raku will generally need to default to copying, but a
decent optimizer should be able to whitelist routines where it's OK not to.
Many simple built-in operators should make the cut, which will take the edge
off the cases where compile-time inlining isn't possible.
## Call Code Generation
A call looks something like:
prepargs callsite # set the callsite
argconst_i 0, 42 # set arg slot 0 to native int arg 42
arg_o 1, r0 # set arg slot 1 to object in register 0
call r1 # invoke the object in r1
The bytecode loader will analyse the callsite info, and ensure that between
it and the call all of the required slots are given values, and that these are
of the correct types. This is verified once when the bytecode is loaded, and
at execution time needs no further consideration.
Note that there's no reason you can't have things like:
prepargs callsite # set the callsite
argconst_i 0, 42 # set arg slot 0 to native int arg 42
get_lex_named r0, '$x' # load lexical "$x" by name into register 0
arg_o 1, r0) # set arg slot 1 to object in register 0
call r1 # invoke the object in r1
That is, do lookups or computations of arguments while building the callsite.
However, it is NOT allowable to nest prepargs/call sequences in the bytecode.
There is only one chunk of memory available to store the arguments. Thus a
call like:
foo(bar(42))
Will have to store the result of bar(42) in a register, then prepargs..call
for foo(...) afterwards.
# Parameter Handling
While some languages may have their own binding support driven by their own
signature objects, the core instruction set provides a mechanism that should
handle most needs.
Parameter handling may start with a use of the checkarity op, specifying the
minimum and maximum number of positional arguments that may be passed.
checkarity 1, 1 # require 1 argument
checkarity 2, 3 # require 2 arguments, accept up to 3 (1 optional)
Required positional arguments can then be obtained by type:
param_rp_o r0, 0 # get 1st positional argument, which is an object
param_rp_s r1, 1 # get 2nd positional argument, which is a string
Optional positional arguments are fetched by a range of similar ops, except
that they include a branch offset (that is, a label). If the argument is
present, it is put into the register and we jump to the branch offset. If
not, the next instruction is executed, which presumably is code to populate
the register with a default value.
param_op_i r2, 2, L1
const_i64 r2, 0
L1:
The handling of named arguments is similar, with both required and optional
variants of the ops.
|