How Monty Implements External Function Callbacks and Iterative Execution with start()/resume()

Monty pauses execution when bytecode hits an external function call, returning a serializable snapshot containing the full VM state, which the host can inspect, fulfill with native code, and resume via snapshot.run() to continue iterative execution.

Monty is Pydantic's sandboxed Python interpreter designed for secure execution of untrusted code. Unlike traditional embedded Python runtimes that block on I/O, Monty implements an iterative execution model where external function callbacks pause the VM and yield control back to the host, enabling fine-grained control over side effects and resource limits.

Core Rust Architecture

MontyRun::start() Entry Point

Execution begins in crates/monty/src/run.rs where MontyRun::start() initializes the heap, namespaces, and VM before running the module bytecode:

pub fn start<T: ResourceTracker>(
    self,
    inputs: Vec<MontyObject>,
    resource_tracker: T,
    print: &mut impl PrintWriter,
) -> Result<RunProgress<T>, MontyException> {
    // 1️⃣ create heap & namespaces
    let mut heap = Heap::new(executor.namespace_size, resource_tracker);
    let mut namespaces = executor.prepare_namespaces(inputs, &mut heap)?;

    // 2️⃣ instantiate VM
    let mut vm = VM::new(&mut heap, &mut namespaces, &executor.interns, print);

    // 3️⃣ run the module
    let vm_result = vm.run_module(&executor.module_code);
    let vm_state = vm.check_snapshot(&vm_result);

    // 4️⃣ turn VM outcome into a high‑level progress value
    handle_vm_result(vm_result, vm_state, executor, heap, namespaces)
}

Source: crates/monty/src/run.rs#L146-L166

RunProgress Enum

When the VM encounters an external function, it returns RunProgress::FunctionCall containing the function name, arguments, and a resumable snapshot:

pub enum RunProgress<T: ResourceTracker> {
    /// Paused at an external function call.
    FunctionCall {
        function_name: String,
        args: Vec<MontyObject>,
        kwargs: Vec<(MontyObject, MontyObject)>,
        call_id: u32,
        state: Snapshot<T>,
    },
    /// Paused for pending async futures.
    ResolveFutures(FutureSnapshot<T>),
    /// Finished execution.
    Complete(MontyObject),
}

Source: crates/monty/src/run.rs#L185-L203

Snapshot Struct for Resumable State

The Snapshot struct in crates/monty/src/run.rs captures the entire VM state including heap, namespaces, and the pending call ID:

pub struct Snapshot<T: ResourceTracker> {
    executor: Executor,
    vm_state: VMSnapshot,
    heap: Heap<T>,
    namespaces: Namespaces,
    pending_call_id: u32,
}

Source: crates/monty/src/run.rs#L162-L173

Key methods on Snapshot:

  • run(result, print) – Consumes the snapshot, injects a return value or exception, and continues execution, producing a new RunProgress.
  • tracker_mut() – Provides mutable access to the resource tracker for adjusting limits before resuming.

Source: crates/monty/src/run.rs#L84-L104

VM External Call Handling

When the bytecode VM encounters an external function call, it generates a deterministic call_id using the interning system in crates/monty/src/intern.rs and produces the RunProgress::FunctionCall variant.

Source for call-ID generation: crates/monty/src/intern.rs#L499-L505

Host-Side Bindings

Python Binding External Function Registry

The Python binding in crates/monty-python/src/external.rs provides ExternalFunctionRegistry to map external names to Python callables:

pub struct ExternalFunctionRegistry<'py> {
    py: Python<'py>,
    functions: &'py Bound<'py, PyDict>,
    dc_registry: &'py Bound<'py, PyDict>,
}

Source: crates/monty-python/src/external.rs#L19-L27

The call method performs bidirectional conversion between MontyObject and Python objects:

  1. Lookup the named callable in the registry.
  2. Convert arguments using monty_to_py.
  3. Invoke the Python function via call1 or call.
  4. Convert the result back using py_to_monty.
  5. Wrap any Python exception as a Monty exception.

Source: crates/monty-python/src/external.rs#L39-L56

JavaScript Binding N-API Bridge

