Skip to content
This repository was archived by the owner on Aug 12, 2026. It is now read-only.

Commit 0ede3fd

Browse files
committed
0030: S4's first slice takes the survey's three decisions
utf8, because it is the spec default, Kotlin's shipped choice, zwasm's staging lesson, and identity-class against cljwit.host's own UTF-8 paths today — latin1+utf16 is the named revisit, gated on a string heap representation that does not exist yet. A bump-arena cabi_realloc with a grow-last-block fast path, reset at next entry — provably conservative, since canon_lift lifts results inside the call and sync reentry traps (spec lines cited), not hoped-safe as the first draft hedged. Per-export trampolines with the worked echo signature pinned: a lifted func(string)->string is (func (param i32 i32) (result i32)), the callee RETURNS the retptr — emitting a return-area param instead is zwasm's exact recorded bug, now impossible to re-derive wrong from the note alone. The survey behind it re-verified 0007 by running (no gc canonopt, wasmtime 47 panics at the same line), and the roadmap's 8,574-line sizing gains its decomposition: ~2,250 analogous lines, the rest engine-side work this project delegates. The review also caught a delegated figure absorbed unverified — 87 round-trip assertions where the test says 80 — four citations were spot-checked and the fifth check was the one that mattered; recorded in the note's own header. Claude-Session: https://claude.ai/code/session_01XF5Hfq4Ca2N2XYEzWQQuHt
1 parent 84661b6 commit 0ede3fd

3 files changed

Lines changed: 182 additions & 7 deletions

File tree

‎doc/design/0030-s4-first-slice.md‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# 0030 — S4's first slice: utf8 strings, a bump arena, per-export trampolines
2+
3+
**Status:** proposed · 2026-07-30 · adversarially reviewed the same day.
4+
The review strengthened three sections with spec citations the first
5+
draft hedged around (the arena's safety is provable, not hoped; the
6+
worked core signature; utf8 as identity-class on this host today), made
7+
the echo slice's honest scope explicit, and caught a delegated figure
8+
absorbed unverified — the survey said 87 round-trip assertions, the
9+
test says 80 — four citations were spot-checked and this number was
10+
not among them; the fifth check is the one that mattered.
11+
12+
## The question
13+
14+
S4's premise is verified (`bb spike-s4`: a cljwit-compiled component
15+
answers 6765 through `cljwit.host`), and the entry survey re-read the
16+
Canonical ABI at component-model `73b7ad51` (2026-07-28), re-verified
17+
`0007`'s frontier by running it, and read the two closest prior arts
18+
(Kotlin's WasmGC→component pipeline, zwasm's lift/lower layer). It
19+
ended with exactly three decisions the first real slice cannot dodge:
20+
the string encoding and what stands behind it on the guest heap, the
21+
`cabi_realloc` design, and the codegen strategy. This note takes them.
22+
Survey citations live here so the claims stay checkable; four were
23+
spot-checked verbatim before this note was written.
24+
25+
## The decision
26+
27+
1. **String encoding: `utf8`, declared in every `canon lift`.** The
28+
spec default (Explainer.md:1401 — "If no `string-encoding` option
29+
is specified, the default is `utf8`"), Kotlin's shipped choice
30+
(its wit-bindgen fork's `STRING_TO_MEM` is
31+
`encodeToByteArray()` — utf8 — at kotlin/src/lib.rs:708-709), and
32+
zwasm's staging lesson (debt D-502: utf8-only shipped and tested
33+
for weeks; the late pain was threading the option, not the codec).
34+
`latin1+utf16` — which the Explainer (line 1398) pitches as the
35+
JVM-family encoding — is **named as the revisit**, gated on the
36+
day cljwit has a general string heap representation whose code
37+
units are UTF-16-shaped; deciding it now would couple this slice
38+
to an S5 decision nothing measures yet. And the double-transcode
39+
worry resolves in utf8's favor *today* *(review)*: `cljwit.host`
40+
already feeds wasmtime UTF-8 bytes on every string path
41+
(`host.clj` 236/385 lower, 143/249/396 lift), so a `utf8` guest
42+
makes the host-side transcode identity-class, while
43+
`latin1+utf16` would *add* one on this exact path. The B6-class
44+
measurement belongs to the revisit, not to licensing this slice.
45+
2. **The v0 guest string is boundary-only: utf8 bytes in
46+
`(array i8)`.** No general string type lands here — literals stay
47+
out of the compile slice until S5 — but the marshalling layer needs
48+
*something* to lift into and lower out of, and a byte array is the
49+
smallest thing the Canonical ABI's `(ptr, code-units)` pair maps
50+
onto. B6's wide-payload lever (`(array i64)`, 7.5×) is **not**
51+
taken yet: for payloads under ~30 KB the call dominates the copy
52+
(`0013`/B6), and taking the lever before a measured workload wants
53+
it would be `0002`'s incident shape. The lever stays licensed
54+
(`0008`) and priced (B6), one representation swap away.
55+
3. **`cabi_realloc` is a bump arena with a grow-last-block fast path,
56+
reset at each export entry.** The shape Kotlin's stdlib ships
57+
(`MemoryAllocation.kt`: bump allocation, grow-in-place when the
58+
growing block is the newest — exactly what `store_string`'s
59+
optimistic utf8 transcode calls for, CanonicalABI.md:2857-2860)
60+
and the shape zwasm found to be "the ONE real coupling into the
61+
core". The free point is the *next entry's* reset — and that is
62+
**provably conservative, not hoped** *(review)*: the Canonical ABI
63+
lifts results *inside* the call (`canon_lift` runs
64+
`lift_flat_values` before returning, CanonicalABI.md:3657-3670;
65+
`post-return` exists only to free afterwards, and is a no-op in
66+
wasmtime 47 per `0013`), so any conforming host has copied out
67+
before the call completes, and `cljwit.host` lifts eagerly on top
68+
of that. Reentrancy cannot reach the reset in the sync world:
69+
`may_enter` is cleared for the call's duration and reentry *traps*
70+
(CanonicalABI.md:127, 166-180). Dated constraint carried from the
71+
survey: spec #680 (2026-07-24) gives `realloc` fresh-thread
72+
semantics — no layout change, but nothing in our `realloc` may
73+
depend on context state.
74+
4. **Per-export specialized trampolines, emitted by the compiler — no
75+
generic interpreted layer.** What every wit-bindgen backend and
76+
zwasm's fused store/load converge on, and where the spill and
77+
asymmetry rules get baked in rather than consulted: flat params
78+
over 16 spill through memory, flat results over 1 spill through a
79+
single pointer (CanonicalABI.md:3138-3140, 3611-3614), and the
80+
**retptr asymmetry** zwasm recorded as a lesson — a lifted export
81+
returning a non-flat value takes no return-area param and *returns*
82+
the pointer; a lowered import takes the caller's return area *in*
83+
(zwasm lesson 2026-06-20, read in `.ref/`, quoted in the survey).
84+
The worked example, so no implementer re-derives it wrong
85+
*(review)*: a lifted sync `func(string) → string` has core
86+
signature **`(func (param i32 i32) (result i32))`** — the params
87+
are the caller's (ptr, code-units), and the single result is a
88+
pointer the *callee returns* to its own 4-aligned 8-byte
89+
(ptr, code-units) pair (`flatten_functype`'s lift branch,
90+
CanonicalABI.md:3142-3155). Emitting a `(param i32)` return-area
91+
export instead is zwasm's exact recorded bug.
92+
5. **The slice order**: `func(string) -> string` (echo — forces
93+
realloc, both directions, the retptr shape) and `list<u8>` first;
94+
flat records as the memcpy-class case after; variants/options
95+
later — each landing with round-trip coverage against
96+
`cljwit.host`, whose lanes already marshal every `0012` row from
97+
the host side (80 assertions in `roundtrip_test.clj` — measured,
98+
after the survey's figure of 87 failed verification *(review)*),
99+
so every guest-side slice has a finished counterpart to meet.
100+
Honest scope *(review)*: the echo body is **identity on an opaque
101+
value** — no Clojure-level string exists in the slice, so echo
102+
tests trampolines and realloc, not string semantics; lift into
103+
`(array i8)`, hand the same reference back, lower out.
104+
6. **The roadmap's sizing figure gains its decomposition**: zwasm's
105+
8,574 lines verified by `wc -l`, but the lift/lower + realloc +
106+
handle-table core analogous to S4 is **~2,250 lines** — the rest
107+
is a component binary decoder, a validator, and WASI 0.3 async,
108+
all engine-side work this project delegates to wasm-tools and
109+
wasmtime. `doc/roadmap.md` carries a one-line pointer here.
110+
111+
## Why
112+
113+
- Every load-bearing external claim above was read at a pinned commit
114+
or run on the pinned toolchain on 2026-07-30, by a fresh-context
115+
survey whose citations carry paths and line numbers, four of them
116+
spot-checked verbatim afterward — the `0022`-incident protocol.
117+
- `0007` re-verified by running, unchanged: no `gc` canonopt exists in
118+
the grammar (Explainer.md:1381-1391); #525 is an open pre-proposal
119+
idle since 2026-04-23; wasm-tools 1.254 validates `(canon lift … gc)`
120+
behind `cm-gc`; wasmtime 47 panics on the call path at the same line
121+
`0007` recorded. The boundary is linear memory, still.
122+
- Kotlin — the closest production prior art — crosses the boundary
123+
with a per-byte loop and a bump arena. B6's 10× is the frontier,
124+
not a local deficiency; nobody has a bulk GC↔memory path.
125+
126+
## Alternatives rejected
127+
128+
- **`latin1+utf16` now.** The JVM-fit argument is real, but it binds
129+
the boundary encoding to a string representation that does not
130+
exist, and wasmtime transcodes host-side either way (`0012` L6).
131+
Revisit with the string heap type, as a measured decision.
132+
- **A generic runtime marshalling interpreter.** zwasm needed one — it
133+
is a host serving arbitrary components; a compiler knows every
134+
signature at build time, and the trampoline is exactly the code a
135+
generic layer would interpret.
136+
- **Taking B6's wide-representation lever now.** Unmeasured need;
137+
the swap is local when a workload demands it.
138+
- **`post-return` for arena reset.** Deprecated and a no-op in
139+
wasmtime 47 (`0013`, measured); resetting at next entry needs no
140+
cooperation that does not exist.
141+
- **Building `wit` parsing.** `wasm-tools component embed` consumes
142+
the WIT; the compiler only needs the WAT-level `canon` scaffolding
143+
`0007` already hand-verified.
144+
145+
## What would falsify this
146+
147+
- **The echo slice failing under `component new`'s export-name
148+
contract** (`memory`, `cabi_realloc`) — `0007` measured the
149+
requirement list once; the slice is its second, executable check.
150+
- **Arena-reset-at-entry surprising some host after all** — the spec
151+
says a conforming host lifts before the call returns
152+
(CanonicalABI.md:3657-3670), so this would mean a host violating
153+
the ABI; the slice's round-trip test covers it for `cljwit.host`,
154+
and a pipelined-calls test is named future coverage.
155+
- **#680's fresh-thread semantics acquiring teeth** (a wasmtime
156+
release actually running realloc on a fresh context) — nothing in
157+
the bump arena depends on context state, by construction; the
158+
constraint is carried so a future `realloc` never grows that
159+
dependency.
160+
- **The utf8 default colliding with a measured JVM-string workload**
161+
— that is the `latin1+utf16` revisit trigger, with B6-style
162+
numbers.

‎doc/roadmap.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ executes carries GC references across a component boundary, so this stage is a
124124
lift/lower layer over **linear memory**, plus `cabi_realloc`, plus resource
125125
handle tables. Scalar-only exports skip all of it. The sibling zwasm's
126126
equivalent is 8,574 lines and required no core changes — bounded work rather
127-
than a research problem, but a stage, not a detail of one.
127+
than a research problem, but a stage, not a detail of one. (Decomposed
128+
2026-07-30, `doc/design/0030`: the share analogous to S4's guest side is
129+
~2,250 lines — the rest is engine-side machinery this project delegates.)
128130

129131
### Running through all of it: two modes
130132

‎doc/status.md‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,23 @@ _Short by design, and printed at every session start — so findings live in
103103
component (`0007`'s scalar path, zero Canonical ABI machinery, the
104104
tag export tolerated), and **`cljwit.host` calls it from the JVM:
105105
6765 through the component boundary** (2026-07-30). Next unit: the
106-
S4 survey proper — the Canonical ABI's string/list lowering as
107-
specced *today*, the Kotlin sample read closely, `0007`'s dated
108-
claims re-verified — before a design note shapes S4's first real
109-
slice (strings are also what the corpus's message comparisons and
110-
printing wait on). S2's watcher waits for namespace loading,
111-
post-S3 compiler work either way.
106+
S4 survey proper — **done, and its design note is written**
107+
(`0030`, proposed, reviewed): the Canonical ABI re-read at a
108+
pinned commit (2026-07-28 HEAD), `0007` re-verified by running
109+
(no `gc` canonopt; #525 idle since April; wasmtime 47 still panics
110+
on the gc call path), Kotlin and zwasm read as prior art — Kotlin
111+
crosses the boundary with a per-byte loop and a bump arena, so
112+
B6's 10× is the frontier, not our deficiency. Decided: **utf8**
113+
(identity-class against `cljwit.host`'s own UTF-8 paths), a
114+
**bump-arena `cabi_realloc`** reset at next entry (provably
115+
conservative — hosts lift inside the call), **per-export
116+
trampolines** with the worked echo core signature pinned, slice
117+
order string-echo and `list<u8>` first — marshalling only, body is
118+
identity. Next unit: **the echo slice** — the emitter grows
119+
`memory` + `cabi_realloc` + string trampolines, round-tripped
120+
against `cljwit.host` (80 assertions on the other side — the
121+
survey said 87; the review measured). S2's watcher waits for
122+
namespace loading, post-S3 compiler work either way.
112123

113124
Done since the last update: `0016` `own<T>` handles, `0017` host imports
114125
(A–F), `0018` host-defined resources, `0012`'s `ex-data` contract shrunk to

0 commit comments

Comments
 (0)