Pyex TODO — bugs and missing features to fix
=============================================

All tests pass (5346 tests, 0 failures, 2 skipped).
All 16 fixtures pass.

Heap-based reference system is complete for lists, dicts, sets, and
class instances.  Aliasing works: `b = a; b.val = 99` → `a.val == 99`.
Intrusive linked lists work: `node.prev.next = node.next`.

## Recently fixed (async/await — cooperative scheduling)

Full CPython parity for the async surface LLMs emit.  Coroutines
are tagged generators driven by a cooperative trampoline:

- `async def`, `await`, `async for`, `async with`, async list
  comprehensions all parse and run.  Coroutines are tagged on the
  unified `:function` pyvalue (kind: `:sync | :async`); calling
  an async function defers body execution and returns a
  `:coroutine` wrapping a `:gen_unstarted` iter-pool entry.
- `await EXPR` is yield-from on the inner iterator: each yield
  propagates up to the surrounding trampoline, and the inner's
  `StopIteration` value becomes the await's result (PEP 380).
- New `asyncio` module: `run`, `gather`, `sleep`, `create_task`,
  `ensure_future`, `wait_for`, `iscoroutine`,
  `iscoroutinefunction`.  `gather(return_exceptions=True)` returns
  real exception instances so `isinstance(r, ValueError)` works.
  Tasks expose `.result()` / `.done()` / `.cancel()` /
  `.exception()`; pending tasks report `done() = False`.
- `asyncio.sleep(t)` yields an `{:asyncio_sleep, ms}` sentinel
  the trampoline interprets, so `gather` of two `await
  asyncio.sleep(0)` calls interleaves children at the awaits.
- Nested `asyncio.run` raises `RuntimeError` per CPython.
- Async methods on classes (instance / `@staticmethod` /
  `@classmethod` / subclass override) return coroutines via the
  bound-method dispatcher.

Observable behaviors verified against CPython in
`test/pyex/async_conformance_test.exs` (54 tests):

- `gather(step("A"), step("B"))` over coroutines yielding at each
  `await asyncio.sleep(0)` produces "ABABAB", matching CPython's
  event-loop interleaving order.
- `create_task` is lazy — the body runs on `await t`, not at
  `create_task(coro)` call time.
- `[x async for x in gen()]` iterates an async generator.
- `await` on a non-awaitable raises CPython-shaped TypeError.

## Recently fixed (lazy iteration)

- `itertools.islice` over an infinite generator no longer hangs.
  `list(islice(g(), 5))` against `def g(): while True: yield i`
  now terminates with the first 5 items.  Fix: register `islice` in
  the no-drain set so the iterator arg isn't materialized before
  the builtin runs, then return an `:islice_call` signal handled
  by a bounded-step iterator in `BuiltinResults`.  Same shape can
  extend to `takewhile`/`dropwhile`/`filterfalse` if those become
  load-bearing.

## Recently fixed (class system, type(), descriptors, subclassing)

- `type(cls) is type` holds: `type` is now the canonical class singleton
  (`type_class()`), bound directly in the builtins env.  `isinstance(Foo, type)`,
  `isinstance(int, type)` all return True.
- `type(x)` for primitives returns a class value structurally equal to the
  corresponding builtin (`type(42) is int`, `type("x") is str`, etc.).
- Subclassing stdlib classes preserves subclass identity.  `class MyDT(datetime.datetime)`
  — `MyDT(...)` returns a `MyDT` instance, `type(m).__name__ == "MyDT"`, and
  subclass method overrides beat the baked-in parent closures.  `super().foo()`
  falls back to baked-in parent methods when the class MRO doesn't find `foo`.
- Subclassing built-in types (`list`, `dict`, `set`, `str`, `int`, `tuple`,
  ...) now works via a `__wrapped__` pattern.  `class MyList(list): def total(self): return sum(self)`
  works for iteration, `len`, subscript, `in`, `isinstance(ml, list)` etc.
  Methods on the wrapped value (`ml.append`, `md.values`) forward automatically.
