Skip to main content

CHANGELOG.md

# Changelog

## [Unreleased]

## 0.4.2 - 2026-06-20

## 0.4.1 - 2026-06-20

## 0.4.0

### New ExMonty Features

- **Buffered `open()` builtin and file handles.** Python `open(path, mode)`
  (with context-manager `with` support, `read`/`write`, and `a`/`w`/`r`
  modes) now works against `:read_write` and `:overlay` mounts. File objects
  round-trip as a new `{:file_handle, %{path, mode, position}}` tagged value,
  and three new os calls — `:open`, `:append_text`, `:append_bytes` — are
  dispatchable through `Sandbox` `os:` handler maps. `PseudoFS` does not yet
  service `:open` (returns `NotImplementedError`); use a mount for file I/O.

- **`ExMonty.Mount` — host filesystem mounts.** Map virtual paths in the
  sandbox to real host directories with `:read_only`, `:read_write`, or
  `:overlay` access modes. Path canonicalisation, boundary checks, and
  symlink-escape detection are always enforced.

  ```elixir
  mounts =
    ExMonty.Mount.new!()
    |> ExMonty.Mount.add!("/data",    "/var/lib/myapp/data", :read_only)
    |> ExMonty.Mount.add!("/scratch", "/tmp/sandbox-scratch", :overlay)

  ExMonty.Sandbox.run(code, mounts: mounts)
  ```

  Composes with the existing `:os` option — mounts handle FS calls; the
  `:os` map handles non-FS fallbacks (`:getenv`, `:datetime_now`, etc.).
  Unmounted paths raise `PermissionError` (upstream's
  `OsFunction::on_no_handler` semantics) instead of ExMonty's previous
  generic `:os_error`.

  Mount state (overlay writes, `write_bytes_used` counter) is cumulative
  on the mount object across runs. Construct a fresh mount to discard.
  See `proposals/MOUNT_TABLE.md` for the design rationale.

- **`datetime` support.** `date`, `datetime`, `timedelta`, and `timezone`
  Python values round-trip via new tagged tuples:

  ```elixir
  # Output direction (Python → Elixir)
  ExMonty.eval("from datetime import date\ndate(2026, 5, 1)")
  # {:ok, {:date, %{year: 2026, month: 5, day: 1}}, ""}

  # Input direction (Elixir → Python)
  ExMonty.eval("x.year",
    inputs: %{"x" => {:date, %{year: 2026, month: 5, day: 1}}})
  # {:ok, 2026, ""}

  # date.today() / datetime.now() via host handlers
  ExMonty.Sandbox.run("from datetime import date\ndate.today()",
    os: %{date_today: fn _, _ -> {:ok, {:date, %{year: 2026, month: 5, day: 1}}} end})
  ```

  Encoded shapes:
  - `{:date, %{year, month, day}}`
  - `{:datetime, %{year, month, day, hour, minute, second, microsecond, offset_seconds, tz_name}}`
    (`offset_seconds` and `tz_name` are `nil` for naive datetimes)
  - `{:timedelta, %{days, seconds, microseconds}}`
  - `{:timezone, %{offset_seconds, name}}`

- **New OS handler atoms** `:date_today` and `:datetime_now` surfaced through
  `ExMonty.Sandbox` for hosts to provide deterministic clocks.

### Bug Fixes

- **Sandbox handler allowlist.** `ExMonty.Sandbox` now accepts `:date_today` /
  `:datetime_now` keys in `os:` handler maps (previously they were silently
  dropped).

### Upstream Python Features (monty v0.0.8 → v0.0.18)

These come from upstream and don't change the ExMonty API — but they affect
what Python code you can run:

- **`open()` builtin** (v0.0.18). `with open(...) as f:`, buffered read/write,
  file objects. The `with` statement works for built-in context managers like
  `open()`; user-defined `__enter__`/`__exit__` classes are still unsupported
  (no class definitions). See "Buffered `open()`" above.
- **`json` module.** `import json; json.loads(...) / json.dumps(...)`.
- **`datetime` module.** `date`, `datetime`, `timedelta`, `timezone` (see above).
- **Multi-module imports.** `import a, b, c` in one statement.
- **Chain assignment.** `a = b = c = 1`.
- **Nested subscript assignment.** `d[k][i] = v`, `matrix[i][j] = 0`.
- **`hasattr` / `setattr`.** Work on host-provided objects (dataclasses, modules).
  Class definitions in Python source are still not supported.
- **`zip(..., strict=True)`.** Raises `ValueError` on length mismatch.
- **`str.expandtabs(tabsize)`.**
- **Named single-kwarg calls.** `f(x=1)` (single-kwarg edge case fixed).
- **`INT_MAX_STR_DIGITS` guard.** `int("1" * 5000)` raises `ValueError`,
  matching CPython's quadratic-time DoS protection (default 4300 digits).
- **Bug fixes:** `i64::MIN` negation panic, source-line >65535 parse panic,
  partial future-resolution panic in mixed gathers, GC interval ignored in
  tracker, GC reference release in cycles, empty-tuple singleton counted
  against memory limit.
