fix(hermes): one delivery path -- a VM takes its own messages on its own turn, never on another's stack

FABRIC-3.5 SXXXII, closing FABRIC-3 SXXIX bug 2 (SXXXVI).  A send marks the
receiver for the switch signal; the VM's own loop on its native stack takes
its messages and yields; Hera's idle beat makes the pending switch instead
of running other VMs' messages on her stack; every VM gets switcher turns
from birth; the drain refuses any call outside the VM's own context (the
test: it held every relayed line before, none now).

Found on the way: amd64's first-entry frame left rsp 8 bytes off the SysV
entry alignment (#GP on the first interpreter run on a VM's own stack), and
a VM yielding from its loop was marked SWITCHED_OUT, which stopped console
relay after one message and would have made KILL refuse it.

Relay of four lines to a WIREBIND identity on all three ISAs, five
hot-plugged identities, and standard acceptance (POST 1052, hash
0x08873e0f44b7cb2a) all pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-14 10:37:57 -04:00
co-authored by Claude Opus 5.5
parent b7e8e4b058
commit 029a8ed9cf
34 changed files with 110307 additions and 119 deletions
+11
View File
@@ -46,6 +46,13 @@ struct VM;
int sk_vm_context_switch(struct VM *from, struct VM *to);
/* Performs the switch the switch signal has pending, if any, from `from`
* -- the VM that owns the native stack now running. Returns 1 once it has
* switched and been switched back to, 0 when there was nothing to do.
* Called at the colon-word checkpoint (vm_core.c) and at Hera's idle beat
* (repl.c). */
int sk_vm_switch_service_pending(struct VM *from);
/* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): which VM is
* currently physically running, tracked by the switch mechanism itself --
* see switch.c's own doc comment on g_switch_current_vm for why this
@@ -55,6 +62,10 @@ int sk_vm_context_switch(struct VM *from, struct VM *to);
struct VM *sk_vm_switch_current_vm(void);
void sk_vm_switch_set_current(struct VM *vm);
/* True when at most one vm_interpret() is in progress, i.e. the word now
* running belongs to the outermost text on this stack (vm_core.c). */
int sk_vm_at_outermost_interpret(void);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_SWITCH_H */
+9 -1
View File
@@ -68,7 +68,15 @@ sk_vm_switch_to:
.global sk_vm_switch_prime
sk_vm_switch_prime:
mov rax, rdi
sub rax, 56
/* 64, not 56 (2026-10-14): one more slot above the return address,
* where a caller's own return address would sit. `ret` into `entry`
* then leaves rsp at stack_top - 8, i.e. rsp + 8 a multiple of 16,
* exactly as after a real `call` -- the SysV ABI's entry alignment.
* With 56, entry began 8 bytes off: harmless while the entry function
* only switched back, a #GP (an aligned SSE store to the stack) the
* first time it ran the interpreter on this stack. */
sub rax, 64
mov qword ptr [rax+56], 0 /* no caller: a null return address */
mov qword ptr [rax+0], 0 /* r15 */
mov qword ptr [rax+8], 0 /* r14 */
mov qword ptr [rax+16], 0 /* r13 */
+9
View File
@@ -346,6 +346,8 @@ void capsule_vm_force_reap(VMUuid vm_id) {
* capsule_vm_kill()'s own unconditional rule. */
if (vm_uuid_is_hera(entry->vm_id)) return;
(void)sk_vm_switch_signal_unregister(entry->vm_id); /* no turns for the dead */
VM *vm = (VM *)entry->vm_ptr;
/* Deliberately NOT capsule_vm_kill()'s VM_STATE_SWITCHED_OUT refusal
@@ -467,6 +469,8 @@ int capsule_vm_kill(const char *name) {
vm_id = entry->vm_id;
vm = (VM *)entry->vm_ptr;
(void)sk_vm_switch_signal_unregister(vm_id); /* no turns for the dead */
/* FABRIC-2.md SS B, VM-COOL: reap this VM's own Stadium patron cell for
* real, dispatching COOL. Refusal (already naturally reclaimed by
* unrelated quota pressure, or never admitted) is silently tolerated --
@@ -724,6 +728,11 @@ CapsuleRunResult capsule_birth_baby(
((VM *)new_vm)->native_stack_paddr = ns.paddr;
((VM *)new_vm)->native_stack_guard_vaddr = ns.guard_vaddr;
((VM *)new_vm)->native_stack_top = ns.stack_top;
/* One path (2026-10-14): a VM that can be switched to is one
* the switcher can give a turn, so that a message sent to it
* is taken by the VM itself, on its own stack (switch.c) --
* every VM, not only the Tripod and WIREBIND identities. */
(void)sk_vm_switch_signal_register(vm_id);
} else {
log_message(LOG_WARN, "capsule_birth_baby: native stack allocation failed for %s",
capsule_name);
@@ -132,6 +132,11 @@ int sk_vm_switch_signal_boot_init(void) {
}
int sk_vm_switch_signal_register(VMUuid vm_id) {
/* Idempotent (2026-10-14): every VM is registered at its birth now
* (capsule_birth.c), and the Tripod and WIREBIND callers that predate
* that still register their own -- the second call finds the first. */
int slot = slot_for_vm_id(vm_id);
if (slot >= 0) return slot;
if (g_slot_count >= g_slot_capacity) return -1;
g_slots[g_slot_count].vm_id = vm_id;
g_slots[g_slot_count].readiness = 0;
+30 -68
View File
@@ -46,6 +46,7 @@
#include "starkernel/capsule_run.h"
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.8 -- sk_hermes_send_one() */
#include "starkernel/vm/switch.h" /* one path: sk_vm_switch_service_pending() */
#include "starkernel/vm/stadium.h" /* task 3.8 -- stadium_conserved() evidence print */
#include "block_subsystem.h"
#include "word_source/include/keyboard_words.h"
@@ -534,10 +535,11 @@ static void sk_cursor_show(void)
*
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: this used to guard a
* VM-EXEC "MSG-TICK" dispatch into every live child VM (FABRIC-2.md
* Phase C, common:messaging.4th, now removed). The mechanism changed --
* sk_repl_idle() now calls sk_hermes_drain_checkpoint() directly per VM,
* not VM-EXEC -- but the reentrancy hazard these two flags exist for is
* unchanged: drain_checkpoint() still calls vm_interpret() internally
* Phase C, common:messaging.4th, now removed). The mechanism changed
* twice -- to a direct sk_hermes_drain_checkpoint() per VM (2026-09-22),
* then to one switch per beat plus Hera's own drain (2026-10-14, one
* path) -- but the reentrancy hazard these two flags exist for is
* unchanged for Hera's own drain: it calls vm_interpret() internally
* (on the drained message's own payload), and sk_repl_idle() is itself
* called from the blocking KEY/EXPECT/QUERY reads (sk_console_getkey()/
* sk_console_readline(), which run *from inside* the executing VM's own
@@ -563,9 +565,9 @@ static int g_idle_pump_active; /* zero-initialized (BSS) */
* cursor, a fixed number of dispatches per beat). FABRIC-3.6.md Phase 4
* (Stage E), 2026-09-22: that whole mechanism (MSG-TICK, the batching
* cap, and this comment's own prior form) is gone along with
* common:messaging.4th itself -- see sk_repl_idle()'s own current
* comment, just below, for why the replacement (kernel-Hermes drain)
* does not carry the same O(N) cost and needs no cap. */
* common:messaging.4th itself. Since 2026-10-14 a beat makes at most one
* switch (the one the switch signal has pending) and drains only Hera:
* O(1), no cap needed. */
static void sk_repl_idle(VM *active_vm)
{
@@ -853,34 +855,24 @@ static void sk_repl_idle(VM *active_vm)
* comment. */
capsule_wirebind_reap_idle_check();
/* FABRIC-3.6.md Phase 4 (Stage E, Category B strip), 2026-09-22:
* this used to be a distributed FORTH messaging pump -- FABRIC-2.md
* Phase C's own design, VM-EXEC'ing MSG-TICK into every live VM's
* own dictionary once per idle beat (common:messaging.4th, now
* removed entirely). That mechanism, and the comment that used to
* sit here explaining it, are both gone: kernel-Hermes's own drain
* (below) has needed no FORTH word since task 3.9, and every
* messaging.4th-owned message type had a real cutover by task 3.10
* (FABRIC-3.6.md task 3.11, Phase 3 gate). The prior version of this
* comment also claimed "Hera never loads common:messaging.4th" --
* that was already wrong before this strip (capsules/init.4th line
* 21 EXECs it directly; task 3.10's own live FIND-based check
* confirmed it), not just made moot by removing the file. She still
* has the only persistent idle tick, so she is still the one that
* walks the registry once per idle beat -- now purely to give every
* live VM's own kernel-Hermes queue a chance to drain, batched the
* same round-robin way the old MSG-TICK dispatch was (SK_MSG_PUMP_
* BATCH below), kept for the same reason: bounding this to O(K)
* regardless of total VM count, not O(N) every beat. */
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14). Hera has the only
* persistent idle beat, so this is where a VM with messages waiting
* gets its turn while she sits at her prompt: the switch the switch
* signal has pending is made here, from whichever VM owns this stack,
* and the target takes its own messages on its own stack (switch.c's
* sk_vm_switch_entry()). Until 2026-10-14 this walked every live VM
* and ran each one's messages for it, here, on Hera's stack -- the
* second delivery path, now gone (and refused: kernel_hermes.c). Before
* 2026-09-22 it VM-EXEC'd MSG-TICK into each VM instead (FABRIC-2.md
* Phase C, common:messaging.4th, removed). */
VM *mama = (VM *)sk_get_mama_vm();
/* Reentrancy guard: never run the pump while mama is mid-interpret
* (a dispatched line, or recursively from within the pump's own
* vm_interpret). vm_interpret() clobbers the VM's single input
* buffer, so re-entering it here while KEY/EXPECT/QUERY blocks inside
* a live parse truncates the rest of that line. Deferring the MSG-TICK
* drain to the next prompt boundary is safe -- draining is best-effort
* and simply resumes next beat. */
/* Reentrancy guard: not while Hera is mid-line (a dispatched line, or
* recursively from within this beat's own work). A switch made from
* inside a KEY/EXPECT/QUERY wait would be safe -- the whole stack is
* parked and comes back intact -- but Hera's own messages run in her
* interpreter, which must not be entered mid-line; and nothing here is
* urgent: it resumes next beat, at the prompt. */
if (g_mama_interpreting || g_idle_pump_active) {
if (console_tx_count() == tx_before_idle) {
console_cancel_deferred_line_start();
@@ -890,42 +882,12 @@ static void sk_repl_idle(VM *active_vm)
g_idle_pump_active = 1;
{
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: the SK_MSG_PUMP_
* BATCH cap this loop used to enforce existed to bound the cost
* of the old MSG-TICK VM-EXEC dispatch -- a genuinely expensive
* per-VM vm_interpret() call, confirmed live as a real O(N)
* bottleneck at just 9 VMs on riscv64 (see this section's own
* history above). sk_hermes_drain_checkpoint() (task 3.4) is not
* that: its own fast path is a single global-counter read
* (sk_hermes_pending_total == 0 -> return immediately,
* kernel_hermes.c's own doc comment), so walking every live VM
* every beat costs one cheap integer read per VM in the common
* case (nothing in flight), not an O(N) wall -- the cap this
* loop's own comment history worried about does not apply to
* this call. Removed: every live VM (except mama, handled
* separately below) is now visited every idle beat, matching
* what task 3.9's own testing already exercised, without the
* ceil(N/SK_MSG_PUMP_BATCH)-beat delivery latency the cap was
* accidentally imposing on kernel-Hermes drain specifically. */
uint32_t count = capsule_vm_registry_count();
uint32_t i;
for (i = 0; i < count; i++) {
VMRegistryEntry ent;
if (capsule_vm_registry_get_by_index(i, &ent) == 0 &&
ent.state == VM_STATE_LIVE &&
ent.vm_ptr != (void *)mama) {
(void)sk_hermes_drain_checkpoint((VM *)ent.vm_ptr);
}
}
VM *cur = sk_vm_switch_current_vm();
if (cur) (void)sk_vm_switch_service_pending(cur);
/* Hera's own messages, when this beat is running on her stack:
* she is the VM switched in, at her prompt -- her own context. */
if (!cur || cur == mama) (void)sk_hermes_drain_checkpoint(mama);
}
/* Hera's own kernel-Hermes queue needs draining too, same as every
* other VM's -- she is excluded from the loop above not because she
* has nothing to drain, but because she IS the pump: a self-
* targeting call here runs directly, no VM-EXEC/reentrancy concern
* (sk_hermes_drain_checkpoint() is a plain C call, not a dispatch
* into anyone else's input buffer). */
(void)sk_hermes_drain_checkpoint(mama);
g_idle_pump_active = 0;
if (console_tx_count() == tx_before_idle) {
+28
View File
@@ -52,6 +52,9 @@
#include "starkernel/kmalloc.h"
#include "console.h"
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
#include "starkernel/vm/switch.h" /* one path: sk_vm_switch_current_vm() */
#include "starkernel/log_attrib.h" /* one path: vm_log_attributed_vm() */
#include "starkernel/capsule_vm_switch_signal.h" /* one path: sk_vm_switch_signal_mark_work() */
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
@@ -531,6 +534,9 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
return -1;
}
/* One path (2026-10-14): the receiver takes it on its own turn -- ask
* the switcher to give it one. */
sk_vm_switch_signal_mark_work(to);
return 0;
}
@@ -594,6 +600,28 @@ int sk_hermes_drain_checkpoint(VM *vm) {
msg = sk_hermes_pending_peek(self);
if (!msg) return 0;
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14): a VM takes its
* messages itself, in its own context, and nowhere else -- either
* - its own interpreter is the one running, at the outermost text
* (the checkpoint in execute_colon_word()), or
* - it is the VM switched in, and its interpreter is not mid-line on
* some other, parked, stack (switch.c's sk_vm_switch_entry(), and
* Hera's own idle beat at her prompt).
* Anything else would run one VM's message on another's stack, or
* enter an interpreter that is already mid-line. Refused, and the
* message stays queued for the VM's own turn. */
{
VM *cur = sk_vm_switch_current_vm();
int running_here = vm->interpret_depth > 0 &&
vm == vm_log_attributed_vm() &&
sk_vm_at_outermost_interpret();
int switched_in = vm->interpret_depth == 0 && (!cur || vm == cur);
if (!running_here && !switched_in) {
console_println("Kernel-Hermes: message held -- its VM is not the one running");
return 0;
}
}
/* See kernel_hermes.h's own doc comment on this function for why all
* six of these must be preserved, not just the obvious ones. */
memcpy(saved_input, vm->input_buffer, sizeof(saved_input));
+74 -24
View File
@@ -30,6 +30,8 @@
#include "starkernel/vm/switch.h"
#include "vm.h"
#include "starkernel/capsule_birth.h" /* capsule_vm_set_state */
#include "starkernel/capsule_vm_switch_signal.h" /* one path: take_pending(), mark_work() */
#include "starkernel/vm/kernel_hermes.h" /* one path: sk_hermes_drain_checkpoint() */
#include <stdint.h>
/* Arch-specific (switch.S in each arch dir). sk_vm_switch_prime() builds
@@ -51,6 +53,8 @@ extern uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
* who most recently switched into it does. */
static VM *g_switch_entry_vm;
static int switch_core(struct VM *from, struct VM *to, VMState from_state);
/* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): which VM is
* currently running, from the switch mechanism's own point of view --
* independent of vm_log_attributed_vm() (vm_core.c), which is scoped to
@@ -72,39 +76,81 @@ VM *sk_vm_switch_current_vm(void) { return g_switch_current_vm; }
void sk_vm_switch_set_current(VM *vm) { g_switch_current_vm = vm; }
/**
* sk_vm_switch_entry - trampoline for a VM's first-ever entry.
* sk_vm_switch_entry - what a VM runs on its own native stack.
*
* A freshly-entered VM has no production behavior defined yet -- what a
* switched-to VM should actually DO once running is later-stage work
* (message dispatch, an idle loop, whatever Stage 3+ needs). For now this
* is a minimal, permanent placeholder: immediately yield back to whoever
* most recently switched to it, forever. Reads self->native_switch_back_to
* Entered once, at a VM's first-ever switch-in, and never left: each turn
* it takes its own pending kernel-Hermes messages, then yields back to
* whoever most recently switched to it. Reads self->native_switch_back_to
* fresh every iteration (not a value captured once) so a later switch-in
* from a different VM is honored, not silently bounced to whoever entered
* first. Must never fall off the end -- there is no legitimate return
* address below a synthetic first-entry frame, unlike an ordinary
* function.
* from a different VM is honored. Must never fall off the end -- there is
* no legitimate return address below a synthetic first-entry frame.
*
* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): an attempt to make
* this attributable by calling vm_interpret(self, "MSG-TICK") here was
* tried and reverted -- it introduced a second, distinct hang (a stall in
* sk_repl_headless_wait()'s WIREBIND-attachment wait, not root-caused) in
* place of the switch-storm it was meant to fix. The switch-storm's real
* cause (sk_vm_switch_signal_tick()'s "who is current" check via
* vm_log_attributed_vm() never seeing a VM that only ever runs this raw
* C trampoline) is instead fixed at the source in switch.c/
* capsule_vm_switch_signal.c: sk_vm_context_switch() now tracks "current"
* itself, independent of vm_interpret()'s own attribution bookkeeping.
* This placeholder's behavior is therefore unchanged from Stage 2.
* History. Until 2026-10-14 this only yielded (a Stage 2 placeholder), and
* a VM's messages were run for it from Hera's idle beat, on her stack --
* the second of the two delivery paths FABRIC-3.5.md §XXXII ruled down to
* one. A first attempt at giving it work (2026-09-14: vm_interpret(self,
* "MSG-TICK") here) was reverted after a stall in
* sk_repl_headless_wait()'s WIREBIND wait, never root-caused; the switch
* storm it was aimed at was fixed at its source instead (switch.c and
* capsule_vm_switch_signal.c track "current" themselves).
*/
void sk_vm_switch_entry(void) {
VM *self = g_switch_entry_vm;
for (;;) {
sk_vm_context_switch(self, self->native_switch_back_to);
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14): what a VM does
* on its turn is take its own messages, here, on its own stack --
* the only place a VM that nothing else drives ever takes one
* (Hera's idle beat used to run them for it, on her stack). Not
* while its interpreter is mid-line on some other, parked, stack
* (a console VM waiting in KEY, say): then they wait, and it asks
* for another turn. */
if (self->interpret_depth == 0) {
while (sk_hermes_drain_checkpoint(self) != 0) { }
}
if (sk_hermes_pending_count(self->stadium_vm_id) > 0) {
sk_vm_switch_signal_mark_work(self->stadium_vm_id);
}
switch_core(self, self->native_switch_back_to, VM_STATE_LIVE);
}
}
int sk_vm_context_switch(VM *from, VM *to) {
int sk_vm_switch_service_pending(VM *from) {
VMRegistryEntry target;
VMUuid target_id = sk_vm_switch_signal_take_pending();
if (!from || vm_uuid_equal(target_id, vm_uuid_none())) return 0;
if (capsule_vm_registry_get(target_id, &target) != 0 ||
!target.vm_ptr || target.vm_ptr == (void *)from) {
return 0;
}
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target marked
* pending_reap is a WIREBIND identity whose owning device has
* physically detached while it sat parked SWITCHED_OUT --
* capsule_vm_kill() already refused to free it when that happened
* (see VMRegistryEntry.pending_reap's own doc comment). This is the
* safe point the mark-and-defer design calls for: reap instead of
* resuming, and skip the switch -- take_pending() already consumed the
* pending flag, so no other caller will try this same dead end. */
if (target.pending_reap) {
capsule_vm_force_reap(target_id);
return 0;
}
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(from, (VM *)target.vm_ptr);
/* Resumes here once something later switches back. */
return 1;
}
/* `from_state` is what `from` is while it is parked. VM_STATE_SWITCHED_OUT
* for a VM switched out mid-line (the colon-word checkpoint, Hera's idle
* beat): its stack holds live frames, and capsule_vm_kill() must refuse
* it. VM_STATE_LIVE for a VM yielding from the top of its own loop in
* sk_vm_switch_entry() (2026-10-14): its stack holds nothing but that loop,
* so it is as alive, as killable and as reachable by name as a VM that has
* never had a turn -- marking it SWITCHED_OUT made the console stop
* relaying to a WIREBIND identity after its first message, and made KILL
* refuse every VM that had ever taken one. */
static int switch_core(VM *from, VM *to, VMState from_state) {
uint64_t new_sp;
if (!from || !to || to->native_stack_top == 0) {
@@ -126,7 +172,7 @@ int sk_vm_context_switch(VM *from, VM *to) {
}
capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE);
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_SWITCHED_OUT);
capsule_vm_set_state(from->stadium_vm_id, from_state);
g_switch_current_vm = to;
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
@@ -138,4 +184,8 @@ int sk_vm_context_switch(VM *from, VM *to) {
return 0;
}
int sk_vm_context_switch(VM *from, VM *to) {
return switch_core(from, to, VM_STATE_SWITCHED_OUT);
}
#endif /* __STARKERNEL__ */
+3 -25
View File
@@ -958,31 +958,7 @@ void execute_colon_word(VM* vm)
* eligibility state directly (2026-09-14 correction) -- see its
* own doc comment for why that can't be left to ISR polling. */
if (sk_vm_at_outermost_interpret()) {
VMUuid target_id = sk_vm_switch_signal_take_pending();
if (!vm_uuid_equal(target_id, vm_uuid_none())) {
VMRegistryEntry target_entry;
if (capsule_vm_registry_get(target_id, &target_entry) == 0 &&
target_entry.vm_ptr && target_entry.vm_ptr != (void *)vm) {
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target
* marked pending_reap is a WIREBIND identity whose
* owning device has physically detached while it sat
* parked SWITCHED_OUT -- capsule_vm_kill() already
* refused to free it when that happened (see
* VMRegistryEntry.pending_reap's own doc comment).
* This checkpoint is the safe point the mark-and-
* defer design calls for: reap instead of resuming,
* and skip the switch this tick -- take_pending()
* already consumed the pending flag, so no other
* checkpoint will try this same dead end. */
if (target_entry.pending_reap) {
capsule_vm_force_reap(target_id);
} else {
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
/* Resumes here once something later switches back. */
}
}
}
(void)sk_vm_switch_service_pending(vm);
}
#endif
@@ -1202,6 +1178,7 @@ void vm_interpret(VM* vm, const char* input)
outer_buffer[outer_length] = '\0';
}
g_vm_interpret_depth++;
vm->interpret_depth++;
/* Load into input buffer (cap + NUL) */
size_t n = 0, cap = INPUT_BUFFER_SIZE ? INPUT_BUFFER_SIZE - 1 : 0;
@@ -1235,6 +1212,7 @@ void vm_interpret(VM* vm, const char* input)
}
g_vm_interpret_depth--;
vm->interpret_depth--;
if (nested) {
size_t k;
for (k = 0; k <= outer_length; k++) vm->input_buffer[k] = outer_buffer[k];