|
libose
|
describes the OSE virtual machine
The VM has no state outside the bundle it runs in. Its registers are ordinary context messages inside that bundle, addressed by name, so the running machine and a message on the wire are the same bytes. Nothing is serialised on the way out because nothing was ever in another form.
The addresses are given in ose_vm.h:
| Address | Register | What it holds |
|---|---|---|
/_i | input | elements waiting to become control |
/_s | stack | operands and results |
/_e | environment | bindings visible to the running program |
/_c | control | the instructions currently executing |
/_d | dump | saved contexts, for return |
/_x | host | what the host bound; searched after /_e |
/_l | stdlib | the standard library, if one is loaded |
/_0 | cache | lookup cache |
osevm_init() creates /_0 and then the five registers – six context messages in all. /_x is the host's to push: a VM without one is legal, and osekit_lookup() checks for its absence rather than assuming it (osekit_lookup.c). /_l appears when a stdlib is loaded.
/_0 is a context bundle of its own and should not be confused with OSE_CONTEXT_CACHE_SIZE, which is a 256-byte blob reserved inside every context message (ose_context.h). The two are separate; the second is why a five-register VM pays 1,660 bytes of bookkeeping before it holds anything.
osevm_run() (ose_vm.c) drains control, and when control is empty takes the next element of input and makes it control. In outline:
The order is worth stating because it reads backwards. A host pushes instructions onto /_i and then bundles and unwraps them – ose_bundleAll() followed by ose_popAllDrop() – which reverses them, and osevm_popInputToControl() takes the last element first. The net effect is that what was pushed first runs first.
osevm_step() performs one instruction and returns OSETT_TRUE while there is work left. It mirrors the inner loop above, errno handling included, deliberately: a stepper whose semantics differ from running is not a debugger, it is a second implementation to keep in agreement.
It was for a long time both untested and broken – it applied the element on top of control without consuming it, so stepping never terminated and /,/i/3 pushed 3 onto the stack forever.
An instruction is an address, and its leading characters select the operation. The authoritative list is applyControl() in ose_vm.c, which routes on the four-byte tokens defined just above it; the table below names the ones that also have OSEVM_ADDR_* constants in ose_vm.h, and is a subset. applyControl() additionally routes /./, /:/, /;/, /|/, /(/, /)/ and /#/, and a wider set of /,/X type conversions than the four listed here.
| Prefix | Meaning |
|---|---|
/! | funcall – look the rest up and apply it |
/$ | lookup – push the value bound to the rest |
/@ | assign |
‘/’\ilinebr </td> <td class="markdownTableBodyNone"> quote \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone">/,\ilinebr </td> <td class="markdownTableBodyNone"> to type;/,/i,/,/f,/,/s,/,/bfor int32, float, string, blob \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone">/>\ilinebr </td> <td class="markdownTableBodyNone"> copy a register onto the stack, as one element \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone">/<\ilinebr </td> <td class="markdownTableBodyNone"> replace a register with the element on top of the stack \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone">/<<\ilinebr </td> <td class="markdownTableBodyNone"> append the element on top of the stack to a register \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone">/-\ilinebr </td> <td class="markdownTableBodyNone"> move the element on top of the stack to a register \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone">/&` | append a byte |
/< and /<< differ only when the register is not empty, which is what makes the distinction easy to miss: on an empty environment they are indistinguishable. /> copies the whole register as a single element, not one element of it.
/!/exec1, /!/exec2 and /!/exec3 all take the bundle on top of the stack and arrange for it to run. /!/exec is osevm_exec2().
They save the current context to the dump and install a new one. In osevm_exec2() (osevm_lib.c) that is, abridged to the steps that matter here:
Two consequences follow, and neither is obvious from the outside:
/!/exec consumes an environment as well as a program.** The caller must leave one on the stack beneath the bundle to be run. A caller that does not will have its next stack element taken as the environment instead, and the program will run against the wrong bindings – or, if that element was the program's argument, the program will appear not to run at all./!/exec cannot durably write to the environment. The environment it sees is a copy lifted off the stack, and the real one is restored from the dump when control empties. Any binding installed during the call is discarded on return. A handler that needs to leave something behind must be run at top level – put the bundle in the input and call osevm_run() – or must be followed by a separate step after the exec has returned. max.o.se does the latter, and its /hook/inlet/finalize exists for exactly this reason./!/if is affected too: it copies the environment onto the stack and then execs the chosen branch, so a branch runs under the same restriction, and the restored environment is left on the stack above the branch's result.
Each register carries its own errno, and a child's is not visible on the VM. The wrapper macros in osevm_lib.h are what move it: they clear the child's errno on entry, call the operation, and copy any errno the call produced up to the VM bundle.
The clear on entry is load-bearing. Without it a value left on /_s by anything earlier is indistinguishable from one the current operation set, and is charged to whichever instruction happens to run next – a host that calls ose_drop() on an empty stack during setup is enough to produce it, and the diagnostic then lands on the first instruction of the first program the user runs.
osevm_run() checks the VM's errno after every instruction and pushes /!/exception onto control when it is set. Nothing binds /!/exception by default, so a host that does not bind it gets silence: the name is not found, and the failure is invisible.
The minimum is small, and deliberately so. A host:
osevm_init() on it;/_x with ose_pushContextMessage(), if anything is to be bound;/print, /println and /exception;That last point is the design, not a summary of it. The CLI's whole main loop pushes a file descriptor and the name /!/repl/run and calls osevm_run(); everything a REPL does – reading, line editing, history, the prompt, parse/eval/print – is ose code, in repl.ose. Max's objects push the atoms that arrived at an inlet and the name of the method, and look that name up in ose. A host that decides what an event means in C has taken work that belongs in the bundle.
OSE_CONF_VALIDATE_VM_STEP (ose.h) compiles in a check that validates every register after every instruction. It is off by default and costs accordingly; it exists because "the state is always a valid bundle" is a claim, and a claim that nothing tests is a hope. It has already caught a malformed message produced by a wrong length passed to ose_pushMessage().
The operations in ose_stackops.c carry a denotational description of what each one does to the bundle, written as LaTeX in the doxygen comment above the function and rendered into this documentation. Those are the authoritative statement of behaviour for the stack operations; this page describes the machine that applies them.