Cleanup async tracebacks patterns builtins#2
Merged
dylan-sutton-chavez merged 20 commits intomainfrom May 9, 2026
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.