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. - If
__exit__itself raises during the exception path, the new exception replaces the original and the original is dropped. This matches CPython’s behavior, and is listed here because some readers expect the original to be preserved as__context__; Monty does not track exception chaining. - 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.