The JavaScript binding in crates/monty-js/src/monty_cls.rs exposes MontySnapshot and handles external function calls via N-API:

// Snapshot structure exposed to JS
pub struct MontySnapshot {
    // fields filled by start()
}

Source: crates/monty-js/src/monty_cls.rs#L24-L40

The call_external_function bridge:

  1. Retrieves the JS object containing external functions.
  2. Converts each MontyObject argument to a N-API napi_value.
  3. Packs kwargs into a final object argument if present.
  4. Calls the JS function via napi_call_function.
  5. Converts the result back using js_to_monty.
  6. Returns an ExternalResult (Return, Error, or Future).

Source: crates/monty-js/src/monty_cls.rs#L18-L46

Resuming execution after the callback:

let snap = MontySnapshot { /* fields */ };
let result = snap.resume(ResumeOptions { 
    return_value: Some(js_val), 
    exception: None 
})?;

The resume method internally calls snapshot.run(result, print_callback) and converts the new RunProgress back to either MontySnapshot, MontyComplete, or an exception.

Source: crates/monty-js/src/monty_cls.rs#L84-L102

End-to-End Execution Flow

The iterative execution follows this data flow:


Host (JS/Py)          MontyRun::start          VM (bytecode)
     | --------------------> | --------------------> |
     |                       |                       |
     | <--------------------| <--------------------|
     |  RunProgress::FunctionCall (Snapshot)        |
     |                       |                       |
     |  call_external        |                       |
     | <--------------------|                       |
     |                       |                       |
     |  resume()             |                       |
     | --------------------> |                       |
     |                       |                       |

  1. Start: MontyRun::start() initializes the VM and runs bytecode until completion or an external call.
  2. Pause: When the VM hits an external function, it returns RunProgress::FunctionCall containing the function name, arguments, and a Snapshot.
  3. Inspect: The host examines the snapshot to determine which external function to invoke.
  4. Execute: The host runs the native implementation (Python, JavaScript, etc.).
  5. Resume: The host calls snapshot.run(result, print) (or binding equivalents like MontySnapshot.resume()), injecting the return value and continuing execution.
  6. Iterate: The resumed execution may yield another FunctionCall (nested calls), ResolveFutures (async), or Complete.

Practical Code Examples

Rust Core Implementation

For direct integration with the Rust crate, manually iterate through execution steps:

use monty::{MontyRun, MontyObject, RunProgress};

let runner = MontyRun::new(
    "def greet(name):\n    external(name)\n\ngreet('Bob')".to_owned(),
    "demo.py",
    vec![],
    vec!["external".into()],   // declare external function name
).unwrap();

match runner.start(vec![], monty::resource::NoLimitTracker, &mut monty::io::StdPrint)? {
    RunProgress::FunctionCall { function_name, args, kwargs, state, .. } => {
        // Host decides what to do
        assert_eq!(function_name, "external");
        let name = if let MontyObject::String(s) = &args[0] { s } else { "" };
        // Return a greeting
        let result = MontyObject::String(format!("Hello, {name}!"));
        // Resume execution
        let next = state.run(result, &mut monty::io::StdPrint)?;
        // `next` should be Complete(...)
    }
    _ => panic!("unexpected progress"),
}

Key types: RunProgress::FunctionCall, Snapshot::run.

Python Host Implementation

Using the monty Python package to register and handle callbacks:

from monty import Monty, MontySnapshot

def hello(name):
    return f"Hi, {name}!"

# Register the external function name and the Python implementation

m = Monty(
    "print(hello('world'))",
    externalFunctions=['hello'],
    externalFunctionsImpl={'hello': hello},
)

# Start execution – we get a MontySnapshot because of the external call

snapshot: MontySnapshot = m.start()
assert snapshot.function_name() == "hello"

# The host (Python) already knows the implementation, but we could also

# fetch args from the snapshot if needed:

args = snapshot.args()

# Resume with the value returned by `hello`

snapshot = snapshot.resume(return_value=hello(args[0]))

# Execution finishes, result available via `snapshot.result()`

File containing the Python binding: crates/monty-python/src/monty_cls.rs.

