vm/native-bridge: review fixes — mixed-path double-pop, fourth dispatch site, cleanups
From the adversarial review of the branch and the /simplify pass:
- bridgeraise popped the fired handler at dispatch even when the home guard was BYTECODE-pushed and non-tail. That home's delivery resumes the installer frame at its unconditional OPPOPHANDLER, so a native-compiled callee raising under a bytecode guard still removed an ENCLOSING guard (t-41f2 defect 3, mixed-path face). Bytecode non-tail homes now stay in-flight, symmetric with OP_RAISE.
- processerror's vm-error conversion was a FOURTH dispatch site with the temp-root/sp ordering bug ("all three sites" had the wrong denominator): the pops after the pushes left sp at handlerbp, so the rest-parameter push clobbered the closure register and the rest-list cons allocated with closure/exn unrooted. Reordered like the others.
- weh capture's malloc-failure path now truncates the global stack before degrading, instead of leaving the stale entries whose desync the helper exists to prevent.
- FRAMESMAX check hoisted before the stack pushes in OPRAISE's dispatch; dead sp bump in its rest branch removed; empty else debris removed.
- New sigil_ehconsumefired states the fired-entry invariant once (popped or flagged, never silently left live) for all four vm.c dispatch sites; fiberrestorehandlersonresume now calls the shared sigilnativewehrestore_segment instead of duplicating it.
Documented residual (pre-existing, NOT fixed here): the VM-side native-unwind consumption returns a single frame, so a native raiser sitting more than one bytecode frame below its guard thunk can resume the intermediate frame with the handler result (defect-2 shape, mixed path only). Follow-up filed in sigil-ecosystem.
Part of sigil-ecosystem t-41f2 / t-ecdf.
packages/sigil-lib/src/native-bridge.c | 51 +++++++++++++++++++++++++++++++++++++--------------
packages/sigil-lib/src/vm.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------
2 files changed, 90 insertions(+), 77 deletions(-)packages/sigil-lib/src/native-bridge.cmodified
static int native_weh_level_stack[SIGIL_EXCEPTION_HANDLERS_MAX];static int native_weh_level_count = 0;/* Defined below (exported for vm.c's dcont capture); declared here for the * fiber save/restore twins. */int sigil_native_weh_capture_segment(int handler_start, int handler_count, int **out);void sigil_native_weh_restore_segment(const int *levels, int n, int base);/* Move the suspended fiber's own exception handlers off the shared * handler stack into the fiber's save buffer, along with the matching * native_weh_level_stack home-frame entries (t-79a0 residual). vm->exception_handler_count += count; f->saved_handler_count = 0; } for (int i = 0; i < f->saved_weh_count; i++) { if (native_weh_level_count >= SIGIL_EXCEPTION_HANDLERS_MAX) { fprintf(stderr, "FATAL: SIGIL_EXCEPTION_HANDLERS_MAX exceeded restoring " "fiber weh entries (count=%d max=%d)\n", native_weh_level_count, SIGIL_EXCEPTION_HANDLERS_MAX); abort(); } native_weh_level_stack[native_weh_level_count++] = f->saved_weh_levels[i] + eh_base; } sigil_native_weh_restore_segment(f->saved_weh_levels, f->saved_weh_count, eh_base); f->saved_weh_count = 0;} } if (saved > 0) { int *buf = (int *)malloc((size_t)saved * sizeof(int)); if (!buf) return 0; /* degrade: entries dropped, as pre-capture */ if (!buf) { /* Degrade by DROPPING the entries (matching the truncation * below) — leaving them on the stack with soon-to-be-stale * indexes would recreate the exact desync this helper * exists to prevent. */ native_weh_level_count = seg_base; return 0; } int w = 0; for (int i = 0; i < seg; i++) { int idx = native_weh_level_stack[seg_base + i]; if (native_weh_level_count >= SIGIL_EXCEPTION_HANDLERS_MAX) { fprintf(stderr, "FATAL: SIGIL_EXCEPTION_HANDLERS_MAX exceeded restoring " "dcont weh entries (count=%d max=%d)\n", "weh entries (count=%d max=%d)\n", native_weh_level_count, SIGIL_EXCEPTION_HANDLERS_MAX); abort(); } int handler_wind_mark = vm->exception_handlers[handler_idx].wind_mark; bool handler_pushed_by_bytecode = (vm->exception_handlers[handler_idx].push_ip != NULL); vm->exception_handler_count = handler_idx; /* Handler-stack accounting, mirroring OP_RAISE (vm.c): * * - Native-pushed homes and tail homes: pop at dispatch. Native * non-tail homes are balanced by the unwinding-flag guard in * sigil_native_bridge_pop_handler (the home POP_HANDLER clears * the flag instead of popping); tail homes have no POP at all. * - BYTECODE-pushed NON-TAIL homes: the unwind is consumed on the * VM side and delivery resumes the installer frame at its * OP_POPHANDLER, which pops UNCONDITIONALLY. Popping here too * removed an ENCLOSING guard — the same double-pop as t-41f2 * defect 3, reachable whenever a native-compiled callee raises * under a bytecode guard. Keep the fired entry, marked * in-flight, for that pop to consume. */ if (handler_pushed_by_bytecode && !vm->exception_handlers[handler_idx].tail && !continuable) { vm->exception_handler_count = handler_idx + 1; vm->exception_handlers[handler_idx].in_flight = true; } else { vm->exception_handler_count = handler_idx; } /* A non-continuable raise escapes every dynamic-wind entered inside * the handler's extent: run their after-thunks (innermost first)packages/sigil-lib/src/vm.cmodified
* * Returns true on success, false if the handler stack would overflow. *//* Consume a fired exception-handler entry at dispatch time. * * Entries above idx are always dropped. The fired entry itself is either * popped with them (tail guards — no OP_POPHANDLER exists in their * continuation — and continuable raises, which keep the historical * behavior), or KEPT, marked in-flight, so the guard continuation's * OP_POPHANDLER pops exactly it instead of an enclosing guard (t-41f2 * defect 3). A fired entry is always either popped or flagged — never * silently left live. Searches skip in-flight entries so a handler never * catches raises from its own extent (see SigilExceptionHandler.in_flight). */static inline void sigil__eh_consume_fired(SigilVM *vm, int idx, bool leave_in_flight){ if (leave_in_flight) { vm->exception_handler_count = idx + 1; vm->exception_handlers[idx].in_flight = true; } else { vm->exception_handler_count = idx; }}static bool sigil__dcont_restore_handlers(SigilVM *vm, SigilDelimitedCont *dcont, int saved_frame_count, uint32_t dinst = rip[-1]; uint8_t dest_a = DECODE_A(dinst); frame->bp[dest_a] = dcont_result; } else { } ip = rip; base = frame->bp; bool handler_tail = eh->tail; size_t target_stack_mark = eh->stack_mark; int target_wind_mark = eh->wind_mark; /* Handler-stack accounting. Tail guards have no OP_POPHANDLER * in their continuation (the pop happens here); continuable * raises keep the historical pop-at-dispatch behavior. A * NON-TAIL non-continuable guard's continuation resumes at * the OP_POPHANDLER after the thunk call, so the fired entry * must STAY, marked in-flight, for that pop to consume — * otherwise it pops an enclosing guard and a later raise on * the same path escapes it (t-41f2 defect 3). */ if (continuable || handler_tail) { vm->exception_handler_count = handler_idx; } else { vm->exception_handler_count = handler_idx + 1; eh->in_flight = true; } /* Tail guards have no OP_POPHANDLER in their continuation (the * pop happens here); continuable raises keep the historical * pop-at-dispatch behavior. See sigil__eh_consume_fired. */ sigil__eh_consume_fired(vm, handler_idx, !continuable && !handler_tail); sigil__gc_push_temp_root(vm, handler_closure); sigil__gc_push_temp_root(vm, exn); vm->exception_handlers[vm->exception_handler_count - 1].frame_mark > vm->frame_count) vm->exception_handler_count--; /* Frame-capacity check BEFORE touching the stack, so the * regvm_error path is entered with no stray values above * the unwound area. */ if (!handler_tail && vm->frame_count >= SIGIL_FRAMES_MAX) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "stack overflow in exception handler"); goto regvm_error; } /* Set up handler args on the value stack (rooted there). */ push(vm, handler_closure); push(vm, exn); if (handler_tail) { frame = &vm->frames[vm->frame_count - 1]; } else { if (vm->frame_count >= SIGIL_FRAMES_MAX) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "stack overflow in exception handler"); goto regvm_error; } frame = &vm->frames[vm->frame_count++]; } frame->bp = handler_bp; frame->srcloc = vm->current_srcloc; /* Handle rest parameter */ /* Handle rest parameter (sp is set to the full register * window just below, so no interim sp bump is needed). */ if (handler_clo->code->has_rest && handler_clo->code->arity == 0) { Value rest = sigil_cons(vm, exn, SIGIL_EMPTY); handler_bp[1] = rest; vm->sp = handler_bp + 2; } else if (handler_clo->code->has_rest && handler_clo->code->arity == 1) { push(vm, SIGIL_EMPTY); } bool handler_tail = eh->tail; size_t target_stack_mark = eh->stack_mark; int target_wind_mark = eh->wind_mark; /* Tail: pop the fired entry (no OP_POPHANDLER runs for it). * Non-tail: keep it, in-flight, for the continuation's * OP_POPHANDLER — see OP_RAISE. */ if (handler_tail) { vm->exception_handler_count = eh_idx; } else { vm->exception_handler_count = eh_idx + 1; eh->in_flight = true; } sigil__eh_consume_fired(vm, eh_idx, !handler_tail); sigil__gc_push_temp_root(vm, handler_closure); sigil__gc_push_temp_root(vm, exn); vm->exception_handler_count - captured_handler_start; dcont->handler_count = captured_handler_count; /* Move the captured segment's native weh shadow entries with it. * Native non-tail guards record their handler's ABSOLUTE stack * index at push time (native_weh_level_stack) so the home * POP_HANDLER can be recognised during an unwind. Relocating the * handlers without their shadow entries left those indexes * stale: after one suspend/resume that re-based the segment, the * fired guard's POP_HANDLER no longer matched the recorded * unwind target, sigil_native_exception_unwinding stayed set * forever, and every subsequent native call silently * short-circuited (t-ecdf: kiln's static controller dying with * no log line, 7/9 setup-failure cases). Runs even with zero * captured handlers: entries above the segment shadow handlers * this unwind discards, and must be dropped either way. */ /* Move the captured segment's native weh shadow entries with it * (full story: SigilDelimitedCont.weh_levels in sigil.h). Runs * even with zero captured handlers: entries above the segment * shadow handlers this unwind discards, and must drop with it. */ dcont->weh_count = sigil_native_weh_capture_segment( captured_handler_start, captured_handler_count, &dcont->weh_levels); bool handler_tail = peh->tail; size_t target_stack_mark = peh->stack_mark; int target_wind_mark = peh->wind_mark; /* Tail: pop the fired entry. Non-tail: keep it in-flight * for the continuation's OP_POPHANDLER — see OP_RAISE. */ if (handler_tail) { vm->exception_handler_count = peh_idx; } else { vm->exception_handler_count = peh_idx + 1; peh->in_flight = true; } sigil__eh_consume_fired(vm, peh_idx, !handler_tail); sigil__gc_push_temp_root(vm, handler_closure); sigil__gc_push_temp_root(vm, exn); bool handler_tail = eh->tail; size_t target_stack_mark = eh->stack_mark; int target_wind_mark = eh->wind_mark; /* Tail: pop the fired entry. Non-tail: keep it in-flight for * the continuation's OP_POPHANDLER — see OP_RAISE. */ if (handler_tail) { vm->exception_handler_count = eh_idx; } else { vm->exception_handler_count = eh_idx + 1; eh->in_flight = true; } sigil__eh_consume_fired(vm, eh_idx, !handler_tail); sigil__gc_push_temp_root(vm, handler_closure); sigil__gc_push_temp_root(vm, exn); SigilClosure *handler_clo = (SigilClosure *)sigil_as_ptr(handler_closure); /* Pop the temp roots BEFORE moving sp: temp roots live ON the * value stack. The old order (pop after the pushes below) left * vm->sp at handler_bp — the rest-parameter push then clobbered * the handler-closure register, and the rest-list cons * allocated while closure/exn sat ABOVE sp, unrooted. Same * class as the other three dispatch sites. */ sigil__gc_pop_temp_root(vm); /* exn */ sigil__gc_pop_temp_root(vm); /* handler_closure */ int target_fc = (vm->frame_count < target_frame_mark) ? vm->frame_count : target_frame_mark; vm->frame_count = target_fc; push(vm, exn); Value *handler_bp = vm->sp - 2; sigil__gc_pop_temp_root(vm); /* exn */ sigil__gc_pop_temp_root(vm); /* handler_closure */ if (handler_tail) { SigilFrame *frame = &vm->frames[vm->frame_count - 1]; frame->closure = handler_clo;