with statement (context managers)
Monty supports the with statement for built-in types that implement
__enter__ / __exit__ (currently just file objects produced by open(),
see open.md) and for user-defined classes that define the two
dunders. Semantics follow CPython for the supported subset: __enter__ runs
before the body, __exit__ runs on every exit path (normal completion,
exception, return, break, continue), and a truthy return from
__exit__ suppresses an in-flight exception.
User-class __enter__ / __exit__ run as real frames, so unlike
__repr__ / __str__ (see classes.md) they may suspend on
external/OS calls and resume mid-with. The protocol check matches CPython:
a value whose class lacks __exit__ raises
TypeError: '...' object does not support the context manager protocol (missed __exit__ method); one with
__exit__ but no __enter__ gets the (missed __enter__ method) variant.
Lookup is type-level, as in CPython: an instance attribute named
__enter__/__exit__ is ignored by the with statement, but used by an
explicit obj.__enter__() call, which is an ordinary method call.
-
Multiple context managers in a single
with(with a() as x, b() as y:) is parsed as semantically equivalent nestedwithblocks: the leftmost manager enters first and exits last. This matches CPython’s left-to-right enter, right-to-left exit ordering exactly. Tracebacks point at the inner-mostwithline, not the original multi-item line.Each extra item counts against the parser’s nesting budget as if it were written as explicitly nested
withblocks; see language.md.
- Async
with(async with EXPR:) is rejected at parse time withSyntaxError: async context managers (async with) is not yet implemented. contextlib(@contextmanager,ExitStack, etc.) — the module is not available; only the language-levelwithstatement is.
- The third argument to
__exit__(the traceback object) is alwaysNone. Monty has no traceback objects; the type and value arguments are passed through unchanged (typ is ValueErroretc. works). Code that inspects the traceback object inside__exit__seesNonewhere CPython would provide atracebackinstance. - CPython’s
BEFORE_WITHlooks up and binds__enter__/__exit__once, when thewithstatement is entered; Monty’sWithExit/WithExceptStartopcodes look__exit__up again when the block exits. A class whose__exit__attribute is reassigned during the body calls the new function in Monty but the originally-bound one in CPython, and a reassignment that removes it raisesAttributeErrorin Monty where CPython would still call the original. - Direct
obj.__exit__(typ, val, tb)invocation on a built-in context manager forwardsvalto the type’spy_exitonly when it isNoneor a heap-allocated value, matching CPython for theNone/ exception-instance cases real callers use. A non-Nonescalarval(e.g.f.__exit__(int, 5, None)) cannot be expressed through the internalOption<HeapId>abstraction and is treated as ifvalwereNone. Every built-in context manager ignoresval’s content beyondis None, so this is not observable in practice. User-class instances are exempt: their explicit dunder calls are ordinary method calls and receive all three arguments verbatim. - Direct
obj.__exit__(...)on a built-in context manager requires exactly three positional arguments; any other arity raisesTypeError. CPython’s file/IOBase.__exit__is declared*argsand accepts any number, sof.__exit__()returnsNonethere but raises in Monty. User-class__exit__is an ordinary method, so its arity matches whatever the user defined, exactly as in CPython.
| Type | Notes |
|---|---|
open() | Closes the file on exit; see open.md for details. |
| user classes | Class must define __exit__ (and __enter__); see above. |
Adding a new context-manager-capable built-in takes three pieces on the
type’s HeapRead impl:
- Override
PyTrait::py_is_context_managerto returntrue. TheBeforeWithopcode checks it to raise CPython’s specificTypeErrorfor non-CM values, beforepy_enterruns. - Override
PyTrait::py_enter/PyTrait::py_exit. - Add the type’s arms in
HeapReadOutput::py_is_context_manager,py_enter, andpy_exit(inheap_data.rs) so the dispatch reaches the overridden methods.
Direct obj.__enter__() / obj.__exit__(...) invocation on built-in types
is wired centrally in VM::call_attr via dispatch_dunder, so no per-type
StaticStrings::Enter / StaticStrings::Exit arms are needed in the
type’s py_call_attr. Instances skip that interception and use normal
method dispatch.