- Custom data descriptors honored: classes with `__get__`/`__set__` on
  instance attributes now work as in CPython.
- `__slots__` is enforced: slotted classes raise `AttributeError` on
  assignment to undeclared attributes.  Plain classes are unaffected.
- Dict keys with custom `__eq__`/`__hash__` resolve correctly:
  `d = {K(1): "v"}; d[K(1)]` returns `"v"`.  Sets dedupe by value.
- `KeyError` messages use Python-style repr (`KeyError: 'y'`) instead of
  leaking internal `{:ref, N}` tuples.  Affects all `d["missing"]`,
  `dict.pop`, `operator.getitem`, `set.remove`, etc.
- `hasattr` no longer crashes on non-`{:class, ...}` pyvalues (generators,
  caught exceptions, builtin types).  Each returns the correct boolean.
- Function dunders expose `__name__`, `__doc__`, `__annotations__`,
  `__defaults__`, `__kwdefaults__`, `__qualname__`, `__module__`,
  `__class__`.  `hasattr`, `getattr`, attribute access all work uniformly.
- Class dunders: `__qualname__`, `__module__`, `__doc__`, `__dict__`,
  `__mro__`, `__bases__`, `__name__`, `__class__` (→ `type`).
- Module dunders: `__name__` (settable after mutation), `__class__`,
  `__dict__`, `__doc__`.  `vars(module)` works.
- Built-in type dunders: `int.__mro__`, `bool.__mro__` (includes `int`),
  `str.__name__`, `list.__bases__`, etc.
- `typing.List[int]`, `Optional[str]`, `Dict[str, int]` (with multi-arg
  subscripts) parse and evaluate without errors.  Parser now supports
  `a[x, y]` sugar for `a[(x, y)]`.

## Recently fixed (module & datetime class hierarchy)

- Modules are now `{:module, name, attrs}` pyvalues (were bare maps)
  - `type(m)` returns `"module"` (was `"dict"`)
  - `repr(m)` renders as `<module 'name'>`
  - `m.__name__` and `m.__class__` work correctly
  - `dir(m)` returns module attribute names
  - `os.path`, `urllib.parse/request/error`, `fastapi.responses` are
    now real submodule pyvalues (were plain maps)
  - Attribute access on modules now raises
    `AttributeError: module 'X' has no attribute 'Y'` (Pythonic)
  - `getattr(module, attr, default)` and `hasattr(module, attr)` work
- `datetime.datetime` is now a subclass of `datetime.date`
  - `issubclass(datetime, date)` → True
  - `isinstance(dt, date)` → True
  - `datetime.__mro__` → `(datetime, date, object)`
- `datetime.datetime.combine(date, time, tzinfo=...)` classmethod added
- `getattr(cls, "name")` now walks the class MRO (was failing with
  AttributeError for inherited/classmethod attributes)

## Recently fixed (third batch — staff-level Python)

