Skip to content

Commit 2c63fe6

Browse files
authored
Merge pull request #5 from clojurewasm/develop/zig-016-migration
Migrate to Zig 0.16.0 + zwasm v1.11.0
2 parents 995d468 + 798d794 commit 2c63fe6

64 files changed

Lines changed: 1498 additions & 2913 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ClojureWasm
22

3-
Full-scratch Clojure implementation in Zig 0.15.2. Behavioral compatibility target.
3+
Full-scratch Clojure implementation in Zig 0.16.0. Behavioral compatibility target.
44
Reference: ClojureWasmBeta (via add-dir). Design: `.dev/future.md`. Memo: `.dev/memo.md`.
55

66
## Language Policy
@@ -287,10 +287,10 @@ Notes: `"JVM interop"`, `"builtin (upstream is pure clj)"`, `"stub"`, `"UPSTREAM
287287
See `.claude/rules/java-interop.md` (auto-loads on .clj/analyzer/builtin edits).
288288
Do NOT skip features that look JVM-specific — try Zig equivalents first.
289289

290-
## Zig 0.15.2 Pitfalls
290+
## Zig 0.16.0 Pitfalls
291291

292292
Check `.claude/references/zig-tips.md` first, then Zig stdlib at
293-
`/opt/homebrew/Cellar/zig/0.15.2/lib` or Beta's `docs/reference/zig_guide.md`.
293+
`/opt/homebrew/Cellar/zig/0.16.0/lib` or Beta's `docs/reference/zig_guide.md`.
294294

295295
## References
296296

.claude/references/zig-tips.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Zig 0.15.2 Tips & Pitfalls
1+
# Zig 0.16.0 Tips & Pitfalls
22

33
Common mistakes and workarounds discovered during development.
44

@@ -23,15 +23,15 @@ try list.append(allocator, 42); // allocator passed per call
2323

2424
```zig
2525
var buf: [4096]u8 = undefined;
26-
var writer = std.fs.File.stdout().writer(&buf);
26+
var writer = std.Io.File.stdout().writer(io_default.get(), &buf);
2727
const stdout = &writer.interface;
2828
// ... write ...
2929
try stdout.flush(); // don't forget
3030
```
3131

3232
## Use std.Io.Writer (type-erased) instead of anytype for writers
3333

34-
In 0.15.2, `std.Io.Writer` is the new type-erased writer.
34+
In 0.16.0, `std.Io.Writer` is the new type-erased writer.
3535
`GenericWriter` and `fixedBufferStream` are deprecated.
3636

3737
Prefer `*std.Io.Writer` over `anytype` for writer parameters.

.dev/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ the project direction.
3030

3131
### Prerequisites
3232

33-
- [Zig 0.15.2](https://ziglang.org/download/) (exact version required)
33+
- [Zig 0.16.0](https://ziglang.org/download/) (exact version required)
3434
- macOS Apple Silicon (primary development platform)
3535

3636
### Build & Test

.dev/archive/zig-016-migration.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Zig 0.15.2 → 0.16.0 Migration — Working Document
2+
3+
**Status**: In progress (branch `develop/zig-016-migration`)
4+
**Baseline**: commit `8bfbf5b` (`pre-zig-016` in `bench/history.yaml`)
5+
**Target zwasm**: v1.11.0 (released)
6+
7+
This is a **temporary** working doc. Delete after Phase 7 completion (or
8+
move learnings into `.dev/decisions.md` D## entry and `.dev/zig-tips.md`).
9+
10+
## Phase -1: zwasm Dependency Audit
11+
12+
### Existing -Dwasm Infrastructure (already in place)
13+
14+
`build.zig` already supports `-Dwasm=true|false` (default true) with full
15+
conditional gating. Zig source files using zwasm are already wrapped in
16+
`if (enable_wasm) ...` patterns. **No new build-side gating needed**.
17+
18+
- `build.zig:10``-Dwasm` flag definition
19+
- `build.zig:17` — propagated to `build_options.enable_wasm`
20+
- `build.zig:22-37``zwasm_mod` / `zwasm_native_mod` conditional dep
21+
- `build.zig:44,59,84` — conditional `addImport("zwasm", ...)`
22+
- `build.zig:115``wasm32-wasi` target does NOT depend on zwasm (correct)
23+
24+
Source-side gates already present:
25+
26+
- `src/runtime/wasm_types.zig:20``const zwasm = if (enable_wasm) @import("zwasm") else struct {};`
27+
- `src/lang/lib/cljw_wasm.zig:16``.enabled = wasm_types.enable_wasm`
28+
(NamespaceDef level — `cljw.wasm` namespace is unregistered when disabled)
29+
30+
### Verified working under `-Dwasm=false` on Zig 0.15.2
31+
32+
- `zig build -Dwasm=false` → exit 0 ✓
33+
- `zig build test -Dwasm=false` → exit 0 ✓ (Zig unit tests auto-skip)
34+
- `zig build -Doptimize=ReleaseSafe -Dwasm=false` → exit 0 ✓
35+
36+
### What FAILS under `-Dwasm=false` (needs Phase 0 work)
37+
38+
#### 1. E2E Wasm tests (test/e2e/wasm/, 6 files)
39+
40+
```
41+
test/e2e/wasm/01_basic_test.clj
42+
test/e2e/wasm/02_tinygo_test.clj
43+
test/e2e/wasm/03_host_functions_test.clj
44+
test/e2e/wasm/04_module_objects_test.clj
45+
test/e2e/wasm/05_wit_test.clj
46+
test/e2e/wasm/06_multi_module_test.clj
47+
```
48+
49+
All start with `(require '[cljw.wasm :as wasm])` → fail with "Could not
50+
locate cljw.wasm on load path" because the namespace is not registered.
51+
52+
#### 2. Test runners that unconditionally invoke wasm tests/benchmarks
53+
54+
| Runner | What breaks |
55+
|---|---|
56+
| `test/run_all.sh` | step "e2e tests (wasm)" calls `bash test/e2e/run_e2e.sh` (no dir filter) → all e2e dirs incl. wasm |
57+
| `test/e2e/run_e2e.sh` | no `--no-wasm` flag; finds all `*_test.clj` recursively |
58+
| `bench/wasm_bench.sh` | runs wasm benchmarks via TinyGo .wasm modules — needs cljw.wasm |
59+
| `bench/run_bench.sh` | runs benchmarks 21-25, 28-31 (9 wasm benchmarks) under `bench/benchmarks/` |
60+
61+
#### 3. Wasm benchmarks (bench/benchmarks/)
62+
63+
```
64+
21_wasm_load 22_wasm_call 23_wasm_memory 24_wasm_fib 25_wasm_sieve
65+
28_wasm_tgo_fib 29_wasm_tgo_tak 30_wasm_tgo_arith 31_wasm_tgo_sieve
66+
```
67+
68+
### Source files referencing WasmModule type (for migration awareness)
69+
70+
Already-gated, but require io threading in Phase 2:
71+
72+
- `src/runtime/wasm_types.zig` — main bridge
73+
- `src/runtime/wasm_wit_parser.zig` — WIT parser, uses @embedFile (no io)
74+
- `src/runtime/value.zig``.wasm_module` variant
75+
- `src/runtime/dispatch.zig` — invokeWasmFn dispatch
76+
- `src/runtime/gc.zig` — WasmModule finalizer registry
77+
- `src/lang/lib/cljw_wasm.zig` — NamespaceDef
78+
- `src/lang/lib/cljw_wasm_builtins.zig` — wasm/load, wasm/fn impl
79+
- `src/engine/vm/vm.zig`, `src/engine/evaluator/tree_walk.zig` — call sites
80+
- `src/app/repl/nrepl.zig:1427``#<WasmModule>` formatter
81+
- `src/app/deps.zig``cljw/wasm-deps` config parsing (test data only)
82+
83+
## Phase 0: Plan
84+
85+
Reduced scope thanks to existing infrastructure:
86+
87+
1. **Add `--no-wasm` flag to test runners**:
88+
- `test/run_all.sh` — skip "e2e tests (wasm)" step when `--no-wasm`
89+
- `test/e2e/run_e2e.sh` — skip `wasm/` directory when `--no-wasm` (or `WASM_DISABLED=1` env)
90+
- `bench/wasm_bench.sh` — early exit with friendly message when `--no-wasm`
91+
- `bench/run_bench.sh` — filter out wasm_* benchmarks when `--no-wasm`
92+
93+
2. **Update build.zig.zon**: `minimum_zig_version = "0.16.0"` (will be done as
94+
part of Phase 0 commit, even though we still build with 0.15.2 during the
95+
actual code migration phases — `.zon` is just metadata until we actually
96+
bump zig).
97+
98+
Actually: defer this to first 0.16-only commit so we can keep building
99+
with 0.15.2 during preparatory commits.
100+
101+
3. **Update zwasm dep tag**: defer to Phase 6 (currently v1.9.1, target v1.11.0).
102+
Until Phase 6, build with `-Dwasm=false` so the v1.9.1 zwasm dep is never resolved.
103+
104+
4. **Update `.dev/baselines.md`**: relax binary size cap (≤5.0MB → provisional
105+
≤5.5MB during migration, finalize in Phase 7).
106+
107+
5. **Doc/CI sweep**: grep "0.15.2", "Zig 0.15", update to "Zig 0.16.0":
108+
- `.claude/CLAUDE.md`
109+
- `.dev/baselines.md`, `.dev/decisions.md`, `.dev/references/*.md`
110+
- `README.md`
111+
- `flake.nix`, `flake.lock` (if present)
112+
- `.github/workflows/*.yml` (if present)
113+
- `scripts/*.sh`
114+
115+
## Decision: Gating mechanism for test runners
116+
117+
Use **`--no-wasm` flag** on each runner (matches existing `--quick`,
118+
`--tree-walk` patterns). Avoid env vars to keep behavior explicit.
119+
120+
`test/run_all.sh` will pass `--no-wasm` down to `run_e2e.sh` when invoked
121+
with `--no-wasm`, and skip `wasm_bench.sh` entirely.
122+
123+
## Open questions for Phase 6 (deferred)
124+
125+
- Does zwasm v1.11.0 export the same module interface as v1.9.1?
126+
(`zwasm.WasmModule`, `zwasm.Capabilities`, `zwasm.ImportEntry`, etc.)
127+
- Are there breaking API changes in zwasm v1.10.0 → v1.11.0 we'd need
128+
to absorb at the `wasm_types.zig` bridge?
129+
- Action: read `~/Documents/MyProducts/zwasm/CHANGELOG.md` v1.10.0 + v1.11.0
130+
notes when entering Phase 6.
131+
132+
## Phase 7: Atomic Toolchain Flip (deferred)
133+
134+
Once code migration is complete and tests are green on Zig 0.16.0,
135+
flip all toolchain pins and version-mention strings in a single commit.
136+
Doing this earlier creates a window where neither 0.15.2 nor 0.16.0 builds
137+
cleanly.
138+
139+
Files to update:
140+
141+
| File | Lines | Change |
142+
|---|---|---|
143+
| `build.zig.zon` | 11 | `.minimum_zig_version = "0.16.0"` |
144+
| `flake.nix` | 9, 20, 23, 27, 31, 35, 46, 58 | URLs and comments → 0.16.0 |
145+
| `flake.lock` | 71 | regenerate via `nix flake update zig-overlay` |
146+
| `.github/workflows/ci.yml` | 16, 74, 117 | `version: 0.16.0` |
147+
| `.github/workflows/nightly.yml` | 15, 59 | `version: 0.16.0` |
148+
| `.github/workflows/release.yml` | 32 | `version: 0.16.0` |
149+
| `README.md` | 5, 34 | badge + install link |
150+
| `.claude/CLAUDE.md` | 3, 290, 293 | intro + "Pitfalls" section header + path hint |
151+
| `.claude/references/zig-tips.md` | 1, 34 | title + body content |
152+
| `.dev/baselines.md` | 4 | "Zig 0.15.2" → "Zig 0.16.0" platform line |
153+
| `.dev/CONTRIBUTING.md` | 33 | install requirement |
154+
| `.dev/references/setup-orbstack.md` | 19, 30 | install + version check |
155+
| `.dev/references/ubuntu-testing-guide.md` | 56 | describe 0.16-specific behavior if changed |
156+
| `docs/differences.md` | 10 | runtime row |
157+
| `.dev/future.md` | 365 | check if still relevant |
158+
159+
DO NOT touch:
160+
- `.dev/archive/**` — historical phase notes
161+
- `.dev/decisions.md` D## entries that reference 0.15.2 — these are immutable history
162+
(D## about ArenaAllocator.free, @call always_tail, etc. — those decisions remain valid context)
163+
164+
After flip:
165+
- Re-run `bash test/run_all.sh` (no --no-wasm) on Zig 0.16.0
166+
- OrbStack Ubuntu validation: `--seed 0` still required? Re-test
167+
- Update binary size baseline to actual measured value
168+
- Add D## entry in `.dev/decisions.md` for the migration
169+
- Add F## in `.dev/checklist.md` for the libc strip follow-up (zwasm W46 equivalent)
170+
- Delete this file (`.dev/zig-016-migration.md`)

.dev/baselines.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
# Non-Functional Baselines
22

3-
Measured on: 2026-02-25 (v0.4.0 + GPA leak fix + JIT register fix)
4-
Platform: macOS ARM64 (Apple M4 Pro), Zig 0.15.2
3+
Measured on: 2026-04-27 (Zig 0.16.0 migration complete; HTTP / nREPL / line
4+
editor / `cljw build` runtime stubs remain — see Phase 7 follow-ups in
5+
.dev/checklist.md F##).
6+
Platform: macOS ARM64 (Apple M4 Pro), Zig 0.16.0
57
Binary: ReleaseSafe
68

79
## Profiles
810

911
| Profile | Binary | Startup | RSS | Notes |
1012
|---------|--------|---------|-----|-------|
11-
| wasm=true (default) | 4.76MB | 4.5ms | 7.9MB | Full feature set |
13+
| wasm=true (default) | 4.12MB | 4.1ms | 8.2MB | Full feature set, libc linked |
1214
| wasm=false | (not measured) ||| No zwasm dependency |
1315

1416
## Thresholds
1517

16-
All-Zig migration complete (Phases A-F, C.1). Binary size threshold RESTORED.
17-
Binary grew ~0.5MB due to embedded Clojure multiline strings (pprint, spec.alpha).
18-
Phase E optimization target: reduce back toward 4.3MB.
18+
Post-migration baselines (matched against pre-zig-016 history.yaml entry —
19+
no benchmark regressed beyond noise; lazy_chain actually improved).
20+
Binary is currently smaller than 0.15.2 because http_server / nrepl / the
21+
fancy line editor / `cljw build` were stubbed during the migration; restoring
22+
them under std.Io.net will likely add several hundred KB back.
1923

2024
| Metric | Baseline | Threshold | Margin | How to measure |
2125
|---------------------|------------|------------|--------|---------------------------------------------|
22-
| Binary size | 4.76 MB | 5.0 MB | +5% | `ls -la zig-out/bin/cljw` (after ReleaseSafe build) |
23-
| Startup time | 4.5 ms | 6.0 ms | 1.3x | `hyperfine -N --warmup 5 --runs 10 './zig-out/bin/cljw -e nil'` |
24-
| RSS (light) | 7.9 MB | 10 MB | +27% | `/usr/bin/time -l ./zig-out/bin/cljw -e nil 2>&1 \| grep 'maximum resident'` |
26+
| Binary size | 4.12 MB | 5.5 MB | +33% | `ls -la zig-out/bin/cljw` (after ReleaseSafe build) — slack for stub-restoration + libc |
27+
| Startup time | 4.1 ms | 6.0 ms | 1.5x | `hyperfine -N --warmup 5 --runs 10 './zig-out/bin/cljw -e nil'` |
28+
| RSS (light) | 8.2 MB | 10 MB | +22% | `/usr/bin/time -l ./zig-out/bin/cljw -e nil 2>&1 \| grep 'maximum resident'` |
2529
| Benchmark (any) | see below | 1.2x | +20% | Per-benchmark: `bash bench/run_bench.sh --bench=NAME --runs=10 --warmup=5` |
2630

2731
## `cljw build` Artifact Baselines (2026-02-20)

.dev/checklist.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,15 @@ Target Phase references: see `.dev/roadmap.md` Phase Tracker + Open Checklist It
3232
| F104 | Profile-guided optimization (extend IC) | 89 | Extend inline caching beyond monomorphic |
3333
| F105 | JIT compilation (expand beyond ARM64 PoC) | 90 | ARM64 hot-loop JIT done (Phase 37.4, D87). Future: x86_64 port, expand beyond integer loops. |
3434
| F120 | Native SIMD optimization (CW internals) | 89 | Investigate Zig `@Vector` for CW hot paths. Profile first. |
35+
36+
## Open follow-ups from the Zig 0.16.0 migration (D111)
37+
38+
| ID | Item | Trigger / notes |
39+
|------|------------------------------------------------------------|--------------------------------------------------------------------------------|
40+
| F140 | Restore HTTP server (`cljw.http/run-server`) on `std.Io.net` | Server / Stream / Connection were stubbed in `lang/builtins/http_server.zig` (D111). Reimplement accept loop on `std.Io.net.Server`, plumb `io` through handler dispatch, restore Ring request/response building. Original logic preserved in git history pre-`40d2f20`. |
41+
| F141 | Restore HTTP client (`cljw.http/get|post|put|delete`) | `std.http.Client` now has a `.io` field (D111). Wire `io_default.get()` and unstub `doHttpRequest`. |
42+
| F142 | Restore nREPL server | Whole `src/app/repl/nrepl.zig` (~1818 lines) collapsed to a stub during D111. Needs the same `std.Io.net` + accept loop work as F140 plus `std.posix.poll` replacement; sessions / mutex use `io_default` helpers. |
43+
| F143 | Restore raw-mode line editor | `src/app/repl/line_editor.zig` not yet ported (still on `std.fs.File` + `std.io.fixedBufferStream`). `runRepl` falls through to `runReplSimple` until this is done. |
44+
| F144 | Restore `cljw build` self-bundling | `std.fs.selfExePath` + `std.fs.openFileAbsolute` were removed in 0.16. Reimplement via argv[0] + `std.c.realpath` (or `_NSGetExecutablePath` / `/proc/self/exe`) and migrate file write loop. Stub in `runner.zig handleBuildCommand`. |
45+
| F145 | OrbStack Ubuntu re-validation under Zig 0.16.0 | `--seed 0` workaround was discovered on 0.15.2; re-test on 0.16.0 (Random.zig line numbers may have shifted). Run full `bash test/run_all.sh` + `bash bench/run_bench.sh` on Linux ARM64 + x86_64. |
46+
| F146 | Strip libc back out (`link_libc = false`) | zwasm v1.11.0 enables libc to satisfy the `std.posix.*` removals (D111). cf. zwasm W46. Once `std.Io` and the std.c usages in CW (`getcwd`, `getenv`, `realpath`, `mprotect`, `write`) all get pure-zig equivalents, drop libc to recover the pre-migration ~290 KB on Linux. |

.dev/decisions.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,3 +938,49 @@ instance state). Wasm linear memory remains separately managed per spec.
938938
at Engine construction. Requires zwasm D128 to be implemented first.
939939

940940
Related: zwasm D128, cw-new D13.
941+
942+
## D111: Zig 0.15.2 → 0.16.0 Migration
943+
944+
**Date**: 2026-04-27
945+
**Status**: Done
946+
**Decision**: Migrate the entire ClojureWasm tree from Zig 0.15.2 to 0.16.0,
947+
together with bumping zwasm to v1.11.0 (the first 0.16-compatible tag).
948+
Centralize the new `std.Io` model behind a process-wide accessor module
949+
`runtime/io_default.zig` so existing module-level mutexes, time helpers,
950+
env lookups, and sleeps don't have to thread `io` through every call site.
951+
952+
**Why now**: Zig 0.16 reshapes `std.Io` (Mutex/Condition/sleep/Timestamp
953+
all take `io: Io`), removes `std.fs.cwd` (replaced by `std.Io.Dir`), removes
954+
`std.posix.{getenv,write,isatty}`, and changes `pub fn main()` to
955+
`pub fn main(init: std.process.Init)`. Staying on 0.15.2 indefinitely
956+
forfeits stdlib improvements and forces zwasm to maintain a parallel branch.
957+
958+
**Approach**:
959+
960+
- *zwasm-first vs detach-then-reattach*: chose to upgrade zwasm to v1.11.0
961+
from the start (rejected the original "detach + Phase 6 reattach" plan).
962+
Reason: v1.11.0 is already 0.16-ready, so keeping zwasm in saved a whole
963+
reattach phase and let wasm e2e/bridge tests stay green throughout.
964+
- *io_default module*: production entry points (main, cache_gen) call
965+
`io_default.set(init.io)` at startup, so all module-level mutexes /
966+
Condition variables / nanoTimestamp / sleep / getenv pick up the real
967+
cancelable io. Tests fall through to a process-wide
968+
`std.Io.Threaded.init_single_threaded` default, except for the few that
969+
need real spawn semantics (shell tests) which install a local Threaded.
970+
- *libc linkage*: zwasm v1.11.0 enables `link_libc = true` by default
971+
(D135 in zwasm). CW inherits the libc-linked binary; we use std.c.getenv
972+
/ std.c.realpath / std.c.write / std.c.mprotect / std.c.getcwd in places
973+
where stdlib equivalents were removed. Stripping libc back out is a
974+
follow-up (F##; cf. zwasm's W46 sequence).
975+
- *temporary stubs*: HTTP server, nREPL, fancy line editor, and `cljw build`
976+
rely on `std.net` / `std.posix.poll` / raw-mode termios / `std.fs.selfExePath`
977+
— all gone or reshaped in 0.16. The full rewrite to `std.Io.net` + Smith
978+
fuzzing is non-trivial and was scoped out of this migration. Each is
979+
stubbed with a clear runtime error and tracked as a separate F## item.
980+
981+
**Verification**: 1324/1324 unit tests, 83/83 cljw test namespaces, 6/6 wasm
982+
e2e, deps.edn e2e all green on macOS aarch64. Bench history records
983+
`pre-zig-016` and `post-zig-016` entries; no individual benchmark regressed
984+
beyond noise; lazy_chain actually improved.
985+
986+
Related: zwasm D135 (Vm.io infra), Phase 7 follow-ups in `.dev/checklist.md`.

.dev/future.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ Two tracks that do not fully converge. GC and bytecode diverge.
362362

363363
- MarkSweepGc works on wasm32-wasi as-is (GPA→WasmPageAllocator, PoC validated)
364364
- Free-pool recycling ideal for Wasm (memory grows only, never shrinks)
365-
- WasmGC not usable: Zig 0.15.2 can't emit WasmGC instructions (struct.new, i31ref)
365+
- WasmGC not usable: Zig 0.16.0 can't emit WasmGC instructions (struct.new, i31ref)
366366
- Dynamic languages on Wasm (Python, Ruby) all use self-managed GC in linear memory
367367
- No comptime GC switching needed for MVP — same MarkSweepGc on both tracks
368368

0 commit comments

Comments
 (0)