JavaScript Host Implementation

Using the @pydantic/monty npm package:

import { Monty } from '@pydantic/monty';

// Host implementation
function double(x) {
  return x * 2;
}

// Create interpreter; declare external function name
const m = new Monty('y = double(21)', { externalFunctions: ['double'] });

let progress = m.start();   // → MontySnapshot
if (progress instanceof MontySnapshot) {
  // The VM paused awaiting `double`
  console.log(progress.function_name()); // "double"

  // Call the host function (could also be async)
  const result = double(21);
  // Resume interpreter with the return value
  progress = progress.resume({ returnValue: result });
}

// `progress` is now a MontyComplete with final result
console.log(progress.output_value()); // 42

JS binding entry: crates/monty-js/src/monty_cls.rs.

Key Source Files

Component File What it Provides
Core runner & snapshot crates/monty/src/run.rs MontyRun::start, RunProgress, Snapshot
External‑function ID generation crates/monty/src/intern.rs ExtFunctionId deterministic IDs
VM pause semantics crates/monty/src/bytecode/vm/mod.rs FrameExit::ExternalCall handling
Python external‑function registry crates/monty-python/src/external.rs ExternalFunctionRegistry and conversion helpers
JavaScript bridge to N‑API crates/monty-js/src/monty_cls.rs call_external_function, MontySnapshot, ResumeOptions
Public JS API crates/monty-js/src/lib.rs Monty, MontyComplete, MontySnapshot exported via N‑API
Public Python API crates/monty-python/src/monty_cls.rs Monty, RunResult, external‑function handling

Each file can be opened directly on GitHub, e.g.:

Summary

  • Execution begins with MontyRun::start(), which builds a heap, namespaces and a VM, then runs the bytecode.
  • When the VM encounters an external call, it pauses and returns RunProgress::FunctionCall.
  • The FunctionCall payload contains a snapshot (Snapshot) that captures the full interpreter state.
  • Host code (Python, JavaScript, or any language with a binding) inspects the function name and arguments, invokes the corresponding external implementation, then resumes via snapshot.run(result, …).
  • The resumed execution yields either another FunctionCall, a ResolveFutures (for async), or Complete.
  • Bindings (monty-python, monty-js) wrap this flow in ergonomic high‑level objects (MontySnapshot, ResumeOptions, etc.) so developers can write simple “start → … → resume” loops without touching the internal VM.

This design cleanly separates the sandboxed Python interpreter from host side side‑effects, ensuring that all I/O, networking, or other unsafe operations are performed only by explicitly supplied external callbacks, preserving Monty’s security guarantees while still providing full extensibility.

Frequently Asked Questions

What is the difference between start() and resume() in Monty?

MontyRun::start() initializes a fresh VM instance and begins executing bytecode from the entry point of the provided module. When execution hits an external function, it returns a RunProgress::FunctionCall containing a Snapshot. Snapshot::run() (exposed as resume() in bindings) consumes this snapshot, injects the external function's return value or exception back into the VM, and continues execution until the next pause or completion.

How does Monty handle nested external function calls?

Monty supports nested external calls through iterative resumption. When you call snapshot.run() with a return value, the VM continues execution. If the resumed code immediately calls another external function, run() returns a new RunProgress::FunctionCall with a fresh snapshot. The host simply repeats the inspect → execute → resume loop until receiving RunProgress::Complete.

Can external functions be asynchronous in Monty?

Yes. Monty's RunProgress enum includes a ResolveFutures variant for async execution. When an external function returns a future or promise (depending on the binding), the VM pauses with ResolveFutures instead of FunctionCall. The host can then poll or await the native async operation and resume with the resolved value, allowing non-blocking I/O while maintaining Monty's sandboxed execution model.

How are exceptions handled when resuming execution?

When resuming via snapshot.run(), the host can pass either a return value or an exception. In Rust, this is handled by wrapping the result in the appropriate variant before calling run(). In Python and JavaScript bindings, the resume() method accepts optional return_value or exception parameters. If an exception is provided, Monty injects it into the VM as if the external function raised it, allowing standard Python exception handling to propagate naturally.

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 →