{"owner":"pydantic","repo":"monty","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md"],"skills":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\nDON'T COMMIT UNLESS EXPLICITLY ASKED TO DO SO BY THE USER! Previous commit requests do not matter - don't commit unless you've just been explicitly asked to do so.\n\n## Project Overview\n\nMonty is a sandboxed Python interpreter written in Rust. It parses Python code using Ruff's `ruff_python_parser` but implements its own runtime execution model for safety and performance. This is a work-in-progress project that currently supports a subset of Python features.\n\nProject goals:\n\n- **Safety**: Execute untrusted Python code safely without FFI or C dependencies, instead sandbox will call back to host to run foreign/external functions.\n- **Performance**: Fast execution through compile-time optimizations and efficient memory layout\n- **Simplicity**: Clean, understandable implementation focused on a Python subset\n- **Snapshotting and iteration**: Plan is to allow code to be iteratively executed and snapshotted at each function call\n- **Cross-platform**: Runs on Linux, macOS, and Windows (and any other OS that can run Rust)\n- Targets the latest stable version of Python, currently Python 3.14\n\n## `monty-types` — shared boundary types\n\nThe public data types (`MontyObject`, `MontyException`/`ExcType`, `OsFunctionCall` +\nits arg structs, `ResourceLimits`/`ResourceTracker`, `PrintStream`/`PrintWriter`,\n`CompileOptions`, `ExtFunctionResult`, `FileMode`, ...) live in `crates/monty-types`,\nwhich depends on no other monty crate except the `monty-macros` derives. `monty`\ndepends on `monty-types` but does not blanket re-export it — only a few types\nare re-exported inline where they appear in `monty`'s public API (e.g.\n`run::CompileOptions`, `run_progress::{ExtFunctionResult, NameLookupResult}`).\nCode needing `MontyObject`, `MontyException`, `OsFunctionCall`, etc. must\ndepend on `monty-types` directly.\n\nHost-side crates (`monty-fs`, `monty-pool`, `monty-proto` without its `worker`\nfeature, `monty-python`, `monty-js`) MUST depend on `monty-types`, NOT `monty` —\nthis keeps the interpreter out of their binaries. Only the worker side\n(`monty-runtime`, `monty-wasm-runtime`, `monty-proto` with `worker`) links the\ninterpreter. Don't add a `monty` dependency to a host-side crate; if it needs a\ntype, that type belongs in `monty-types`.\n\nInterpreter-coupled methods on these types live in `monty` as `pub(crate)`\nextension traits (`ExcTypeExt`, `MontyObjectExt`, `MontyTypeExt`, `StackFrameExt`,\n`FileModeExt`, `BuiltinsFunctionsExt`, `ExtFunctionResultExt`) — import the trait\nto call e.g. `ExcType::type_error(...)` or `MontyObject::new(value, vm)`.\n\n## Cross-Platform Requirements\n\nMonty must work identically on Linux, macOS, and Windows. Within the Monty sandbox,\npaths always use POSIX/Linux-style forward slashes (`/`) regardless of the host OS.\nThe `MountTable` handles translating between virtual POSIX paths and host-native paths.\n\nKey rules:\n- **Virtual paths** are always POSIX-style (`/mnt/data/file.txt`), never Windows-style\n- **Host paths** use `std::path::Path`/`PathBuf` which handles OS differences automatically\n- Avoid `#[cfg(unix)]`-only code in the main crate — all features must work on all platforms\n- Tests in `crates/*/tests/` should be cross-platform; use helper functions for\n  OS-specific APIs like symlink creation (see `symlink_file`/`symlink_dir` in\n  `crates/monty-fs/tests/common/mod.rs`, shared via `mod common;` — each\n  `tests/*.rs` is its own crate, so helpers used by more than one belong there)\n- CI runs `cargo test -p monty --features memory-model-checks` and `cargo test -p monty-fs`\n  on Linux, macOS, and Windows\n\n## Important Security Notice\n\nIt's ABSOLUTELY CRITICAL that there's no way for code run in a Monty sandbox to access the host filesystem, or environment or to in any way \"escape the sandbox\".\n\n**Monty will be used to run untrusted, potentially malicious code.**\n\nMake sure there's no risk of this, either in the implementation, or in the public API that makes it more like that a developer using the pydantic_monty package might make such a mistake.\n\nPossible security risks to consider:\n* filesystem access\n* path traversal to access files the users did not intend to expose to the monty sandbox\n* memory errors - use of unsafe memory operations\n* excessive memory usage - evading monty's resource limits\n* infinite loops - evading monty's resource limits\n* network access - sockets, HTTP requests\n* subprocess/shell execution - os.system, subprocess, etc.\n* import system abuse - importing modules with side effects or accessing `__import__`\n* external function/callback misuse - callbacks run in host environment\n* deserialization attacks - loading untrusted serialized Monty/snapshot data\n* regex/string DoS - catastrophic backtracking or operations bypassing limits\n* information leakage via timing or error messages\n* Python/Javascript/Rust APIs that accidentally allow developers to expose their host to monty code\n\n## Filesystem Mounts (`crates/monty-fs/`)\n\nThe `MountTable` allows mounting real host directories into the sandbox at virtual paths,\nwith configurable access modes (ReadWrite, ReadOnly, OverlayMemory).\n\nMounts are HOST-side code: the `monty` interpreter crate performs no filesystem\nI/O and does not depend on `monty-fs`. Sandboxed code suspends with an\n`OsFunctionCall`, which a host holding a `MountTable` (the pool parent, the CLI,\nbindings) services via `MountTable::handle_os_call`.\n\n**CRITICAL SECURITY INVARIANT:** The monty runtime MUST NEVER read, write, or\nobtain any information about any file or directory outside the specific directory\nthat is mounted. This is enforced by:\n\n- A `cap_std::fs::Dir` descriptor opened once at mount time, which every\n  operation runs relative to — so `..`, symlinks and intermediate directories\n  swapped mid-operation cannot reach out\n- Virtual-space normalization that prevents `..` escape in the sandbox namespace\n- `Resolve` and `Absolute` returning virtual paths, never host paths\n- Null byte rejection in all paths\n\nPath confinement is **structural**, not a check: `Mount::dir` (in\n`crates/monty-fs/src/mount_table.rs`) is the boundary; `path_security.rs` is\nnow only path policy. The cost is that an absolute symlink target is never\nfollowed, even inside the mount (see `limitations/filesystem.md`) — do not\n\"fix\" that by comparing against the mount's host path, which restores the\ncheck-then-use this removes.\n\n**Changes to `mount_table.rs` or `path_security.rs` require careful security\nreview.** `heap.rs` and the mount boundary are the most security-critical\ncode in the codebase.\n\n## Subprocess isolation (`monty-proto`, `monty subprocess`, `monty-pool`)\n\nA monty process can never be made fully crash-proof against memory errors\n(stack overflow aborts, allocator aborts), so monty can run as isolated worker\nsubprocesses:\n\n- `crates/monty-proto` — the wire protocol: a protobuf schema\n  (`proto/monty/v1/monty.proto`), checked-in prost-generated code (regenerate\n  with `make generate-proto`; CI enforces sync via `make check-proto`),\n  4-byte LE length-prefixed framing, and fallible conversions between wire\n  types and `MontyException`/etc. Values are special-cased for performance:\n  the `monty.v1.MontyObject` message is mapped via prost `extern_path` onto\n  `WireObject` (`src/wire.rs`), a hand-written `prost::Message` impl that\n  encodes borrowed `MontyObject`s and validates *while* decoding — no mirror\n  struct, no deep clone on the hot path. `tests/differential.rs` proves it\n  byte-compatible against a fully prost-generated oracle (`tests/oracle/`,\n  regenerated and CI-checked together with the main codegen). Parents must\n  treat frames from a (possibly compromised) child as untrusted — wire\n  decoding and proto→Rust conversions validate everything and never panic.\n  `monty-proto` depends only on `monty-types` by default; its `worker` feature\n  (enabled by `monty-runtime`/`monty-wasm-runtime`) pulls in the full `monty`\n  interpreter for the child-side `worker` state machine.\n- `monty subprocess` (in `crates/monty-runtime/src/subprocess.rs`) — the child:\n  reads framed requests on stdin, writes framed events on stdout, serving one\n  REPL session per checkout. Strict alternation: one request in, zero or more\n  streamed `Print` events out, then exactly one turn-ending event.\n- `crates/monty-pool` — the parent: an async (tokio) elastic pool of workers\n  with crash detection/replacement and a hard per-turn timeout. Frame reads\n  are cancel-safe (partial-frame state lives in the worker, no pump task),\n  and turn deadlines are tokio timers rather than a watchdog thread.\n- `crates/monty-alloc` — the `#[global_allocator]` both workers run under: it\n  counts live bytes against soft and hard session limits (via\n  `Child::session_budget`, re-armed after every request). The interpreter reads\n  the soft limit at execution checkpoints; crossing the hard limit ends the\n  process rather than letting Rust abort. Its `exit-code` feature picks how:\n  `monty-runtime` enables it and exits with `OOM_EXIT_CODE` for the pool to\n  classify, `monty-wasm-runtime` leaves it off and traps, having no exit status\n  to offer. Only a binary or a wasm module may declare a global allocator, so\n  the crate provides the type and each declares its own. Direct interpreter use\n  must install and arm this allocator before configuring `max_memory`.\n- `pydantic_monty.Monty` / `pydantic_monty.AsyncMonty` — the ONLY Python\n  execution surface (there is no in-process Python API): sync and async pools\n  of workers (`with Monty() as pool: with pool.checkout() as session:\n  session.feed_run(...)`, and the `async with` / `await feed_run` equivalents).\n\nThe contract for crash detection: a child that exits or EOFs *without* a\n`FatalError` event crashed hard; the parent discards it and replaces it. See\n`limitations/pool-architecture.md` for host-API divergences from in-process execution.\n\n## Bytecode VM Architecture\n\nMonty is implemented as a bytecode VM, same as CPython.\n\n### Opcode space is scarce\n\nOpcodes serialize as a single byte, so the `Opcode` enum (`crates/monty/src/bytecode/op.rs`)\nis hard-capped at 256 variants and roughly half are already taken. Use slots sparingly:\nprefer a flags/operand encoding on one opcode (e.g. `Assert`/`FormatValue`) over a family\nof near-identical opcodes, unless the instruction is hot enough that decoding the\ndiscriminating operand would cost measurable dispatch time.\n\n### HeapReader API — Safe Heap Access\n\nAll heap-allocated Python objects (lists, dicts, strings, etc.) are stored in a paged arena (`Heap`). The `HeapReader` API provides **compile-time safe** access to heap data. This is the primary mechanism for reading and mutating heap objects throughout the codebase.\n\n**`heap.rs` is a critical safety boundary.** It contains `unsafe` code that underpins the soundness of the entire `HeapReader`/`HeapRead` system (pointer arithmetic, `UnsafeCell` access, reader-count invariants). Do NOT modify `heap.rs` without explicit user approval. Changes to this file require careful review of the safety invariants documented in the code comments.\n\n#### Core concepts\n\n- **`HeapReader<'a, T>`** — A scoped borrow of the heap that produces `HeapRead` handles. Created exclusively via `HeapReader::with`, which takes a `for<'a>` closure bound makes the lifetime `'a` universally quantified, so `HeapRead` pointers cannot escape the closure.\n- **`HeapRead<'a, T>`** — A typed handle to a specific heap entry. Created by `heap.read(id)` which returns a `HeapReadOutput<'a>` enum that you match on. Tracks a reader count that prevents the entry from being freed while the handle exists.\n- **`HeapReadOutput<'a>`** — Enum over all `HeapRead<'a, T>` variants (one per `HeapData` variant). Pattern match to get the typed handle.\n\n#### Reading and mutating heap data\n\n```rust\n// Scoped heap access.\n// The second argument allows for extra data to be\n// passed into the closure, will be rebranded as\n// `&'a mut ...` to match the `'a` lifetime of the\n// `HeapRead` handle, so the closure can have additional\n// context while still having the `for <'a>` safety guarantee.\nHeapReader::with(heap, &mut (), |heap, ()| {\n    let output = heap.read(some_id);  // returns HeapReadOutput<'a>\n    match output {\n        HeapReadOutput::List(list) => {\n            let items = list.get(heap);           // &List, borrows heap immutably\n            let items_mut = list.get_mut(heap);   // &mut List, borrows heap mutably\n        }\n        _ => { /* ... */ }\n    }\n})\n```\n\nKey borrowing rules:\n- `get(&self, &HeapReader)` → `&T` — immutable access, prevents heap mutation while reference lives\n- `get_mut(&mut self, &mut HeapReader)` → `&mut T` — mutable access, exclusive\n- Multiple `HeapRead` handles can coexist, but only one can be accessed via `get_mut` at a time\n- `dec_ref()` panics if any reader is active — prevents use-after-free\n\n#### Implementing type methods with HeapRead\n\nType methods are implemented as `impl<'h> HeapRead<'h, T>` blocks. The `PyTrait<'h>` trait provides the common interface:\n\n```rust\n// Methods on a heap type\nimpl<'h> HeapRead<'h, List> {\n    pub fn append(&mut self, vm: &mut VM<'h>, item: Value) -> RunResult<()> {\n        self.get_mut(vm.heap).items.push(item);\n        Ok(())\n    }\n}\n\n// PyTrait implementation\nimpl<'h> PyTrait<'h> for HeapRead<'h, List> {\n    fn py_type(&self, vm: &VM<'h>) -> Type { Type::List }\n    fn py_len(&self, vm: &VM<'h>) -> Option<usize> {\n        Some(self.get(vm.heap).items.len())\n    }\n    // ...\n}\n```\n\n### Reference Count Safety\n\nAll types that implement `DropWithContext<C>` hold heap (and possibly VM-side) references and **must** be cleaned up correctly on every code path — not just the happy path, but also early returns via `?`, `continue`, conditional branches, etc. A missed `drop_with` on any branch leaks reference counts.\n\n`DropWithContext<C>` is generic over the *cleanup context* `C` — whatever borrow the caller has on hand: a `Heap`, a `HeapReader`, the `VM`, or the json `Encoder`. The bound on each impl states the capability the value needs: heap-only values bound `C` by `ContainsHeap` (one impl then covers all four contexts), while values holding a `RecursionToken` (the container iterators) bound `C` by `ContainsVM` — satisfied only by `VM`/`Encoder`, since the recursion counter is unreachable through a bare heap. The same `drop_with` / `DropGuard` / `defer_drop!` machinery serves both. There are three mechanisms for ensuring cleanup, listed in order of preference:\n\n#### 1. `defer_drop!` macro (preferred)\n\nThe simplest and safest approach. Use `defer_drop!` (or `defer_drop_mut!` when mutable access to the value is needed) to bind a value into a guard that automatically drops it when scope exits — whether that's normal completion, early return via `?`, `continue`, or any other branch. The macro rebinds the value and heap variables as borrows from the guard, so you keep using them by name as before:\n\n```rust\nlet value = self.pop();\ndefer_drop!(value, heap);          // value is now &Value, heap is now &mut Heap\nlet result = value.py_repr(heap)?; // guard handles cleanup on all paths\n```\n\nBeyond safety, `defer_drop!` is often much more concise than inserting `drop_with` calls in every branch of complex control flow.\n\n`defer_drop!` gives you an immutable reference to the value. Use `defer_drop_mut!` when you need a mutable reference (e.g. iterators, values you may swap):\n\n```rust\nlet iter = vm.heap.get_iter(iter_ref);\ndefer_drop_mut!(iter, vm);\nwhile let Some(item) = iter.for_next(vm)? { ... }\n```\n\n**Limitation:** because the macro rebinds the context, it cannot be used inside `&mut self` methods on the VM where `self` owns the heap — first assign `let this = self;` and pass `this` instead.\n\n#### 2. `DropGuard` (when you need control over the value's fate)\n\nUse `DropGuard` directly when `defer_drop!` is too restrictive — specifically when you need to conditionally extract the value instead of dropping it. `DropGuard` provides `into_inner()` and `into_parts()` to reclaim ownership, while its `Drop` impl still guarantees cleanup on all other paths.\n\nDo not use `DropGuard` when the value is never moved back out of it. A guard used only through `as_parts()` or `as_parts_mut()` must be replaced with `defer_drop!` or `defer_drop_mut!`; explicit guards are reserved for code that later calls `into_inner()` or `into_parts()`. This keeps the ownership intent visible and avoids unnecessary guard bookkeeping:\n\n```rust\n// DropGuard needed here because on success we push lhs back onto the stack\n// instead of dropping it\nlet mut lhs_guard = DropGuard::new(self.pop(), self);\nlet (lhs, this) = lhs_guard.as_parts_mut();\n\nif lhs.py_iadd(rhs, this.heap)? {\n    let (lhs, this) = lhs_guard.into_parts(); // reclaim lhs, don't drop\n    this.push(lhs);\n    return Ok(());\n}\n// otherwise lhs_guard drops lhs automatically at scope exit\n```\n\n#### 3. Manual `drop_with` (for trivially simple cases)\n\nFor very simple cases with a single linear code path and no branching between acquiring and releasing the value, a direct `drop_with` call is acceptable as long as it produces more concise code than `defer_drop!`:\n\n```rust\nlet iter = self.pop();\niter.drop_with(self); // single path, no branching\n```\n\n`drop_with` should be used **only** when it is genuinely simpler than `defer_drop!` or `DropGuard`. The latter two are safer and more maintainable, especially in complex control flow. Multiple manual cleanup calls for the same owned value are a poor substitute for a guard.\n\n**Do not use `drop_with` if any of the following are true:**\n- The same value has `drop_with` called in multiple places (e.g. a loop with `continue` or `?` in the middle). This implies `defer_drop!` or `DropGuard` will be easier to read.\n- The explicit call to `drop_with` produces more lines of code than `defer_drop!` or `DropGuard` would. The latter often avoid rightward drift and make the cleanup logic easier.\n- The value is part of a container (e.g. `Vec<Value>`). Ideally the container itself implements `DropWithContext` and so `defer_drop` or `DropGuard` can be used on the whole container. Consider if a `DropWithContext` implementation for the container might be missing.\n\n### Resource-tracked string construction (`StringBuilder`)\n\nAny code that builds a `String` whose final size is not already bounded by an existing input **must** use `StringBuilder` (in `crates/monty/src/string_builder.rs`) rather than `String::with_capacity(...).push(...)`. A loop-built string can otherwise jump past both allocator limits before an execution checkpoint — this is exactly the class of bug that hit `str.expandtabs` (huge `tabsize` amplifying a single tab into a multi-gigabyte allocation).\n\n`StringBuilder` preflights capacity growth against allocator-backed usage. The in-progress buffer is itself visible to the allocator, so nested builders share the same real-byte budget. Growth is amortized via 2× doubling:\n\n```rust\n// Bounded size known up front (padding to a given width):\nlet mut builder = StringBuilder::with_capacity(width * fillchar.len_utf8(), &vm.heap.tracker)?;\nbuilder.push_str(s)?;\nfor _ in 0..pad { builder.push(fillchar)?; }\nbuilder.finish(vm.heap)\n\n// Size not bounded up front (e.g. attacker-controlled multiplier):\nlet mut builder = StringBuilder::new(&vm.heap.tracker);\nfor c in input.chars() { builder.push(c)?; }\nbuilder.finish(vm.heap)\n```\n\n`StringBuilder` also implements `fmt::Write`, so `write!(builder, ...)`, `format_args!`, and the existing `py_repr_fmt(f, ...)` machinery work against a tracker-protected buffer. `fmt::Error` is payload-free, so any `ResourceError` raised by a write is stashed on the builder and surfaced by `finish(heap)` — callers using `write!` don't need to thread the tracker error themselves.\n\nWhen the input *is* already bounded (e.g. `s.to_lowercase()`, slicing, `to_owned()` of an existing tracked string), passing a plain `String` / `&str` to `allocate_string` is fine — the result is bounded by a known multiple of an already-tracked input, so no amplification is possible.\n\n### Soft memory-limit checks — when and why\n\n`max_memory` is a **soft** limit: the VM polls allocator-backed usage every 255\ninstructions (`check_memory_time`), and everything pathological is caught by the hard\nlimits — the allocator's hard ceiling (soft + headroom, worker exits with\n`OOM_EXIT_CODE` and the pool replaces it) and the pool's turn timeout. Soft\nchecks exist ONLY to turn *common* overshoots into a graceful `MemoryError`\nthat keeps the session alive; they are not a safety boundary, so do not\nsprinkle them everywhere — every check is code noise and hot-path cost.\n\nAdd a check only where ordinary code commonly allocates a multi-MiB burst\ninside a single builtin call (i.e. before the next instruction checkpoint):\n\n- Known-size bulk allocation: one up-front `tracker.check_allocation(n * VALUE_SIZE)`\n  (container clone/copy, e.g. `clone_all_items`, `list_copy`) or\n  `check_repeat_size`-style estimate (`resource_checks.rs`).\n- Iterator collection: `collect_python_iterator` / `checked_preallocation_hint`\n  already handle it; for push-loops that bypass them, a one-shot size-hint\n  preflight (see `deque_extend`) — never a per-item poll.\n- Unbounded/amplifying string building: `StringBuilder` (above).\n\nDo NOT add per-iteration `check_time()` polls to Rust-side loops for memory's\nsake, and do NOT preflight results bounded by a constant multiple of an\nalready-tracked input (path joins, `*args` tuples, regex match lists, parsed\nJSON) — rare oversized cases there are the hard limit's job. Test each graceful\npath in `large_allocations_are_rejected_before_the_hard_limit`\n(`crates/monty-runtime/tests/subprocess.rs`) — the interpreter's own tests\nnever arm the allocator, so only subprocess tests exercise `max_memory`.\n\n## Dev Commands\n\n**IMPORTANT**: before running `cargo build` or `cargo run`, it is likely necessary to run `make install-py` to ensure that the Python virtual environment is available for build.\n\nInstead use the following `make` commands:\n\n```bash\nmake install-py           Install python dependencies\nmake install-js           Install JS package dependencies\nmake install              Install the package, dependencies, and pre-commit for local development\nmake dev-py               Install the python package for development\nmake build-js             Build the JS package (compile TypeScript)\nmake lint-js              Lint JS code with oxlint\nmake test-js              Test the JS package (builds the monty binary the workers run)\nmake dev-py-release       Install the python package for development with a release build\nmake build-wasm           Build the lean wasm worker module (requires the wasm32-wasip1 target)\nmake test-wasm            Test the wasm worker module from node, with no browser\nmake test-browser         Browser (Vitest) test of the wasm path in a real headless browser\nmake dev-py-pgo           Install the python package for development with profile-guided optimization\nmake format-rs            Format Rust code with fmt\nmake format-py            Format Python code - WARNING be careful about this command as it may modify code and break tests silently!\nmake format-js            Format JS code with prettier\nmake format               Format Rust code, this does not format Python code as we have to be careful with that\nmake lint-rs              Lint Rust code with clippy and import checks\nmake clippy-fix           Fix Rust code with clippy\nmake generate-proto       Regenerate monty-proto's checked-in code from the .proto schema\nmake check-proto          Verify monty-proto's checked-in code matches the .proto schema\nmake lint-py              Lint Python code with ruff\nmake lint                 Lint the code with ruff and clippy\nmake test-no-features     Run rust tests without any features enabled\nmake test-memory-model-checks Run rust tests with memory-model-checks enabled - THIS IS EXTREMELY SLOW, SHOULD MOSTLY BE RUN IN CI OR IF ABSOLUTELY NECESSARY\nmake test-ref-count-return Run rust tests with ref-count-return enabled\nmake test-cases           Run tests cases only\nmake test-type-checking   Run rust tests on monty-type-checking\nmake pytest               Run Python tests with pytest\nmake test-py              Build the python package (debug profile) and run tests\nmake test-docs            Test docs examples only\nmake test                 Run rust tests\nmake testcov              Run Rust tests with coverage, print table, and generate HTML report\nmake complete-tests       Fill in incomplete test expectations using CPython\nmake update-typeshed      Update vendored typeshed from upstream\nmake bench                Run benchmarks\nmake bench-pool           Run subprocess pool benchmarks (spawn, checkout, wire round-trips)\nmake dev-bench            Run benchmarks to test with dev profile\nmake profile              Profile the code with pprof and generate flamegraphs\nmake type-sizes           Write type sizes for the crate to ./type-sizes.txt (requires nightly and top-type-sizes)\nmake main                 run linting and the most important tests\nmake help                 Show this help (usage: make help)\n```\n\nUse the /python-playground skill to check cpython and monty behavior.\n\n## Releasing\n\nSee [RELEASING.md](RELEASING.md) for the release process.\n\n## Exception\n\nIt's important that exceptions raised/returned by this library match those raised by Python.\n\nWherever you see an Exception with a repeated message, create a dedicated method to create that exception `src/exceptions.rs`.\n\nWhen writing exception messages, always check `src/exceptions.rs` for existing methods to generate that message.\n\n## Argument extraction — ALWAYS use `#[derive(FromArgs)]`\n\n**Whenever you add or modify a Rust-side function, method, type\nconstructor, or `OsFunction` handler that takes anything beyond the\ntrivial 0/1/2-positional shapes already covered by\n`ArgValues::check_zero_args` / `get_one_arg` / `get_two_args` /\n`get_zero_one_arg` / `into_pos_only`, you MUST use\n`#[derive(FromArgs)]` (re-exported as `monty::args::FromArgs`).**\n\nHand-written `args.into_parts()` loops are not acceptable for any\nsignature that has multiple positionals with defaults, keyword\narguments, `*args`, or `**kwargs` — they are a known source of\nreference-count leaks, divergent error messages, and duplicated\nboilerplate. `FromArgs` emits a static param spec driven by the runtime\nbinder (`crates/monty/src/args/bind_native.rs`), which handles dispatch,\nconflict detection, default handling, and refcount cleanup mechanically.\nPick `style = def | clinic | c | c_named | unpack` by the CPython parser\nfamily the target function uses — see\n[`crates/monty-macros/README.md`](crates/monty-macros/README.md) for the\nfamily table and the full attribute surface (`style`, `at_most_total`,\n`bad_arg[_named]`, `pos_only`, `kw_only`, `varargs`, `varkwargs`,\n`default`, `static_string`, …) and how to extend the macro or add new\n`FromValue` impls.\n\nIf a callsite needs custom per-argument coercion (e.g. `value_to_float`\nfor math, a `TimeDelta` type check, a `bytes`-or-`str` union), declare\nthe field as `Value` and run the coercion in the function body *after*\nthe `from_args` call — the macro still handles the parsing, your code\njust adds the final validation step.\n\n## Code style\n\nAvoid local imports, unless there's a very good reason, all imports should be at the top of the file.\n\nAvoid `fn my_func<T: MyTrait>(..., param: T)` style function definitions, STRONGLY prefer `fn my_func(param: impl MyTrait)` syntax since changes are more localized. This includes in trait definitions and implementations.\n\nAlso avoid using functions and structs via a path like `std::borrow::Cow::Owned(...)`, instead import `Cow` globally with `use std::borrow::Cow;`.\n\nSTRONGLY prefer expression-oriented style: use `if`/`match` as expressions with a trailing (tail) expression rather than early `return` with a guard clause. E.g. prefer\n\n```rs\nif cond { a } else { b }\n```\n\nover\n\n```rs\nif cond {\n    return a;\n}\nb\n```\n\nThis applies to function bodies and block expressions alike. Only use early `return` when it genuinely simplifies control flow (e.g. several guard clauses at the top of a function).\n\nThis applies even more strongly to long `if cond { ... } else if cond2 { ... } ... else { ... }` chains — keep them as a single expression yielding a value, rather than scattering `return` statements through each branch.\n\nNEVER use `allow()` in rust lint markers, instead use `expect()` so any unnecessary markers are removed. E.g. use\n\n```rs\n#[expect(clippy::too_many_arguments)]\n```\n\nNOT!\n\n```rs\n#[allow(clippy::too_many_arguments)]\n```\n\n### Docstrings and comments.\n\nIMPORTANT: every struct, enum and function should have a concise docstring to\nexplain what it does and why; and any considerations or potential foot-guns of using that type.\n\nThe only exception is trait implementation methods where a docstring is not necessary if the method is self-explanatory.\n\nIt's important that docstrings cover the motivation and primary usage patterns of code, not just the simple \"what it does\".\n\nSimilarly, you should add comments to code, especially if the code is complex or esoteric.\n\nComments and field docstrings should almost never be more than 3 lines, mostly 1 line. Function and struct docstrings should be concise, generally <= 5 lines.\n\nOnly add examples to docstrings of public functions and structs, examples should be <=8 lines, if the example is more, remove it.\n\nIf you add example code to docstrings, it must be run in tests. NEVER add examples that are ignored.\n\nIf you encounter a comment or docstring that's out of date - you MUST update it to be correct.\n\nSimilarly, if you encounter code that has no docstrings or comments, or they are minimal, you should add more detail.\n\nAlways use single back-ticks in python docstrings - they should be markdown, not rst!\n\nNOTE: COMMENTS AND DOCSTRINGS ARE EXTREMELY IMPORTANT TO THE LONG TERM HEALTH OF THE PROJECT.\n\nNOTE: COMMENTS AND DOCSTRINGS SHOULD BE CONCISE - EXCESSIVELY VERBOSE DOCSTRINGS MAKE THE CODE HARDER TO READ AND MAINTAIN!\n\n## Tests\n\nDo **NOT** write tests within modules unless explicitly prompted to do so.\n\nTests should live in the relevant `tests/` directory.\n\nCommands:\n\n```bash\n# Build the project\ncargo build\n\n# Run tests\ncargo test -p monty\n\n# Run crates/monty/test_cases tests only\nmake test-cases\n\n# Run a specific test\ncargo test -p monty --test TEST str__ops\ncargo run -p monty-datatest str__ops\n\n# Run the interpreter on a Python file\ncargo run -- <file.py>\n```\n\nThe `memory-model-checks` feature (`make test-memory-model-checks`, or\n`--features memory-model-checks` on the commands above) is VERY SLOW — it is\nrun in CI, so do NOT enable it by default. Only reach for it when a change\nspecifically touches refcount/heap/GC behavior (e.g. new opcodes that retain\nvalues, `drop_with` paths, cycle collection) and then run just the relevant\ntest binary, e.g. `cargo test -p monty --test TEST --features memory-model-checks`.\n\nSee more test commands above.\n\n### Experimentation and Playground\n\nRead `Makefile` for other useful commands.\n\nYou can use the `./playground` directory (excluded from git, create with `mkdir -p playground`) to write files\nwhen you want to experiment by running a file with cpython or monty, e.g.:\n* `python3 playground/test.py` to run the file with cpython\n* `cargo run -- playground/test.py` to run the file with monty\n\nDO NOT use `/tmp` or pipe code to the interpreter, or use `python3 -c ...` as it requires extra permissions and can slow you down!\n\nMore details in the \"python-playground\" skill.\n\n### Test File Structure\n\nMost functionality should be tested via python files in the `crates/monty/test_cases` directory.\n\n**DO NOT create many small test files.** This would be unmaintainable.\n\nALWAYS consolidate related tests into single files using multiple `assert` statements. Follow `crates/monty/test_cases/fstring__all.py` as the gold standard pattern:\n\n```python\n# === Section name ===\n# brief comment if needed\nassert condition\nassert another_condition\n\n# === Next section ===\nx = setup_value\nassert x == expected\n```\n\nDo NOT add messages to `assert` statements — Monty's assert message annotations\n(see `limitations/assert.md`) already show the failing values, so a hand-written\nmessage is clutter. The ONE exception: tests whose failure would show nothing,\ni.e. `assert False` sentinels in try/except blocks (`assert False, 'expected\nTypeError'`) and tests that evaluate to a bare bool (`not` expressions, chained\ncomparisons, boolean ops) — there a message is required since introspection\nshows nothing.\n\nDo NOT Write tests like `assert 'thing' in msg` it's lazy and inexact unless explicitly told to do so, instead write tests like `assert msg == 'expected message'` to ensure clarity and accuracy and most importantly, to identify differences between Monty and CPython.\n\n### When to Create Separate Test Files\n\nOnly create a separate test file when you MUST use one of these special expectation formats:\n\n- `\"\"\"TRACEBACK:...\"\"\"` - Test expects an exception with full traceback (PREFERRED for error tests)\n- `# Raise=Exception('message')` - Test expects an exception without traceback verification - NOT RECOMMENDED, use `TRACEBACK` instead\n- `# ref-counts={...}` - Test checks reference counts (special mode)\n- you're writing tests for a different behavior or section of the language\n\nFor everything else, **add asserts to an existing test file** or create ONE consolidated file for the feature.\n\n### File Naming\n\nName files by feature, not by micro-variant:\n- ✅ `str__ops.py` - all string operations (add, iadd, len, etc.)\n- ✅ `list__methods.py` - all list method tests\n- ❌ `str__add_basic.py`, `str__add_empty.py`, `str__add_multiple.py` - TOO GRANULAR\n\n### Expectation Formats (use sparingly)\n\nOnly use these when `assert` won't work (on last line of file):\n- `# Return=value` - Check `repr()` output (prefer assert instead)\n- `# Return.str=value` - Check `str()` output (prefer assert instead)\n- `# Return.type=typename` - Check `type()` output (prefer assert instead)\n- `# Raise=Exception('message')` - Expect exception without traceback (REQUIRES separate file)\n- `\"\"\"TRACEBACK:...\"\"\"` - Expect exception with full traceback (PREFERRED over `# Raise=`)\n- `# ref-counts={...}` - Check reference counts (REQUIRES separate file)\n- No expectation comment - Assert-based test (PREFERRED)\n\nDo NOT use `# Return=` when you could use `assert` instead\n\n### Traceback Tests (Preferred for Errors)\n\nFor tests that expect exceptions, **prefer traceback tests over `# Raise=` or `try` / `except`** because they verify:\n- The full traceback with all stack frames\n- Correct line numbers for each frame\n- Function names in the traceback\n- The caret markers (`~`) pointing to the error location\n\nTraceback test format - add a triple-quoted string at the end of the file starting with `\\nTRACEBACK:`:\n```python\ndef foo():\n    raise ValueError('oops')\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"my_test.py\", line 4, in <module>\n    foo()\n    ~~~~~\n  File \"my_test.py\", line 2, in foo\n    raise ValueError('oops')\nValueError: oops\n\"\"\"\n```\n\nKey points:\n- The filename in the traceback should match the test file name (just the basename, not the full path)\n- Use `~` for caret markers (the test runner normalizes CPython's `^` to `~`)\n- The `<module>` frame name is used for top-level code\n- Tests run against both Monty and CPython, so the traceback must match both\n\nIf you don't care about the traceback or it intentionally differs from cpython (e.g. for `json`) and you want to test\nmultiple cases in the same file, use this style\n\n```py\ntry:\n    ...\n    assert False, 'expected <task> to fail'\nexcept <ErrorType> as exc:\n    assert str(exc) = '<expected exception message>'\n```\n\nIMPORTANT: don't just check that an exception is raised, you should always check the exception message.\n\nIMPORTANT: DON'T BE LAZY. If the exception differs between cpython and Monty, either fix the exception message, or\nstop and report the problem!\n\nOnly use `# Raise=` when you only care about the exception type/message and not the traceback and you can't use a try/except block.\n\n### Python fixture markers\n\nYou may mark python files with:\n* `# call-external` to support calling external functions\n* `# run-async` to support running async code\n\nNEVER MARK TESTS AS XFAIL UNDER ANY CIRCUMSTANCES!!! INSTEAD FIX THE BEHAVIOR SO THAT THE TEST PASSES.\n\nNever mark tests as:\n- `# xfail=cpython` - Test is required to fail on CPython\n- `# xfail=monty` - Test is required to fail on Monty\n\nNEVER MARK TESTS AS XFAIL UNDER ANY CIRCUMSTANCES!!! INSTEAD FIX THE BEHAVIOR SO THAT THE TEST PASSES.\n\nAll these markers must be at the start of comment lines to be recognized.\n\n### Other Notes\n\n- Prefer single quotes for strings in Python tests\n- Do NOT add `# noqa` or  `# pyright: ignore` comments to test code, instead add the failing code to `pyproject.toml`\n- The ONLY exception is `await` expressions outside of async functions, where you should add `# pyright: ignore`\n- Run `make lint-py` after adding tests\n- Use `make complete-tests` to fill in blank expectations\n- Regression tests run via `datatest-stable` harness in `crates/monty-datatest/src/main.rs`, use `make test-cases` to run them\n\n### Rust integration tests and `insta` snapshots\n\nIn `crates/*/tests/*.rs` (but **not** `crates/monty/test_cases/`), use [`insta`](https://insta.rs) `assert_snapshot!` for multi-line strings, serialized output, error messages otherwise fuzz-checked via `.contains(...)`, and any fixture currently compared via a hand-rolled `UPDATE_EXPECT` helper (use external snapshots under `tests/snapshots/`).\n\nKeep `assert_eq!` for scalars, enums, and structural values (`MontyObject`, `Vec`, etc.), and for principled membership checks like `vec.contains(...)`.\n\nWorkflow: write `assert_snapshot!(value, @\"\");`, then `cargo insta test --accept` to populate (plain `INSTA_UPDATE=always` does **not** update inline `@\"...\"` snapshots — you need the `cargo insta` subcommand, installed via `cargo install cargo-insta`). Add `insta = { workspace = true }` to `[dev-dependencies]` when introducing it to a new crate.\n\n## Python Package (`pydantic-monty`)\n\nThree PyPI distributions are built from this repo:\n\n- `pydantic-monty-client` (`crates/monty-python/`, Cargo package\n  `pydantic-monty-client`) — the PyO3 bindings, i.e. the `pydantic_monty`\n  module. It deliberately does **not** depend on the runtime, so it can be\n  installed where the `monty` binary comes from a base image or system package.\n- `pydantic-monty-runtime` (`crates/monty-runtime/`) — the `monty` worker binary.\n- `pydantic-monty` (`packages/pydantic-monty/`) — a hatchling metapackage with\n  no code, exactly pinning the other two. This is what users install. Its\n  version and both pins are rewritten from the Cargo workspace version by\n  `crates/monty-python/build.rs`; never edit them by hand.\n\nExecution always happens in `monty` worker subprocesses — there is no in-process execution API.\nThe surface is `Monty` (sync pool) and `AsyncMonty` (async pool), each with\n`pool.checkout(...)` sessions driven by `feed_run` (a coroutine on async sessions).\n\n### Structure\n\n- `crates/monty-python/src/` - Rust source for PyO3 bindings\n- `crates/monty-python/python/pydantic_monty/_monty.pyi` - Type stubs for the Python module\n- `crates/monty-python/tests/` - Python tests using pytest\n- `crates/monty-python/README.md` - the `pydantic-monty-client` readme (binary\n  resolution); the full user-facing docs live in `packages/pydantic-monty/README.md`\n\n### Building and Testing\n\nDependencies needed for python testing are installed in `crates/monty-python/pyproject.toml`.\nTo install these dependencies, use `uv sync --all-packages --only-dev`.\n\n```bash\n# Build the Python package for development (required before running tests)\nmake dev-py\n\n# Run Python tests\nmake test-py\n\n# Or run pytest directly (after dev-py)\nuv run pytest\n\n# Run a specific test file\nuv run pytest crates/monty-python/tests/test_basic.py\n\n# Run a specific test\nuv run pytest crates/monty-python/tests/test_basic.py::test_simple_expression\n```\n\n### Python Test Guidelines\n\nCheck and follow the style of other python tests.\n\nMake sure you put tests in the correct file.\n\n**DO NOT use python/pytest tests for `monty` core functionality!** When testing core functionality, add tests to `crates/monty/test_cases/` or `crates/monty/tests/`. Only use python/pytest tests for `pydantic_monty` functionality testing.\n\n**NEVER use class-based tests.** All tests should be simple functions.\n\nUse `@pytest.mark.parametrize` whenever testing multiple similar cases.\n\nUse `snapshot` from `inline-snapshot` for all test asserts.\n\nNEVER do the lazy `assert '...' in ...` instead always do `assert value == snapshot()`,\nthen run the test and inline-snapshot will fill in the missing value in the `snapshot()` call.\n\nUse `pytest.raises` for expected exceptions, like this\n\n```py\nwith pytest.raises(ValueError) as exc_info:\n    session.feed_run(code, print_callback=callback)\nassert exc_info.value.args[0] == snapshot('stopped at 3')\n```\n\n## Reference Counting\n\nHeap-allocated values (`Value::Ref`) use manual reference counting. Key rules:\n\n- **Cloning**: Use `clone_with_heap(heap)` which increments refcounts for `Ref` variants.\n- **Dropping**: Call `drop_with(ctx)` (the [`DropWithContext`] method) when discarding a `Value` that may be a `Ref`.\n\nContainer types (`List`, `Tuple`, `Dict`) also have `clone_with_heap()` methods.\n\n### Raw `HeapId` ownership\n\n`HeapId` does not encode whether a reference is owned or borrowed. Locally owned IDs should typically be wrapped in `Value::Ref` immediately so `defer_drop!` and `DropGuard` can manage cleanup; a local raw `HeapId` should otherwise be presumed borrowed.\n\nOwned `HeapId` fields remain the preferred representation where a structure needs the raw ID, such as `ListIterator::list`. Such fields must be documented as owned and cleaned up exactly once:\n\n- Heap-stored `HeapItem` implementations must push every owned ID from `py_dec_ref_ids`; this is preferred to calling `Heap::dec_ref` directly because destruction uses the heap's iterative cleanup stack.\n- Non-`HeapItem` owners should release owned IDs through their `DropWithContext` implementation, where a direct `dec_ref` is acceptable.\n- Direct `dec_ref` in ordinary control flow is discouraged. As with `drop_with`, never scatter cleanup for the same owned reference across branches; use an owning `Value` and a guard instead.\n\nRaw ownership is also acceptable when immediately transferred into a documented owned field or across an API whose contract explicitly transfers ownership.\n\n**Mutability of the heap parameter is asymmetric** — do not assume the two methods take the same kind of borrow:\n\n- `clone_with_heap` takes `&impl ContainsHeap` (immutable). The refcount field lives behind interior mutability, so `inc_ref` is `&self` on `Heap`. This means you can call `clone_with_heap` while other immutable borrows of the heap (e.g. a `HeapRead` handle obtained via `.get(heap)`) are still live.\n- `Heap::allocate` is also `&self` because entry storage is behind interior mutability. New heap entries can be created without a `&mut Heap`.\n- `drop_with` takes `&mut C` (the cleanup context — `Heap` / `HeapReader` / `VM` / `Encoder`), because dropping may free entries and run destructors, which mutates the heap.\n\nIf you find yourself fighting the borrow checker around `clone_with_heap` or `allocate`, the fix is almost never `&mut` — it is more likely that you are passing the wrong receiver (e.g. `vm` instead of `vm.heap`) or holding a `&mut` borrow elsewhere that should be `&`.\n\n### Cycle collection — Bacon–Rajan trial deletion\n\nReference counting alone cannot reclaim cycles. Monty uses **Bacon–Rajan trial deletion**\n(`Heap::collect_cycles` in `crates/monty/src/heap.rs`).\n\n**Resource limits**: When a memory or time limit is exceeded, execution terminates with a `ResourceError`. No guarantees are made about the state of the heap or reference counts after a resource limit is exceeded. The heap may contain orphaned objects with incorrect refcounts. This is acceptable because resource exhaustion is a terminal error - the execution context should be discarded.\n\n## JavaScript Package (`@pydantic/monty`, `crates/monty-js/`)\n\nThe JavaScript package is a **napi-rs binding over `monty-pool`** — the same\nRust pool/protocol engine `pydantic_monty` uses — wrapped by a thin\nTypeScript layer. The native binding exposes turn-level primitives\n(`NativePool`, `NativeSession.feed/resume*`); the TypeScript drive loop\nanswers suspension events (external functions, `os` callbacks, async\nfutures) where promises are native. Pool elasticity, turn deadlines, crash\nrecovery, framing and value conversion all live in Rust.\n\n### Structure\n\n- `crates/monty-js/src/` - Rust napi crate (native-only): `pool.rs`\n  (NativePool / NativeSession over `monty-pool`), `convert.rs`\n  (JS ↔ MontyObject), `exceptions.rs`, `limits.rs`\n- `crates/monty-js/ts/` - TypeScript wrapper: `pool.ts` (Monty),\n  `session.ts` (MontySession + drive loop), `errors.ts`, `binary.ts`\n  (monty binary resolution), `mount.ts`, `native.ts` (turn-object typings)\n- `crates/monty-js/ts/worker/` - the browser/wasm worker path (exported as\n  `@pydantic/monty/wasm`): `proto.ts`/`value.ts` (TS `monty-proto` codec),\n  `transport.ts` (WorkerTransport, the `NativeSession`-shaped seam),\n  `host.ts`/`channel.ts` (in-process and message-channel dispatch),\n  `pool.ts` (WorkerPool, the TS `monty-pool` analog), `nodeFactory.ts` /\n  `browserFactory.ts` (Worker backends), `index.ts` (`createWorkerPool`)\n- `index.js` / `index.d.ts` - napi-generated loader (created by\n  `npm run build:napi`; gitignored)\n- `crates/monty-js/npm/` - generated platform packages shipping the napi\n  `.node` library *and* the `monty` binary (`@pydantic/monty-<platform>`,\n  selected via optionalDependencies; `napi create-npm-dirs` +\n  `scripts/create-platform-packages.mjs`)\n- `crates/monty-js/__test__/` - Tests using vitest (`wasm_*.spec.ts` drive the\n  wasm worker pool/transport without the napi build, and need `make build-wasm`\n  first — `npm test` excludes them, `npm run test:wasm` runs them)\n\n### Current API\n\n```ts\nimport { Monty } from '@pydantic/monty'\n\nawait using pool = await Monty.create({ maxProcesses: 8, requestTimeout: 30 })\nawait using session = await pool.checkout({ typeCheck: false })\n\nawait session.feedRun('x = 21') // session state persists across feeds\nconst result = await session.feedRun('x * 2', {\n  inputs: { y: 1 },\n  externalLookup: { fetch: async (url: string) => '...' }, // sync or async\n  printCallback: (stream, text) => {},\n})\n```\n\nErrors: `MontyError` (base), `MontySyntaxError`, `MontyRuntimeError`,\n`MontyTypingError`, and `MontyCrashedError` (worker death; pool recovers).\n`MountDir` and the `os`/`NOT_HANDLED` callback work like the Python package.\n\nSee `crates/monty-js/README.md` for full API documentation.\n\n### Building and Testing\n\n```bash\nmake install-js   # npm install\nmake build-js     # napi debug build + compile TypeScript\nmake test-js      # builds the napi binding + debug monty binary, then runs vitest\nmake lint-js      # oxlint\nmake format-js    # prettier\nmake smoke-test-js  # packs + installs the package and platform binary package\n```\n\nTests run straight from `ts/` via `@oxc-node/core` against the locally built\n`.node`; the workers resolve the `monty` binary from the workspace\n`target/debug` build automatically.\n\n### JavaScript Test Guidelines\n\n- Tests use [vitest](https://vitest.dev) and live in `crates/monty-js/__test__/`\n- Tests are written in TypeScript; use the `setupPool` helper from `__test__/helpers.ts`\n- Follow the existing test style in the `__test__/` directory\n\n## WebAssembly build (`@pydantic/monty/wasm`)\n\nBrowsers (and anywhere subprocesses are impossible) run the sandbox in a **Web\nWorker** instead of a subprocess, exposed under the `/wasm` subpath. The same\npool → checkout → session → `feedRun` model and drive loop are used; only the\ntransport differs. The pieces:\n\n- `crates/monty-wasm-runtime` — a lean `wasm32-wasip1` module: a WASI reactor wrapping\n  the transport-agnostic `monty-worker` `Child` state machine, exporting one\n  `monty_dispatch_turn` (read a framed request from stdin, run one turn, write\n  framed events to stdout). No napi, no threads, no `SharedArrayBuffer`. It\n  declares the `monty-alloc` global allocator, so a session's `max_memory`\n  bounds what the module allocates too; exceeding it traps, which the host\n  already reads as a dead instance.\n- `crates/monty-js/ts/worker/` — the TS pool/transport that drives it\n  (`createWorkerPool`): a browser `Worker` backend (`browserFactory.ts`, whose\n  `Worker.terminate()` is the watchdog's hard kill), a Node `worker_threads`\n  backend (`nodeFactory.ts`), and an in-process degrade for environments with\n  no `Worker` (same API, but no crash isolation or preemption). Values cross as\n  `monty-proto` frames decoded in TypeScript (`proto.ts`/`value.ts`), not via\n  napi.\n\nBuild the worker module locally with `make build-wasm` (needs the\n`wasm32-wasip1` target); it is built and tested in CI. `make test-browser` runs\nthe whole suite against it in headless Chromium, and `make test-wasm` drives it\nfrom Node with no browser (`__test__/wasm_*.spec.ts`, run by their own\n`vitest.wasm.config.ts` — `npm test` excludes them, since it does not build the\nmodule).\n\n## Limitations documentation (`./limitations/`)\n\nEvery pull request that adds, changes, or removes user-visible behavior MUST\nland (or update) a markdown document under `./limitations/` describing how\nthe feature DIVERGES from CPython and what subset of the CPython surface\narea Monty actually implements. The directory is the single source of truth\nfor \"what does Monty *not* do that CPython does\" — module-level docstrings\nand inline comments are not sufficient on their own.\n\n**NOTE**: `./limitations/` SHOULD **ONLY** INCLUDE INFORMATION ABOUT BEHAVIOR DIVERGENCES FROM CPython, not points that describe behavior that matches CPython's behavior.\n\nOne file per feature, named after the builtin / module / construct it\ncovers (e.g. `limitations/open.md`, `limitations/asyncio.md`,\n`limitations/bytecode_interpretter.md`). Add new sections to an existing file when the feature\nis already documented; only create a new file when there is no fit.\n\nKeep entries concise but comprehensive — list every known divergence,\nincluding ones that \"feel obvious\". A divergence that is not written down\nis one that future readers (and future Claude) will assume does not exist.\nReviewers should reject PRs that change behavior without updating\n`./limitations/` if necessary.\n\nStructure each file around what a Python user would actually try:\n\n- Arguments/options that are rejected or ignored.\n- Methods/attributes that raise `AttributeError`.\n- Behaviour that differs from CPython even when the API exists.\n- Error types / messages that differ from CPython.\n\nAvoid implementation detail unless it explains a user-visible quirk.\n\n## NOTES\n\nALWAYS consider code quality when adding new code, if functions are getting too complex or code is duplicated, move relevant logic to a new file.\nMake sure functions are added in the most logical place, e.g. as methods on a struct where appropriate.\n\nThe code should follow the \"newspaper\" style where public and primary functions are at the top of the file, followed by private functions and utilities.\nALWAYS put utility, private functions and \"sub functions\" underneath the function they're used in.\n\nIt is important to the long term health of the project and maintainability of the codebase that code is well structured and organized, this is very important.\n\nALWAYS run `make format-rs` and `make lint-rs` after making changes to rust code and fix all suggestions to maintain code quality.\n\nALWAYS run `make lint-py` after making changes to python code and fix all suggestions to maintain code quality.\n\nALWAYS update this file when it is out of date.\n\nNEVER add imports anywhere except at the top of the file, this applies to both python and rust.\n\nNEVER write `unsafe` code, if you think you need to write unsafe code, explicitly ask the user or leave a `todo!()` with a suggestion and explanation.\n\nWhen you get asked a question like \"Is X really the best approach\" ANSWER THE QUESTION! don't try to make a chance based on a perceived instruction in the question!\n"},"files":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\nDON'T COMMIT UNLESS EXPLICITLY ASKED TO DO SO BY THE USER! Previous commit requests do not matter - don't commit unless you've just been explicitly asked to do so.\n\n## Project Overview\n\nMonty is a sandboxed Python interpreter written in Rust. It parses Python code using Ruff's `ruff_python_parser` but implements its own runtime execution model for safety and performance. This is a work-in-progress project that currently supports a subset of Python features.\n\nProject goals:\n\n- **Safety**: Execute untrusted Python code safely without FFI or C dependencies, instead sandbox will call back to host to run foreign/external functions.\n- **Performance**: Fast execution through compile-time optimizations and efficient memory layout\n- **Simplicity**: Clean, understandable implementation focused on a Python subset\n- **Snapshotting and iteration**: Plan is to allow code to be iteratively executed and snapshotted at each function call\n- **Cross-platform**: Runs on Linux, macOS, and Windows (and any other OS that can run Rust)\n- Targets the latest stable version of Python, currently Python 3.14\n\n## `monty-types` — shared boundary types\n\nThe public data types (`MontyObject`, `MontyException`/`ExcType`, `OsFunctionCall` +\nits arg structs, `ResourceLimits`/`ResourceTracker`, `PrintStream`/`PrintWriter`,\n`CompileOptions`, `ExtFunctionResult`, `FileMode`, ...) live in `crates/monty-types`,\nwhich depends on no other monty crate except the `monty-macros` derives. `monty`\ndepends on `monty-types` but does not blanket re-export it — only a few types\nare re-exported inline where they appear in `monty`'s public API (e.g.\n`run::CompileOptions`, `run_progress::{ExtFunctionResult, NameLookupResult}`).\nCode needing `MontyObject`, `MontyException`, `OsFunctionCall`, etc. must\ndepend on `monty-types` directly.\n\nHost-side crates (`monty-fs`, `monty-pool`, `monty-proto` without its `worker`\nfeature, `monty-python`, `monty-js`) MUST depend on `monty-types`, NOT `monty` —\nthis keeps the interpreter out of their binaries. Only the worker side\n(`monty-runtime`, `monty-wasm-runtime`, `monty-proto` with `worker`) links the\ninterpreter. Don't add a `monty` dependency to a host-side crate; if it needs a\ntype, that type belongs in `monty-types`.\n\nInterpreter-coupled methods on these types live in `monty` as `pub(crate)`\nextension traits (`ExcTypeExt`, `MontyObjectExt`, `MontyTypeExt`, `StackFrameExt`,\n`FileModeExt`, `BuiltinsFunctionsExt`, `ExtFunctionResultExt`) — import the trait\nto call e.g. `ExcType::type_error(...)` or `MontyObject::new(value, vm)`.\n\n## Cross-Platform Requirements\n\nMonty must work identically on Linux, macOS, and Windows. Within the Monty sandbox,\npaths always use POSIX/Linux-style forward slashes (`/`) regardless of the host OS.\nThe `MountTable` handles translating between virtual POSIX paths and host-native paths.\n\nKey rules:\n- **Virtual paths** are always POSIX-style (`/mnt/data/file.txt`), never Windows-style\n- **Host paths** use `std::path::Path`/`PathBuf` which handles OS differences automatically\n- Avoid `#[cfg(unix)]`-only code in the main crate — all features must work on all platforms\n- Tests in `crates/*/tests/` should be cross-platform; use helper functions for\n  OS-specific APIs like symlink creation (see `symlink_file`/`symlink_dir` in\n  `crates/monty-fs/tests/common/mod.rs`, shared via `mod common;` — each\n  `tests/*.rs` is its own crate, so helpers used by more than one belong there)\n- CI runs `cargo test -p monty --features memory-model-checks` and `cargo test -p monty-fs`\n  on Linux, macOS, and Windows\n\n## Important Security Notice\n\nIt's ABSOLUTELY CRITICAL that there's no way for code run in a Monty sandbox to access the host filesystem, or environment or to in any way \"escape the sandbox\".\n\n**Monty will be used to run untrusted, potentially malicious code.**\n\nMake sure there's no risk of this, either in the implementation, or in the public API that makes it more like that a developer using the pydantic_monty package might make such a mistake.\n\nPossible security risks to consider:\n* filesystem access\n* path traversal to access files the users did not intend to expose to the monty sandbox\n* memory errors - use of unsafe memory operations\n* excessive memory usage - evading monty's resource limits\n* infinite loops - evading monty's resource limits\n* network access - sockets, HTTP requests\n* subprocess/shell execution - os.system, subprocess, etc.\n* import system abuse - importing modules with side effects or accessing `__import__`\n* external function/callback misuse - callbacks run in host environment\n* deserialization attacks - loading untrusted serialized Monty/snapshot data\n* regex/string DoS - catastrophic backtracking or operations bypassing limits\n* information leakage via timing or error messages\n* Python/Javascript/Rust APIs that accidentally allow developers to expose their host to monty code\n\n## Filesystem Mounts (`crates/monty-fs/`)\n\nThe `MountTable` allows mounting real host directories into the sandbox at virtual paths,\nwith configurable access modes (ReadWrite, ReadOnly, OverlayMemory).\n\nMounts are HOST-side code: the `monty` interpreter crate performs no filesystem\nI/O and does not depend on `monty-fs`. Sandboxed code suspends with an\n`OsFunctionCall`, which a host holding a `MountTable` (the pool parent, the CLI,\nbindings) services via `MountTable::handle_os_call`.\n\n**CRITICAL SECURITY INVARIANT:** The monty runtime MUST NEVER read, write, or\nobtain any information about any file or directory outside the specific directory\nthat is mounted. This is enforced by:\n\n- A `cap_std::fs::Dir` descriptor opened once at mount time, which every\n  operation runs relative to — so `..`, symlinks and intermediate directories\n  swapped mid-operation cannot reach out\n- Virtual-space normalization that prevents `..` escape in the sandbox namespace\n- `Resolve` and `Absolute` returning virtual paths, never host paths\n- Null byte rejection in all paths\n\nPath confinement is **structural**, not a check: `Mount::dir` (in\n`crates/monty-fs/src/mount_table.rs`) is the boundary; `path_security.rs` is\nnow only path policy. The cost is that an absolute symlink target is never\nfollowed, even inside the mount (see `limitations/filesystem.md`) — do not\n\"fix\" that by comparing against the mount's host path, which restores the\ncheck-then-use this removes.\n\n**Changes to `mount_table.rs` or `path_security.rs` require careful security\nreview.** `heap.rs` and the mount boundary are the most security-critical\ncode in the codebase.\n\n## Subprocess isolation (`monty-proto`, `monty subprocess`, `monty-pool`)\n\nA monty process can never be made fully crash-proof against memory errors\n(stack overflow aborts, allocator aborts), so monty can run as isolated worker\nsubprocesses:\n\n- `crates/monty-proto` — the wire protocol: a protobuf schema\n  (`proto/monty/v1/monty.proto`), checked-in prost-generated code (regenerate\n  with `make generate-proto`; CI enforces sync via `make check-proto`),\n  4-byte LE length-prefixed framing, and fallible conversions between wire\n  types and `MontyException`/etc. Values are special-cased for performance:\n  the `monty.v1.MontyObject` message is mapped via prost `extern_path` onto\n  `WireObject` (`src/wire.rs`), a hand-written `prost::Message` impl that\n  encodes borrowed `MontyObject`s and validates *while* decoding — no mirror\n  struct, no deep clone on the hot path. `tests/differential.rs` proves it\n  byte-compatible against a fully prost-generated oracle (`tests/oracle/`,\n  regenerated and CI-checked together with the main codegen). Parents must\n  treat frames from a (possibly compromised) child as untrusted — wire\n  decoding and proto→Rust conversions validate everything and never panic.\n  `monty-proto` depends only on `monty-types` by default; its `worker` feature\n  (enabled by `monty-runtime`/`monty-wasm-runtime`) pulls in the full `monty`\n  interpreter for the child-side `worker` state machine.\n- `monty subprocess` (in `crates/monty-runtime/src/subprocess.rs`) — the child:\n  reads framed requests on stdin, writes framed events on stdout, serving one\n  REPL session per checkout. Strict alternation: one request in, zero or more\n  streamed `Print` events out, then exactly one turn-ending event.\n- `crates/monty-pool` — the parent: an async (tokio) elastic pool of workers\n  with crash detection/replacement and a hard per-turn timeout. Frame reads\n  are cancel-safe (partial-frame state lives in the worker, no pump task),\n  and turn deadlines are tokio timers rather than a watchdog thread.\n- `crates/monty-alloc` — the `#[global_allocator]` both workers run under: it\n  counts live bytes against soft and hard session limits (via\n  `Child::session_budget`, re-armed after every request). The interpreter reads\n  the soft limit at execution checkpoints; crossing the hard limit ends the\n  process rather than letting Rust abort. Its `exit-code` feature picks how:\n  `monty-runtime` enables it and exits with `OOM_EXIT_CODE` for the pool to\n  classify, `monty-wasm-runtime` leaves it off and traps, having no exit status\n  to offer. Only a binary or a wasm module may declare a global allocator, so\n  the crate provides the type and each declares its own. Direct interpreter use\n  must install and arm this allocator before configuring `max_memory`.\n- `pydantic_monty.Monty` / `pydantic_monty.AsyncMonty` — the ONLY Python\n  execution surface (there is no in-process Python API): sync and async pools\n  of workers (`with Monty() as pool: with pool.checkout() as session:\n  session.feed_run(...)`, and the `async with` / `await feed_run` equivalents).\n\nThe contract for crash detection: a child that exits or EOFs *without* a\n`FatalError` event crashed hard; the parent discards it and replaces it. See\n`limitations/pool-architecture.md` for host-API divergences from in-process execution.\n\n## Bytecode VM Architecture\n\nMonty is implemented as a bytecode VM, same as CPython.\n\n### Opcode space is scarce\n\nOpcodes serialize as a single byte, so the `Opcode` enum (`crates/monty/src/bytecode/op.rs`)\nis hard-capped at 256 variants and roughly half are already taken. Use slots sparingly:\nprefer a flags/operand encoding on one opcode (e.g. `Assert`/`FormatValue`) over a family\nof near-identical opcodes, unless the instruction is hot enough that decoding the\ndiscriminating operand would cost measurable dispatch time.\n\n### HeapReader API — Safe Heap Access\n\nAll heap-allocated Python objects (lists, dicts, strings, etc.) are stored in a paged arena (`Heap`). The `HeapReader` API provides **compile-time safe** access to heap data. This is the primary mechanism for reading and mutating heap objects throughout the codebase.\n\n**`heap.rs` is a critical safety boundary.** It contains `unsafe` code that underpins the soundness of the entire `HeapReader`/`HeapRead` system (pointer arithmetic, `UnsafeCell` access, reader-count invariants). Do NOT modify `heap.rs` without explicit user approval. Changes to this file require careful review of the safety invariants documented in the code comments.\n\n#### Core concepts\n\n- **`HeapReader<'a, T>`** — A scoped borrow of the heap that produces `HeapRead` handles. Created exclusively via `HeapReader::with`, which takes a `for<'a>` closure bound makes the lifetime `'a` universally quantified, so `HeapRead` pointers cannot escape the closure.\n- **`HeapRead<'a, T>`** — A typed handle to a specific heap entry. Created by `heap.read(id)` which returns a `HeapReadOutput<'a>` enum that you match on. Tracks a reader count that prevents the entry from being freed while the handle exists.\n- **`HeapReadOutput<'a>`** — Enum over all `HeapRead<'a, T>` variants (one per `HeapData` variant). Pattern match to get the typed handle.\n\n#### Reading and mutating heap data\n\n```rust\n// Scoped heap access.\n// The second argument allows for extra data to be\n// passed into the closure, will be rebranded as\n// `&'a mut ...` to match the `'a` lifetime of the\n// `HeapRead` handle, so the closure can have additional\n// context while still having the `for <'a>` safety guarantee.\nHeapReader::with(heap, &mut (), |heap, ()| {\n    let output = heap.read(some_id);  // returns HeapReadOutput<'a>\n    match output {\n        HeapReadOutput::List(list) => {\n            let items = list.get(heap);           // &List, borrows heap immutably\n            let items_mut = list.get_mut(heap);   // &mut List, borrows heap mutably\n        }\n        _ => { /* ... */ }\n    }\n})\n```\n\nKey borrowing rules:\n- `get(&self, &HeapReader)` → `&T` — immutable access, prevents heap mutation while reference lives\n- `get_mut(&mut self, &mut HeapReader)` → `&mut T` — mutable access, exclusive\n- Multiple `HeapRead` handles can coexist, but only one can be accessed via `get_mut` at a time\n- `dec_ref()` panics if any reader is active — prevents use-after-free\n\n#### Implementing type methods with HeapRead\n\nType methods are implemented as `impl<'h> HeapRead<'h, T>` blocks. The `PyTrait<'h>` trait provides the common interface:\n\n```rust\n// Methods on a heap type\nimpl<'h> HeapRead<'h, List> {\n    pub fn append(&mut self, vm: &mut VM<'h>, item: Value) -> RunResult<()> {\n        self.get_mut(vm.heap).items.push(item);\n        Ok(())\n    }\n}\n\n// PyTrait implementation\nimpl<'h> PyTrait<'h> for HeapRead<'h, List> {\n    fn py_type(&self, vm: &VM<'h>) -> Type { Type::List }\n    fn py_len(&self, vm: &VM<'h>) -> Option<usize> {\n        Some(self.get(vm.heap).items.len())\n    }\n    // ...\n}\n```\n\n### Reference Count Safety\n\nAll types that implement `DropWithContext<C>` hold heap (and possibly VM-side) references and **must** be cleaned up correctly on every code path — not just the happy path, but also early returns via `?`, `continue`, conditional branches, etc. A missed `drop_with` on any branch leaks reference counts.\n\n`DropWithContext<C>` is generic over the *cleanup context* `C` — whatever borrow the caller has on hand: a `Heap`, a `HeapReader`, the `VM`, or the json `Encoder`. The bound on each impl states the capability the value needs: heap-only values bound `C` by `ContainsHeap` (one impl then covers all four contexts), while values holding a `RecursionToken` (the container iterators) bound `C` by `ContainsVM` — satisfied only by `VM`/`Encoder`, since the recursion counter is unreachable through a bare heap. The same `drop_with` / `DropGuard` / `defer_drop!` machinery serves both. There are three mechanisms for ensuring cleanup, listed in order of preference:\n\n#### 1. `defer_drop!` macro (preferred)\n\nThe simplest and safest approach. Use `defer_drop!` (or `defer_drop_mut!` when mutable access to the value is needed) to bind a value into a guard that automatically drops it when scope exits — whether that's normal completion, early return via `?`, `continue`, or any other branch. The macro rebinds the value and heap variables as borrows from the guard, so you keep using them by name as before:\n\n```rust\nlet value = self.pop();\ndefer_drop!(value, heap);          // value is now &Value, heap is now &mut Heap\nlet result = value.py_repr(heap)?; // guard handles cleanup on all paths\n```\n\nBeyond safety, `defer_drop!` is often much more concise than inserting `drop_with` calls in every branch of complex control flow.\n\n`defer_drop!` gives you an immutable reference to the value. Use `defer_drop_mut!` when you need a mutable reference (e.g. iterators, values you may swap):\n\n```rust\nlet iter = vm.heap.get_iter(iter_ref);\ndefer_drop_mut!(iter, vm);\nwhile let Some(item) = iter.for_next(vm)? { ... }\n```\n\n**Limitation:** because the macro rebinds the context, it cannot be used inside `&mut self` methods on the VM where `self` owns the heap — first assign `let this = self;` and pass `this` instead.\n\n#### 2. `DropGuard` (when you need control over the value's fate)\n\nUse `DropGuard` directly when `defer_drop!` is too restrictive — specifically when you need to conditionally extract the value instead of dropping it. `DropGuard` provides `into_inner()` and `into_parts()` to reclaim ownership, while its `Drop` impl still guarantees cleanup on all other paths.\n\nDo not use `DropGuard` when the value is never moved back out of it. A guard used only through `as_parts()` or `as_parts_mut()` must be replaced with `defer_drop!` or `defer_drop_mut!`; explicit guards are reserved for code that later calls `into_inner()` or `into_parts()`. This keeps the ownership intent visible and avoids unnecessary guard bookkeeping:\n\n```rust\n// DropGuard needed here because on success we push lhs back onto the stack\n// instead of dropping it\nlet mut lhs_guard = DropGuard::new(self.pop(), self);\nlet (lhs, this) = lhs_guard.as_parts_mut();\n\nif lhs.py_iadd(rhs, this.heap)? {\n    let (lhs, this) = lhs_guard.into_parts(); // reclaim lhs, don't drop\n    this.push(lhs);\n    return Ok(());\n}\n// otherwise lhs_guard drops lhs automatically at scope exit\n```\n\n#### 3. Manual `drop_with` (for trivially simple cases)\n\nFor very simple cases with a single linear code path and no branching between acquiring and releasing the value, a direct `drop_with` call is acceptable as long as it produces more concise code than `defer_drop!`:\n\n```rust\nlet iter = self.pop();\niter.drop_with(self); // single path, no branching\n```\n\n`drop_with` should be used **only** when it is genuinely simpler than `defer_drop!` or `DropGuard`. The latter two are safer and more maintainable, especially in complex control flow. Multiple manual cleanup calls for the same owned value are a poor substitute for a guard.\n\n**Do not use `drop_with` if any of the following are true:**\n- The same value has `drop_with` called in multiple places (e.g. a loop with `continue` or `?` in the middle). This implies `defer_drop!` or `DropGuard` will be easier to read.\n- The explicit call to `drop_with` produces more lines of code than `defer_drop!` or `DropGuard` would. The latter often avoid rightward drift and make the cleanup logic easier.\n- The value is part of a container (e.g. `Vec<Value>`). Ideally the container itself implements `DropWithContext` and so `defer_drop` or `DropGuard` can be used on the whole container. Consider if a `DropWithContext` implementation for the container might be missing.\n\n### Resource-tracked string construction (`StringBuilder`)\n\nAny code that builds a `String` whose final size is not already bounded by an existing input **must** use `StringBuilder` (in `crates/monty/src/string_builder.rs`) rather than `String::with_capacity(...).push(...)`. A loop-built string can otherwise jump past both allocator limits before an execution checkpoint — this is exactly the class of bug that hit `str.expandtabs` (huge `tabsize` amplifying a single tab into a multi-gigabyte allocation).\n\n`StringBuilder` preflights capacity growth against allocator-backed usage. The in-progress buffer is itself visible to the allocator, so nested builders share the same real-byte budget. Growth is amortized via 2× doubling:\n\n```rust\n// Bounded size known up front (padding to a given width):\nlet mut builder = StringBuilder::with_capacity(width * fillchar.len_utf8(), &vm.heap.tracker)?;\nbuilder.push_str(s)?;\nfor _ in 0..pad { builder.push(fillchar)?; }\nbuilder.finish(vm.heap)\n\n// Size not bounded up front (e.g. attacker-controlled multiplier):\nlet mut builder = StringBuilder::new(&vm.heap.tracker);\nfor c in input.chars() { builder.push(c)?; }\nbuilder.finish(vm.heap)\n```\n\n`StringBuilder` also implements `fmt::Write`, so `write!(builder, ...)`, `format_args!`, and the existing `py_repr_fmt(f, ...)` machinery work against a tracker-protected buffer. `fmt::Error` is payload-free, so any `ResourceError` raised by a write is stashed on the builder and surfaced by `finish(heap)` — callers using `write!` don't need to thread the tracker error themselves.\n\nWhen the input *is* already bounded (e.g. `s.to_lowercase()`, slicing, `to_owned()` of an existing tracked string), passing a plain `String` / `&str` to `allocate_string` is fine — the result is bounded by a known multiple of an already-tracked input, so no amplification is possible.\n\n### Soft memory-limit checks — when and why\n\n`max_memory` is a **soft** limit: the VM polls allocator-backed usage every 255\ninstructions (`check_memory_time`), and everything pathological is caught by the hard\nlimits — the allocator's hard ceiling (soft + headroom, worker exits with\n`OOM_EXIT_CODE` and the pool replaces it) and the pool's turn timeout. Soft\nchecks exist ONLY to turn *common* overshoots into a graceful `MemoryError`\nthat keeps the session alive; they are not a safety boundary, so do not\nsprinkle them everywhere — every check is code noise and hot-path cost.\n\nAdd a check only where ordinary code commonly allocates a multi-MiB burst\ninside a single builtin call (i.e. before the next instruction checkpoint):\n\n- Known-size bulk allocation: one up-front `tracker.check_allocation(n * VALUE_SIZE)`\n  (container clone/copy, e.g. `clone_all_items`, `list_copy`) or\n  `check_repeat_size`-style estimate (`resource_checks.rs`).\n- Iterator collection: `collect_python_iterator` / `checked_preallocation_hint`\n  already handle it; for push-loops that bypass them, a one-shot size-hint\n  preflight (see `deque_extend`) — never a per-item poll.\n- Unbounded/amplifying string building: `StringBuilder` (above).\n\nDo NOT add per-iteration `check_time()` polls to Rust-side loops for memory's\nsake, and do NOT preflight results bounded by a constant multiple of an\nalready-tracked input (path joins, `*args` tuples, regex match lists, parsed\nJSON) — rare oversized cases there are the hard limit's job. Test each graceful\npath in `large_allocations_are_rejected_before_the_hard_limit`\n(`crates/monty-runtime/tests/subprocess.rs`) — the interpreter's own tests\nnever arm the allocator, so only subprocess tests exercise `max_memory`.\n\n## Dev Commands\n\n**IMPORTANT**: before running `cargo build` or `cargo run`, it is likely necessary to run `make install-py` to ensure that the Python virtual environment is available for build.\n\nInstead use the following `make` commands:\n\n```bash\nmake install-py           Install python dependencies\nmake install-js           Install JS package dependencies\nmake install              Install the package, dependencies, and pre-commit for local development\nmake dev-py               Install the python package for development\nmake build-js             Build the JS package (compile TypeScript)\nmake lint-js              Lint JS code with oxlint\nmake test-js              Test the JS package (builds the monty binary the workers run)\nmake dev-py-release       Install the python package for development with a release build\nmake build-wasm           Build the lean wasm worker module (requires the wasm32-wasip1 target)\nmake test-wasm            Test the wasm worker module from node, with no browser\nmake test-browser         Browser (Vitest) test of the wasm path in a real headless browser\nmake dev-py-pgo           Install the python package for development with profile-guided optimization\nmake format-rs            Format Rust code with fmt\nmake format-py            Format Python code - WARNING be careful about this command as it may modify code and break tests silently!\nmake format-js            Format JS code with prettier\nmake format               Format Rust code, this does not format Python code as we have to be careful with that\nmake lint-rs              Lint Rust code with clippy and import checks\nmake clippy-fix           Fix Rust code with clippy\nmake generate-proto       Regenerate monty-proto's checked-in code from the .proto schema\nmake check-proto          Verify monty-proto's checked-in code matches the .proto schema\nmake lint-py              Lint Python code with ruff\nmake lint                 Lint the code with ruff and clippy\nmake test-no-features     Run rust tests without any features enabled\nmake test-memory-model-checks Run rust tests with memory-model-checks enabled - THIS IS EXTREMELY SLOW, SHOULD MOSTLY BE RUN IN CI OR IF ABSOLUTELY NECESSARY\nmake test-ref-count-return Run rust tests with ref-count-return enabled\nmake test-cases           Run tests cases only\nmake test-type-checking   Run rust tests on monty-type-checking\nmake pytest               Run Python tests with pytest\nmake test-py              Build the python package (debug profile) and run tests\nmake test-docs            Test docs examples only\nmake test                 Run rust tests\nmake testcov              Run Rust tests with coverage, print table, and generate HTML report\nmake complete-tests       Fill in incomplete test expectations using CPython\nmake update-typeshed      Update vendored typeshed from upstream\nmake bench                Run benchmarks\nmake bench-pool           Run subprocess pool benchmarks (spawn, checkout, wire round-trips)\nmake dev-bench            Run benchmarks to test with dev profile\nmake profile              Profile the code with pprof and generate flamegraphs\nmake type-sizes           Write type sizes for the crate to ./type-sizes.txt (requires nightly and top-type-sizes)\nmake main                 run linting and the most important tests\nmake help                 Show this help (usage: make help)\n```\n\nUse the /python-playground skill to check cpython and monty behavior.\n\n## Releasing\n\nSee [RELEASING.md](RELEASING.md) for the release process.\n\n## Exception\n\nIt's important that exceptions raised/returned by this library match those raised by Python.\n\nWherever you see an Exception with a repeated message, create a dedicated method to create that exception `src/exceptions.rs`.\n\nWhen writing exception messages, always check `src/exceptions.rs` for existing methods to generate that message.\n\n## Argument extraction — ALWAYS use `#[derive(FromArgs)]`\n\n**Whenever you add or modify a Rust-side function, method, type\nconstructor, or `OsFunction` handler that takes anything beyond the\ntrivial 0/1/2-positional shapes already covered by\n`ArgValues::check_zero_args` / `get_one_arg` / `get_two_args` /\n`get_zero_one_arg` / `into_pos_only`, you MUST use\n`#[derive(FromArgs)]` (re-exported as `monty::args::FromArgs`).**\n\nHand-written `args.into_parts()` loops are not acceptable for any\nsignature that has multiple positionals with defaults, keyword\narguments, `*args`, or `**kwargs` — they are a known source of\nreference-count leaks, divergent error messages, and duplicated\nboilerplate. `FromArgs` emits a static param spec driven by the runtime\nbinder (`crates/monty/src/args/bind_native.rs`), which handles dispatch,\nconflict detection, default handling, and refcount cleanup mechanically.\nPick `style = def | clinic | c | c_named | unpack` by the CPython parser\nfamily the target function uses — see\n[`crates/monty-macros/README.md`](crates/monty-macros/README.md) for the\nfamily table and the full attribute surface (`style`, `at_most_total`,\n`bad_arg[_named]`, `pos_only`, `kw_only`, `varargs`, `varkwargs`,\n`default`, `static_string`, …) and how to extend the macro or add new\n`FromValue` impls.\n\nIf a callsite needs custom per-argument coercion (e.g. `value_to_float`\nfor math, a `TimeDelta` type check, a `bytes`-or-`str` union), declare\nthe field as `Value` and run the coercion in the function body *after*\nthe `from_args` call — the macro still handles the parsing, your code\njust adds the final validation step.\n\n## Code style\n\nAvoid local imports, unless there's a very good reason, all imports should be at the top of the file.\n\nAvoid `fn my_func<T: MyTrait>(..., param: T)` style function definitions, STRONGLY prefer `fn my_func(param: impl MyTrait)` syntax since changes are more localized. This includes in trait definitions and implementations.\n\nAlso avoid using functions and structs via a path like `std::borrow::Cow::Owned(...)`, instead import `Cow` globally with `use std::borrow::Cow;`.\n\nSTRONGLY prefer expression-oriented style: use `if`/`match` as expressions with a trailing (tail) expression rather than early `return` with a guard clause. E.g. prefer\n\n```rs\nif cond { a } else { b }\n```\n\nover\n\n```rs\nif cond {\n    return a;\n}\nb\n```\n\nThis applies to function bodies and block expressions alike. Only use early `return` when it genuinely simplifies control flow (e.g. several guard clauses at the top of a function).\n\nThis applies even more strongly to long `if cond { ... } else if cond2 { ... } ... else { ... }` chains — keep them as a single expression yielding a value, rather than scattering `return` statements through each branch.\n\nNEVER use `allow()` in rust lint markers, instead use `expect()` so any unnecessary markers are removed. E.g. use\n\n```rs\n#[expect(clippy::too_many_arguments)]\n```\n\nNOT!\n\n```rs\n#[allow(clippy::too_many_arguments)]\n```\n\n### Docstrings and comments.\n\nIMPORTANT: every struct, enum and function should have a concise docstring to\nexplain what it does and why; and any considerations or potential foot-guns of using that type.\n\nThe only exception is trait implementation methods where a docstring is not necessary if the method is self-explanatory.\n\nIt's important that docstrings cover the motivation and primary usage patterns of code, not just the simple \"what it does\".\n\nSimilarly, you should add comments to code, especially if the code is complex or esoteric.\n\nComments and field docstrings should almost never be more than 3 lines, mostly 1 line. Function and struct docstrings should be concise, generally <= 5 lines.\n\nOnly add examples to docstrings of public functions and structs, examples should be <=8 lines, if the example is more, remove it.\n\nIf you add example code to docstrings, it must be run in tests. NEVER add examples that are ignored.\n\nIf you encounter a comment or docstring that's out of date - you MUST update it to be correct.\n\nSimilarly, if you encounter code that has no docstrings or comments, or they are minimal, you should add more detail.\n\nAlways use single back-ticks in python docstrings - they should be markdown, not rst!\n\nNOTE: COMMENTS AND DOCSTRINGS ARE EXTREMELY IMPORTANT TO THE LONG TERM HEALTH OF THE PROJECT.\n\nNOTE: COMMENTS AND DOCSTRINGS SHOULD BE CONCISE - EXCESSIVELY VERBOSE DOCSTRINGS MAKE THE CODE HARDER TO READ AND MAINTAIN!\n\n## Tests\n\nDo **NOT** write tests within modules unless explicitly prompted to do so.\n\nTests should live in the relevant `tests/` directory.\n\nCommands:\n\n```bash\n# Build the project\ncargo build\n\n# Run tests\ncargo test -p monty\n\n# Run crates/monty/test_cases tests only\nmake test-cases\n\n# Run a specific test\ncargo test -p monty --test TEST str__ops\ncargo run -p monty-datatest str__ops\n\n# Run the interpreter on a Python file\ncargo run -- <file.py>\n```\n\nThe `memory-model-checks` feature (`make test-memory-model-checks`, or\n`--features memory-model-checks` on the commands above) is VERY SLOW — it is\nrun in CI, so do NOT enable it by default. Only reach for it when a change\nspecifically touches refcount/heap/GC behavior (e.g. new opcodes that retain\nvalues, `drop_with` paths, cycle collection) and then run just the relevant\ntest binary, e.g. `cargo test -p monty --test TEST --features memory-model-checks`.\n\nSee more test commands above.\n\n### Experimentation and Playground\n\nRead `Makefile` for other useful commands.\n\nYou can use the `./playground` directory (excluded from git, create with `mkdir -p playground`) to write files\nwhen you want to experiment by running a file with cpython or monty, e.g.:\n* `python3 playground/test.py` to run the file with cpython\n* `cargo run -- playground/test.py` to run the file with monty\n\nDO NOT use `/tmp` or pipe code to the interpreter, or use `python3 -c ...` as it requires extra permissions and can slow you down!\n\nMore details in the \"python-playground\" skill.\n\n### Test File Structure\n\nMost functionality should be tested via python files in the `crates/monty/test_cases` directory.\n\n**DO NOT create many small test files.** This would be unmaintainable.\n\nALWAYS consolidate related tests into single files using multiple `assert` statements. Follow `crates/monty/test_cases/fstring__all.py` as the gold standard pattern:\n\n```python\n# === Section name ===\n# brief comment if needed\nassert condition\nassert another_condition\n\n# === Next section ===\nx = setup_value\nassert x == expected\n```\n\nDo NOT add messages to `assert` statements — Monty's assert message annotations\n(see `limitations/assert.md`) already show the failing values, so a hand-written\nmessage is clutter. The ONE exception: tests whose failure would show nothing,\ni.e. `assert False` sentinels in try/except blocks (`assert False, 'expected\nTypeError'`) and tests that evaluate to a bare bool (`not` expressions, chained\ncomparisons, boolean ops) — there a message is required since introspection\nshows nothing.\n\nDo NOT Write tests like `assert 'thing' in msg` it's lazy and inexact unless explicitly told to do so, instead write tests like `assert msg == 'expected message'` to ensure clarity and accuracy and most importantly, to identify differences between Monty and CPython.\n\n### When to Create Separate Test Files\n\nOnly create a separate test file when you MUST use one of these special expectation formats:\n\n- `\"\"\"TRACEBACK:...\"\"\"` - Test expects an exception with full traceback (PREFERRED for error tests)\n- `# Raise=Exception('message')` - Test expects an exception without traceback verification - NOT RECOMMENDED, use `TRACEBACK` instead\n- `# ref-counts={...}` - Test checks reference counts (special mode)\n- you're writing tests for a different behavior or section of the language\n\nFor everything else, **add asserts to an existing test file** or create ONE consolidated file for the feature.\n\n### File Naming\n\nName files by feature, not by micro-variant:\n- ✅ `str__ops.py` - all string operations (add, iadd, len, etc.)\n- ✅ `list__methods.py` - all list method tests\n- ❌ `str__add_basic.py`, `str__add_empty.py`, `str__add_multiple.py` - TOO GRANULAR\n\n### Expectation Formats (use sparingly)\n\nOnly use these when `assert` won't work (on last line of file):\n- `# Return=value` - Check `repr()` output (prefer assert instead)\n- `# Return.str=value` - Check `str()` output (prefer assert instead)\n- `# Return.type=typename` - Check `type()` output (prefer assert instead)\n- `# Raise=Exception('message')` - Expect exception without traceback (REQUIRES separate file)\n- `\"\"\"TRACEBACK:...\"\"\"` - Expect exception with full traceback (PREFERRED over `# Raise=`)\n- `# ref-counts={...}` - Check reference counts (REQUIRES separate file)\n- No expectation comment - Assert-based test (PREFERRED)\n\nDo NOT use `# Return=` when you could use `assert` instead\n\n### Traceback Tests (Preferred for Errors)\n\nFor tests that expect exceptions, **prefer traceback tests over `# Raise=` or `try` / `except`** because they verify:\n- The full traceback with all stack frames\n- Correct line numbers for each frame\n- Function names in the traceback\n- The caret markers (`~`) pointing to the error location\n\nTraceback test format - add a triple-quoted string at the end of the file starting with `\\nTRACEBACK:`:\n```python\ndef foo():\n    raise ValueError('oops')\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"my_test.py\", line 4, in <module>\n    foo()\n    ~~~~~\n  File \"my_test.py\", line 2, in foo\n    raise ValueError('oops')\nValueError: oops\n\"\"\"\n```\n\nKey points:\n- The filename in the traceback should match the test file name (just the basename, not the full path)\n- Use `~` for caret markers (the test runner normalizes CPython's `^` to `~`)\n- The `<module>` frame name is used for top-level code\n- Tests run against both Monty and CPython, so the traceback must match both\n\nIf you don't care about the traceback or it intentionally differs from cpython (e.g. for `json`) and you want to test\nmultiple cases in the same file, use this style\n\n```py\ntry:\n    ...\n    assert False, 'expected <task> to fail'\nexcept <ErrorType> as exc:\n    assert str(exc) = '<expected exception message>'\n```\n\nIMPORTANT: don't just check that an exception is raised, you should always check the exception message.\n\nIMPORTANT: DON'T BE LAZY. If the exception differs between cpython and Monty, either fix the exception message, or\nstop and report the problem!\n\nOnly use `# Raise=` when you only care about the exception type/message and not the traceback and you can't use a try/except block.\n\n### Python fixture markers\n\nYou may mark python files with:\n* `# call-external` to support calling external functions\n* `# run-async` to support running async code\n\nNEVER MARK TESTS AS XFAIL UNDER ANY CIRCUMSTANCES!!! INSTEAD FIX THE BEHAVIOR SO THAT THE TEST PASSES.\n\nNever mark tests as:\n- `# xfail=cpython` - Test is required to fail on CPython\n- `# xfail=monty` - Test is required to fail on Monty\n\nNEVER MARK TESTS AS XFAIL UNDER ANY CIRCUMSTANCES!!! INSTEAD FIX THE BEHAVIOR SO THAT THE TEST PASSES.\n\nAll these markers must be at the start of comment lines to be recognized.\n\n### Other Notes\n\n- Prefer single quotes for strings in Python tests\n- Do NOT add `# noqa` or  `# pyright: ignore` comments to test code, instead add the failing code to `pyproject.toml`\n- The ONLY exception is `await` expressions outside of async functions, where you should add `# pyright: ignore`\n- Run `make lint-py` after adding tests\n- Use `make complete-tests` to fill in blank expectations\n- Regression tests run via `datatest-stable` harness in `crates/monty-datatest/src/main.rs`, use `make test-cases` to run them\n\n### Rust integration tests and `insta` snapshots\n\nIn `crates/*/tests/*.rs` (but **not** `crates/monty/test_cases/`), use [`insta`](https://insta.rs) `assert_snapshot!` for multi-line strings, serialized output, error messages otherwise fuzz-checked via `.contains(...)`, and any fixture currently compared via a hand-rolled `UPDATE_EXPECT` helper (use external snapshots under `tests/snapshots/`).\n\nKeep `assert_eq!` for scalars, enums, and structural values (`MontyObject`, `Vec`, etc.), and for principled membership checks like `vec.contains(...)`.\n\nWorkflow: write `assert_snapshot!(value, @\"\");`, then `cargo insta test --accept` to populate (plain `INSTA_UPDATE=always` does **not** update inline `@\"...\"` snapshots — you need the `cargo insta` subcommand, installed via `cargo install cargo-insta`). Add `insta = { workspace = true }` to `[dev-dependencies]` when introducing it to a new crate.\n\n## Python Package (`pydantic-monty`)\n\nThree PyPI distributions are built from this repo:\n\n- `pydantic-monty-client` (`crates/monty-python/`, Cargo package\n  `pydantic-monty-client`) — the PyO3 bindings, i.e. the `pydantic_monty`\n  module. It deliberately does **not** depend on the runtime, so it can be\n  installed where the `monty` binary comes from a base image or system package.\n- `pydantic-monty-runtime` (`crates/monty-runtime/`) — the `monty` worker binary.\n- `pydantic-monty` (`packages/pydantic-monty/`) — a hatchling metapackage with\n  no code, exactly pinning the other two. This is what users install. Its\n  version and both pins are rewritten from the Cargo workspace version by\n  `crates/monty-python/build.rs`; never edit them by hand.\n\nExecution always happens in `monty` worker subprocesses — there is no in-process execution API.\nThe surface is `Monty` (sync pool) and `AsyncMonty` (async pool), each with\n`pool.checkout(...)` sessions driven by `feed_run` (a coroutine on async sessions).\n\n### Structure\n\n- `crates/monty-python/src/` - Rust source for PyO3 bindings\n- `crates/monty-python/python/pydantic_monty/_monty.pyi` - Type stubs for the Python module\n- `crates/monty-python/tests/` - Python tests using pytest\n- `crates/monty-python/README.md` - the `pydantic-monty-client` readme (binary\n  resolution); the full user-facing docs live in `packages/pydantic-monty/README.md`\n\n### Building and Testing\n\nDependencies needed for python testing are installed in `crates/monty-python/pyproject.toml`.\nTo install these dependencies, use `uv sync --all-packages --only-dev`.\n\n```bash\n# Build the Python package for development (required before running tests)\nmake dev-py\n\n# Run Python tests\nmake test-py\n\n# Or run pytest directly (after dev-py)\nuv run pytest\n\n# Run a specific test file\nuv run pytest crates/monty-python/tests/test_basic.py\n\n# Run a specific test\nuv run pytest crates/monty-python/tests/test_basic.py::test_simple_expression\n```\n\n### Python Test Guidelines\n\nCheck and follow the style of other python tests.\n\nMake sure you put tests in the correct file.\n\n**DO NOT use python/pytest tests for `monty` core functionality!** When testing core functionality, add tests to `crates/monty/test_cases/` or `crates/monty/tests/`. Only use python/pytest tests for `pydantic_monty` functionality testing.\n\n**NEVER use class-based tests.** All tests should be simple functions.\n\nUse `@pytest.mark.parametrize` whenever testing multiple similar cases.\n\nUse `snapshot` from `inline-snapshot` for all test asserts.\n\nNEVER do the lazy `assert '...' in ...` instead always do `assert value == snapshot()`,\nthen run the test and inline-snapshot will fill in the missing value in the `snapshot()` call.\n\nUse `pytest.raises` for expected exceptions, like this\n\n```py\nwith pytest.raises(ValueError) as exc_info:\n    session.feed_run(code, print_callback=callback)\nassert exc_info.value.args[0] == snapshot('stopped at 3')\n```\n\n## Reference Counting\n\nHeap-allocated values (`Value::Ref`) use manual reference counting. Key rules:\n\n- **Cloning**: Use `clone_with_heap(heap)` which increments refcounts for `Ref` variants.\n- **Dropping**: Call `drop_with(ctx)` (the [`DropWithContext`] method) when discarding a `Value` that may be a `Ref`.\n\nContainer types (`List`, `Tuple`, `Dict`) also have `clone_with_heap()` methods.\n\n### Raw `HeapId` ownership\n\n`HeapId` does not encode whether a reference is owned or borrowed. Locally owned IDs should typically be wrapped in `Value::Ref` immediately so `defer_drop!` and `DropGuard` can manage cleanup; a local raw `HeapId` should otherwise be presumed borrowed.\n\nOwned `HeapId` fields remain the preferred representation where a structure needs the raw ID, such as `ListIterator::list`. Such fields must be documented as owned and cleaned up exactly once:\n\n- Heap-stored `HeapItem` implementations must push every owned ID from `py_dec_ref_ids`; this is preferred to calling `Heap::dec_ref` directly because destruction uses the heap's iterative cleanup stack.\n- Non-`HeapItem` owners should release owned IDs through their `DropWithContext` implementation, where a direct `dec_ref` is acceptable.\n- Direct `dec_ref` in ordinary control flow is discouraged. As with `drop_with`, never scatter cleanup for the same owned reference across branches; use an owning `Value` and a guard instead.\n\nRaw ownership is also acceptable when immediately transferred into a documented owned field or across an API whose contract explicitly transfers ownership.\n\n**Mutability of the heap parameter is asymmetric** — do not assume the two methods take the same kind of borrow:\n\n- `clone_with_heap` takes `&impl ContainsHeap` (immutable). The refcount field lives behind interior mutability, so `inc_ref` is `&self` on `Heap`. This means you can call `clone_with_heap` while other immutable borrows of the heap (e.g. a `HeapRead` handle obtained via `.get(heap)`) are still live.\n- `Heap::allocate` is also `&self` because entry storage is behind interior mutability. New heap entries can be created without a `&mut Heap`.\n- `drop_with` takes `&mut C` (the cleanup context — `Heap` / `HeapReader` / `VM` / `Encoder`), because dropping may free entries and run destructors, which mutates the heap.\n\nIf you find yourself fighting the borrow checker around `clone_with_heap` or `allocate`, the fix is almost never `&mut` — it is more likely that you are passing the wrong receiver (e.g. `vm` instead of `vm.heap`) or holding a `&mut` borrow elsewhere that should be `&`.\n\n### Cycle collection — Bacon–Rajan trial deletion\n\nReference counting alone cannot reclaim cycles. Monty uses **Bacon–Rajan trial deletion**\n(`Heap::collect_cycles` in `crates/monty/src/heap.rs`).\n\n**Resource limits**: When a memory or time limit is exceeded, execution terminates with a `ResourceError`. No guarantees are made about the state of the heap or reference counts after a resource limit is exceeded. The heap may contain orphaned objects with incorrect refcounts. This is acceptable because resource exhaustion is a terminal error - the execution context should be discarded.\n\n## JavaScript Package (`@pydantic/monty`, `crates/monty-js/`)\n\nThe JavaScript package is a **napi-rs binding over `monty-pool`** — the same\nRust pool/protocol engine `pydantic_monty` uses — wrapped by a thin\nTypeScript layer. The native binding exposes turn-level primitives\n(`NativePool`, `NativeSession.feed/resume*`); the TypeScript drive loop\nanswers suspension events (external functions, `os` callbacks, async\nfutures) where promises are native. Pool elasticity, turn deadlines, crash\nrecovery, framing and value conversion all live in Rust.\n\n### Structure\n\n- `crates/monty-js/src/` - Rust napi crate (native-only): `pool.rs`\n  (NativePool / NativeSession over `monty-pool`), `convert.rs`\n  (JS ↔ MontyObject), `exceptions.rs`, `limits.rs`\n- `crates/monty-js/ts/` - TypeScript wrapper: `pool.ts` (Monty),\n  `session.ts` (MontySession + drive loop), `errors.ts`, `binary.ts`\n  (monty binary resolution), `mount.ts`, `native.ts` (turn-object typings)\n- `crates/monty-js/ts/worker/` - the browser/wasm worker path (exported as\n  `@pydantic/monty/wasm`): `proto.ts`/`value.ts` (TS `monty-proto` codec),\n  `transport.ts` (WorkerTransport, the `NativeSession`-shaped seam),\n  `host.ts`/`channel.ts` (in-process and message-channel dispatch),\n  `pool.ts` (WorkerPool, the TS `monty-pool` analog), `nodeFactory.ts` /\n  `browserFactory.ts` (Worker backends), `index.ts` (`createWorkerPool`)\n- `index.js` / `index.d.ts` - napi-generated loader (created by\n  `npm run build:napi`; gitignored)\n- `crates/monty-js/npm/` - generated platform packages shipping the napi\n  `.node` library *and* the `monty` binary (`@pydantic/monty-<platform>`,\n  selected via optionalDependencies; `napi create-npm-dirs` +\n  `scripts/create-platform-packages.mjs`)\n- `crates/monty-js/__test__/` - Tests using vitest (`wasm_*.spec.ts` drive the\n  wasm worker pool/transport without the napi build, and need `make build-wasm`\n  first — `npm test` excludes them, `npm run test:wasm` runs them)\n\n### Current API\n\n```ts\nimport { Monty } from '@pydantic/monty'\n\nawait using pool = await Monty.create({ maxProcesses: 8, requestTimeout: 30 })\nawait using session = await pool.checkout({ typeCheck: false })\n\nawait session.feedRun('x = 21') // session state persists across feeds\nconst result = await session.feedRun('x * 2', {\n  inputs: { y: 1 },\n  externalLookup: { fetch: async (url: string) => '...' }, // sync or async\n  printCallback: (stream, text) => {},\n})\n```\n\nErrors: `MontyError` (base), `MontySyntaxError`, `MontyRuntimeError`,\n`MontyTypingError`, and `MontyCrashedError` (worker death; pool recovers).\n`MountDir` and the `os`/`NOT_HANDLED` callback work like the Python package.\n\nSee `crates/monty-js/README.md` for full API documentation.\n\n### Building and Testing\n\n```bash\nmake install-js   # npm install\nmake build-js     # napi debug build + compile TypeScript\nmake test-js      # builds the napi binding + debug monty binary, then runs vitest\nmake lint-js      # oxlint\nmake format-js    # prettier\nmake smoke-test-js  # packs + installs the package and platform binary package\n```\n\nTests run straight from `ts/` via `@oxc-node/core` against the locally built\n`.node`; the workers resolve the `monty` binary from the workspace\n`target/debug` build automatically.\n\n### JavaScript Test Guidelines\n\n- Tests use [vitest](https://vitest.dev) and live in `crates/monty-js/__test__/`\n- Tests are written in TypeScript; use the `setupPool` helper from `__test__/helpers.ts`\n- Follow the existing test style in the `__test__/` directory\n\n## WebAssembly build (`@pydantic/monty/wasm`)\n\nBrowsers (and anywhere subprocesses are impossible) run the sandbox in a **Web\nWorker** instead of a subprocess, exposed under the `/wasm` subpath. The same\npool → checkout → session → `feedRun` model and drive loop are used; only the\ntransport differs. The pieces:\n\n- `crates/monty-wasm-runtime` — a lean `wasm32-wasip1` module: a WASI reactor wrapping\n  the transport-agnostic `monty-worker` `Child` state machine, exporting one\n  `monty_dispatch_turn` (read a framed request from stdin, run one turn, write\n  framed events to stdout). No napi, no threads, no `SharedArrayBuffer`. It\n  declares the `monty-alloc` global allocator, so a session's `max_memory`\n  bounds what the module allocates too; exceeding it traps, which the host\n  already reads as a dead instance.\n- `crates/monty-js/ts/worker/` — the TS pool/transport that drives it\n  (`createWorkerPool`): a browser `Worker` backend (`browserFactory.ts`, whose\n  `Worker.terminate()` is the watchdog's hard kill), a Node `worker_threads`\n  backend (`nodeFactory.ts`), and an in-process degrade for environments with\n  no `Worker` (same API, but no crash isolation or preemption). Values cross as\n  `monty-proto` frames decoded in TypeScript (`proto.ts`/`value.ts`), not via\n  napi.\n\nBuild the worker module locally with `make build-wasm` (needs the\n`wasm32-wasip1` target); it is built and tested in CI. `make test-browser` runs\nthe whole suite against it in headless Chromium, and `make test-wasm` drives it\nfrom Node with no browser (`__test__/wasm_*.spec.ts`, run by their own\n`vitest.wasm.config.ts` — `npm test` excludes them, since it does not build the\nmodule).\n\n## Limitations documentation (`./limitations/`)\n\nEvery pull request that adds, changes, or removes user-visible behavior MUST\nland (or update) a markdown document under `./limitations/` describing how\nthe feature DIVERGES from CPython and what subset of the CPython surface\narea Monty actually implements. The directory is the single source of truth\nfor \"what does Monty *not* do that CPython does\" — module-level docstrings\nand inline comments are not sufficient on their own.\n\n**NOTE**: `./limitations/` SHOULD **ONLY** INCLUDE INFORMATION ABOUT BEHAVIOR DIVERGENCES FROM CPython, not points that describe behavior that matches CPython's behavior.\n\nOne file per feature, named after the builtin / module / construct it\ncovers (e.g. `limitations/open.md`, `limitations/asyncio.md`,\n`limitations/bytecode_interpretter.md`). Add new sections to an existing file when the feature\nis already documented; only create a new file when there is no fit.\n\nKeep entries concise but comprehensive — list every known divergence,\nincluding ones that \"feel obvious\". A divergence that is not written down\nis one that future readers (and future Claude) will assume does not exist.\nReviewers should reject PRs that change behavior without updating\n`./limitations/` if necessary.\n\nStructure each file around what a Python user would actually try:\n\n- Arguments/options that are rejected or ignored.\n- Methods/attributes that raise `AttributeError`.\n- Behaviour that differs from CPython even when the API exists.\n- Error types / messages that differ from CPython.\n\nAvoid implementation detail unless it explains a user-visible quirk.\n\n## NOTES\n\nALWAYS consider code quality when adding new code, if functions are getting too complex or code is duplicated, move relevant logic to a new file.\nMake sure functions are added in the most logical place, e.g. as methods on a struct where appropriate.\n\nThe code should follow the \"newspaper\" style where public and primary functions are at the top of the file, followed by private functions and utilities.\nALWAYS put utility, private functions and \"sub functions\" underneath the function they're used in.\n\nIt is important to the long term health of the project and maintainability of the codebase that code is well structured and organized, this is very important.\n\nALWAYS run `make format-rs` and `make lint-rs` after making changes to rust code and fix all suggestions to maintain code quality.\n\nALWAYS run `make lint-py` after making changes to python code and fix all suggestions to maintain code quality.\n\nALWAYS update this file when it is out of date.\n\nNEVER add imports anywhere except at the top of the file, this applies to both python and rust.\n\nNEVER write `unsafe` code, if you think you need to write unsafe code, explicitly ask the user or leave a `todo!()` with a suggestion and explanation.\n\nWhen you get asked a question like \"Is X really the best approach\" ANSWER THE QUESTION! don't try to make a chance based on a perceived instruction in the question!\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\nDON'T COMMIT UNLESS EXPLICITLY ASKED TO DO SO BY THE USER! Previous commit requests do not matter - don't commit unless you've just been explicitly asked to do so.\n\n## Project Overview\n\nMonty is a sandboxed Python interpreter written in Rust. It parses Python code using Ruff's `ruff_python_parser` but implements its own runtime execution model for safety and performance. This is a work-in-progress project that currently supports a subset of Python features.\n\nProject goals:\n\n- **Safety**: Execute untrusted Python code safely without FFI or C dependencies, instead sandbox will call back to host to run foreign/external functions.\n- **Performance**: Fast execution through compile-time optimizations and efficient memory layout\n- **Simplicity**: Clean, understandable implementation focused on a Python subset\n- **Snapshotting and iteration**: Plan is to allow code to be iteratively executed and snapshotted at each function call\n- **Cross-platform**: Runs on Linux, macOS, and Windows (and any other OS that can run Rust)\n- Targets the latest stable version of Python, currently Python 3.14\n\n## `monty-types` — shared boundary types\n\nThe public data types (`MontyObject`, `MontyException`/`ExcType`, `OsFunctionCall` +\nits arg structs, `ResourceLimits`/`ResourceTracker`, `PrintStream`/`PrintWriter`,\n`CompileOptions`, `ExtFunctionResult`, `FileMode`, ...) live in `crates/monty-types`,\nwhich depends on no other monty crate except the `monty-macros` derives. `monty`\ndepends on `monty-types` but does not blanket re-export it — only a few types\nare re-exported inline where they appear in `monty`'s public API (e.g.\n`run::CompileOptions`, `run_progress::{ExtFunctionResult, NameLookupResult}`).\nCode needing `MontyObject`, `MontyException`, `OsFunctionCall`, etc. must\ndepend on `monty-types` directly.\n\nHost-side crates (`monty-fs`, `monty-pool`, `monty-proto` without its `worker`\nfeature, `monty-python`, `monty-js`) MUST depend on `monty-types`, NOT `monty` —\nthis keeps the interpreter out of their binaries. Only the worker side\n(`monty-runtime`, `monty-wasm-runtime`, `monty-proto` with `worker`) links the\ninterpreter. Don't add a `monty` dependency to a host-side crate; if it needs a\ntype, that type belongs in `monty-types`.\n\nInterpreter-coupled methods on these types live in `monty` as `pub(crate)`\nextension traits (`ExcTypeExt`, `MontyObjectExt`, `MontyTypeExt`, `StackFrameExt`,\n`FileModeExt`, `BuiltinsFunctionsExt`, `ExtFunctionResultExt`) — import the trait\nto call e.g. `ExcType::type_error(...)` or `MontyObject::new(value, vm)`.\n\n## Cross-Platform Requirements\n\nMonty must work identically on Linux, macOS, and Windows. Within the Monty sandbox,\npaths always use POSIX/Linux-style forward slashes (`/`) regardless of the host OS.\nThe `MountTable` handles translating between virtual POSIX paths and host-native paths.\n\nKey rules:\n- **Virtual paths** are always POSIX-style (`/mnt/data/file.txt`), never Windows-style\n- **Host paths** use `std::path::Path`/`PathBuf` which handles OS differences automatically\n- Avoid `#[cfg(unix)]`-only code in the main crate — all features must work on all platforms\n- Tests in `crates/*/tests/` should be cross-platform; use helper functions for\n  OS-specific APIs like symlink creation (see `symlink_file`/`symlink_dir` in\n  `crates/monty-fs/tests/common/mod.rs`, shared via `mod common;` — each\n  `tests/*.rs` is its own crate, so helpers used by more than one belong there)\n- CI runs `cargo test -p monty --features memory-model-checks` and `cargo test -p monty-fs`\n  on Linux, macOS, and Windows\n\n## Important Security Notice\n\nIt's ABSOLUTELY CRITICAL that there's no way for code run in a Monty sandbox to access the host filesystem, or environment or to in any way \"escape the sandbox\".\n\n**Monty will be used to run untrusted, potentially malicious code.**\n\nMake sure there's no risk of this, either in the implementation, or in the public API that makes it more like that a developer using the pydantic_monty package might make such a mistake.\n\nPossible security risks to consider:\n* filesystem access\n* path traversal to access files the users did not intend to expose to the monty sandbox\n* memory errors - use of unsafe memory operations\n* excessive memory usage - evading monty's resource limits\n* infinite loops - evading monty's resource limits\n* network access - sockets, HTTP requests\n* subprocess/shell execution - os.system, subprocess, etc.\n* import system abuse - importing modules with side effects or accessing `__import__`\n* external function/callback misuse - callbacks run in host environment\n* deserialization attacks - loading untrusted serialized Monty/snapshot data\n* regex/string DoS - catastrophic backtracking or operations bypassing limits\n* information leakage via timing or error messages\n* Python/Javascript/Rust APIs that accidentally allow developers to expose their host to monty code\n\n## Filesystem Mounts (`crates/monty-fs/`)\n\nThe `MountTable` allows mounting real host directories into the sandbox at virtual paths,\nwith configurable access modes (ReadWrite, ReadOnly, OverlayMemory).\n\nMounts are HOST-side code: the `monty` interpreter crate performs no filesystem\nI/O and does not depend on `monty-fs`. Sandboxed code suspends with an\n`OsFunctionCall`, which a host holding a `MountTable` (the pool parent, the CLI,\nbindings) services via `MountTable::handle_os_call`.\n\n**CRITICAL SECURITY INVARIANT:** The monty runtime MUST NEVER read, write, or\nobtain any information about any file or directory outside the specific directory\nthat is mounted. This is enforced by:\n\n- A `cap_std::fs::Dir` descriptor opened once at mount time, which every\n  operation runs relative to — so `..`, symlinks and intermediate directories\n  swapped mid-operation cannot reach out\n- Virtual-space normalization that prevents `..` escape in the sandbox namespace\n- `Resolve` and `Absolute` returning virtual paths, never host paths\n- Null byte rejection in all paths\n\nPath confinement is **structural**, not a check: `Mount::dir` (in\n`crates/monty-fs/src/mount_table.rs`) is the boundary; `path_security.rs` is\nnow only path policy. The cost is that an absolute symlink target is never\nfollowed, even inside the mount (see `limitations/filesystem.md`) — do not\n\"fix\" that by comparing against the mount's host path, which restores the\ncheck-then-use this removes.\n\n**Changes to `mount_table.rs` or `path_security.rs` require careful security\nreview.** `heap.rs` and the mount boundary are the most security-critical\ncode in the codebase.\n\n## Subprocess isolation (`monty-proto`, `monty subprocess`, `monty-pool`)\n\nA monty process can never be made fully crash-proof against memory errors\n(stack overflow aborts, allocator aborts), so monty can run as isolated worker\nsubprocesses:\n\n- `crates/monty-proto` — the wire protocol: a protobuf schema\n  (`proto/monty/v1/monty.proto`), checked-in prost-generated code (regenerate\n  with `make generate-proto`; CI enforces sync via `make check-proto`),\n  4-byte LE length-prefixed framing, and fallible conversions between wire\n  types and `MontyException`/etc. Values are special-cased for performance:\n  the `monty.v1.MontyObject` message is mapped via prost `extern_path` onto\n  `WireObject` (`src/wire.rs`), a hand-written `prost::Message` impl that\n  encodes borrowed `MontyObject`s and validates *while* decoding — no mirror\n  struct, no deep clone on the hot path. `tests/differential.rs` proves it\n  byte-compatible against a fully prost-generated oracle (`tests/oracle/`,\n  regenerated and CI-checked together with the main codegen). Parents must\n  treat frames from a (possibly compromised) child as untrusted — wire\n  decoding and proto→Rust conversions validate everything and never panic.\n  `monty-proto` depends only on `monty-types` by default; its `worker` feature\n  (enabled by `monty-runtime`/`monty-wasm-runtime`) pulls in the full `monty`\n  interpreter for the child-side `worker` state machine.\n- `monty subprocess` (in `crates/monty-runtime/src/subprocess.rs`) — the child:\n  reads framed requests on stdin, writes framed events on stdout, serving one\n  REPL session per checkout. Strict alternation: one request in, zero or more\n  streamed `Print` events out, then exactly one turn-ending event.\n- `crates/monty-pool` — the parent: an async (tokio) elastic pool of workers\n  with crash detection/replacement and a hard per-turn timeout. Frame reads\n  are cancel-safe (partial-frame state lives in the worker, no pump task),\n  and turn deadlines are tokio timers rather than a watchdog thread.\n- `crates/monty-alloc` — the `#[global_allocator]` both workers run under: it\n  counts live bytes against soft and hard session limits (via\n  `Child::session_budget`, re-armed after every request). The interpreter reads\n  the soft limit at execution checkpoints; crossing the hard limit ends the\n  process rather than letting Rust abort. Its `exit-code` feature picks how:\n  `monty-runtime` enables it and exits with `OOM_EXIT_CODE` for the pool to\n  classify, `monty-wasm-runtime` leaves it off and traps, having no exit status\n  to offer. Only a binary or a wasm module may declare a global allocator, so\n  the crate provides the type and each declares its own. Direct interpreter use\n  must install and arm this allocator before configuring `max_memory`.\n- `pydantic_monty.Monty` / `pydantic_monty.AsyncMonty` — the ONLY Python\n  execution surface (there is no in-process Python API): sync and async pools\n  of workers (`with Monty() as pool: with pool.checkout() as session:\n  session.feed_run(...)`, and the `async with` / `await feed_run` equivalents).\n\nThe contract for crash detection: a child that exits or EOFs *without* a\n`FatalError` event crashed hard; the parent discards it and replaces it. See\n`limitations/pool-architecture.md` for host-API divergences from in-process execution.\n\n## Bytecode VM Architecture\n\nMonty is implemented as a bytecode VM, same as CPython.\n\n### Opcode space is scarce\n\nOpcodes serialize as a single byte, so the `Opcode` enum (`crates/monty/src/bytecode/op.rs`)\nis hard-capped at 256 variants and roughly half are already taken. Use slots sparingly:\nprefer a flags/operand encoding on one opcode (e.g. `Assert`/`FormatValue`) over a family\nof near-identical opcodes, unless the instruction is hot enough that decoding the\ndiscriminating operand would cost measurable dispatch time.\n\n### HeapReader API — Safe Heap Access\n\nAll heap-allocated Python objects (lists, dicts, strings, etc.) are stored in a paged arena (`Heap`). The `HeapReader` API provides **compile-time safe** access to heap data. This is the primary mechanism for reading and mutating heap objects throughout the codebase.\n\n**`heap.rs` is a critical safety boundary.** It contains `unsafe` code that underpins the soundness of the entire `HeapReader`/`HeapRead` system (pointer arithmetic, `UnsafeCell` access, reader-count invariants). Do NOT modify `heap.rs` without explicit user approval. Changes to this file require careful review of the safety invariants documented in the code comments.\n\n#### Core concepts\n\n- **`HeapReader<'a, T>`** — A scoped borrow of the heap that produces `HeapRead` handles. Created exclusively via `HeapReader::with`, which takes a `for<'a>` closure bound makes the lifetime `'a` universally quantified, so `HeapRead` pointers cannot escape the closure.\n- **`HeapRead<'a, T>`** — A typed handle to a specific heap entry. Created by `heap.read(id)` which returns a `HeapReadOutput<'a>` enum that you match on. Tracks a reader count that prevents the entry from being freed while the handle exists.\n- **`HeapReadOutput<'a>`** — Enum over all `HeapRead<'a, T>` variants (one per `HeapData` variant). Pattern match to get the typed handle.\n\n#### Reading and mutating heap data\n\n```rust\n// Scoped heap access.\n// The second argument allows for extra data to be\n// passed into the closure, will be rebranded as\n// `&'a mut ...` to match the `'a` lifetime of the\n// `HeapRead` handle, so the closure can have additional\n// context while still having the `for <'a>` safety guarantee.\nHeapReader::with(heap, &mut (), |heap, ()| {\n    let output = heap.read(some_id);  // returns HeapReadOutput<'a>\n    match output {\n        HeapReadOutput::List(list) => {\n            let items = list.get(heap);           // &List, borrows heap immutably\n            let items_mut = list.get_mut(heap);   // &mut List, borrows heap mutably\n        }\n        _ => { /* ... */ }\n    }\n})\n```\n\nKey borrowing rules:\n- `get(&self, &HeapReader)` → `&T` — immutable access, prevents heap mutation while reference lives\n- `get_mut(&mut self, &mut HeapReader)` → `&mut T` — mutable access, exclusive\n- Multiple `HeapRead` handles can coexist, but only one can be accessed via `get_mut` at a time\n- `dec_ref()` panics if any reader is active — prevents use-after-free\n\n#### Implementing type methods with HeapRead\n\nType methods are implemented as `impl<'h> HeapRead<'h, T>` blocks. The `PyTrait<'h>` trait provides the common interface:\n\n```rust\n// Methods on a heap type\nimpl<'h> HeapRead<'h, List> {\n    pub fn append(&mut self, vm: &mut VM<'h>, item: Value) -> RunResult<()> {\n        self.get_mut(vm.heap).items.push(item);\n        Ok(())\n    }\n}\n\n// PyTrait implementation\nimpl<'h> PyTrait<'h> for HeapRead<'h, List> {\n    fn py_type(&self, vm: &VM<'h>) -> Type { Type::List }\n    fn py_len(&self, vm: &VM<'h>) -> Option<usize> {\n        Some(self.get(vm.heap).items.len())\n    }\n    // ...\n}\n```\n\n### Reference Count Safety\n\nAll types that implement `DropWithContext<C>` hold heap (and possibly VM-side) references and **must** be cleaned up correctly on every code path — not just the happy path, but also early returns via `?`, `continue`, conditional branches, etc. A missed `drop_with` on any branch leaks reference counts.\n\n`DropWithContext<C>` is generic over the *cleanup context* `C` — whatever borrow the caller has on hand: a `Heap`, a `HeapReader`, the `VM`, or the json `Encoder`. The bound on each impl states the capability the value needs: heap-only values bound `C` by `ContainsHeap` (one impl then covers all four contexts), while values holding a `RecursionToken` (the container iterators) bound `C` by `ContainsVM` — satisfied only by `VM`/`Encoder`, since the recursion counter is unreachable through a bare heap. The same `drop_with` / `DropGuard` / `defer_drop!` machinery serves both. There are three mechanisms for ensuring cleanup, listed in order of preference:\n\n#### 1. `defer_drop!` macro (preferred)\n\nThe simplest and safest approach. Use `defer_drop!` (or `defer_drop_mut!` when mutable access to the value is needed) to bind a value into a guard that automatically drops it when scope exits — whether that's normal completion, early return via `?`, `continue`, or any other branch. The macro rebinds the value and heap variables as borrows from the guard, so you keep using them by name as before:\n\n```rust\nlet value = self.pop();\ndefer_drop!(value, heap);          // value is now &Value, heap is now &mut Heap\nlet result = value.py_repr(heap)?; // guard handles cleanup on all paths\n```\n\nBeyond safety, `defer_drop!` is often much more concise than inserting `drop_with` calls in every branch of complex control flow.\n\n`defer_drop!` gives you an immutable reference to the value. Use `defer_drop_mut!` when you need a mutable reference (e.g. iterators, values you may swap):\n\n```rust\nlet iter = vm.heap.get_iter(iter_ref);\ndefer_drop_mut!(iter, vm);\nwhile let Some(item) = iter.for_next(vm)? { ... }\n```\n\n**Limitation:** because the macro rebinds the context, it cannot be used inside `&mut self` methods on the VM where `self` owns the heap — first assign `let this = self;` and pass `this` instead.\n\n#### 2. `DropGuard` (when you need control over the value's fate)\n\nUse `DropGuard` directly when `defer_drop!` is too restrictive — specifically when you need to conditionally extract the value instead of dropping it. `DropGuard` provides `into_inner()` and `into_parts()` to reclaim ownership, while its `Drop` impl still guarantees cleanup on all other paths.\n\nDo not use `DropGuard` when the value is never moved back out of it. A guard used only through `as_parts()` or `as_parts_mut()` must be replaced with `defer_drop!` or `defer_drop_mut!`; explicit guards are reserved for code that later calls `into_inner()` or `into_parts()`. This keeps the ownership intent visible and avoids unnecessary guard bookkeeping:\n\n```rust\n// DropGuard needed here because on success we push lhs back onto the stack\n// instead of dropping it\nlet mut lhs_guard = DropGuard::new(self.pop(), self);\nlet (lhs, this) = lhs_guard.as_parts_mut();\n\nif lhs.py_iadd(rhs, this.heap)? {\n    let (lhs, this) = lhs_guard.into_parts(); // reclaim lhs, don't drop\n    this.push(lhs);\n    return Ok(());\n}\n// otherwise lhs_guard drops lhs automatically at scope exit\n```\n\n#### 3. Manual `drop_with` (for trivially simple cases)\n\nFor very simple cases with a single linear code path and no branching between acquiring and releasing the value, a direct `drop_with` call is acceptable as long as it produces more concise code than `defer_drop!`:\n\n```rust\nlet iter = self.pop();\niter.drop_with(self); // single path, no branching\n```\n\n`drop_with` should be used **only** when it is genuinely simpler than `defer_drop!` or `DropGuard`. The latter two are safer and more maintainable, especially in complex control flow. Multiple manual cleanup calls for the same owned value are a poor substitute for a guard.\n\n**Do not use `drop_with` if any of the following are true:**\n- The same value has `drop_with` called in multiple places (e.g. a loop with `continue` or `?` in the middle). This implies `defer_drop!` or `DropGuard` will be easier to read.\n- The explicit call to `drop_with` produces more lines of code than `defer_drop!` or `DropGuard` would. The latter often avoid rightward drift and make the cleanup logic easier.\n- The value is part of a container (e.g. `Vec<Value>`). Ideally the container itself implements `DropWithContext` and so `defer_drop` or `DropGuard` can be used on the whole container. Consider if a `DropWithContext` implementation for the container might be missing.\n\n### Resource-tracked string construction (`StringBuilder`)\n\nAny code that builds a `String` whose final size is not already bounded by an existing input **must** use `StringBuilder` (in `crates/monty/src/string_builder.rs`) rather than `String::with_capacity(...).push(...)`. A loop-built string can otherwise jump past both allocator limits before an execution checkpoint — this is exactly the class of bug that hit `str.expandtabs` (huge `tabsize` amplifying a single tab into a multi-gigabyte allocation).\n\n`StringBuilder` preflights capacity growth against allocator-backed usage. The in-progress buffer is itself visible to the allocator, so nested builders share the same real-byte budget. Growth is amortized via 2× doubling:\n\n```rust\n// Bounded size known up front (padding to a given width):\nlet mut builder = StringBuilder::with_capacity(width * fillchar.len_utf8(), &vm.heap.tracker)?;\nbuilder.push_str(s)?;\nfor _ in 0..pad { builder.push(fillchar)?; }\nbuilder.finish(vm.heap)\n\n// Size not bounded up front (e.g. attacker-controlled multiplier):\nlet mut builder = StringBuilder::new(&vm.heap.tracker);\nfor c in input.chars() { builder.push(c)?; }\nbuilder.finish(vm.heap)\n```\n\n`StringBuilder` also implements `fmt::Write`, so `write!(builder, ...)`, `format_args!`, and the existing `py_repr_fmt(f, ...)` machinery work against a tracker-protected buffer. `fmt::Error` is payload-free, so any `ResourceError` raised by a write is stashed on the builder and surfaced by `finish(heap)` — callers using `write!` don't need to thread the tracker error themselves.\n\nWhen the input *is* already bounded (e.g. `s.to_lowercase()`, slicing, `to_owned()` of an existing tracked string), passing a plain `String` / `&str` to `allocate_string` is fine — the result is bounded by a known multiple of an already-tracked input, so no amplification is possible.\n\n### Soft memory-limit checks — when and why\n\n`max_memory` is a **soft** limit: the VM polls allocator-backed usage every 255\ninstructions (`check_memory_time`), and everything pathological is caught by the hard\nlimits — the allocator's hard ceiling (soft + headroom, worker exits with\n`OOM_EXIT_CODE` and the pool replaces it) and the pool's turn timeout. Soft\nchecks exist ONLY to turn *common* overshoots into a graceful `MemoryError`\nthat keeps the session alive; they are not a safety boundary, so do not\nsprinkle them everywhere — every check is code noise and hot-path cost.\n\nAdd a check only where ordinary code commonly allocates a multi-MiB burst\ninside a single builtin call (i.e. before the next instruction checkpoint):\n\n- Known-size bulk allocation: one up-front `tracker.check_allocation(n * VALUE_SIZE)`\n  (container clone/copy, e.g. `clone_all_items`, `list_copy`) or\n  `check_repeat_size`-style estimate (`resource_checks.rs`).\n- Iterator collection: `collect_python_iterator` / `checked_preallocation_hint`\n  already handle it; for push-loops that bypass them, a one-shot size-hint\n  preflight (see `deque_extend`) — never a per-item poll.\n- Unbounded/amplifying string building: `StringBuilder` (above).\n\nDo NOT add per-iteration `check_time()` polls to Rust-side loops for memory's\nsake, and do NOT preflight results bounded by a constant multiple of an\nalready-tracked input (path joins, `*args` tuples, regex match lists, parsed\nJSON) — rare oversized cases there are the hard limit's job. Test each graceful\npath in `large_allocations_are_rejected_before_the_hard_limit`\n(`crates/monty-runtime/tests/subprocess.rs`) — the interpreter's own tests\nnever arm the allocator, so only subprocess tests exercise `max_memory`.\n\n## Dev Commands\n\n**IMPORTANT**: before running `cargo build` or `cargo run`, it is likely necessary to run `make install-py` to ensure that the Python virtual environment is available for build.\n\nInstead use the following `make` commands:\n\n```bash\nmake install-py           Install python dependencies\nmake install-js           Install JS package dependencies\nmake install              Install the package, dependencies, and pre-commit for local development\nmake dev-py               Install the python package for development\nmake build-js             Build the JS package (compile TypeScript)\nmake lint-js              Lint JS code with oxlint\nmake test-js              Test the JS package (builds the monty binary the workers run)\nmake dev-py-release       Install the python package for development with a release build\nmake build-wasm           Build the lean wasm worker module (requires the wasm32-wasip1 target)\nmake test-wasm            Test the wasm worker module from node, with no browser\nmake test-browser         Browser (Vitest) test of the wasm path in a real headless browser\nmake dev-py-pgo           Install the python package for development with profile-guided optimization\nmake format-rs            Format Rust code with fmt\nmake format-py            Format Python code - WARNING be careful about this command as it may modify code and break tests silently!\nmake format-js            Format JS code with prettier\nmake format               Format Rust code, this does not format Python code as we have to be careful with that\nmake lint-rs              Lint Rust code with clippy and import checks\nmake clippy-fix           Fix Rust code with clippy\nmake generate-proto       Regenerate monty-proto's checked-in code from the .proto schema\nmake check-proto          Verify monty-proto's checked-in code matches the .proto schema\nmake lint-py              Lint Python code with ruff\nmake lint                 Lint the code with ruff and clippy\nmake test-no-features     Run rust tests without any features enabled\nmake test-memory-model-checks Run rust tests with memory-model-checks enabled - THIS IS EXTREMELY SLOW, SHOULD MOSTLY BE RUN IN CI OR IF ABSOLUTELY NECESSARY\nmake test-ref-count-return Run rust tests with ref-count-return enabled\nmake test-cases           Run tests cases only\nmake test-type-checking   Run rust tests on monty-type-checking\nmake pytest               Run Python tests with pytest\nmake test-py              Build the python package (debug profile) and run tests\nmake test-docs            Test docs examples only\nmake test                 Run rust tests\nmake testcov              Run Rust tests with coverage, print table, and generate HTML report\nmake complete-tests       Fill in incomplete test expectations using CPython\nmake update-typeshed      Update vendored typeshed from upstream\nmake bench                Run benchmarks\nmake bench-pool           Run subprocess pool benchmarks (spawn, checkout, wire round-trips)\nmake dev-bench            Run benchmarks to test with dev profile\nmake profile              Profile the code with pprof and generate flamegraphs\nmake type-sizes           Write type sizes for the crate to ./type-sizes.txt (requires nightly and top-type-sizes)\nmake main                 run linting and the most important tests\nmake help                 Show this help (usage: make help)\n```\n\nUse the /python-playground skill to check cpython and monty behavior.\n\n## Releasing\n\nSee [RELEASING.md](RELEASING.md) for the release process.\n\n## Exception\n\nIt's important that exceptions raised/returned by this library match those raised by Python.\n\nWherever you see an Exception with a repeated message, create a dedicated method to create that exception `src/exceptions.rs`.\n\nWhen writing exception messages, always check `src/exceptions.rs` for existing methods to generate that message.\n\n## Argument extraction — ALWAYS use `#[derive(FromArgs)]`\n\n**Whenever you add or modify a Rust-side function, method, type\nconstructor, or `OsFunction` handler that takes anything beyond the\ntrivial 0/1/2-positional shapes already covered by\n`ArgValues::check_zero_args` / `get_one_arg` / `get_two_args` /\n`get_zero_one_arg` / `into_pos_only`, you MUST use\n`#[derive(FromArgs)]` (re-exported as `monty::args::FromArgs`).**\n\nHand-written `args.into_parts()` loops are not acceptable for any\nsignature that has multiple positionals with defaults, keyword\narguments, `*args`, or `**kwargs` — they are a known source of\nreference-count leaks, divergent error messages, and duplicated\nboilerplate. `FromArgs` emits a static param spec driven by the runtime\nbinder (`crates/monty/src/args/bind_native.rs`), which handles dispatch,\nconflict detection, default handling, and refcount cleanup mechanically.\nPick `style = def | clinic | c | c_named | unpack` by the CPython parser\nfamily the target function uses — see\n[`crates/monty-macros/README.md`](crates/monty-macros/README.md) for the\nfamily table and the full attribute surface (`style`, `at_most_total`,\n`bad_arg[_named]`, `pos_only`, `kw_only`, `varargs`, `varkwargs`,\n`default`, `static_string`, …) and how to extend the macro or add new\n`FromValue` impls.\n\nIf a callsite needs custom per-argument coercion (e.g. `value_to_float`\nfor math, a `TimeDelta` type check, a `bytes`-or-`str` union), declare\nthe field as `Value` and run the coercion in the function body *after*\nthe `from_args` call — the macro still handles the parsing, your code\njust adds the final validation step.\n\n## Code style\n\nAvoid local imports, unless there's a very good reason, all imports should be at the top of the file.\n\nAvoid `fn my_func<T: MyTrait>(..., param: T)` style function definitions, STRONGLY prefer `fn my_func(param: impl MyTrait)` syntax since changes are more localized. This includes in trait definitions and implementations.\n\nAlso avoid using functions and structs via a path like `std::borrow::Cow::Owned(...)`, instead import `Cow` globally with `use std::borrow::Cow;`.\n\nSTRONGLY prefer expression-oriented style: use `if`/`match` as expressions with a trailing (tail) expression rather than early `return` with a guard clause. E.g. prefer\n\n```rs\nif cond { a } else { b }\n```\n\nover\n\n```rs\nif cond {\n    return a;\n}\nb\n```\n\nThis applies to function bodies and block expressions alike. Only use early `return` when it genuinely simplifies control flow (e.g. several guard clauses at the top of a function).\n\nThis applies even more strongly to long `if cond { ... } else if cond2 { ... } ... else { ... }` chains — keep them as a single expression yielding a value, rather than scattering `return` statements through each branch.\n\nNEVER use `allow()` in rust lint markers, instead use `expect()` so any unnecessary markers are removed. E.g. use\n\n```rs\n#[expect(clippy::too_many_arguments)]\n```\n\nNOT!\n\n```rs\n#[allow(clippy::too_many_arguments)]\n```\n\n### Docstrings and comments.\n\nIMPORTANT: every struct, enum and function should have a concise docstring to\nexplain what it does and why; and any considerations or potential foot-guns of using that type.\n\nThe only exception is trait implementation methods where a docstring is not necessary if the method is self-explanatory.\n\nIt's important that docstrings cover the motivation and primary usage patterns of code, not just the simple \"what it does\".\n\nSimilarly, you should add comments to code, especially if the code is complex or esoteric.\n\nComments and field docstrings should almost never be more than 3 lines, mostly 1 line. Function and struct docstrings should be concise, generally <= 5 lines.\n\nOnly add examples to docstrings of public functions and structs, examples should be <=8 lines, if the example is more, remove it.\n\nIf you add example code to docstrings, it must be run in tests. NEVER add examples that are ignored.\n\nIf you encounter a comment or docstring that's out of date - you MUST update it to be correct.\n\nSimilarly, if you encounter code that has no docstrings or comments, or they are minimal, you should add more detail.\n\nAlways use single back-ticks in python docstrings - they should be markdown, not rst!\n\nNOTE: COMMENTS AND DOCSTRINGS ARE EXTREMELY IMPORTANT TO THE LONG TERM HEALTH OF THE PROJECT.\n\nNOTE: COMMENTS AND DOCSTRINGS SHOULD BE CONCISE - EXCESSIVELY VERBOSE DOCSTRINGS MAKE THE CODE HARDER TO READ AND MAINTAIN!\n\n## Tests\n\nDo **NOT** write tests within modules unless explicitly prompted to do so.\n\nTests should live in the relevant `tests/` directory.\n\nCommands:\n\n```bash\n# Build the project\ncargo build\n\n# Run tests\ncargo test -p monty\n\n# Run crates/monty/test_cases tests only\nmake test-cases\n\n# Run a specific test\ncargo test -p monty --test TEST str__ops\ncargo run -p monty-datatest str__ops\n\n# Run the interpreter on a Python file\ncargo run -- <file.py>\n```\n\nThe `memory-model-checks` feature (`make test-memory-model-checks`, or\n`--features memory-model-checks` on the commands above) is VERY SLOW — it is\nrun in CI, so do NOT enable it by default. Only reach for it when a change\nspecifically touches refcount/heap/GC behavior (e.g. new opcodes that retain\nvalues, `drop_with` paths, cycle collection) and then run just the relevant\ntest binary, e.g. `cargo test -p monty --test TEST --features memory-model-checks`.\n\nSee more test commands above.\n\n### Experimentation and Playground\n\nRead `Makefile` for other useful commands.\n\nYou can use the `./playground` directory (excluded from git, create with `mkdir -p playground`) to write files\nwhen you want to experiment by running a file with cpython or monty, e.g.:\n* `python3 playground/test.py` to run the file with cpython\n* `cargo run -- playground/test.py` to run the file with monty\n\nDO NOT use `/tmp` or pipe code to the interpreter, or use `python3 -c ...` as it requires extra permissions and can slow you down!\n\nMore details in the \"python-playground\" skill.\n\n### Test File Structure\n\nMost functionality should be tested via python files in the `crates/monty/test_cases` directory.\n\n**DO NOT create many small test files.** This would be unmaintainable.\n\nALWAYS consolidate related tests into single files using multiple `assert` statements. Follow `crates/monty/test_cases/fstring__all.py` as the gold standard pattern:\n\n```python\n# === Section name ===\n# brief comment if needed\nassert condition\nassert another_condition\n\n# === Next section ===\nx = setup_value\nassert x == expected\n```\n\nDo NOT add messages to `assert` statements — Monty's assert message annotations\n(see `limitations/assert.md`) already show the failing values, so a hand-written\nmessage is clutter. The ONE exception: tests whose failure would show nothing,\ni.e. `assert False` sentinels in try/except blocks (`assert False, 'expected\nTypeError'`) and tests that evaluate to a bare bool (`not` expressions, chained\ncomparisons, boolean ops) — there a message is required since introspection\nshows nothing.\n\nDo NOT Write tests like `assert 'thing' in msg` it's lazy and inexact unless explicitly told to do so, instead write tests like `assert msg == 'expected message'` to ensure clarity and accuracy and most importantly, to identify differences between Monty and CPython.\n\n### When to Create Separate Test Files\n\nOnly create a separate test file when you MUST use one of these special expectation formats:\n\n- `\"\"\"TRACEBACK:...\"\"\"` - Test expects an exception with full traceback (PREFERRED for error tests)\n- `# Raise=Exception('message')` - Test expects an exception without traceback verification - NOT RECOMMENDED, use `TRACEBACK` instead\n- `# ref-counts={...}` - Test checks reference counts (special mode)\n- you're writing tests for a different behavior or section of the language\n\nFor everything else, **add asserts to an existing test file** or create ONE consolidated file for the feature.\n\n### File Naming\n\nName files by feature, not by micro-variant:\n- ✅ `str__ops.py` - all string operations (add, iadd, len, etc.)\n- ✅ `list__methods.py` - all list method tests\n- ❌ `str__add_basic.py`, `str__add_empty.py`, `str__add_multiple.py` - TOO GRANULAR\n\n### Expectation Formats (use sparingly)\n\nOnly use these when `assert` won't work (on last line of file):\n- `# Return=value` - Check `repr()` output (prefer assert instead)\n- `# Return.str=value` - Check `str()` output (prefer assert instead)\n- `# Return.type=typename` - Check `type()` output (prefer assert instead)\n- `# Raise=Exception('message')` - Expect exception without traceback (REQUIRES separate file)\n- `\"\"\"TRACEBACK:...\"\"\"` - Expect exception with full traceback (PREFERRED over `# Raise=`)\n- `# ref-counts={...}` - Check reference counts (REQUIRES separate file)\n- No expectation comment - Assert-based test (PREFERRED)\n\nDo NOT use `# Return=` when you could use `assert` instead\n\n### Traceback Tests (Preferred for Errors)\n\nFor tests that expect exceptions, **prefer traceback tests over `# Raise=` or `try` / `except`** because they verify:\n- The full traceback with all stack frames\n- Correct line numbers for each frame\n- Function names in the traceback\n- The caret markers (`~`) pointing to the error location\n\nTraceback test format - add a triple-quoted string at the end of the file starting with `\\nTRACEBACK:`:\n```python\ndef foo():\n    raise ValueError('oops')\n\nfoo()\n\"\"\"\nTRACEBACK:\nTraceback (most recent call last):\n  File \"my_test.py\", line 4, in <module>\n    foo()\n    ~~~~~\n  File \"my_test.py\", line 2, in foo\n    raise ValueError('oops')\nValueError: oops\n\"\"\"\n```\n\nKey points:\n- The filename in the traceback should match the test file name (just the basename, not the full path)\n- Use `~` for caret markers (the test runner normalizes CPython's `^` to `~`)\n- The `<module>` frame name is used for top-level code\n- Tests run against both Monty and CPython, so the traceback must match both\n\nIf you don't care about the traceback or it intentionally differs from cpython (e.g. for `json`) and you want to test\nmultiple cases in the same file, use this style\n\n```py\ntry:\n    ...\n    assert False, 'expected <task> to fail'\nexcept <ErrorType> as exc:\n    assert str(exc) = '<expected exception message>'\n```\n\nIMPORTANT: don't just check that an exception is raised, you should always check the exception message.\n\nIMPORTANT: DON'T BE LAZY. If the exception differs between cpython and Monty, either fix the exception message, or\nstop and report the problem!\n\nOnly use `# Raise=` when you only care about the exception type/message and not the traceback and you can't use a try/except block.\n\n### Python fixture markers\n\nYou may mark python files with:\n* `# call-external` to support calling external functions\n* `# run-async` to support running async code\n\nNEVER MARK TESTS AS XFAIL UNDER ANY CIRCUMSTANCES!!! INSTEAD FIX THE BEHAVIOR SO THAT THE TEST PASSES.\n\nNever mark tests as:\n- `# xfail=cpython` - Test is required to fail on CPython\n- `# xfail=monty` - Test is required to fail on Monty\n\nNEVER MARK TESTS AS XFAIL UNDER ANY CIRCUMSTANCES!!! INSTEAD FIX THE BEHAVIOR SO THAT THE TEST PASSES.\n\nAll these markers must be at the start of comment lines to be recognized.\n\n### Other Notes\n\n- Prefer single quotes for strings in Python tests\n- Do NOT add `# noqa` or  `# pyright: ignore` comments to test code, instead add the failing code to `pyproject.toml`\n- The ONLY exception is `await` expressions outside of async functions, where you should add `# pyright: ignore`\n- Run `make lint-py` after adding tests\n- Use `make complete-tests` to fill in blank expectations\n- Regression tests run via `datatest-stable` harness in `crates/monty-datatest/src/main.rs`, use `make test-cases` to run them\n\n### Rust integration tests and `insta` snapshots\n\nIn `crates/*/tests/*.rs` (but **not** `crates/monty/test_cases/`), use [`insta`](https://insta.rs) `assert_snapshot!` for multi-line strings, serialized output, error messages otherwise fuzz-checked via `.contains(...)`, and any fixture currently compared via a hand-rolled `UPDATE_EXPECT` helper (use external snapshots under `tests/snapshots/`).\n\nKeep `assert_eq!` for scalars, enums, and structural values (`MontyObject`, `Vec`, etc.), and for principled membership checks like `vec.contains(...)`.\n\nWorkflow: write `assert_snapshot!(value, @\"\");`, then `cargo insta test --accept` to populate (plain `INSTA_UPDATE=always` does **not** update inline `@\"...\"` snapshots — you need the `cargo insta` subcommand, installed via `cargo install cargo-insta`). Add `insta = { workspace = true }` to `[dev-dependencies]` when introducing it to a new crate.\n\n## Python Package (`pydantic-monty`)\n\nThree PyPI distributions are built from this repo:\n\n- `pydantic-monty-client` (`crates/monty-python/`, Cargo package\n  `pydantic-monty-client`) — the PyO3 bindings, i.e. the `pydantic_monty`\n  module. It deliberately does **not** depend on the runtime, so it can be\n  installed where the `monty` binary comes from a base image or system package.\n- `pydantic-monty-runtime` (`crates/monty-runtime/`) — the `monty` worker binary.\n- `pydantic-monty` (`packages/pydantic-monty/`) — a hatchling metapackage with\n  no code, exactly pinning the other two. This is what users install. Its\n  version and both pins are rewritten from the Cargo workspace version by\n  `crates/monty-python/build.rs`; never edit them by hand.\n\nExecution always happens in `monty` worker subprocesses — there is no in-process execution API.\nThe surface is `Monty` (sync pool) and `AsyncMonty` (async pool), each with\n`pool.checkout(...)` sessions driven by `feed_run` (a coroutine on async sessions).\n\n### Structure\n\n- `crates/monty-python/src/` - Rust source for PyO3 bindings\n- `crates/monty-python/python/pydantic_monty/_monty.pyi` - Type stubs for the Python module\n- `crates/monty-python/tests/` - Python tests using pytest\n- `crates/monty-python/README.md` - the `pydantic-monty-client` readme (binary\n  resolution); the full user-facing docs live in `packages/pydantic-monty/README.md`\n\n### Building and Testing\n\nDependencies needed for python testing are installed in `crates/monty-python/pyproject.toml`.\nTo install these dependencies, use `uv sync --all-packages --only-dev`.\n\n```bash\n# Build the Python package for development (required before running tests)\nmake dev-py\n\n# Run Python tests\nmake test-py\n\n# Or run pytest directly (after dev-py)\nuv run pytest\n\n# Run a specific test file\nuv run pytest crates/monty-python/tests/test_basic.py\n\n# Run a specific test\nuv run pytest crates/monty-python/tests/test_basic.py::test_simple_expression\n```\n\n### Python Test Guidelines\n\nCheck and follow the style of other python tests.\n\nMake sure you put tests in the correct file.\n\n**DO NOT use python/pytest tests for `monty` core functionality!** When testing core functionality, add tests to `crates/monty/test_cases/` or `crates/monty/tests/`. Only use python/pytest tests for `pydantic_monty` functionality testing.\n\n**NEVER use class-based tests.** All tests should be simple functions.\n\nUse `@pytest.mark.parametrize` whenever testing multiple similar cases.\n\nUse `snapshot` from `inline-snapshot` for all test asserts.\n\nNEVER do the lazy `assert '...' in ...` instead always do `assert value == snapshot()`,\nthen run the test and inline-snapshot will fill in the missing value in the `snapshot()` call.\n\nUse `pytest.raises` for expected exceptions, like this\n\n```py\nwith pytest.raises(ValueError) as exc_info:\n    session.feed_run(code, print_callback=callback)\nassert exc_info.value.args[0] == snapshot('stopped at 3')\n```\n\n## Reference Counting\n\nHeap-allocated values (`Value::Ref`) use manual reference counting. Key rules:\n\n- **Cloning**: Use `clone_with_heap(heap)` which increments refcounts for `Ref` variants.\n- **Dropping**: Call `drop_with(ctx)` (the [`DropWithContext`] method) when discarding a `Value` that may be a `Ref`.\n\nContainer types (`List`, `Tuple`, `Dict`) also have `clone_with_heap()` methods.\n\n### Raw `HeapId` ownership\n\n`HeapId` does not encode whether a reference is owned or borrowed. Locally owned IDs should typically be wrapped in `Value::Ref` immediately so `defer_drop!` and `DropGuard` can manage cleanup; a local raw `HeapId` should otherwise be presumed borrowed.\n\nOwned `HeapId` fields remain the preferred representation where a structure needs the raw ID, such as `ListIterator::list`. Such fields must be documented as owned and cleaned up exactly once:\n\n- Heap-stored `HeapItem` implementations must push every owned ID from `py_dec_ref_ids`; this is preferred to calling `Heap::dec_ref` directly because destruction uses the heap's iterative cleanup stack.\n- Non-`HeapItem` owners should release owned IDs through their `DropWithContext` implementation, where a direct `dec_ref` is acceptable.\n- Direct `dec_ref` in ordinary control flow is discouraged. As with `drop_with`, never scatter cleanup for the same owned reference across branches; use an owning `Value` and a guard instead.\n\nRaw ownership is also acceptable when immediately transferred into a documented owned field or across an API whose contract explicitly transfers ownership.\n\n**Mutability of the heap parameter is asymmetric** — do not assume the two methods take the same kind of borrow:\n\n- `clone_with_heap` takes `&impl ContainsHeap` (immutable). The refcount field lives behind interior mutability, so `inc_ref` is `&self` on `Heap`. This means you can call `clone_with_heap` while other immutable borrows of the heap (e.g. a `HeapRead` handle obtained via `.get(heap)`) are still live.\n- `Heap::allocate` is also `&self` because entry storage is behind interior mutability. New heap entries can be created without a `&mut Heap`.\n- `drop_with` takes `&mut C` (the cleanup context — `Heap` / `HeapReader` / `VM` / `Encoder`), because dropping may free entries and run destructors, which mutates the heap.\n\nIf you find yourself fighting the borrow checker around `clone_with_heap` or `allocate`, the fix is almost never `&mut` — it is more likely that you are passing the wrong receiver (e.g. `vm` instead of `vm.heap`) or holding a `&mut` borrow elsewhere that should be `&`.\n\n### Cycle collection — Bacon–Rajan trial deletion\n\nReference counting alone cannot reclaim cycles. Monty uses **Bacon–Rajan trial deletion**\n(`Heap::collect_cycles` in `crates/monty/src/heap.rs`).\n\n**Resource limits**: When a memory or time limit is exceeded, execution terminates with a `ResourceError`. No guarantees are made about the state of the heap or reference counts after a resource limit is exceeded. The heap may contain orphaned objects with incorrect refcounts. This is acceptable because resource exhaustion is a terminal error - the execution context should be discarded.\n\n## JavaScript Package (`@pydantic/monty`, `crates/monty-js/`)\n\nThe JavaScript package is a **napi-rs binding over `monty-pool`** — the same\nRust pool/protocol engine `pydantic_monty` uses — wrapped by a thin\nTypeScript layer. The native binding exposes turn-level primitives\n(`NativePool`, `NativeSession.feed/resume*`); the TypeScript drive loop\nanswers suspension events (external functions, `os` callbacks, async\nfutures) where promises are native. Pool elasticity, turn deadlines, crash\nrecovery, framing and value conversion all live in Rust.\n\n### Structure\n\n- `crates/monty-js/src/` - Rust napi crate (native-only): `pool.rs`\n  (NativePool / NativeSession over `monty-pool`), `convert.rs`\n  (JS ↔ MontyObject), `exceptions.rs`, `limits.rs`\n- `crates/monty-js/ts/` - TypeScript wrapper: `pool.ts` (Monty),\n  `session.ts` (MontySession + drive loop), `errors.ts`, `binary.ts`\n  (monty binary resolution), `mount.ts`, `native.ts` (turn-object typings)\n- `crates/monty-js/ts/worker/` - the browser/wasm worker path (exported as\n  `@pydantic/monty/wasm`): `proto.ts`/`value.ts` (TS `monty-proto` codec),\n  `transport.ts` (WorkerTransport, the `NativeSession`-shaped seam),\n  `host.ts`/`channel.ts` (in-process and message-channel dispatch),\n  `pool.ts` (WorkerPool, the TS `monty-pool` analog), `nodeFactory.ts` /\n  `browserFactory.ts` (Worker backends), `index.ts` (`createWorkerPool`)\n- `index.js` / `index.d.ts` - napi-generated loader (created by\n  `npm run build:napi`; gitignored)\n- `crates/monty-js/npm/` - generated platform packages shipping the napi\n  `.node` library *and* the `monty` binary (`@pydantic/monty-<platform>`,\n  selected via optionalDependencies; `napi create-npm-dirs` +\n  `scripts/create-platform-packages.mjs`)\n- `crates/monty-js/__test__/` - Tests using vitest (`wasm_*.spec.ts` drive the\n  wasm worker pool/transport without the napi build, and need `make build-wasm`\n  first — `npm test` excludes them, `npm run test:wasm` runs them)\n\n### Current API\n\n```ts\nimport { Monty } from '@pydantic/monty'\n\nawait using pool = await Monty.create({ maxProcesses: 8, requestTimeout: 30 })\nawait using session = await pool.checkout({ typeCheck: false })\n\nawait session.feedRun('x = 21') // session state persists across feeds\nconst result = await session.feedRun('x * 2', {\n  inputs: { y: 1 },\n  externalLookup: { fetch: async (url: string) => '...' }, // sync or async\n  printCallback: (stream, text) => {},\n})\n```\n\nErrors: `MontyError` (base), `MontySyntaxError`, `MontyRuntimeError`,\n`MontyTypingError`, and `MontyCrashedError` (worker death; pool recovers).\n`MountDir` and the `os`/`NOT_HANDLED` callback work like the Python package.\n\nSee `crates/monty-js/README.md` for full API documentation.\n\n### Building and Testing\n\n```bash\nmake install-js   # npm install\nmake build-js     # napi debug build + compile TypeScript\nmake test-js      # builds the napi binding + debug monty binary, then runs vitest\nmake lint-js      # oxlint\nmake format-js    # prettier\nmake smoke-test-js  # packs + installs the package and platform binary package\n```\n\nTests run straight from `ts/` via `@oxc-node/core` against the locally built\n`.node`; the workers resolve the `monty` binary from the workspace\n`target/debug` build automatically.\n\n### JavaScript Test Guidelines\n\n- Tests use [vitest](https://vitest.dev) and live in `crates/monty-js/__test__/`\n- Tests are written in TypeScript; use the `setupPool` helper from `__test__/helpers.ts`\n- Follow the existing test style in the `__test__/` directory\n\n## WebAssembly build (`@pydantic/monty/wasm`)\n\nBrowsers (and anywhere subprocesses are impossible) run the sandbox in a **Web\nWorker** instead of a subprocess, exposed under the `/wasm` subpath. The same\npool → checkout → session → `feedRun` model and drive loop are used; only the\ntransport differs. The pieces:\n\n- `crates/monty-wasm-runtime` — a lean `wasm32-wasip1` module: a WASI reactor wrapping\n  the transport-agnostic `monty-worker` `Child` state machine, exporting one\n  `monty_dispatch_turn` (read a framed request from stdin, run one turn, write\n  framed events to stdout). No napi, no threads, no `SharedArrayBuffer`. It\n  declares the `monty-alloc` global allocator, so a session's `max_memory`\n  bounds what the module allocates too; exceeding it traps, which the host\n  already reads as a dead instance.\n- `crates/monty-js/ts/worker/` — the TS pool/transport that drives it\n  (`createWorkerPool`): a browser `Worker` backend (`browserFactory.ts`, whose\n  `Worker.terminate()` is the watchdog's hard kill), a Node `worker_threads`\n  backend (`nodeFactory.ts`), and an in-process degrade for environments with\n  no `Worker` (same API, but no crash isolation or preemption). Values cross as\n  `monty-proto` frames decoded in TypeScript (`proto.ts`/`value.ts`), not via\n  napi.\n\nBuild the worker module locally with `make build-wasm` (needs the\n`wasm32-wasip1` target); it is built and tested in CI. `make test-browser` runs\nthe whole suite against it in headless Chromium, and `make test-wasm` drives it\nfrom Node with no browser (`__test__/wasm_*.spec.ts`, run by their own\n`vitest.wasm.config.ts` — `npm test` excludes them, since it does not build the\nmodule).\n\n## Limitations documentation (`./limitations/`)\n\nEvery pull request that adds, changes, or removes user-visible behavior MUST\nland (or update) a markdown document under `./limitations/` describing how\nthe feature DIVERGES from CPython and what subset of the CPython surface\narea Monty actually implements. The directory is the single source of truth\nfor \"what does Monty *not* do that CPython does\" — module-level docstrings\nand inline comments are not sufficient on their own.\n\n**NOTE**: `./limitations/` SHOULD **ONLY** INCLUDE INFORMATION ABOUT BEHAVIOR DIVERGENCES FROM CPython, not points that describe behavior that matches CPython's behavior.\n\nOne file per feature, named after the builtin / module / construct it\ncovers (e.g. `limitations/open.md`, `limitations/asyncio.md`,\n`limitations/bytecode_interpretter.md`). Add new sections to an existing file when the feature\nis already documented; only create a new file when there is no fit.\n\nKeep entries concise but comprehensive — list every known divergence,\nincluding ones that \"feel obvious\". A divergence that is not written down\nis one that future readers (and future Claude) will assume does not exist.\nReviewers should reject PRs that change behavior without updating\n`./limitations/` if necessary.\n\nStructure each file around what a Python user would actually try:\n\n- Arguments/options that are rejected or ignored.\n- Methods/attributes that raise `AttributeError`.\n- Behaviour that differs from CPython even when the API exists.\n- Error types / messages that differ from CPython.\n\nAvoid implementation detail unless it explains a user-visible quirk.\n\n## NOTES\n\nALWAYS consider code quality when adding new code, if functions are getting too complex or code is duplicated, move relevant logic to a new file.\nMake sure functions are added in the most logical place, e.g. as methods on a struct where appropriate.\n\nThe code should follow the \"newspaper\" style where public and primary functions are at the top of the file, followed by private functions and utilities.\nALWAYS put utility, private functions and \"sub functions\" underneath the function they're used in.\n\nIt is important to the long term health of the project and maintainability of the codebase that code is well structured and organized, this is very important.\n\nALWAYS run `make format-rs` and `make lint-rs` after making changes to rust code and fix all suggestions to maintain code quality.\n\nALWAYS run `make lint-py` after making changes to python code and fix all suggestions to maintain code quality.\n\nALWAYS update this file when it is out of date.\n\nNEVER add imports anywhere except at the top of the file, this applies to both python and rust.\n\nNEVER write `unsafe` code, if you think you need to write unsafe code, explicitly ask the user or leave a `todo!()` with a suggestion and explanation.\n\nWhen you get asked a question like \"Is X really the best approach\" ANSWER THE QUESTION! don't try to make a chance based on a perceived instruction in the question!\n","category":"root","tokens":13145}]}