← board

Async language surface + stackless coroutine backend

Combined 2026-06-17. The async language surface (; async; + await, stackful default, stackless backend, small stacks) is shipped byte-identical on all four targets. The remaining open items were tracked under feature-cross-target-feature-parity (its "Async sub-track").

2026-07-16 re-home. That parent ticket is now in done/ (the parity audit closed) but it deliberately left the async depth items unbuilt ("opportunistic, not gating"). To avoid orphaning them, they are re-listed here as this ticket's own optional remainder — verified still-open:

  1. Async I/O on cross — the epoll reactor / asyncnet / CoSleep are x86-64-only (hard-coded syscall numbers). Cross needs per-arch numbers: epoll_pwait (not epoll_wait) on aarch64/arm32, i386 socketcall, per-arch socket numbers, plus the cross test wiring. The one real target-parity item.
  2. Stackless v1 depth — params via instance slots (mirror the generator for-in arg store); a Task/Future for await-with-result.
  3. Nil-Python async def/await shim over AsyncGo/SLRunUntilDone.

The language surface itself is done and needs no more work; the above are feature depth, not blockers. This file also keeps the locked spelling + transform design detail — do not re-litigate that, just build the items above when they become worth it.

Motivation

The concurrency engine is shipped (coroutine scheduler + epoll reactor + channels

But two forces want a real surface:

  1. A stackless coroutine backend (state machine, no per-coroutine stack) is the RAM-cheap path for constrained devices. A stackless lowering requires suspension markers — the compiler must know where to split the state machine, and suspension is transitive (a coroutine calls Foo calls Bar which suspends → Foo/Bar must be transformed too). A per-call await marker is exactly what makes that transform local (no whole-program analysis, works with separate compilation / indirect calls). This is why Python must have async/await: its coroutines are stackless.
  2. Nil Python (.npy) has async def / await as native, expected keywords. A Python programmer writes them. So the surface isn't optional there — it's the frontend's natural shape. (Pascal frontend: keyword-optional; Nil Python: native.)

Design principle: this mirrors the generator surface we already shipped (; generator; + yield + ; stackful; / ; stackless;). Same two-backend structure, same forcing directives, same auto-selection question.

Surface (proposed)

Generators (done) Coroutines/async (this ticket)
; generator; routine directive ; async; routine directive
yield (suspension marker) await (suspension marker)
; stackful; / ; stackless; / auto same
stackless = state-machine transform stackless = state-machine transform

Stackful vs stackless on device (ESP32)

Both are viable on ESP — correcting an earlier note that said stackful is out:

Mixing

Stackful and stackless coroutines can share one scheduler: tag each coroutine with its strategy and resume it the right way (stackful → CoSwitch; stackless → re-enter the step fn). The generator for-in already branches on ProcIsStackless — same mechanism. Coexistence is mechanical; the only hard part is auto-deciding stackless eligibility for transitive suspension. Practical rule: default stackful (always works); auto-stackless only for locally-eligible bodies; ; stackless; to force.

Build order (Pascal-first, Nil-Python-reused)

The stackless generator proved the machinery is frontend-agnostic: built and tested in Pascal (cross-bootstrap + QEMU oracles), the transform/engine are shared AST/IR. So:

  1. ESP stackful small-stack: configurable CO_STK + canary. (Cheap; may even land independently of the surface.)
  2. Async surface in Pascal: ; async; directive + await marker; backend selection directives; non-viral async; stackful await = documentation.
  3. Stackless coroutine backend: the transform behind the directive (the hard part). Auto-select only for locally-eligible bodies.
  4. Nil Python: async def / await + a small asyncio shim (sleepCoSleep, runSpawn+RunUntilDone, gather→spawn-N-await-all) mapping onto the same AST nodes — near-free after the Pascal work.
  5. (Optional) a Task/Future type for "spawn work, await its result" (t := Spawn(...), v := AwaitResult(t)) — the one thing bare await over stackful doesn't give; expose as a function, keyword optional.

Acceptance

; async; + await parse and lower on the Pascal frontend; the stackless coroutine backend runs a multi-coroutine test byte-identical across targets (as the stackless generator does); a configurable small coroutine stack runs the scheduler suite; Nil Python async def/await lowers to the same engine. Bootstrap + cross-bootstrap stay byte-identical.

Decided spelling (locked 2026-06-16)

Log