- `@property` / `@property.setter` — getter/setter descriptor protocol
- `@staticmethod` / `@classmethod` — both work including factory methods
- `defaultdict[missing_key]` — was infinite recursion, now returns default
- `filter(None, iterable)` — was infinite loop, now filters by truthiness
- `round(2.5)` / `round(2.675, 2)` — banker's rounding using full IEEE 754 precision
- `json.dumps` now adds spaces after `:` and `,` (matching CPython default)
- `*args` is now a `tuple` not a list (`isinstance(args, tuple)` works)
- Tuple equality with heap-allocated contents (`((1,2), {"x":3}) == ((1,2), {"x":3})`)
- `float("inf") > 1e308` and `float("-inf") < -1e308` (infinity comparisons)
- `float("inf") + 1 == float("inf")` (infinity arithmetic)
- `float("nan") != float("nan")` (NaN != NaN semantics)
- NaN in < / > / == comparisons returns False
- `issubclass(bool, int)` (builtin type hierarchy)
- `__getattr__` dunder dispatch for dynamic attribute access
- Slice assignment `a[1:3] = [10, 20]`
- `%-*s` / `%*d` / `%.*f` dynamic width/precision in % formatting
- Generator output_buffer now synced back (print in generator body works)
- `functools` module: `reduce`, `partial`, `lru_cache`, `wraps`, `cached_property`
- `collections.namedtuple` — lightweight immutable value objects
- `collections.deque` — double-ended queue with full method support
- `contextlib.contextmanager` — generator-based context managers work correctly
- `io.StringIO` — in-memory text buffer for CSV, test capture, etc.
- `abc` module — `ABC`, `abstractmethod`
- `@contextmanager` exit code now runs in correct order (before/yield/inside/after)
- `sync_generator_ctx` now syncs `output_buffer` back to outer context
- `hasattr` returns `True` when `__getattr__` is defined
- Slice assignment deref fix (inserting lists not refs)
- Decorator evaluation order: decorator expr evaluated BEFORE def, enabling `@prop.setter`

## Previously fixed