- **More hardening** (v0.0.18): trial-deletion cycle GC, duplicate-parameter
  and duplicate-coroutine panics, exception-stack corruption on `raise`,
  further integer-op panics, comprehension generator/AST depth limits, and
  JSON BigInt limits — all converted from panics to proper exceptions or
  bounded errors.

### Not Yet Exposed to Elixir

- **Filesystem mounting** (`MountTable` API) — upstream supports overlay /
  read-only / layered filesystems. ExMonty's `PseudoFS` does not yet wrap this.
- **Async in Rust** — internal VM async support; no surface change for Elixir
  callers today.
- **`MontyRepl` / `JsonMontyObject`** — Rust-only APIs.

### Internal

- **monty pinned to v0.0.18** (commit `45a3b2d5`). Update procedure now tracks
  tagged releases by default; see `UPDATE_PROCEDURE.md`.
- **Upstream `OsFunction` → `OsFunctionCall` refactor absorbed.** The os-call
  surface now carries typed args inline; our NIF extracts the
  `(positional, keyword)` view via `OsFunctionCall::to_args` and routes mount
  calls through the new `MountTable::handle_os_call(&call)` signature. No
  Elixir-side API change beyond the new `:open`/`:append_*` os atoms.
- **`PrintWriter::Collect` renamed to `PrintWriter::CollectString`** in monty
  upstream — internal NIF change only, no Elixir-side impact.

## 0.3.0

### Breaking Changes

- **Removed `external_functions` option from `compile/2` and `Sandbox.run/2`.**
  External functions are now auto-detected at runtime via name lookup. The
  `:external_functions` option is no longer accepted.
- **`ExMonty.Native.compile/4` is now `compile/3`** (removed `external_fns` parameter).

### New Features

- **Name lookup progress tag.** When Python code references an undefined name
  (without calling it), execution pauses with `{:name_lookup, name, snapshot, output}`.
  Resume with `{:ok, {:function, name}}` to provide a callable, `{:ok, value}` for a
  constant, or `:undefined` to raise `NameError`.
- **`MontyObject::Function` type.** A new tagged tuple `{:function, name}` (or
  `{:function, name, docstring}`) can be returned from name lookups to provide
  callable function objects to the Python VM.
- **`handle_name_lookup/1` callback** in `ExMonty.Sandbox` behaviour (optional).
  Called when the sandbox encounters an undefined name. The sandbox auto-resolves
  names found in the `:functions` map.

### Upstream Improvements (monty v0.0.8)

- `re` module implementation (regex support).
- Full Python `math` module (~50 functions).
- PEP 448 generalised unpacking (`[*a, *b]`, `{**a, **b}`).
- Tuple comparison operators (`<`, `>`, `<=`, `>=`).
- Dict view and set/frozenset operators.
- `str` and `bytes` comparison operators.
- Augmented assignment on subscript targets (`x[i] += 1`).
- `max()` kwargs/default support.
- Bug fixes: stack-depth tracking, loop iterator depth, variable shadowing panic,
  `os.environ` panic in JS bindings.

## 0.2.1

- Fix dataclass round-trip: `ExMonty.Dataclass` structs returned from handlers are now decoded back to `MontyObject::Dataclass`, preserving field access (`p.x`), frozen semantics, and method calls.
- Add `field_names` and `type_id` fields to `ExMonty.Dataclass` struct for full fidelity with monty's dataclass representation.
- Fix float inf/nan encoding: `float('inf')`, `float('-inf')`, and `float('nan')` now encode as `:infinity`, `:neg_infinity`, and `:nan` atoms instead of crashing.
- Float special atoms round-trip through inputs.

## 0.2.0

- Update monty to 47427c0 (v0.0.7).
- Support dataclass method calls (new `:method_call` tag in interactive mode).
- Upstream: `filter()`, `map()`, `getattr()` builtins, `dict(iterable)`, dataclass methods, `asyncio.run()`, user-defined function calling.
- Upstream: `MontyException` and `StackFrame` now serializable.
- Upstream: improved resource limit enforcement (time check rate-limiting, 4x pow safety multiplier, catchable RecursionError).
- Upstream: bug fixes for string comparison, async stack overflow, i64::MIN division overflow.
- PrintWriter refactored from trait to enum (internal change, no public API impact).

## 0.1.1

- Update monty to v0.0.4 (86712b6) — includes fuzz testing, small-tuple optimizations, AST depth overflow fix, and `id()` memory leak fix.

## 0.1.0

- Initial release.
- `ExMonty.eval/2`, `compile/2`, `run/3` for sandboxed Python execution.
- Interactive pause/resume (`start/3`, `resume/2`, `resume_futures/2`) for external function calls and OS calls.
- `ExMonty.Sandbox` high-level handler + `ExMonty.PseudoFS` in-memory filesystem.
- Runner/snapshot serialization (`dump/1`, `load_runner/1`, `dump_snapshot/1`, `load_snapshot/1`).