A Guided Example of VM Execution
This document provides a step-by-step walkthrough of the Ribbon VM’s execution model, demonstrating how the core components specified in the VM documentation work together. We will trace a small but feature-rich program from invocation to completion.
The Example Scenario
Consider the following high-level Ribbon program. It defines an Exception effect, which can be performed by calling its throw operation. The calculate function uses this effect to signal a division-by-zero error. The main function calls calculate inside a with...do block that provides a handler for the Exception effect, catching the error and yielding a specific status code.
Ribbon Source Code:
;; Builtin function provided by the host
;; "host/print_val" (prints a u64)
;; Define the Exception effect and its 'throw' operation.
;; '!' is the bottom type (no return).
Exception := effect E.
throw : E -> !
;; A function that might perform the Exception effect.
calculate := fun a, b.
if b == 0 then
Exception/throw 'DivisionByZero ;; Perform the effect, passing a symbol literal.
else
a / b
;; Main entry point
main := fun ().
result := ;; Use a 'with...do' block to handle effects performed by 'calculate'.
with Exception _. ;; We don't care about the exception type, so we use a type hole.
throw _ => ;; Handler for 'throw'. It takes one argument but ignores it.
cancel -1 ;; Cancel the 'do' block and yield -1 as its result.
do
calculate 20 0 ;; Perform `calculate` with our handler bound to all `Exception/throw` prompts.
;; Print the final result, which will be the cancellation value.
host/print_val result
;; Return an exit code from main.
0
The Compiled Bytecode (Disassembly)
A Ribbon compiler would translate the above code into bytecode. The with...do block is syntactic sugar that compiles down to a push_set, the call(s), and a pop_set at the cancellation address. The following is a plausible disassembly listing.
;; AddressTable IDs
;; F_main: Id.of(Function) for main
;; F_calc: Id.of(Function) for calculate
;; E_ex: Id.of(Effect) for Exception
;; H_ex: Id.of(HandlerSet) for the anonymous handler in main
;; B_prnt: Id.of(BuiltinAddress) for host/print_val
;; C_d0: Id.of(Constant) for the symbol literal 'DivisionByZero
;; --- Function: calculate (F_calc) ---
;; Expects args in r0, r1.
calculate:
i_eq64c r2, r1, 0 ;; r2 = (r1 == 0)
br_if r2, handle_zero ;; if r1 was 0, jump
u_div64 r0, r0, r1 ;; r0 = r0 / r1
return r0 ;; return result in r0
handle_zero:
addr_c r0, C_d0 ;; r0 = address of 'DivisionByZero symbol
prompt r0, E_ex, 1; r0 ;; prompt for the 'throw' handler, passing the symbol
return r0 ;; return result from handler (if it continues)
;; --- Function: main (F_main) ---
main:
push_set H_ex ;; Make the handler active
bit_copy64c r0, 20 ;; r0 = 20
bit_copy64c r1, 0 ;; r1 = 0
call_c r0, F_calc, 2; r0, r1 ;; r0 = calculate(r0, r1)
pop_set ;; Deactivate the handler in the normal execution path
cancellation_addr:
call_c _, B_prnt, 1; r0 ;; host/print_val(r0)
bit_copy64c r0, 0 ;; r0 = 0
return r0 ;; implicit (trailing expression) return from main
;; --- Handler for Exception/throw ---
;; This code is part of an anonymous function referenced by H_ex.
;; It takes one argument (in r0), which it ignores.
handler_exception_throw:
bit_copy64c r0, -1 ;; Load the cancellation value into r0
cancel r0 ;; Initiate cancellation
Note: The cancellation.address for the HandlerSet H_ex would point to the call_c instruction at cancellation_addr. The handler entry for E_ex would point to handler_exception_throw.
The Walkthrough
Let’s trace the execution of interpreter.invokeBytecode(fiber, F_main, &[]).
1. Invoking main
The interpreter sets up the initial CallFrame for main.
CallStack:[Frame(main)]ip: points to the first instruction ofmain.- Registers: All registers are zeroed.
2. push_set H_ex
The first instruction in main activates our effect handler.
- Action: A new
SetFrameis pushed to theSetStack, linkingH_extoFrame(main). TheEvidencebuffer slot forE_exis updated to point to a newEvidencestructure containing the handler and a pointer to thisSetFrame. SetStack:[SetFrame(H_ex)]Evidence[E_ex]:-> Evidence(for H_ex)
3. call_c r0, F_calc, ...
main calls calculate. A new stack frame is created for calculate.
- Action: A new
CallFrameforcalculateis pushed. The arguments (20fromr0,0fromr1ofmain) are copied tor0andr1of the newRegisterArray. CallStack:[Frame(main), Frame(calc)]ip: points to the first instruction ofcalculate.Frame(calc).vregs:{r0: 20, r1: 0, ...}
4. i_eq64c r2, r1, 0 and br_if r2, handle_zero
Inside calculate, we check if the denominator is zero.
- Action:
r1(which is0) is compared to the immediate0. The result (1) is stored inr2. Thebr_ifinstruction sees thatr2is non-zero and jumps to thehandle_zerolabel. ip: now points toaddr_c r0, C_d0.
5. prompt r0, E_ex, ...
The Exception effect is triggered. This is a critical step.
- Action:
- The
addr_cinstruction loads the address of the'DivisionByZerosymbol intor0. - The
promptinstruction is executed. It looks upE_exin theEvidencebuffer and finds the handler fromH_ex. - A new
CallFrameis pushed for the handler function (handler_exception_throw). The argument (r0fromFrame(calc)) is copied tor0in the new handler frame. - The new
CallFrame’sevidencefield is set to point to theEvidencestructure that was just found.
- The
CallStack:[Frame(main), Frame(calc), Frame(handler)]ip: points to the first instruction of the handler (bit_copy64c r0, -1).
6. cancel r0
The handler executes cancel, triggering a non-local exit.
- Action:
- Find Origin: The VM inspects
Frame(handler)and follows itsevidencepointer to find the originatingSetFrame(H_ex). ThisSetFrametells the VM that the scope to unwind to isFrame(main). - Unwind Stacks: The VM pops
Frame(handler)andFrame(calc)from theCallStack, along with their correspondingRegisterArrays. It popsSetFrame(H_ex)from theSetStackand restores theEvidencebuffer. - Transfer Control: The VM is now back in
Frame(main). It looks at thecancellationinformation inH_ex.- The handler first executes
bit_copy64c r0, -1. Then, thecancel r0instruction takes this value (-1) and places it into the designated output register for thewith...doblock, which isr0. - It sets the
ipofFrame(main)to thecancellation.addressfromH_ex, which points tocancellation_addr.
- The handler first executes
- Find Origin: The VM inspects
CallStack:[Frame(main)]ip: points tocancellation_addr.Frame(main).vregs:{r0: -1, ...}
7. call_c _, B_prnt, ...
Execution has resumed in main at the recovery point.
- Action: Execution resumes at
cancellation_addr. This address is located after thepop_setinstruction. This is correct, because thecanceloperation has already performed the stack unwinding and popped the handler set. Thepop_setinstruction is only part of the normal, non-cancellation execution path and is now correctly bypassed. The VM proceeds to call thehost/print_valbuiltin, passing the value inr0(-1). The host function prints the value to the console.
8. return 0
main finishes.
- Action:
mainimplicitly returns its last value. Thereturninstruction takes the value0fromr0and concludes the execution. The initialinvokeBytecodecall in the host finally completes, returning0.