- `filter(None, iterable)` — infinite recursion bug; now correctly filters truthy items
- `round(2.5)` returns `2` (banker's rounding, was `3`)
- `round(2.675, 2)` returns `2.67` (full IEEE 754 precision, was `2.68`)
- `json.dumps({"a":1})` now produces `{"a": 1}` (space after `:` and `,`, matching CPython)
- `*args` is now a tuple, not a list (`isinstance(args, tuple)` now works)
- Tuple equality with heap-allocated contents (`((1,2), {"x":3}) == ((1,2), {"x":3})`)
- Float rounding uses full IEEE 754 precision via `:erlang.float_to_binary` (not truncated `Float.to_string`)

## Previously fixed

- `list.sort(key=...)` storage reversal bug (produced reversed results)
- `"%s" % exception` called `py_str` instead of `__str__` dunder
- `requests.post(timeout=N)` ignored timeout kwarg (now passes to Req)
- `%e` format: wrong precision (was sig figs, now decimal places)
- `%g` format: trailing zeros not stripped, wrong notation threshold
- `{:.2e}` / `{:+d}` format specs in str.format
- `%(name)s` named keys in % formatting
- `{:.0f}` banker's rounding in str.format and %f
- Exception hierarchy: `except AppError` now catches `DBError(AppError)` subclasses
- `list.sort(key=..., reverse=True)` was not stable (fixed with comparator)
- `__exit__` receives real exception class (not string), enabling `t.__name__`
- `with A(), B():` multi-target with statement (parser desugars to nested withs)
- `raise X from Y` parses and raises X
- Keyword-only parameters `def f(a, *, x, y=10):`
- `del obj.attr` attribute deletion
- `str.format()` format specs `{:.2f}`, `{:>10}`, `{!r}`, `{:,}` etc.
- `dict.fromkeys(keys, default)`
- Tuple slicing `(1,2,3)[1:3]`
- `lambda x, i=i: ...` default arg parsing in lambdas
- `%g` precision: rounds before threshold test (matches CPython exactly)
- `repr()` / `print()` on lists/tuples dispatch `__repr__` on nested instances
- `nonlocal` in list comprehensions: mutations now propagate correctly
- `str(KeyError("x"))` wraps in repr-quotes like CPython
- f-string `!r` / `!s` conversion flags now supported
- String `repr()` uses double quotes when string contains single quotes
- Function/lambda default args are evaluated at definition time, not call time

## Recently fixed (conformance pass — LLM code patterns)

- `Counter(gen_expr)` — Counter constructor now accepts generator expressions
  and `Counter.update(gen_expr)` also accepts generators
- `Counter[missing_key]` and `Counter[k] += 1` — missing keys return 0 in
  all subscript positions (read, augmented assign, nested subscript)
- `csv.DictReader(io.StringIO(...))` — DictReader and reader now accept
  StringIO objects as input (not just file handles and lists)
- `csv.DictReader` row mutation — `list()` on a sequence of dicts now
  heap-allocates each dict so for-loop mutations persist through the list
- Dict/set literal conditional expressions — `{"k": v if c else d}`,
  `{"a" if c else "b": v}`, `{x if c else y}`, `{x if c else y for ...}` all
  parse correctly; the parser now uses `parse_expression` for all dict/set
  element and key positions
- `list()` first element — `parse_list_literal` now uses `parse_expression`
  so `[walrus := val, ...]` and `[x if c else y, ...]` parse correctly
- `os.getcwd()` / `os.chdir()` — added to the `os` module
- `statistics` module — `mean`, `fmean`, `geometric_mean`, `harmonic_mean`,
  `median`, `median_low`, `median_high`, `median_grouped`, `mode`,
  `multimode`, `pstdev`, `pvariance`, `stdev`, `variance`, `quantiles`
- Walrus operator `:=` in function call arguments — `len(data := [1,2,3])`
  now parses (was silently rejected; `parse_args` used `parse_or` instead
  of `parse_expression`)
- Walrus operator `:=` in list literals — `[y := 10, y + 1]` now parses
  (same root cause as call-arg walrus)
- `str.removeprefix(prefix)` / `str.removesuffix(suffix)` — Python 3.9
  string methods added
- Nested tuple unpacking in `for` loops — `for i, (a, b) in items:`,
  `for (a, b) in items:`, `for i, (a, (b, c)) in nested:` all work;
  also works in list/set/dict comprehensions and generator expressions
- `generator.send(value)` / `v = yield expr` — full coroutine-style
  generators: `yield` as an expression, `.send(v)`, `.close()`, `.throw()`;
  `v = yield expr` correctly receives the sent value when the generator
  is resumed via `send()`
- Chained method mutation on complex receivers — `d.setdefault("k", []).append(v)`,
  `sys.path.insert(0, p)`, `d["key"].append(v)` all propagate mutations
  correctly back to the container via a two-phase receiver evaluation
- `str.removeprefix(prefix)` / `str.removesuffix(suffix)` — Python 3.9
  string methods added

## Remaining features

See the commented-out `# TODO:` lines in
test/fixtures/programs/sanity/main.py for the full list of unsupported
Python features.

Known limitations:
- Late-binding closures: `lambda x: x + i` in a loop captures final `i`
  (Python late-binding semantics). Use `lambda x, i=i: x + i` as workaround.
- `async`/`await` and `asyncio` not yet implemented
- `bytes`/`bytearray` types not yet implemented
- `datetime.now()` always returns UTC (not local time)
- `next(itertools.count())` must be wrapped with `iter()` first; CPython
  allows it directly because count/cycle return iterator objects natively.
  In Pyex they return bounded lists/ranges (works with `list()`, `for`,
  `islice`, etc., just not `next()` directly).

## Known limitations

- Walrus in comprehension does not leak to enclosing scope (PEP 572):
  `[last := x for x in range(5)]` produces the correct list but `last`
  is not visible outside. Fix requires comprehension eval to write walrus
  bindings to the enclosing env frame rather than the comprehension frame.
- `async`/`await` and `asyncio` not yet implemented
- `bytes`/`bytearray` types not yet implemented
- `datetime.now()` always returns UTC (not local time)
- `str.format_map()` not yet implemented

## Recently fixed (performance)

- Quadratic slowdown on repeated calls to module-level functions
  whose bodies called other module-level functions.  `update_closure_env`
  was rebuilding closures after every call, producing a self-referential
  module-scope graph that grew one level per call.  Fixed by
  short-circuiting closure rebuild when the captured scope is just the
  module: there are no enclosing locals to track.  Manhattanhenge full
  run: 2 min -> 1.9 s (60x).

