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

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, where Python source is parsed into an AST. The compiler in 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
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. 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. 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 (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). 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 (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 (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 CallFrames (converted to SerializedFrames), 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. 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), 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.
  • Execution occurs in a stack-based interpreter implemented in safe Rust within 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, 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 SerializedFrames), 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →