← board

Size the threadvar area from what the program declares, instead of a fixed cap

What shipped, so this is not confused with it

Route A's reach is capped by its uses term, and that term cannot be dropped: without it a program whose imported unit declares a threadvar would be REFUSED, and programs that compile today must keep compiling. This ticket is the only thing that removes that cap.

Why it is deferred rather than blocked

The parent's blocker section named one obstacle and prescribed the wrong precedent for it, which made the whole route look unapproachable. The real map:

consumer site when route B?
the AN_INT_LIT fold pasparser_expr.inc:4451 parse time early
BSS reservation ir_codegen.inc:1992 before parsing early
two prologue displacements ir_codegen.inc:2005, :2009 before parsing early
clone stub carve, ×4 thread_emit.inc:114/120/164/182 at IR_CLONE lowering already late

The block's ADDRESS is already resolved late — the prologue reaches it through mov r9, @glob BSS_TLS_MAIN, a GlobFix[] entry the ELF writer patches at the end — so what is early is the BSS offset assignment, not the reference. And it is one backend: threadvar refuses off x86-64, so every site above is in the x86-64 path. The fold is the only cross-cutting one, and it is 2 of 25 __pxx* builtins that fold to a literal at all; the other 23 already allocate a node that survives to IR and emit.

Patch32 is not the mechanism — it is a within-pass backpatch of already-emitted bytes. PatchProcPrologue (symtab.inc:15533) is the precedent: EmitProcPrologue emits a placeholder 0 for the frame size and returns its code position, and the real number is written after the body, with a per-arch encoder.

So the cost is real but bounded, and the reason to defer is the failure mode, not the difficulty: too small a block means gs-relative slots write past the mapping — silent corruption, in the entry prologue every program runs through — for a measured ~2.9% of bss.

What would revive it

Stated as a mechanism so it does not decay when the demo changes:

  1. A threadvar landing anywhere in lib/rtl. Today there are none, so route A's uses term is a conservative approximation that happens to be exactly wrong 25 times out of 28. The moment one exists, every program that touches a unit pays the full area and this becomes the only way to pay for what is declared. Note lib/rtl/sockets.pas and scheduler.pas both hand-build per-thread tables and both would be candidates if threadvar were not x86-64 only — see their comments, corrected 2026-09-22.
  2. A hosted program whose bss is small enough for 3,072 to be a large share. Nothing under examples/ is: the floor for anything with a uses is ~92 KB.
  3. A thread register that can be SET off x86-64, which would make the area exist on other targets and multiply the value by the number of targets.

Acceptance

Not "bss got smaller". A program declaring N bytes of threadvar must reserve exactly what the slot map plus N needs, a program declaring none must reserve zero without any text scan, and TLSREFUSE_AREAFULL must become unreachable — it exists only because the cap is fixed, and its diagnostic text says so in its own words. Route A's two arms in ApplyTlsUserBytesOption and their fixtures should be DELETED by this change, not left beside it; if they survive, the cap did not really go.