<!-- summary: Virtual Machine Internals -->

# Virtual Machine Internals

This document describes mruby's virtual machine for developers
working on `src/vm.c` and related code.

**Read this if you are:** debugging method dispatch or call frame
issues, working on exception handling, implementing new opcodes,
modifying fiber/coroutine behavior, or optimizing the dispatch loop.

For the instruction set, see [opcode.md](opcode.md). For the
compiler that generates bytecode, see [compiler.md](compiler.md).

## Execution Model

mruby uses a **register-based VM**. Local variables and temporaries
occupy fixed register slots determined at compile time. Each method
call gets its own register window on a shared value stack.

## Execution Context

The VM state is stored in `mrb_context`:

```text
mrb_context
+-- stbase..stend    value stack (mrb_value[])
+-- cibase..ciend    call info stack (mrb_callinfo[])
+-- ci               current call frame pointer
+-- status           fiber state
+-- prev             previous context (fiber chain)
+-- vmexec           VM execution state flag
```

The value stack and call info stack grow independently. Each fiber
has its own `mrb_context`.

### Stack Sizing

- Initial value stack: 128 entries (`STACK_INIT_SIZE`)
- Initial call info stack: 32 entries (`CALLINFO_INIT_SIZE`)
- Growth factor: 1.5x (or 2x with `MRB_STACK_EXTEND_DOUBLING`)
- Minimum growth: 128 entries (`MRB_STACK_GROWTH`)
- Max stack depth: `MRB_STACK_MAX` (0x40000 - 128)
- Max call depth: `MRB_CALL_LEVEL_MAX` (512, or 128 with ASAN)

Exceeding either limit raises `SystemStackError`.

When the value stack is reallocated, all `REnv` objects and
`mrb_callinfo` stack pointers are adjusted by the delta
(`envadjust` function).

## Call Frames

Each method or block call pushes a `mrb_callinfo` frame:

```text
mrb_callinfo
+-- n:4          positional argument count (0-14, 15 = varargs)
+-- nk:4         keyword argument count (0-14, 15 = varargs)
+-- cci          call context info (NONE, DIRECT, SKIP, RESUMED)
+-- vis          visibility flags (public/private/protected)
+-- mid          method symbol
+-- proc         current RProc
+-- blk          block argument (RProc*)
+-- stack        pointer into value stack
+-- pc           program counter (bytecode position)
+-- u.env        closure environment (REnv*)
+-- u.target_class  receiver's class
```

### Stack Layout Per Frame

```text
ci->stack:
  [0]      self (receiver)
  [1..n]   positional arguments
  [n+1..]  keyword argument pairs (key, value, key, value, ...)
  [bidx]   block argument
  [bidx+1..] local variables and temporaries
```

### Argument Count Encoding

The `n` and `nk` fields are 4 bits each (0-15). When `n == 15`,
positional arguments are packed into a single Array in register 1.
When `nk == 15`, keyword arguments are packed into a single Hash.

The block index is calculated by `mrb_bidx(n, nk)`:

```text
if n == 15: n = 1 (array)
if nk == 15: n += 1 (hash)
else: n += nk * 2 (key-value pairs)
return n + 1 (skip self)
```

### Call Context Info (cci)

| Value | Name | Meaning |
| ----- | ---- | ------- |
| 0 | `CINFO_NONE` | Normal VM-to-VM call |
| 1 | `CINFO_DIRECT` | Explicit VM call (block, lambda.call) |
| 2 | `CINFO_SKIP` | Skip frame in stack traces |
| 3 | `CINFO_RESUMED` | Fiber resumed (stop execution) |

## Dispatch Loop

The main loop in `mrb_vm_run()` decodes and dispatches opcodes.
Two dispatch strategies are available:

- **Computed goto** (default on GCC/Clang): a jump table of label
  addresses (`optable[]`) for direct dispatch. Faster due to
  better branch prediction.
- **Switch-based** (`MRB_USE_VM_SWITCH_DISPATCH`): a standard
  `switch(insn)` statement. Default on MSVC and other compilers.

The dispatch loop is wrapped in `MRB_TRY`/`MRB_CATCH` for exception
handling (see [Exception Handling](#exception-handling)).

## Method Dispatch

When `OP_SEND` (or `OP_SSEND`, `OP_SUPER`) executes:

### 1. Prepare Arguments

Determine argument layout. If argument count < 15, the fast path
uses inline registers. Otherwise, arguments are packed into an
Array (varargs mode).

### 2. Push Call Frame

```c
ci = cipush(mrb, a, CINFO_DIRECT, NULL, NULL, blk, mid, argc);
```

The new frame's stack starts at the previous frame's stack + `a`
(the receiver's register index).

### 3. Method Lookup

The lookup sequence:

1. **Method cache check**: hash table lookup by `(class, mid)`.
   Default cache size: `MRB_METHOD_CACHE_SIZE` (256 entries).
2. **Method table walk**: if cache misses, search the receiver's
   class method table (`mt`), then walk the superclass chain.
3. **Cache store**: on successful lookup, store in the cache.

The method cache is invalidated when classes are modified
(`mrb_mc_clear_by_class`).

### 4. Invoke

- **Ruby method** (irep-based): extend the stack to `irep->nregs`,
  set `ci->pc` to `irep->iseq`, and jump to the new bytecode.
- **C function**: call `func(mrb, recv)` directly, then pop the
  call frame and store the return value.

### 5. Visibility Check

Private methods are only callable without an explicit receiver.
Protected methods are callable from the same class hierarchy.
Violations raise `NoMethodError`.

## Exception Handling

### setjmp/longjmp

By default, mruby uses `setjmp`/`longjmp` for exception control
flow:

```c
MRB_TRY(&c_jmp) {
  mrb->jmp = &c_jmp;
  /* dispatch loop */
}
MRB_CATCH(&c_jmp) {
  /* handle exception */
}
MRB_END_EXC(&c_jmp);
```

With `MRB_USE_CXX_EXCEPTION`, C++ `try`/`catch` is used instead.

### Handler Table

Each irep contains a catch handler table (appended after iseq in
memory) with entries for `rescue` and `ensure` blocks:

```text
mrb_irep_catch_handler
+-- type       RESCUE (0) or ENSURE (1)
+-- begin[4]   start PC of protected range
+-- end[4]     end PC of protected range
+-- target[4]  jump target when handler matches
```

### Unwinding Process

When an exception occurs:

1. Search the current irep's catch handler table (reverse order)
   for a handler covering the current PC
2. If an `ensure` handler is found: execute it (may re-raise)
3. If a `rescue` handler is found: jump to handler code
4. If no handler: pop the call frame (`cipop`) and repeat with
   the parent frame
5. `CINFO_DIRECT` frames are destroyed during propagation

## Block and Closure Handling

### REnv (Environment)

Closures capture their enclosing scope's variables through `REnv`:

```text
REnv
+-- stack      pointer to captured variable values
+-- cxt        owning context (NULL if detached from stack)
+-- mid        method symbol
+-- flags      length, block index, visibility
```

While the defining scope is active, `REnv::stack` points directly
into the VM value stack (shared). This avoids copying.

### Environment Unsharing

When a closure outlives its defining scope, `mrb_env_unshare()`
copies the captured variables from the stack to a heap-allocated
buffer:

```c
mrb_env_unshare(mrb, env, noraise);
```

After unsharing, `MRB_ENV_CLOSE(env)` sets `cxt = NULL` to indicate
the environment is detached. A write barrier is issued for GC
correctness.

### Proc Types

| Flag | Meaning |
| ---- | ------- |
| `MRB_PROC_CFUNC_FL` | C function (not irep-based) |
| `MRB_PROC_STRICT` | Lambda (strict argument check) |
| `MRB_PROC_ORPHAN` | No environment attachment |
| `MRB_PROC_ENVSET` | Has captured environment |
| `MRB_PROC_SCOPE` | Defines a new variable scope |

## Fiber Switching

Fibers are lightweight coroutines. Each fiber has its own
`mrb_context` with separate value and call info stacks.

### Fiber States

```text
CREATED --> RUNNING --> SUSPENDED --> TERMINATED
                |           ^
                +-----------+
                  (yield/resume)
            TRANSFERRED (via Fiber#transfer)
```

### Context Switch

On `Fiber#resume`:

1. Save current context state
2. Set `mrb->c` to the fiber's context
3. Push arguments onto the fiber's stack
4. Continue execution in the fiber

On `Fiber.yield`:

1. Save fiber context
2. Restore the parent context (`mrb->c = c->prev`)
3. Return yield values to the parent

### Fiber Termination

When a fiber completes (`fiber_terminate`):

1. Unshare any environments that reference the fiber's stack
2. Set status to `TERMINATED`
3. Free the fiber's stacks
4. Switch to the previous context

### C Function Boundary

Fibers cannot yield across C function boundaries. You cannot call
`Fiber.yield` from within a C-implemented method (except via
`mrb_fiber_yield` at return). This is because C call frames cannot
be suspended and resumed.

## GC Integration

The VM saves the arena index at the start of the dispatch loop:

```c
int ai = mrb_gc_arena_save(mrb);
```

After each C function call, the arena is shrunk back:

```c
mrb_gc_arena_shrink(mrb, ai);
```

This prevents temporary objects created by C functions from
accumulating in the arena.

Write barriers are issued when environments are detached or closed,
ensuring the incremental GC correctly tracks live references.

## Source Files

| File | Contents |
| ---- | -------- |
| `src/vm.c` | Dispatch loop, method invocation (~1900 lines) |
| `include/mruby.h` | `mrb_state`, `mrb_callinfo`, `mrb_context` |
| `include/mruby/proc.h` | `RProc`, `REnv` structures |
| `include/mruby/throw.h` | `MRB_TRY`/`MRB_CATCH` macros |
