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

> Discover how Monty implements external function callbacks and iterative execution using start and resume. Learn to pause, inspect, and resume VM execution with snapshots.

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

---

**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`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) where `MontyRun::start()` initializes the heap, namespaces, and VM before running the module bytecode:

```rust
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`](https://github.com/pydantic/monty/blob/main/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:

```rust
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`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs#L185-L203)

### Snapshot Struct for Resumable State

The `Snapshot` struct in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) captures the entire VM state including heap, namespaces, and the pending call ID:

```rust
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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/crates/monty/src/intern.rs) and produces the `RunProgress::FunctionCall` variant.

*Source for call-ID generation*: [`crates/monty/src/intern.rs#L499-L505`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs) provides `ExternalFunctionRegistry` to map external names to Python callables:

```rust
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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs#L39-L56)

### JavaScript Binding N-API Bridge

The JavaScript binding in [`crates/monty-js/src/monty_cls.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-js/src/monty_cls.rs) exposes `MontySnapshot` and handles external function calls via N-API:

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

```

*Source*: [`crates/monty-js/src/monty_cls.rs#L24-L40`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/crates/monty-js/src/monty_cls.rs#L18-L46)

Resuming execution after the callback:

```rust
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`](https://github.com/pydantic/monty/blob/main/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:

```rust
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:

```python
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`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/monty_cls.rs).

### JavaScript Host Implementation

Using the `@pydantic/monty` npm package:

```ts
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`](https://github.com/pydantic/monty/blob/main/crates/monty-js/src/monty_cls.rs).

## Key Source Files

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

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

* [`run.rs`](https://github.com/pydantic/monty/blob/main/run.rs) – <https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs>
* [`external.rs`](https://github.com/pydantic/monty/blob/main/external.rs) (Python) – <https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs>
* [`monty_cls.rs`](https://github.com/pydantic/monty/blob/main/monty_cls.rs) (JS) – <https://github.com/pydantic/monty/blob/main/crates/monty-js/src/monty_cls.rs>

## 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.