REPL — Stateful Interactive Execution
Overview
The REPL provides stateful Python execution where variables, functions, classes, and closures persist natively across calls. The REPL maintains a persistent Rust heap — everything survives, including non-serialisable objects.
Three API Levels
1. MontyRepl — Low-Level
Direct access to feedRun() (run to completion) and
feedStart()/resume() (suspend at host function calls).
final repl = MontyRepl();
// Run to completion
final result = await repl.feedRun('x = 42');
final r2 = await repl.feedRun('x + 1');
print(r2.value); // MontyInt(43)
// Functions persist
await repl.feedRun('def double(n):\n return n * 2');
final r3 = await repl.feedRun('double(21)');
print(r3.value); // MontyInt(42)
await repl.dispose();
2. ReplPlatform — Bridge Adapter
A 20-line adapter that plugs MontyRepl into DefaultMontyBridge.
This enables the full extension dispatch system — middleware, events,
schema validation — without changing any bridge code.
final repl = MontyRepl();
final platform = ReplPlatform(repl: repl);
final bridge = DefaultMontyBridge(platform: platform);
// Register host functions on the bridge
bridge.register(myHostFunction);
// Bridge uses repl's feedStart/resume internally
final events = bridge.execute(code);
You rarely use ReplPlatform directly — MontyRuntime wraps it.
3. MontyRuntime — High-Level with Extensions
The recommended API. Combines MontyRepl + DefaultMontyBridge +
extension dispatch. All registered extensions work automatically.
final session = MontyRuntime(
extensions: [
JinjaTemplateExtension(),
MessageBusExtension(),
SandboxExtension(
platformFactory: () async => createPlatformMonty(),
),
],
);
// Python calls tmpl_render() -> real Dart Jinja engine
final r = await session.execute(
"tmpl_render(template='Hello {{ name }}!', context={'name': 'World'})",
).result;
print(r.value); // MontyString('Hello World!')
// State persists — x is still 42
await session.execute('x = 42').result;
final r2 = await session.execute('x + 1').result;
print(r2.value); // MontyInt(43)
// Event stream for real-time visibility
final events = session.execute('tmpl_render(...)');
await for (final event in events) {
// BridgeFunctionCallStart, BridgeFunctionCallResult, etc.
}
await session.dispose();
Continuation Detection
The REPL can detect whether a source fragment is syntactically complete or needs more input — useful for building REPL UIs.
final mode = await repl.detectContinuation('def f():');
// ReplContinuationMode.incompleteBlock — needs trailing blank line
final mode2 = await repl.detectContinuation('x = (1 +');
// ReplContinuationMode.incompleteImplicit — unclosed paren
final mode3 = await repl.detectContinuation('x = 1');
// ReplContinuationMode.complete — ready to execute
This maps to the >>> vs ... prompt pattern in CPython's
interactive interpreter.
Host Function Dispatch
When Python calls a registered host function, the REPL suspends
and returns MontyPending with the function name, arguments,
and keyword arguments. The host resolves the call and resumes.
final progress = await repl.feedStart(
'result = fetch("https://example.com")',
externalFunctions: ['fetch'],
);
if (progress is MontyPending) {
print(progress.functionName); // 'fetch'
print(progress.arguments); // [MontyString('https://...')]
// Resolve and resume
final done = await repl.resume('response body');
}
With MontyRuntime, this dispatch happens automatically through
the extension system — you never call feedStart/resume directly.
help() Function
The bridge provides a built-in help() host function that queries
live bridge state — functions registered after initialization are
visible. Results are organized by extension category.
>>> help()
# Returns JSON listing all functions by extension category:
# {"tools": {"tmpl": [{"name": "tmpl_render", ...}],
# "msg": [{"name": "msg_send", ...}, ...],
# "sandbox": [{"name": "sandbox_spawn", ...}, ...]}}
>>> help('tmpl_render')
# Returns schema detail:
# {"name": "tmpl_render",
# "description": "Render a Jinja2 template...",
# "params": [{"name": "template", "type": "string", ...}, ...]}
>>> help('render')
# Bare name — auto-disambiguates if unique, lists candidates if not
The introspection is live — functions registered after bridge
initialization (e.g. EventLoopExtension's el_recv) are
visible in help() without restarting.
Platform Support
The REPL works on both FFI (native) and WASM (browser):
- FFI: Full support including
SandboxExtensionwith grandchildren - WASM: Full support for
MontyRepl,MontyRuntime, template and message bus extensions. Sandbox support requires multi-session Workers (see issue #280)
Comparison with Monty / Monty.exec
Monty(code) and Monty.exec run in a fresh interpreter every call —
state from earlier calls does not persist. Use them for one-shot or
parameterised re-runs of the same program.
MontyRepl maintains a native Rust heap across calls:
- Functions, classes, closures persist
- All heap objects survive across calls
- Continuation detection for REPL UIs
- Direct
feedStart/resumefor host function dispatch