Size the threadvar area from what the program declares, instead of a fixed cap
- Type: feature (codegen / emission size) — Track A, tag O
- Status: rainy-day — split out 2026-09-22 by frankb-8e when the parent closed on its shipped half. Deferred with a price, not parked for want of a mechanism.
- Parent: [[feature-a-the-threadvar-area-is-3072-bytes-of-bss-in-every-program-that-has-no-threadvar]] (routes A and C shipped; this is what was left)
What shipped, so this is not confused with it
- Route C —
-dPXX_TLS_USER_0/_1K/_2K/_4K/_8K/_16Ksets the size explicitly and always wins. - Route A — a Pascal source naming neither
threadvarnorusesgets a zero-byte area automatically, and BASIC got the same arm on the same precedent. It is a text scan because the token array is empty at the only moment the size may be chosen.
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:
- A
threadvarlanding anywhere inlib/rtl. Today there are none, so route A'susesterm 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. Notelib/rtl/sockets.pasandscheduler.pasboth hand-build per-thread tables and both would be candidates ifthreadvarwere not x86-64 only — see their comments, corrected 2026-09-22. - 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 ausesis ~92 KB. - 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.