← board

A windowed-ABI CoSwitch for xtensa

The sequenced remainder of [[feature-a-coswitch-for-xtensa-and-riscv32-the-scheduler-has-no-context-switch-there]], whose body asked for exactly this split: "a windowed CoSwitch is a materially harder problem than a Call0 one, and the two should be sequenced, not bundled." Call0 landed 2026-09-24 with all three of that ticket's parts.

Why it is harder, stated as the thing that breaks

The switch that works on every other target is: push the callee-saved set plus BSS_EXC_TOP onto the current stack, store sp into [pfrom], load sp from [pto], restore, return. It assumes callee-saved state is on the stack being swapped.

Under the windowed ABI it is not. call8 rotates the register file, so a routine's callee-saved values live in physical registers belonging to a window the callee cannot see, and they reach memory only when the window is spilled. Two consequences, and the second is the awkward one:

  1. The window must be spilled before another context can own the stack, or the values are still in the register file when the other context starts using it.
  2. The windowed spill area is at [sp-16], so a switch that moves sp invalidates the region it would have to spill into. ir_codegen_xtensa.inc already records this in a different context — its note that windowed keeps sp CONSTANT because moving it desyncs the window spill area.

So this is not "write the Call0 arm with different register numbers".

What the Call0 work leaves in place for whoever takes this

The negative control that exists today, and what to do with it

test-xtensa carries:

! ./pascal26 --target=xtensa --platform=posix --xtensa-abi=windowed --emit-obj \
      test/lib_asyncnet6.pas ... >...out 2>&1
grep -q "xtensa WINDOWED ABI" ...out

Replace it, do not delete it. It is what stops the refusal quietly becoming a fall-through, and a fall-through here calls CoSwitchAddr = 0 — offset 0 of .text, the program entry, from inside user code, which hangs or re-enters rather than diagnosing. When windowed works, this row should become the six coroutine rows run a second time under --xtensa-abi=windowed.

(Note for whoever edits it: the redirect is > and not 2> because pascal26 writes diagnostics to stdout. The 2>/dev/null spelling on neighbouring refusal rows works only because those rows assert a nonzero exit and never read the text.)

Positive control for whoever fixes this

Three rows, because "it switched" and "it switched correctly" and "exceptions still work" are different claims:

  1. The six coroutine rows under --xtensa-abi=windowed, asserted against the x86-64 build of the same source as the Call0 rows are — so no per-target constant is baked in and neither ABI can encode its own wrong answer.
  2. A perturbation control. The Call0 arm was validated by moving @CoStart one slot in the RTL priming and confirming test_scheduler and test_asyncecho segfault. Do the same for whatever frame windowed uses: if a one-slot error still passes, the rows are not exercising the switch.
  3. test_scheduler_exc, which is the row that matters most here. BSS_EXC_TOP travels with the stack so each coroutine owns its exception chain, and the window spill interacts with that — a windowed switch that gets registers right and the chain wrong passes rows 1 and 2.

Why the priority is 25 and not higher

Call0 is the default on every xtensa target including esp32s3, so coroutines and async work there today and nothing is blocked. This is reachable only by an explicit flag, it refuses loudly when reached, and the refusal is guarded. Rank it up if a real ESP-IDF build wants async — the NilPy xtensa rows show that --xtensa-abi=windowed --platform=esp is a combination this tree already builds, so that is not hypothetical, just not currently asked for.

Umbrella

[[meta-a-pxx-produces-linkable-code]]

Resolution (2026-09-24, frankH)

Mechanism. This is the windowed longjmp's trick (exception_emit.inc, the part measured under QEMU), aimed at a different frame. The switch needs no jmpbuf-style copy of the [sp-16] save area, because a SUSPENDED context's stack is not written by anyone while it is parked.

The one number that is easy to get wrong: entry a1, 48, not 32.

A fresh context is CALLED, not returned into, because CoStart begins with ENTRY. The stub:

  1. moves its own window onto the new stack at P-32;
  2. records P at [a1-12], so a later overflow of that window writes its a4-a7 into [P-32,P-16) and nowhere else;
  3. CALLX8s the entry.

The priming layout is shared with Call0. It is [0] exc_top = 0, [4] = 0 (Call0 reads it as a15; windowed reads it as "never ran"), and [8] = @CoStart.

Verified:

Found beside it, not fixed:

Log