Skip to content

Cleanup async tracebacks patterns builtins#2

Merged
dylan-sutton-chavez merged 20 commits intomainfrom
cleanup-async-tracebacks-patterns-builtins
May 9, 2026
Merged

Cleanup async tracebacks patterns builtins#2
dylan-sutton-chavez merged 20 commits intomainfrom
cleanup-async-tracebacks-patterns-builtins

Conversation

@dylan-sutton-chavez
Copy link
Copy Markdown
Owner

No description provided.

Drops HeapObj::Complex, NativeFnId::Complex, the j/J literal suffix,
TokenType::Complex, .real/.imag/.conjugate, complex branches in
arithmetic ops, format_complex helpers, and the no-libm trig kit
(fsqrt/fsin/fcos/fatan/fatan_small/fatan2). fexp/fpowf/fln remain
because non-integer real-valued pow and format-spec exponentials use
them. Edge workloads do not exercise complex numbers, and removing
them shrinks per-binop dispatch.
Drops the BigInt struct (~350 LOC of base-2^32 limb arithmetic with
Knuth Algorithm D division), HeapObj::BigInt, Value::BigInt, val_tag
#14, and every BigInt promotion path in arithmetic, format, and
runtime helpers. Adds VmErr::Overflow with cold_overflow() trap and
int_or_overflow() helper used by add/sub/mul/neg/abs/shl/divmod/pow.
Integer literals exceeding i64 are now compile-time errors; literals
exceeding the 47-bit Val range trap as OverflowError at constant
materialisation. Edge workloads are bounded; the BigInt machinery
exists for arithmetic that doesn't appear in target programs.
…1.3)

Removes the soft-keyword demotion path for Match and Case so 'match'
and 'case' always tokenise as keywords. Identifiers named 'match' or
'case' are now parse errors, matching the grammar. 'type' keeps the
soft demotion because it collides with the type() builtin and with
attribute names; the alias-statement path it powers is removed in a
later task.
Keeps d/f/F/s/b/o/x/X (decimal, fixed-point, hex/oct/bin) and the
shared modifiers (width, fill+align, .precision, 0, ',', '#'). Drops
scientific/general formatters (~80 LOC), the % percent type, the c
char type, and the n locale type. Removes flog10 from the math kit
since the only non-complex caller was scientific formatting; the
exp/general/strip_trailing/pow10_i/require_int helpers go with them.
- Removes 'type X = T' alias statements and the OpCode::TypeAlias
  handler. The 'type' keyword stays soft (collides with type() builtin
  and attribute names), but no longer emits any opcode.
- Removes the ascii() builtin (NativeFnId::Ascii, call_ascii, dispatch
  arms, builtin registration, docs entry).
- Removes the dead 'chunk.annotations' field used during parsing for
  type hints but never read for codegen or runtime.
- LoadAttr and CallMethod now resolve names against a Class object
  directly, returning the unbound function (no self prepended). Lets
  the namespace-of-functions and stateless-helpers patterns work without
  an instance.
- MakeClass collects every defined slot — methods + class-level
  constants — as members. Builtin shadows (e.g. a class method named
  'print' that the body never assigned) are filtered so they don't
  poison the class table.
- sorted() now accepts key= via decorate-sort-undecorate. dispatch_native
  has a one-builtin special case to extract the 'key' kwarg before the
  generic 'no kwargs' check; the parser drops the CallSorted opcode for
  sorted so kwargs reach the generic LoadName+Call path.
…hods (Tasks 3.2-3.5)

Builtins:
- setattr(obj, name, value), delattr(obj, name) — instance attribute
  store/remove via the existing __dict__ machinery
- slice(stop) | slice(start, stop) | slice(start, stop, step) —
  exposes the existing HeapObj::Slice constructor
- vars(obj) — for instances, snapshot of __dict__; for modules,
  attrs table materialised as a dict

Methods:
- str: removeprefix, removesuffix, splitlines, partition, rpartition
- dict: copy, popitem
- bytes: find, index, count, replace, split (parity with str find/count/
  replace/split, returning bytes results)
- Adds documentation/language/classes.md covering state-machine and
  namespace patterns plus the explicit list of features that don't exist
  (inheritance, super, dunders, properties, ...)
- Updates docs.json to register the new page
- Documents setattr/delattr/slice/vars in reference/builtins.md and
  bumps the count from 45 to 47
- Adds removeprefix/removesuffix/splitlines/partition/rpartition to
  reference/methods.md
- Adds OverflowError to reference/limits-and-errors.md
- Reframes match/case in language/control-flow.md as 'equality dispatch'
- Removes leftover BigInt and complex-numbers mentions in
  implementation/{syntax,design}.md
…arets (Task 3.1)

Errors now render a chain of 'note: called from' frames in the same
rustc-style as the existing single-error diagnostic. The chain walks
across module boundaries: a KeyError raised inside an imported module
shows the module's source line first, then each importer's call site
back to the entry chunk.

- SSAChunk gains source/path Arc<String> + call_byte_pos Vec<(ip, byte)>
- Parser tags every chunk with its source/path (modules with their spec,
  entry chunk with the host-supplied path); after every Call/CallMethod/
  CallExtern emit, records (ip, last_end) for instr-level caret precision
- VmErr::render_traceback walks frames innermost-first, rendering each
  via Diagnostic with note: prefix while keeping the topmost site as the
  red error: line
- vm.call_stack pushes a CallFrame on every user-function entry and pops
  on success; left in place on error for the renderer; cleared on
  swallow by except handlers
- vm.function_names parallel to vm.functions, populated at function
  table build time from the parent chunk's names slot
run() now drives a multi-coroutine scheduler instead of round-robin
ticking. Each handle tracks state (Ready / Sleeping(until_ns) /
CancelPending / Done(Val) / Errored(VmErr) / Cancelled) so concurrent
coroutines run independently and their results are recoverable.

Concrete fixes vs the previous implementation:
- run() returns the first argument's actual return value instead of
  always None
- sleep() takes seconds (int or float) and parks until wall-clock or
  virtual-clock catches up; the scheduler advances time to the next
  wakeup rather than busy-looping
- errors in one coroutine are captured on its handle; peers keep
  running until completion. The target's error still propagates to
  the run() caller
- VM gains time_hook (host-installable wall clock) + virtual_clock_ns
  fallback so deterministic tests interleave without a real clock
…4.3-4.6)

Adds three new builtins on top of the cooperative scheduler:

- gather(*coros) -> list: concurrent fan-out. Adds every coro to the
  scheduler, drains until each is terminal, returns results in argument
  order. First error cancels remaining peers and propagates (CPython
  asyncio style).
- with_timeout(secs, coro) -> result: deadline-bound execution. On
  expiry the coro is flagged CancelPending and a TimeoutError is
  raised. Sleeps inside the coro that would exceed the deadline trip
  the timeout immediately.
- cancel(coro): flag the coro for cancellation; the next scheduler
  step transitions it to Cancelled. Cancelled coros stop executing —
  the body does not see CancelledError raised in-place (cooperative
  limit, see docs).

CancelledError and TimeoutError are now in BUILTIN_TYPES so they
match  clauses as typed exceptions.

Tests: gather basic, gather with error propagation, with_timeout
success, with_timeout failure, concurrent fan-out with sleep(0).
- Adds documentation/language/async.md covering routines vs coroutines,
  the cooperative scheduling model, sleep/run/gather/with_timeout/cancel
  with worked examples, exception types, and limitations (no preemption,
  silent cancel, no async iteration).
- Registers the new page in docs.json navigation.
- Documents gather/with_timeout/cancel in reference/builtins.md and
  bumps the count from 47 to 50.
- Adds TimeoutError + CancelledError to reference/limits-and-errors.md
  exception table.
…le-else

f"{x=}" now expands to the literal text 'x=' followed by the value,
defaulting to !r when no explicit conversion or spec is provided
(CPython parity). Detection lives in the f-string Lbrace branch of
the literal parser; the new in_fstring_expr flag on Parser disables
the assignment fast-path in name() so the trailing '=' isn't consumed
as an assignment operator.

Adds vm.json tests confirming the existing walrus, for-else, and
while-else paths still work as expected:
- walrus in if/while/comprehension/generator
- for-else with and without break
- while-else with and without break
- nested for-else inside a function

Documents async for / async with as parser-only (no __aiter__ /
__aenter__ dispatch — synchronous iteration semantics) so users know
the keywords are accepted but cosmetic.
…args)

