← board

xtensa windowed: every frame costs ≥256 bytes, so recursion dies early on IDF

Symptom

procedure PutIntRec(n: Int64);
begin
  if n >= 10 then PutIntRec(n div 10);
  PutC(48 + Integer(n mod 10));
end;

Printing 922337203685477580 (18 digits, so 18 nested frames) on esp32s3 under ESP-IDF prints 922337 and then panics with Guru Meditation Error ... LoadProhibited. Ten digits are fine. The identical program on esp32c3 (riscv32) prints all 18 — riscv32 frames are a fraction of the size.

Cause

Two costs stack up, and both are paid by EVERY windowed frame:

  1. The constant-sp expression region. Windowed code may not move sp (the window overflow handlers spill the caller's a0-a3 to [sp-16]), so temporaries live in a region reserved up front — XT_EXPR_REGION, a fixed 192 bytes, whether the routine uses two slots or forty-eight. Plus XT_OUTARG_REGION (64) for outgoing stack arguments.
  2. ADDMI granularity. The prologue reserves ONE patchable 3-byte slot and PatchProcPrologue fills it with ADDMI, whose immediate is a multiple of 256. So the frame rounds up to 256 even when 64 would do, and 256 is the floor.

3584 (the IDF default main-task stack) / ~300 bytes per frame ≈ 11 frames.

Two fixes, both contained

Together a leaf routine's frame should drop from 256 to well under 128.

Workaround in place (not a fix)

examples/esp32/*/sdkconfig.defaults now set CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 and the same for the timer task, which makes the case above pass. Any user project needs the same line — that is the part worth removing.

Caution for whoever takes this

XT_EXPR_REGION was 256 and is now 192 precisely so that adding XT_OUTARG_REGION (64) keeps the total at 256 rather than crossing the rounding boundary and DOUBLING every frame. Measured 2026-08-02: at 512 bytes per frame the 19-deep print died where it now survives. Whatever replaces this arithmetic, check the rounding, not just the sum.

Acceptance

DONE 2026-08-02 — both fixes, and the stock stack now passes

Both changes from the plan above, together:

Acceptance met: test/test_esp_hw_validation.pas — whose Int64 print recurses 19 deep, the case that died — passes on esp32s3 with the stock 3584-byte task stack, with sdkconfig.defaults removed entirely.

The 8 KB setting stays in hello-s2/s3/c3 only, reworded as what it now is: headroom for the harness, which links whatever program esp_run.sh / esp_flash.sh is handed. timer-s3 lost it — an ordinary example should show what an ordinary project needs, which is nothing.

Verified: tools/gate.sh quick GREEN, make test-esp-bare 19/19, make test-esp-idf green on both chips, oracle match for the validation program on esp32s3 and esp32c3.

Log