Memory regions in WATX — deleting the magic numbers
Milestone 6 of docs/watx-migration-plan.md. This
document decides the feature before the bulk of it is written. Step 1
(region.declare-fixed, the whole map declared, the compiler validating it) has
landed; everything from §4 on is the design for the rest.
1. The goal
Each fixed base address appears exactly ONCE in the tree — in its region declaration. Every other occurrence — WAT arithmetic, JS mirrors, tools, tests, the docs table — references it symbolically or reads generated output.
Validation is not the goal. Validation is the safety net that makes the cleanup survivable. The goal is that the map becomes data: a thing you can move, because nothing else knows where it was.
That is a real distance from where the tree is. tools/region-census.js
(§7) counts 648 raw literals that are a second copy of the map, and
171 of the 221 (data ...) segments sit at absolute addresses inside no
declared region at all. Every one of those is a nail holding the map in
place.
What guarantees exist today
| Guarantee | Who provides it | What it cannot see |
|---|---|---|
| Regions do not overlap | test/test-wat-memory-map.js (build gate) |
anything not spelled (global $X i32 ...) + (global $X_SIZE i32 ...) |
| Free-space / collision queries | tools/wat-memory-map.js (on demand) |
same |
| WAT↔JS constant agreement | tools/check-wat-js-constants.js (build gate) |
a JS copy whose surrounding code was reshaped until the regex stopped matching |
| An address is inside its region | nobody | — |
| The map could be changed at all | nobody | — |
The last two rows are the feature. (i32.const 0x07F60000) written in two
files is the same token to every tool we have, and
(i32.add (global.get $DX_OBJECTS) (i32.mul (local.get $slot) (i32.const 0x400)))
with a $slot past $DX_MAX is a silent write into COM_WRAPPERS.
2. This is one family, not a new subsystem
The vendored compiler already carries almost everything Milestone 6 needs:
| Facility | Where | State |
|---|---|---|
layout declarations, hard-error unknown layout/field |
compiler-codegen.js ~26-62, ~521-547 |
complete |
load.field / store.field / load.elem / store.elem / size-of / offset-of / elem-addr |
compiler-codegen.js ~1121, 2544+ |
complete |
defmacro |
compiler-stages.js:43 |
complete |
region family: region.declare-static / -bump / -rc, region.alloc, region.enter/exit |
compiler-codegen.js:925, 2533 |
complete |
| a region symbol resolves to its base in operand position | compiler-codegen.js:1320-1327 |
complete |
Reused verbatim: the top-level collection scan; the (size N) spelling;
regionBase, the name→base map, and the bare-symbol resolution it feeds — a
declared region's $NAME in operand position already emits i32.const <base>
with no new code. And layout + load.field/store.field/size-of carry
Milestone 6 step 3 entirely: a WND record becomes a layout, its base stays the
region symbol, and
(load.field WndRecord hwnd (elem-addr WndRecord $WND_RECORDS (local.get $slot)))
is already a compilable sentence. Layouts-on-regions needs no compiler work at all, only source conversion.
Rejected — region.declare-static is FORBIDDEN for this map, as the
migration plan requires: the staticCursor allocation in region.declare-static,
which
lays regions out from STATIC_REGION_BASE = 1024 and would put Wine's map on
top of the decoder scratch, the window tables and NULL_SENTINEL. A
declare-fixed region contributes nothing to staticCursor or DATA_BASE, so
it cannot move the bump heap or the interned-string pool (pinned by a test).
Why not parallel machinery: two grammars for one concept, two name maps, two places to look when an address is wrong, and a permanent fork between what the vendored compiler understands upstream and what this repo understands. The provenance seal exists to keep the vendored copy explainable as a copy.
3. The core principle: allocated by default
A region is compiler-allocated. A fixed pin requires a documented reason, and there are exactly two admissible reasons:
- (a) guest-visible ABI — the value leaks into a pointer the guest itself holds and does arithmetic on.
- (b) an alignment or derivation law — the region's own arithmetic requires a property of the address (a power-of-two stride, a mask, a page alignment) that must be stated and held.
Chasing that test through the real map is what makes it useful, because most of the map fails it:
- Nothing below
GUEST_BASEneeds to be pinned. The string constants, the API hash table, the window/class/control tables, PE staging, the DLL table — every one of them is emulator-private state that the guest reaches only by forming a pointer below its own ImageBase, whichdocs/memory-map.mdalready describes as "safe by convention". Their addresses are historical hand-placement. Allocate them. GUEST_BASEitself is the single parameter of$g2w. It could float: everything downstream isguest - image_base + GUEST_BASE. It is pinned today because a dozen JS files hold the number, which is a §6 problem, not a §3 one.- The guest stack, heap and thunk zone are anchored by their GUEST VAs, not
their wasm offsets. The guest sees
ESPvalues, heap pointers and thunk EIPs; that is the real ABI. So the declaration must be able to say "this region's wasm base isg2w(<guest VA>)" — a derived base, not a magic wasm offset (§4.3). - Thread cache, page arenas and the block index need laws, not addresses.
Their arithmetic is
base + tid * stridewith a power-of-two stride and a mask derived from a capacity. Allocate with constraints (§4.2) rather than pin; the constraint is the thing that actually has to hold.
So region.declare-fixed stays in the family for the rare justified pin — and
as the migration's bridge (§9) — but the target state is allocation.
4. Compiler features
4.1 Allocated declarations, and a DETERMINISTIC allocator
(region.declare $WND_RECORDS (size 0x1800) (align 0x100)
(owner "per-window records, 256 x 24B"))
Reproducible builds are non-negotiable: the same source must yield the same
layout, or nothing downstream — byte identity, the shake test, a diffable
combined.wat — means anything.
Algorithm: declaration-order first-fit above a floor.
- Regions are ordered by the position of their declaration in the module's
top-level form sequence — the same order
src/main.watxfixes andtools/check-wat-manifest.jsgates. Not by name, not by size: those change under an unrelated rename or a capacity bump. - A cursor starts at
ALLOC_FLOOR, declared once per module:(region.floor 0x00000100). Wine's floor is0x100— below it livesNULL_SENTINELat0xF0, pinned by$g2w's sink behaviour. (This section originally said0x1000; wave 2 declared the real low string pool —$STRING_CONSTANTSat0x100,$VK_SCAN_TABLESat0x380— and a floor above them makes first-fit unable to reproduce the map, so §8.1 andtools/region-alloc.jsmoved it to0x100.) - Each region is placed at the first cursor position at or above the cursor
that satisfies its
(align N), and the cursor advances past it. First-fit above the cursor, never backfilling into an earlier gap — backfilling makes the layout depend on the size history of every earlier region. - Pinned (
declare-fixed) and derived regions are placed first, at their stated addresses, and are treated as obstacles the cursor skips.
Reproducing today's map exactly. Stage A of the migration must not move a byte, so the allocator has to be able to land on the current layout. Two mechanisms, and the design commits to both:
- Declaration order.
src/00-regions.watis already emitted in ascending base order, so first-fit reproduces the current sequence wherever the gaps are incidental. - Explicit gaps. Where the current map has a deliberate hole,
(region.gap 0x00040000 (reason "retired CACHE_INDEX_BASE, page compilation"))advances the cursor and documents why, instead of a mystery the next reader has to preserve out of fear. The gaps are load-bearing today precisely because nobody can prove they are not.
node tools/region-alloc.js --diff prints allocated-vs-current for every region
and must be empty before stage A can ship. If a gap cannot be explained, it is
declared with (reason "unknown, preserved") — an honest marker beats a silent
constant.
4.2 Constraints
(region.declare $THREAD_CACHE_BASE
(size 0x02000000) (align 0x00100000)
(stride $THREAD_CACHE_STRIDE (count 8)) ;; size == stride * count, exactly
(size-is-power-of-2)
(mask $CACHE_MASK) ;; mask == (size / entry) - 1
(owner "8 x 4MB per-thread decoded-code arenas"))
Each clause is a law the compiler enforces and, where the value is derived,
emits nowhere — the mask stays an ordinary global whose value is checked
against the region, until step 2 lets (region.mask $R) replace it. This is the
answer to the PAGE_DIR_ENTRIES 1024 / PAGE_DIR_MASK 1023 pair and to
DLL_TABLE_SIZE == DLL_TABLE_CAPACITY * 32, relationships
test/test-wat-memory-map.js today asserts by hand, one assert per pair.
4.3 Derived bases
(region.declare-derived $GUEST_STACK
(base (g2w 0x07100000)) ;; the guest VA is the ABI; the wasm offset follows
(size 0x00100000) (align 0x1000)
(owner "1MB main stack; the guest holds these as ESP"))
g2w here is a compile-time function of the module's own $GUEST_BASE region
and the image base, not a call. It makes the guest address the written
constant — which is the one that is actually an ABI — and lets the wasm offset
follow whatever GUEST_BASE ends up being. Today the relationship is written
backwards: the wasm offset is the constant and the guest VA is derived at
runtime.
4.4 Region-relative data segments
This is the largest single anchor and it is invisible from the WAT side today.
Measured over the real tree: 221 (data (i32.const 0x…) "…") segments, of
which 171 are at absolute addresses inside no declared region at all — the
string-constant pool from 0x100 upward, which has no _SIZE global and is
therefore invisible to wat-memory-map.js, test-wat-memory-map.js and the
declaration set alike. The other 50 sit inside declared regions
(GDI_BITMAP_FONT_STATIC 19, CLASS_NAME_STRINGS 12,
TT_FONT_STRING_STORAGE 8, and singletons).
So: an absolute data offset pins the map forever, and most of them are not even in a region. The design needs
(data (region.addr $CLASS_NAME_STRINGS 0x40) "Button\00")
— an active data segment whose offset is a region-relative constant, checked
against the region's extent at compile time and emitted as the same
i32.const the absolute form emits. Plus a declared home for the string pool
itself ($STRING_CONSTANTS), which is the prerequisite: a segment cannot be
region-relative until its region exists.
tools/check-data-strings.js and tools/wasm-data.js --overlaps keep working
unchanged — they read the compiled offsets, which do not change.
5. The symbolic spelling of every pattern
If a pattern cannot be written symbolically, its magic number survives. The inventory, with the spelling each one converts to:
| # | Pattern | Today | Symbolic spelling |
|---|---|---|---|
| 1 | Bare base | (global.get $DX_OBJECTS) / (i32.const 0x07F60000) |
$DX_OBJECTS |
| 2 | Base + constant offset | (i32.add (global.get $X) (i32.const 0x40)) |
(region.addr $X 0x40) — one i32.const, bounds-checked |
| 3 | Base + index × stride | (i32.add (global.get $WND_RECORDS) (i32.mul $slot (i32.const 24))) |
(elem-addr WndRecord $WND_RECORDS $slot) — stride is size-of the layout |
| 4 | Per-thread partition | (i32.add (global.get $THREAD_CACHE_BASE) (i32.mul $tid (i32.const 0x400000))) |
(region.slot $THREAD_CACHE_BASE $tid) — stride from (stride … (count N)), so the count is checked too |
| 5 | Region end / limit test | $THREAD_END, $THUNK_END — separate globals holding base + size |
(region.end $X); the twin global is generated or deleted |
| 6 | Mask from capacity | $PAGE_DIR_MASK 1023 beside $PAGE_DIR_ENTRIES 1024; $CACHE_MASK |
(region.mask $X) — derived, so it cannot be off by one |
| 7 | Capacity ↔ extent | DLL_TABLE_SIZE == DLL_TABLE_CAPACITY * 32, hand-asserted |
(stride 32 (count $DLL_TABLE_CAPACITY)) on the declaration |
| 8 | Guest↔wasm translation | guest - image_base + 0x12000 in $g2w and in five JS files |
(region.addr $GUEST_BASE …) in WAT; the generated JS mirror (§6) in JS |
| 9 | Window range test | eip >= thunk_guest_base && eip < thunk_guest_end |
(region.contains $THUNK_BASE x) over a derived region (§4.3) |
| 10 | Fixed data segment | (data (i32.const 0x11300) "…") |
(data (region.addr $STRING_CONSTANTS 0x…) "…") (§4.4) |
| 11 | JS copy | const DX_OBJECTS_WA = 0x07F60000 + a regex in check-wat-js-constants.js |
require('./regions.generated').REGIONS.DX_OBJECTS.base (§6) |
| 12 | Docs table | the hand-drawn diagram in docs/memory-map.md |
generated from the declarations (§6) |
Patterns 4-7 and 9-10 are the ones that need new spellings; 1-3 exist already.
Pattern 12 is why docs/memory-map.md currently still draws "Cache indexes
(256KB)" at 0x07152000, a region src/01-header.wat:1460 says page
compilation retired — a hand-drawn map goes stale silently.
5.1 What the survey found that changes the design
A file-by-file survey of how these addresses are actually used turned up six things the naive plan would have walked into.
The offset= memarg is a second, invisible constant addend. Base-plus-offset
is written two ways, and only one of them looks like arithmetic:
(i32.load (i32.add (global.get $PE_STAGING) (i32.const 0x3C))) ;; 08-pe-loader.wat:16
(i32.load offset=4 (global.get $DX_VTBL_REGISTRY)) ;; 09a8-…-directx.wat:210
No region check sees the second form today and none of §5's spellings covers it
either. (region.addr $R 0x…) must therefore be usable as the memarg base
with the offset= folded in and checked — otherwise converting the visible adds
just pushes the debt into the memarg.
The stride is usually a bare literal even when a global exists. WND_RECORDS
is addressed as base + slot * 24 (09c0-window-table.wat:12-13) while
$WND_RECORDS_SIZE 0x1800 = 256×24 sits unasserted next to it; DX_OBJECTS
writes 32 inline (09a8-…-directx.wat:606) although $DX_ENTRY_SIZE 32
exists eight lines away. There is also an inverse shape — address back to
index, (i32.div_u (i32.sub $entry $DX_OBJECTS) (i32.const 32)) at :610 — so
pattern 3 needs a (region.index $R addr) companion, not only elem-addr.
Per-thread partitioning does not use its own globals.
src/13-exports.wat:2665-2667 computes the thread cache partition from the raw
literals 0x05000000 and 0x400000, not from $THREAD_CACHE_BASE and not from
any stride global (none exists). $PAGE_DIR_STRIDE and $PAGE_INDEX_STRIDE do
exist and are used — and none of the three stride × 8 == _SIZE relations is
asserted anywhere. THREAD_RPC's partitioning has no WAT arithmetic at all: it
lives only in lib/guest-rpc.js:133.
A structural obstacle: a mutable global's initializer cannot global.get a
module-defined global. That is why $THREAD_BASE, $THREAD_END, $PAGE_DIR,
$PAGE_INDEX and $thread_alloc are declared with literal initializers
(01-header.wat:1530,1534,1539,1540,2378) that duplicate the map. A constant
region symbol is a i32.const, so (region.slot $R 0) can be a legal
initializer where (global.get $R) cannot — but the design must say so
explicitly, because "just use the global" is the obvious fix and it does not
compile.
Region ends are spelled three different ways, and one of them is another region's base.
- a redundant twin global:
$THUNK_END 0x07152000alongside$THUNK_BASE 0x07112000+$THUNK_BASE_SIZE 0x40000— the end is stated twice and the two are never checked against each other; - a mutable end recomputed at init, consumed as
end - slackwith four different bare slack literals (4096,16384,16384,32768at04-cache.wat:870,241and07-decoder.wat:3026,5353); - and, at
10-helpers.wat:592-596, a bound test against(global.get $PAGE_INDEX_ARENA)— the next region's base used as this region's limit, with a comment explaining thatTHREAD_CACHE_BASEwas the wrong choice. Under an allocator that shape is not merely ugly, it is wrong: it silently encodes an adjacency the allocator is free to change.(region.end $R)is the conversion, and this is the single clearest argument for the whole feature.
$g2w's direct window is a union of regions with no name. Its upper limit
is the bare literal 0x8000000 in three places (03-registers.wat:81,177,179),
which is $VIRTUAL_BACKING_BASE spelled as a number. The window covers
GUEST_BASE + stack + thunks + PE staging + the DLL tables, so it needs a
declared span — a region whose extent is the union of its members
((region.declare-span $DIRECT_WINDOW (covers $GUEST_BASE $GUEST_HEAP_BASE …)))
rather than an unnamed constant. The DIB window next to it is already fully
symbolic (03-registers.wat:86-92) and is the model to copy. The thunk-zone EIP
test is replicated in ~10 places with a bare 8 for the thunk stride.
Precedents that already exist and should be generalized, not reinvented.
test/test-wat-memory-map.js:342 already asserts by regex that the treeview
table uses $TV_TABLE and not a hard-coded 0x9000 — a one-off of exactly what
region-census.js --gate does for every region. And that file's
highFixedAliases allow-list (lines 200-241, 40 entries) is precisely what
(within $R) plus region.addr replaces: those are sub-fields declared as
absolute literals — $D3DIM_UNIMPL_EXEC_OP 0x07FEB000 inside $D3DIM_AUX, the
whole hand-packed 0x07F0CExx page — flattened at declaration so no gate can
see the containment.
5.2 Where the worst concentrations are
The survey's honest estimate is ~45 true magic-address sites in WAT, not the
several hundred a naive grep reports (window styles like 0x04000000 and colour
masks like 0x00FFFFFF fall inside $GUEST_BASE's 60 MB span). They cluster:
src/10b-gdi-font.wat(19) andsrc/10f-gdi-dc.wat(6) — the same six font-name addresses inside$GDI_BITMAP_FONT_STATIC(0x07F0A520,0x07F0A528,0x07F0A534,0x07F0A53C,0x07F0A564,0x07F0A5A0), duplicated across two files with no shared symbol.src/09a8b-handlers-opengl.wat(4) — the GL vendor/renderer/version strings at0x07F0BF60+, whose(data …)segments are declared in01-header.wat:947-950. The literal is written in one file and the data in another: exactly what §4.4's region-relative data segments fix.src/09ab-handlers-d3dim-core.wat(4 at declaration level) — sub-fields of$D3DIM_AUXdeclared as absolute constants.src/13-exports.wat(3),src/03-registers.wat(3),src/01-header.wat(5 initializers) — the per-thread and$g2wliterals above.
And the mask/stride debt is larger and more dangerous than the address debt.
Fourteen derived globals have no assertion tying them to the extent they come
from — $PAGE_DIR_MASK 1023 beside $PAGE_DIR_ENTRIES 1024, $CS_MASK 63
(which is not $CS_TABLE_ENTRIES 256 minus one), $PAGE_INDEX_SLOTS 128,
every $GDI_*_COUNT × _STRIDE vs its _SIZE, $WND_RECORDS_SIZE vs stride 24,
$THREAD_CACHE_BASE_SIZE vs the 8 threads. Two idioms are in use: a mask stored
as a literal, and a mask computed at the use site as SLOTS - 1
(10c-truetype.wat:3144,3160, 10g-gdi-raster.wat:3828). Only the second
cannot drift, and (region.mask $R) makes it the only one available. One
derivation has already rotted all the way through: tools/cache-slots.js:37-49
still greps $CACHE_MASK out of src/01-header.wat, and that global no longer
exists.
6. Generated mirrors
tools/gen-region-constants.js reads src/00-regions.wat (via the vendored
parser — one grammar for the map) and writes:
lib/regions.generated.js—REGIONS.NAME = { base, size, end }, frozen. Consumers convert, and each conversion deletes its clause fromtools/check-wat-js-constants.js: a value generated from the declaration cannot drift from it, so the regex that policed the copy has nothing left to police. That gate stays only for the constants that are not regions (Win16 module ids, the process-handle tag, GPU opcode word counts).The JS side is worse than that gate suggests. It covers named constants plus four hand-picked regex sites, and it does not see the roughly 24 open-coded
- imageBase + 0x12000g2w recomputations inlib/dll-loader.js(8),lib/host-imports.js(11),lib/filesystem.js,lib/gl-compat.jsandlib/app-profiles.js— each an independent copy of theGUEST_BASEABI.lib/mem-utils.js'sDIB_GUEST_BASE = 0x50000000is uncovered too. Intest/there are ~192 hits across ~90 files, of which onlytest/run.js's DX constants are gated;0x07F60000is retyped in at least four DirectDraw tests and0x07112000intest/test-cross-thread-send.js.test/test-mem-utils-dib-g2w.jsis the pattern to copy — it imports the constants instead of retyping them.One disambiguation trap to carry into any JS lint:
0x50000000is both$DIB_GUEST_BASEandWS_CHILD|WS_VISIBLE, and it appears as the latter intest/test-dialog-setfocus-tabstop.js:14andtest/test-def-dlg-proc.js:70. A literal is evidence, never proof — §7 again.the table in
docs/memory-map.md, between generated markers. The prose stays hand-written; the addresses stop being.
--check mode (regenerate, diff, exit 1) joins tools/build.sh beside
gen_dispatch.js --check and gen-host-import-sigs.js --check.
7. The census — a completeness detector
tools/region-census.js counts raw literals that are a second copy of the map,
per region and per file. It is importable (require('./region-census').census())
and has --json, --region=, --file=, --gate and --record.
Calibration is the whole design, and the obvious definition is useless. "Any
literal inside any declared region" counts 7136 sites, almost none of which
are the map: $GUEST_BASE is a 60 MB address space, so every guest VA and
every 0x400000 image base falls inside it, and $CLIENT_RECT is a 4 KB table
low in memory, so the GDI raster tests' colour constants (0x6A6A and friends)
land in it by arithmetic accident. A number that big cannot detect anything,
because nobody can tell a conversion from noise. So a site counts when it is
actually a second copy:
- BASE — the literal equals a declared region's base or exclusive end.
- INTERIOR — the literal is inside a declared table (≤ 64 KB) in the high
WAT-private map (≥
0x07000000), where a number that looks like an address is one.
Measured at the declaration commit: 648 sites — 541 base, 41 end, 66 interior. Banded by what kind of region they name:
| band | sites | confidence |
|---|---|---|
high private map (≥ 0x07000000) |
202 | near-certain debt |
low WAT tables (< 0x12000) |
255 | mixed — bases like 0x2000/0x3000 are also ordinary numbers |
| guest windows and spaces | 191 | mostly the base repeated (the 0x12000 copies in JS) |
Worst files: src/10b-gdi-font.wat 38, test/test-wat-gdi-raster-handlers.js
35, test/run.js 26, src/09a7c-mixer.wat 19.
The census is a completeness detector, not a pacing ratchet. With the
big-bang plan (§9) it answers "how many raw literals remain", and the answer
should go to approximately zero in one wave. --gate still refuses an
increase per file, so nothing regrows afterwards, and a region listed in the
baseline's converted array must stay at zero. Per-file rather than one total,
deliberately: a single number lets a cleanup in one file pay for a regression in
another, which is how ratchets stop ratcheting.
What it cannot do is prove completeness — a literal is evidence, not proof, and a missed conversion that happens to be spelled in decimal, or split across an add, is invisible to it. That is what §8 is for.
8. The shake test
A layout that never moves is a layout nobody has tested. Byte identity proves each conversion was exact; it cannot prove the conversions were complete, because a missed raw literal that still equals the right address produces identical bytes. The only way to find the ones that are left is to move the map and see what breaks.
WINE_REGION_SHAKE=gap bash tools/build.sh # insert a prime gap before each region
WINE_REGION_SHAKE=rotate bash tools/build.sh # rotate the allocation order
WINE_REGION_SHAKE=pad bash tools/build.sh # round every region up to the next prime page count
WINE_REGION_SHAKE=0x9E3779B9 bash tools/build.sh # a numeric value seeds a pseudorandom permutation
The env var reaches the allocator (§4.1) and nothing else; pinned and derived
regions are not shaken — moving GUEST_BASE or a guest-VA-anchored stack
changes the guest ABI, which is a different experiment. Prime-sized gaps are
deliberate: a shift that is a multiple of every stride in the tree can be
absorbed by an off-by-a-stride bug and stay green.
Acceptance: the 234-test pinned pool and the screenshot comparison suite
must be green under at least three distinct permutations, including one
gap and one rotate. A failure under shake localizes a missed literal —
the failing test names the subsystem, and region-census.js --file= names the
line. The loop is shake → fix → reshake, and it is the only evidence that the
map is data.
Running the pool under a shaken build is not free, so it is a
tools/shake-sweep.sh job and an acceptance gate for stage C, not a
per-commit gate. What ships afterwards is the natural allocation.
8.1 Gap reclamation — what has to happen before §8 can run at all
EXECUTED 2026-08-31 (wave 3). The plan below is what was done, and its numbers held: 167 regions allocated above floor
0x00000100, seven pinned, no gap forms,0x0047A000free below0x08000000unshaken and0x00209000in the tightest mode — against the ≈3.9 MB thepadmode needs. Two corrections are recorded in §13:$GUEST_BASEstays fixed rather than derived, and$GUEST_HEAP_BASEhad to declare the 4MB extent it was already using. §8's shake now runs a real app on a permuted map (tools/region-shake-smoke.js).
Stage A finished with a blocker: every shake mode overflows. The regions and their preserved gaps fill the 512 MB span end to end, so the allocator has nowhere to put the displacement a shake exists to create. This section is the measurement behind that sentence and the reclamation plan that clears it. It proposes no change to any region's placement — the whole point is that the shake is what moves things, and it cannot start from a map with no slack.
The arithmetic
Measured over the current declaration set (167 regions), floor 0x00000100:
| region bytes | 0x1F79BC8D |
| the map's last byte | 0x20000000 — exactly the end of memory |
| headroom above the map | 0 |
| holes inside the map | 45, totalling 0x00864273 (≈ 8.39 MiB) |
That is the entire budget. There is no other slack anywhere: three regions —
$VIRTUAL_BACKING_BASE (320 MB), $DIB_BACKING_BASE (63 MB) and $THREAD_RPC
(1 MB) — run from 0x08000000 to the last byte of memory with nothing between
them, because they were sized to consume whatever was left. So the ~8.39 MiB
of holes below 0x08000000 is not merely the cheapest reclamation, it is the
only one. Growing the memory is not an alternative: (memory 8192 8192 shared)
is an ABI the JS side allocates and every $g2w bound is stated against.
The 45 holes, classified
A hole is a stretch between one region's end and the next region's base. Twenty
of them need no action at all — the next region's declared (align N) already
accounts for the whole gap, so the allocator reproduces them for free and no
(region.gap …) form is written. The other 25 are the ones stage A had to spell
out explicitly, and they are what this plan is about.
| class | holes | bytes | reclaim? |
|---|---|---|---|
| round-address hand placement | 6 | 0x006F9780 |
yes — 81% of the budget |
| guest-ABI hole | 2 | 0x00112000 |
no — derive instead |
| retired region | 1 | 0x00040000 |
yes |
| hand-rounding / growth headroom | 16 | 0x00013204 |
yes |
| alignment padding | 20 | 0x000058EF |
n/a — no gap form exists |
Round-address hand placement (6 holes, 0x006F9780). The successor sits at
an address somebody typed because it was round, and the hole is the distance
from wherever the previous region happened to end. Six holes carry 81% of the
whole budget:
| hole | size | between |
|---|---|---|
0x079DE000 |
0x00422000 |
$WIN16_SEG_TABLE → $API_HASH_TABLE (at a round 0x07E00000) |
0x03E12000 |
0x001EE000 |
$GUEST_HEAP_BASE → $HANDLER_PAIR_HIST_COUNTS (at a round 0x04000000) |
0x04920000 |
0x000E0000 |
$PAGE_DIR_BASE → $WIN16_APP_DLL_STAGING (at 0x04A00000) |
0x07F1A000 |
0x00006000 |
$D3DIM_VIEWPORT_LIGHT_HEAD → $HIT_COUNT_BASE (at 0x07F20000) |
0x07F5E000 |
0x00002000 |
$DX_CURSOR_SAVE → $DX_OBJECTS (at 0x07F60000) |
0x079CE880 |
0x00001780 |
$THREAD_MSG_QUEUES → $GDI_NEAREST_CACHE (at 0x079D0000) |
A round base is §3's test failing out loud: none of these six successors has a guest-visible ABI or a derivation law, so the address is decoration. Reclaimed by deleting the gap form and letting the allocator pack.
Guest-ABI holes (2, 0x00112000) — the ones that must NOT be deleted.
| hole | size | between |
|---|---|---|
0x03C12000 |
0x00100000 |
$GUEST_BASE → $GUEST_HEAP_BASE |
0x07000000 |
0x00012000 |
$THREAD_CACHE_BASE → $GUEST_STACK |
Both successors are anchored by a guest address, so their wasm base is
g2w(VA) and the hole in front of them is the translation skew, not a decision.
The second one says so numerically: 0x07012000 − 0x07000000 is 0x00012000,
which is $GUEST_BASE exactly. Deleting these gaps would move a base the guest
holds. They are reclaimed by §4.3 instead — declare the successor
(region.declare-derived $GUEST_STACK (base (g2w 0x…)) …) and the hole becomes
computed from $GUEST_BASE rather than preserved as a mystery. That is
strictly better than reclaiming it: the space is still consumed, but nothing has
to remember why.
Retired region (1, 0x00040000). 0x07152000, between $THUNK_BASE and
$PE_STAGING, is the retired block-cache index — src/01-header.wat:1460 says
page compilation retired it, and §4.1 already names this hole as the example of
one that should carry a (reason …). docs/memory-map.md still draws "Cache
indexes (256KB)" there, which is §5's pattern 12 in the flesh. Reclaiming it and
deleting that row from the hand-drawn map are one change.
Hand-rounding / growth headroom (16, 0x00013204). Small holes where the
author left room beside a table. Individually trivial; the honest fix is that
room for a table to grow belongs in that table's (size N), where a bound gets
checked, not in an anonymous hole beside it where nothing does.
First, a blocker the wave-2 declarations introduced
node tools/region-alloc.js --diff — the gate §4.1 says must be empty before
stage A ships — is currently red:
$STRING_CONSTANTS at 0x00000100 is BELOW where the cursor already reached
(0x00001000); the declaration order is not ascending and first-fit cannot
reproduce it without backfilling
$STRING_CONSTANTS (0x100) and $VK_SCAN_TABLES (0x380) were declared in
wave 2 and both live below ALLOC_FLOOR = 0x1000. The floor was chosen for
NULL_SENTINEL at 0xF0 and the decoder scratch; the string pool starts at
0x100, immediately above the sentinel, so the floor should be
0x00000100, and tools/region-alloc.js's ALLOC_FLOOR with it. With that
one change --diff is empty again and the allocated form reproduces the map
exactly, ending at 0x20000000 with zero bytes to spare — which is the stage-A
finding, reproduced rather than assumed.
Feasibility, measured
Three reclamation policies over the same 167 regions, each asked of the compiler
under each shake mode. OVERFLOW is failure mode 15; a number is the slack left
below 0x08000000 after the allocated map is placed.
| policy | none | gap | pad | rotate | reverse | seed 0x9E3779B9 |
|---|---|---|---|---|---|---|
preserve (today: every hole an explicit region.gap) |
fits, 0 B | OVERFLOW | OVERFLOW | OVERFLOW | OVERFLOW | OVERFLOW |
| reclaim (no gap forms; all 167 allocated) | 8.36 MB | 4.48 MB | 4.52 MB | 8.36 MB | 8.32 MB | 8.25 MB |
| reclaim + pin (the recommendation below) | 4.48 MB | 2.08 MB | 2.06 MB | 4.48 MB | 5.60 MB | 6.49 MB |
Two things fall out of that table. First, the preserve row is the stage-A
blocker stated exactly: the unshaken map fits with zero bytes left, so every
permutation overflows and no amount of cleverness in the shake changes it.
Second, reclaiming the gaps is not merely necessary, it is sufficient — the
worst mode (pad, which spaces all 167 regions by a prime) needs ≈ 3.9 MB and
the budget is 8.39 MB. There is no third step to find.
Recommended shake configuration
(region.floor 0x00000100) ;; not 0x1000 — the string pool is at 0x100
- Floor
0x00000100. AboveNULL_SENTINEL, below$STRING_CONSTANTS. Fixesregion-alloc --diffas a side effect. - No
(region.gap …)forms at all. All 25 explicit gaps are deleted, and the 20 alignment-only holes never needed a form. The two guest-ABI holes come back computed through §4.3, not preserved. - Pin exactly seven regions — the shake must not touch them:
$VIRTUAL_BACKING_BASE,$DIB_BACKING_BASE,$THREAD_RPC— the backing windows. These are the two §8 names plus the RPC block wedged between them. They are guest-visible (the DIB window has its own translation class in$g2w; the sparse map hands guest pointers into the virtual backing) and they are sized to fill memory, so there is no layout in which they move.$GUEST_BASE,$GUEST_HEAP_BASE,$GUEST_STACK,$THUNK_BASE— the guest-VA-anchored set, asregion.declare-derived (base (g2w VA)). Derived regions are already excluded from the shake (§8), which is the right rule: moving these changes the guest ABI, a different experiment.
- Everything else — 160 regions — allocates and shakes.
Measured, that configuration fits under every mode with 2.06–6.49 MB clear of
0x08000000, so §8's "at least three distinct permutations" is available
immediately: gap, rotate and one numeric seed, with pad and reverse as
spares. The tight modes are gap and pad at ≈ 2 MB; if a later region grows
past that, the next reclamation is not another hole — it is
$VIRTUAL_BACKING_BASE's 320 MB, which is the only place left with room.
One property of this plan worth stating separately, because it is what makes
it safe to run at all: reclamation deletes declarations, it does not move
regions. Under WINE_REGION_SHAKE unset the allocator packs to a different map
than today's — that is stage D, not stage C — so the reclamation commit and the
shake commit must be separated by a byte-identity check that is expected to
fail, and the acceptance evidence for it is the test pool, never the hashes.
Until stage D ships, what builds is the pinned map.
9. The migration: big bang, verified by byte identity
Not a gradual per-region staging. The staging was rejected because a half-symbolized region is the worst of both worlds — it still cannot move, and it costs a conversion pass per region.
Stage A — compiler. Allocated declarations with the deterministic allocator,
constraints, derived bases, region-relative data segments. The allocator must
reproduce today's map exactly, proven by tools/region-alloc.js --diff
being empty and by the canonical artifacts staying byte-identical.
Stage B — symbolization, all at once. Convert every raw address literal in
the tree to region-symbolic form: all regions, all files, parallelized per file
across agents, using the spellings in §5. Acceptance is byte identity: with
the allocator reproducing the current map, a correct conversion changes no
bytes and an incorrect one does. 01daf6ccfbd115e3 / 0ee6414668129ac4 is
therefore a per-file, per-agent correctness oracle, not merely a final check —
which is exactly what makes the fan-out safe.
THE INSTRUMENT, RESTATED 2026-09-01 — those two hashes are history, and reading them as the oracle will send you looking for a machine that no longer exists.
01daf6ccfbd115e3/0ee6414668129ac4were the artifacts of one commit and are wrong today; and the two-compiler column that made them quotable — legacy beside WATX, agreeing to the byte — was retired in §11, becauselib/compile-wat.jscompiles aregion.addrtounreachablerather than refusing it and so cannot build this tree at all.What every wave since has actually used, and what the next one should: build-vs-build byte identity ACROSS THE CHANGE, in one tree. Compile
build/wine-assembly.wasm, make the edit, compile again, compare the sha256. Both arms then carry whatever else is uncommitted in a shared worktree, so the comparison stays controlled even when it is not reproducible from a clean checkout — which is the property a fixed hash cannot offer and is why a fixed hash goes stale the same afternoon somebody else lands a commit.The oracle still divides conversions the same way, and the division is the useful part. A change that only adds compile-time CHECKS is byte-identical and the hash proves it outright — the 32 region laws of
bd23c4c5are the worked example, dropping straight out atdab89c62e1279a8ac2b0bfe43ec43bfecb0ddd2b7404a17ac0e68f48caf3a260either side. A change that alters what is EMITTED cannot use it and must say so rather than quietly weakening the claim:region.addrconstant-folds where(i32.add (global.get $R) (i32.const N))emits three instructions, so71481e1e's 23 conversions moved the module by 29 bytes and were verified with a functional oracle instead (sol and notepad pixel-identical against a pristine worktree, plus the subsystem suites). Establish which of the two a spelling is BEFORE converting it, by reading the compiler's emission path, not by hoping the hash matches: discovering it after the fact is how a wave ends up with neither oracle.
Stage C — shake. §8. Iterate until three permutations are green.
Stage D — natural allocation ships. The map becomes data. Pins remain only for guest-visible ABI, expressed as derivations (§4.3), never as bare wasm offsets.
region.declare-fixed is the bridge: a region can be declared fixed at its
current address first — byte-identical, zero risk, already done for all 160 —
and un-pinned to allocated once §5's spellings cover its references.
10. Per-region verdict
The full 160-row table is src/00-regions.wat itself. The classes, and the
verdict for each:
| Region(s) | Current base | Verdict | Reason / constraints |
|---|---|---|---|
GUEST_BASE |
0x00012000 |
derive (pin until §6 lands) | the single parameter of $g2w; ~171 JS/WAT copies must become generated first |
GUEST_STACK |
0x07012000 |
derived base | guest holds these as ESP — pin the guest VA, derive the wasm offset |
GUEST_HEAP_BASE |
0x03D12000 |
derived base | guest holds these as heap pointers |
THUNK_BASE / THUNK_END |
0x07112000 |
derived base | thunk EIPs are guest-visible; THUNK_END becomes (region.end $THUNK_BASE) |
DIB_GUEST_BASE window |
0x50000000 → 0x1C000000 |
pin | a guest-visible ABI window with its own translation class in $g2w |
THREAD_CACHE_BASE, PAGE_INDEX_ARENA, PAGE_DIR_BASE |
0x05000000, 0x04100000, 0x04900000 |
allocate with constraints | base + tid*stride; power-of-two stride, count = thread count, mask derived |
PE_STAGING, DLL_TABLE, API_HASH_TABLE |
0x07192000, 0x07992000, 0x07E00000 |
allocate | emulator-private; historical hand-placement |
every table below GUEST_BASE (WND/class/control/timer/scroll/dialog/paint…) |
0x2000–0x12000 |
allocate | emulator-private; the guest reaches them only by convention |
the high private map (0x07E…–0x07FF…: GDI regions, DX objects, COM wrappers, TV tables, histograms) |
various | allocate | emulator-private; this is also where the census's 202 near-certain literals live |
string constants at 0x100+ |
undeclared | declare, then allocate | 171 data segments sit here with no region at all (§4.4) |
11. Rollback — VERDICT (the door has since been taken)
Read this section as history. Everything below was true of the tree at step 1, and it is kept because it records why the retirement was safe to schedule and what was measured before it.
WINE_WAT_COMPILER=legacyis a hard error as of24b79256— see the addendum at the end of this section. Present tense below means "at step 1", not "today".
WINE_WAT_COMPILER=legacy bash tools/build.sh worked at step 1 because src/
was standard WAT. Did a declaration end that?
Investigated, not assumed. lib/compile-wat.js dispatches top-level forms
through a flat if (head === '…') chain (lines 913-1037) over
iterTopLevel(exprs). There is no else and no whitelist: a form whose head
matches nothing falls out of the chain and the loop moves on. Unknown top-level
forms are silently ignored — a different fact from the previously recorded
"compile-wat only warns on unknown func calls", which is about expression
position.
Proven, twice. First against a synthetic part, then against the real thing:
bash tools/build.sh with all 160 declarations present produces, in both
modes,
watx tail 984347 B 01daf6ccfbd115e3 compat 984796 B 0ee6414668129ac4
legacy tail 984347 B 01daf6ccfbd115e3 compat 984796 B 0ee6414668129ac4
VERDICT AT STEP 1: declarations were legacy-safe, and rollback survived step 1 intact. It did not survive stage B, by design — see immediately below.
The retirement is scheduled, not avoided. Rollback survives top-level
declarations. It cannot survive either addressing spelling: a bare $REGION or
a (region.addr …) in expression position is not ignored harmlessly — and the
failure mode is worse than "fails validation". Legacy compiles an unknown
expression op to unreachable (lib/compile-wat.js:1528-1532, verified by
compiling a (region.addr …) under it: it builds, prints one unknown op
warning, and traps at runtime when that path executes). A legacy rollback of a
partially converted tree can therefore ship a module that instantiates cleanly
and dies mid-app. So:
Stage B's first converted file formally retires
WINE_WAT_COMPILER=legacy. That commit must say so, flip the migration plan's rollback checklist row, and update the mode comment intools/build.sh. It is a one-way door, taken deliberately at the start of stage B rather than as a side effect of some region conversion.
The door was taken 2026-08-31: wave 1 of the symbolization landed
expression- and data-position region spellings (d1a22e79 onward), and
WINE_WAT_COMPILER=legacy is now a hard error in tools/build-compile-wat.js.
The migration plan's §5.1 selector is marked retired.
12. Failure-mode catalogue
Every one is a hard compile error carrying file, line, col. None is a
warning: a memory map that compiles with a diagnostic nobody reads is the status
quo. Rows 1-14 are implemented; rows 15-20 accompany their feature.
| # | Condition | Message shape |
|---|---|---|
| 1 | Two regions' extents intersect | region.declare-fixed $B [0x07F70000,0x07F78000) overlaps $A [0x07F60000,0x07F80000) (declared at 00-regions.wat:41); use (within $A) if the nesting is deliberate |
| 2 | Region ends past initial memory | $A ends at 0x20001000, past the 0x20000000 bytes of initial memory (8192 pages) |
| 3 | Duplicate declaration | $A is already declared at 00-regions.wat:12 |
| 4 | Missing / dual extent | $A needs exactly one of (size N) or (end N) |
| 5 | end at or below base |
(end 0x800) is not above (base 0x1000) |
| 6 | Zero size | (size 0) — a region must have an extent |
| 7 | Unknown clause | unknown clause (sixe ...); expected base, size, end, align, owner, within |
| 8 | Duplicate clause | duplicate (base ...) clause |
| 9 | Misaligned base | base 0x00012004 is not a multiple of its (align 0x1000) |
| 10 | Non-power-of-two align | (align 12) is not a power of two |
| 11 | Non-integer literal | (size "big") is not an integer literal |
| 12 | (within …) names nothing / does not contain |
(within $OUTER) names no declared region · $A […) is not contained in $OUTER […) |
| 13 | region.addr on an unknown region |
unknown region $NOPE; declared regions are … |
| 14 | region.addr offset/span out of bounds, negative, or not a literal |
offset 0x800 runs past the region's 0x800 bytes · offset must be a non-negative integer literal |
| 15 | (4.1) allocation runs out of memory | allocating $A (0x…) past the 0x20000000 bytes of memory; the last placed region was $B |
| 16 | (4.1) an allocated region collides with a pin | $A cannot be allocated at 0x…: pinned $B occupies it (the allocator skips pins, so this means a pin above the floor with no room after it) |
| 17 | (4.2) a stride/count law fails | $A (size 0x2000000) is not (stride 0x400000) x (count 8) |
| 18 | (4.2) a power-of-two law fails | $A declares (size-is-power-of-2) but its size is 0x1800 |
| 19 | (4.3) a derived base has no $GUEST_BASE region |
(g2w 0x07100000) needs a declared $GUEST_BASE region |
| 20 | (4.4) a data segment's region-relative offset is out of bounds | (data (region.addr $A 0x900) …) with 0x20 bytes runs past the region's 0x800 |
13. Status
Landed (step 1):
region.declare-fixed+region.addr/region.size/region.endintools/watx-src/compiler-codegen.js, failure modes 1-14, provenance resealed.test/watx-compiler-regions.test.js— 60 checks.src/00-regions.wat— all 160 fixed regions declared, registered insrc/main.watxandWAT_FILES;tools/check-region-decls.jsholds them against the$NAME/$NAME_SIZEglobals intools/build.sh.tools/region-census.js— the odometer, calibrated in §7.- Byte identity proven in both compiler modes (§11).
Landed (stage A + wave 1, 2026-08-31):
- §4.1 allocator (
region.declarefirst-fit, deterministic;tools/region-alloc.js --emit/--diff/--prove,--diffEMPTY against the hand-placed map), §4.2 constraints (stride/mask/size-is-power-of-2, global operands for the derived assertions), §4.3 derived bases ((base (g2w VA))+region.image-base), §4.4 region-relative data segments, §8 shake (WINE_REGION_SHAKE=, banner, refuses a proves-nothing all-pinned shake), failure modes 15-20, and the round-6 collision lints.test/watx-compiler-alloc.test.js(76) + regions suite at 67. - §6 JS mirror:
tools/gen-region-map.js→lib/region-map.generated.js(single reader via check-region-decls;--checkgated in the build). - Wave 1 of stage B: ~96 sites symbolized across 14 src files + 11 lib files, every conversion proven byte-identical by paired HEAD-vs-HEAD+file compiles; census banked 662→566. Legacy compiler retired (§11, taken).
Wave-1 corrections to this design: the census's per-file counts ran ~44%
false positives in flag-heavy files (0x1000/0x2000/0x4000 VT_/OF_/style
constants colliding with low region bases) — §7's "evidence, never proof"
understated it; treat counts as leads only. Seven real fixed addresses matched
only as an adjacent region's exclusive end (0xD160, 0x5110, 0x11500,
0x11D80, 0x3E00, 0x3180…) — per §5.1 those need their own declarations,
never (region.end $NEIGHBOR). $CLASS_NAME_STRINGS is under-declared (0x80
declared, block runs past 0x3240).
Landed (wave 2, 2026-08-31):
- §5.1's
region.declare-span— a named address LIMIT, transparent to the overlap check because the regions it bounds live inside it, never allocated and never shaken, with(owner "…")mandatory for the reason(reason "…")is mandatory onregion.gap.$DIRECT_WINDOW[0, 0x08000000)is declared, and$g2w's three0x8000000literals insrc/03-registers.watnow read(region.end $DIRECT_WINDOW). Byte-identical,regionssuite 67 → 119. - A hole in §4.4 closed on the way past: active data segments accepted
(region.end $R)and(region.size $R)as offsets with failure mode 20's payload-length bounds check silently skipped. Neither is an addressable location;region.addris now the only region-relative data offset. - §8.1 — the gap-reclamation plan, measured: 45 holes, 8.39 MiB, and the empirical finding that reclaiming them is both necessary and sufficient for every shake mode.
Landed (wave 3, 2026-08-31) — §8.1 EXECUTED:
The mirrors first, because they were the real blocker and this plan did not name them. Every region has a
(global $R i32 …)/$R_SIZEpair and 1066global.getsites read them; a literal mirror pins its region, so the map could not move until all 348 were(region.addr $R 0)/(region.size $R). A region constant may now initialize a global (compiler-codegen.js, resolved through the sameregionConstValueas the operand and data positions; regions suite 119 → 140). Plus 67 interior aliases — globals holding an address inside a region under another name,$STATIC_SYS_DIR=$RESERVED_PAGE_STRINGS + 0x74and the like — and the last two literal-anchored data segments. All byte-identical, which is the oracle a conversion wave has while the regions are still pinned.The map is allocated. 167 of 175 regions are
region.declareabove(region.floor 0x00000100); there are noregion.gapforms and never were any in the source (stage A's 25 were synthesized byregion-alloc --emit). Seven stay pinned:$GUEST_BASEfixed (it is what(g2w …)resolves through, so it cannot be expressed in terms of itself),$GUEST_HEAP_BASE/$GUEST_STACK/$THUNK_BASEderived at(g2w 0x04100000 / 0x07400000 / 0x07500000), and the three backing windows fixed. 35 holes remain, all alignment padding.§8's shake is executed, not merely available.
tools/region-shake-smoke.jsbuilds the shaken wasm AND the matching JS mirror (gen-region-map --shake --out,$WINE_REGION_MAP) — pairing them is not optional, because a shaken artifact against the canonical mirror reads the wrong bytes and draws a plausible wrong picture instead of failing. Measured: sol undergap,rotateand seed0x9E3779B9, marbles underpad, 0 of 307200 pixels differ in every case. The slack each mode left below0x08000000at that commit reproduced §8.1's predicted table to the byte (gap0x0020C000, pad0x00209000, rotate0x00479D80, reverse0x0059AC80, seed0x00608900).Those five numbers are historical and were never re-measured. Every one of them is wrong today, because the map has grown since. Re-measured 2026-08-31: gap
0x001B1000, pad0x001B8000, rotate0x00000000, reverse0x00563D80, seed0x0048B800. Rotate's is not a typo — under that permutation$TV_IMAGE_TABLEends on$VIRTUAL_BACKING_BASE's first byte exactly, with the map perfectly packed and not one spare byte below the ceiling.A written-down slack figure is a copy of the map by another name, and it went stale the same silent way every other copy does — so it is now computed, not quoted:
node tools/region-alloc.js --shake-allprints the table and exits nonzero when any mode cannot be placed, andtools/build.shruns it beside the other region gates. That is the gate §8 always needed. Read the tool's output for the current numbers; the ones above are a record of two measurements, not a specification.Two things the re-measurement corrected. First, rotate's zero is not a one-page knife edge:
placeShakenAroundPinsis best fit, so growing a region by a page re-drains the small regions into different windows and rotate comes back with0x280spare. The zero is what perfect packing looks like. Second, the number that actually bounds growth is therefore the tightest mode's slack,gap's0x001B1000— and about 6 MB of new region below the ceiling is what it takes to make any mode fail.
Wave-3 corrections to this design.
The seven pins are four fixed + three derived, not "four derived". §8.1 asks
for $GUEST_BASE as region.declare-derived; the compiler refuses it, and is
right to — a derived base is g2w(VA), and g2w is defined by $GUEST_BASE.
A region's neighbour is not a bound. Reclaiming the holes put
$PAGE_INDEX_ARENA immediately above the guest heap, and $heap_low_reserve
stopped there — it had been growing 4MB into an anonymous hole that the
hand-placed map happened to leave. $GUEST_HEAP_BASE declares 0x3EE000 now
and the WAT bound is (region.end $GUEST_HEAP_BASE). This is the failure the
reclamation exists to surface, and the only one the whole corpus produced.
The census measures something different now. It counted a literal equal to a
region base as a copy of the map. An allocated base is an output — written
down nowhere, so nothing can be a second copy of it — and packing put regions
at 0x1000, 0x2000, 0x3000, 0x4000 and 0x10000, which took the count
from 351 to 884 on coincidence alone without one line of source changing. It
now counts base/end literals for PINNED regions only, plus the interior rule:
70 across 35 files, which is the guest ABI and the high tables — the part
that was ever evidence.
What a moved region actually breaks is JS, and it breaks quietly. Three
copies survived the mirror sweep because they were in lib/, not in WAT:
$DI_MOUSE_INPUT_STATE in lib/renderer-input.js and $VIRTUAL_MAP_STATE /
$VIRTUAL_MAP_TABLE in both lib/mem-utils.js and renderer-input.js. None
of them threw. The relative-mouse tests read zeros out of a cell nobody writes
any more, and the stale virtual-map walk translated two font files' reads to
the wrong place, so test-wat-font-metrics-reference reported Times New Roman
and Courier New drifting off their measured budgets — a rasterizer-shaped
symptom six regions from its cause. The census gate is the standing defence;
the practical rule is that a JS file may not contain a hex literal that equals
a region base, and the three that did now read the generated mirror.
Next, in order: wire the shake into a scheduled run over more than two apps
(the harness takes --app/--modes, nothing consumes it automatically yet);
then decide whether $VIRTUAL_BACKING_BASE's 320 MB is the right shape now
that it is the only slack left in the map.