Calling a builtin Type now allocates a HeapObj::ExcInstance carrying
the type name and constructor args. Raising one stashes the Val on
vm.pending_exc_val so the matching except-as handler binds the
instance instead of the bare class. .args exposes the constructor
args as a tuple.

- HeapObj::ExcInstance(name, args) variant
- exec_call routes Type calls to ExcInstance allocation
- Raise / RaiseFrom hand the Val to the dispatcher via pending_exc_val
- LoadAttr resolves .args -> tuple
- isinstance, type_name, display, truthy, GC tracing, val_tag updated
- Tests cover bind-as-e, .args access, multi-arg ExcInstance,
  multi-handler dispatch, isinstance against Type
Adds four hex/byte/set primitives commonly needed for binary protocols
and immutable-set use cases:

- bytes_fromhex(s): parse a hex string to bytes (whitespace tolerated)
- int_from_bytes(b, byteorder): parse bytes -> int, byteorder 'big'/'little'
- int_to_bytes(n, length, byteorder): encode int -> bytes
- frozenset() / frozenset(iter): immutable hashable set

Edge Python has no class methods so these are free builtins rather than
the CPython spelling 'bytes.fromhex' / 'int.from_bytes'.

HeapObj::FrozenSet(Rc<HashSet<Val>>) is a new variant. Hashable via the
raw Val bit pattern (same as Set), so frozenset-keyed dicts work when
the SAME Val is used as the key. Two separately-allocated frozensets
with identical contents do NOT collide today (limitation of bit-eq Val
hashing). Equality across Set <-> FrozenSet uses HashSet content compare.
async for now has tested coverage when the iterable is an async
generator (async def + yield) or a regular iterable. The runtime
uses the same coroutine-resume machinery as sync for, so each
yield surfaces as the next loop value. No __aiter__/__anext__
dunder dispatch on user classes — that would require an entire
dunder dispatch layer that Edge Python doesn't have for sync
with/iter either. Documented honestly.

async with stays a syntactic marker (matches sync with behavior:
neither dispatches __enter__/__exit__). Tests confirm both forms
pass through the value as-is.
Async generators (async def + yield) work via the same Coroutine
machinery as sync generators. Tests cover:
- async for over an async gen yielding multiple values
- async generator with comprehension
- empty async generator (no yield reached)
- async generator drained via list()
- async generator iterated via plain 'for' (cosmetic difference)

Known limitations (out of scope here):
- sleep() inside an async gen iterated outside run() has undefined
  semantics — works only when the gen is driven by gather()/run().
- 'return' early-exit from an async gen has interaction issues with
  the loop driver; recommend dropping out via raise StopIteration
  if you need explicit termination.
…#10)

Extends match/case from equality-only dispatch to structural patterns:

- Capture variables:  binds n = subject
- Wildcard:  always matches without binding
- OR patterns:  tries each alternative; first match wins
- Guards:  pattern + boolean predicate
- Sequence patterns: , ,
  Each item may be a literal, a capture name, or _. Length checks against
  list/tuple subjects.
- Literal patterns inside sequences: ,

Not supported (documented): mapping , class ,
nested sequences inside another sequence. Use chained if/elif.

Implementation:
- parse_pattern walks OR alternatives, wiring per-alt fail jumps to the
  next alt's start and the last alt's fails to the case-fail label.
- parse_simple_pattern dispatches on token kind to literal-eq, capture,
  wildcard, or sequence.
- parse_sequence_pattern buffers tokens to count items + locate the star
  in a first pass, then emits per-element bytecode using positive
  indices before the star and negative after.
- globals() returns a fresh dict combining the VM's globals map
  (builtins, types, module values) with the entry-chunk slot names
  (top-level user assignments). When called from inside a function,
  the entry slots are read from live_slots[..N] where N is the
  entry chunk's name count. Useful for dynamic dispatch by name and
  introspection.
- locals() returns the current frame's bindings, deduped by SSA
  version (latest live value per bare name) and filtered to skip
  synthetic matcher slots (#match*) and builtins that haven't been
  rebound in this frame.

Both return *copies* — mutations don't propagate back to the VM.
@dylan-sutton-chavez dylan-sutton-chavez merged commit b4cb7b0 into main May 9, 2026
3 of 4 checks passed
@dylan-sutton-chavez dylan-sutton-chavez deleted the cleanup-async-tracebacks-patterns-builtins branch May 9, 2026 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant