# How Monty's Bytecode VM Executes Python Code: A Deep Dive into the Rust Implementation

> Discover how Monty's bytecode VM executes Python code using a stack-based model in safe Rust. Explore cached frames, external call pausing, and snapshot features.

- Repository: [Pydantic/monty](https://github.com/pydantic/monty)
- Tags: deep-dive
- Published: 2026-02-16

---

**Monty's bytecode VM executes Python code by compiling source into a compact bytecode stream and interpreting it through a stack-based execution model using safe Rust, featuring cached call frames, external call pausing, and snapshot capabilities.**

Monty is a Python implementation written in Rust by Pydantic that prioritizes safety and sandboxing. Unlike CPython, which uses a mix of C and Python, Monty's entire execution engine—including its bytecode virtual machine—is implemented in 100% safe Rust, eliminating entire classes of memory safety vulnerabilities while maintaining CPython-compatible semantics.

## Bytecode Compilation and Structure

Before execution begins, Monty transforms Python source code into a dense, portable bytecode format that the VM interprets at runtime.

### From Source to Bytecode Stream

The compilation pipeline starts in [`crates/monty/src/parse.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/parse.rs), where Python source is parsed into an AST. The **compiler** in [`crates/monty/src/bytecode/compiler.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/compiler.rs) then walks this AST and emits a `Code` object containing:

- A `Vec<u8>` bytecode stream
- A constant pool for literals
- An exception table for try/except blocks

```rust
use pydantic_monty::bytecode::compiler::compile;
use pydantic_monty::bytecode::code::Code;

fn main() {
    let src = "a = 1 + 2";
    let code: Code = compile(src).unwrap();
    println!("Bytecode: {:?}", code.bytecode());
    // Output: [LoadConst, LoadConst, BinaryAdd, StoreLocal, ...]
}

```

### The Opcode Enum and Instruction Format

Each instruction in the bytecode stream is represented by the `Opcode` enum defined in [`crates/monty/src/bytecode/op.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/op.rs). Monty uses a **single-byte opcode** design where the opcode byte is followed immediately by operands in the stream.

For example, `LoadConst` is followed by a `u16` index into the constant pool, while `JumpIfFalse` is followed by an `i16` relative offset. This compact encoding minimizes cache pressure during the tight interpreter loop.

## Core VM Architecture

Monty's execution engine centers on a **stack-based virtual machine** that manipulates Python values through a series of opcode-driven transformations.

### Stack-Based Execution Model

The VM maintains an **operand stack** (`stack: Vec<Value>`) in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs). Every arithmetic operation, function call, and attribute access operates by popping arguments from this stack and pushing results back onto it.

For example, executing `1 + 2` involves:
1. Pushing `1` onto the stack (`LoadConst`)
2. Pushing `2` onto the stack (`LoadConst`)
3. Popping both values, adding them, and pushing the result (`BinaryAdd`)

### Call Frames and Instruction Pointers

Function calls and module execution are managed through **call frames**. The `CallFrame` struct in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) (lines 36-64) tracks:

- A reference to the `Code` object being executed
- The current **instruction pointer** (`ip`) as a byte offset into the bytecode
- A base index into the operand stack (where this frame's local variables begin)
- A namespace index for variable resolution

When a function is called, the VM creates a new `CallFrame` and pushes it onto a frame stack. When the function returns, the frame is popped and control returns to the previous frame's instruction pointer.

### The CachedFrame Optimization

To avoid repeated mutable borrows of the call frame stack during the hot interpreter loop, the VM uses a `CachedFrame` struct (lines 71-78 in [`vm/mod.rs`](https://github.com/pydantic/monty/blob/main/vm/mod.rs)). This cache holds a copy of the current frame's instruction pointer and stack base, allowing the main loop to operate on local variables while only periodically syncing back to the persistent frame storage.

## The Main Execution Loop

The heart of Monty's VM is the `VM::run()` method in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) (lines 64-73), which implements a classic fetch-decode-execute cycle optimized for Rust's ownership model.

### Opcode Fetching and Dispatch

Each iteration of the run loop performs these steps:

1. **Reload cached frame** – Synchronizes the instruction pointer from the persistent `CallFrame` to the `CachedFrame`
2. **Check resource limits** – Validates time, memory, and instruction count constraints via the `ResourceTracker` trait
3. **Fetch opcode** – Reads the next byte from the bytecode stream and converts it to an `Opcode` using `Opcode::try_from(byte)`
4. **Dispatch** – Matches the opcode against a massive `match` statement that implements the operation

This dispatch mechanism is a direct-threaded-style interpreter written as a Rust `match`, avoiding the unsafe function pointer casts used in CPython while maintaining comparable performance characteristics.

### Operand Handling

Multi-byte instructions fetch their operands using helper macros defined in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) (lines 88-119). Macros like `fetch_u8!`, `fetch_i16!`, and `fetch_u16!` read consecutive bytes from the bytecode stream, advance the instruction pointer, and handle endianness conversion.

For control flow operations, the `jump_relative!` macro updates the cached instruction pointer by adding a signed offset, enabling efficient implementation of loops and conditionals.

### Exception Handling Mechanisms

When an operation raises an exception, the VM uses the `try_catch_sync!` and `catch_sync!` macros to intercept the `RunError`. The `handle_exception` function then walks the static exception table stored in the `Code` object to find an appropriate handler.

If a handler is found, the VM unwinds the operand stack to the handler's depth, updates the instruction pointer to the except block, and resumes execution. If no handler is found in the current frame, the exception propagates to the caller frame or becomes an unhandled exception at the module level.

## Advanced Execution Features

Beyond basic interpretation, Monty's VM includes sophisticated capabilities for sandboxing, asynchronous execution, and state persistence.

### External Calls and Async Support

Monty achieves **asynchronous-friendly** execution without compromising the purity of the VM core. When the interpreter encounters an `ExternalCall` or `OsCall` opcode, it returns a `FrameExit::ExternalCall` or `FrameExit::OsCall` variant rather than blocking.

The host application receives a `call_id` and the arguments, performs the I/O or system call asynchronously, and later resumes the VM using `VM::resume(call_id, result)`. This design keeps the VM itself free of `async/await` complexity while allowing seamless integration with async Rust ecosystems.

For Python-level `await` expressions, the `Opcode::Await` instruction handles coroutine suspension. It may push a ready result, spawn a new task in the internal `Scheduler`, or return `FrameExit::ResolveFutures` when all tasks are blocked waiting for I/O.

### Snapshots and State Persistence

A unique feature of Monty's VM is the ability to **serialize and restore execution state**. The `VM::snapshot()` method (defined in the VM module) serializes the current operand stack, the list of active `CallFrame`s (converted to `SerializedFrame`s), and the exception stack into a `VMSnapshot` struct.

This snapshot can be persisted to disk, sent across a network, or stored indefinitely. Later, `VM::restore()` reconstructs the exact execution state, allowing Python programs to be paused, migrated between processes, or resumed after arbitrary delays. This capability is essential for serverless computing environments and long-running workflow systems.

### Resource Limits and Safety

Monty enforces **resource limits** through the generic `ResourceTracker` trait defined in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs). The VM accepts a `T: ResourceTracker` parameter and checks `self.heap.check_time()` (and similar methods) at the start of each instruction loop iteration.

If time, memory, or instruction count limits are exceeded, the VM aborts execution with a controlled error, preventing denial-of-service attacks from untrusted Python code. Combined with the manual reference-counting system (using `Value::Ref` and macros like `defer_drop!` in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs)), Monty ensures memory safety without garbage collection pauses or use of `unsafe` Rust blocks.

## Summary

- Monty's bytecode VM compiles Python source into a compact `Vec<u8>` bytecode stream using the compiler in [`crates/monty/src/bytecode/compiler.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/compiler.rs).
- Execution occurs in a **stack-based interpreter** implemented in safe Rust within [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs), using `CallFrame` structures to manage function calls and instruction pointers.
- The main loop uses a `CachedFrame` optimization to minimize borrow checker overhead while fetching opcodes and dispatching through a massive `match` statement.
- **External calls** and **async/await** are handled by returning `FrameExit` variants to the host, allowing the VM to remain pure while supporting asynchronous I/O via `VM::resume()`.
- **Snapshots** enable serialization of execution state through `VM::snapshot()` and `VM::restore()`, supporting migration and persistence of running Python programs.
- **Resource limits** enforced by the `ResourceTracker` trait prevent abuse, while manual reference counting ensures memory safety without `unsafe` code.

## Frequently Asked Questions

### How does Monty's bytecode VM differ from CPython's execution model?

While both use a stack-based bytecode interpreter, Monty is implemented entirely in **safe Rust** without `unsafe` blocks, whereas CPython relies heavily on C. Monty also introduces **snapshot capabilities** for serializing execution state and uses an **external call mechanism** to handle I/O without blocking the VM, features not present in standard CPython.

### Can Monty execute Python code asynchronously without blocking?

Yes. When Monty encounters an `await` expression or external function call, the VM returns a `FrameExit::ExternalCall` or `FrameExit::ResolveFutures` variant to the host application. The host performs the asynchronous operation and later resumes execution using `VM::resume(call_id, result)`, allowing the VM core to remain synchronous and pure while supporting async workflows.

### How does Monty handle memory safety and resource limits?

Monty uses **manual reference counting** for all heap-allocated objects (`Value::Ref`) with helper macros like `defer_drop!` to ensure decrements occur on all code paths, preventing leaks without a garbage collector. Resource limits are enforced through the `ResourceTracker` trait in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs), which the VM checks at each instruction loop iteration to abort execution if time, memory, or instruction count limits are exceeded.

### Is it possible to pause and resume Python execution in Monty?

Yes. Monty supports **execution snapshots** via `VM::snapshot()`, which serializes the operand stack, call frames (as `SerializedFrame`s), and exception state into a `VMSnapshot`. This snapshot can be persisted or transmitted, and execution can be restored later using `VM::restore()`, enabling use cases like serverless function suspension, debugging, and distributed computing.