Skip to content

Release v1.15.5 to v1.15.8 from BuddhiLW/main - #21

Merged
BuddhiLW merged 134 commits into
clojurewasm:mainfrom
BuddhiLW:main
Nov 11, 2026
Merged

BuddhiLW merged 134 commits into
clojurewasm:mainfrom
BuddhiLW:main

Conversation

@BuddhiLW

@BuddhiLW BuddhiLW commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Brings clojurewasm/ClojureWasm main up to v1.15.7 (fork tag 706d327), covering v1.15.5, v1.15.6 and v1.15.7.

  • v1.15.5: transient maps and sets edit owned HAMT paths in place during into; cheaper small GC cells; inc/dec compile to direct ops.
  • v1.15.6: cljw.http.server accepts a byte-array :body verbatim; the docs/examples/fractal/ Mandelbrot example with its two e2e gates.
  • v1.15.7: reduce-kv on vectors indexes in Zig (-71.7% instructions); derive raises clj's own errors and class ancestors reach Object; glibc mmap threshold pinned at startup; the fractal example grows to nineteen engines and four formulas with Julia sets.

See CHANGELOG.md [1.15.5], [1.15.6] and [1.15.7].

dependabot Bot and others added 30 commits July 22, 2026 00:34
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…ision, double-array reduce, float loop, byte-array from seq)
…O-065)

fastBinaryFixnum becomes fastBinaryNumber: a double paired with a double
or fixnum computes + - * < <= > >= in f64, exactly what
promote.*Promoting and math.pairwise do once either operand is a double.
= not= mod rem quot still defer to the builtin (cross-category equality,
float sign-of-zero rules). The float arm sits out of line
(fastNonFixnum) so the fixnum dispatch keeps its shape; inline, it cost
04_arith_loop +9%. diff_test pins 16 float cases VM == TreeWalk.

User instructions, ReleaseSafe -Dwasm, taskset -c 2, min of 2-3,
O-064 commit -> this commit:
  40 seq_dot_1k        13.25G -> 11.65G  -12%
  41 seq_dot_100k      13.74G -> 12.21G  -11%
  44 float_loop_recur   2.52G ->  1.86G  -26%
  02 fib_loop           67.0M ->  66.8M    0%
  04 arith_loop        778.0M -> 810.8M  +4.2% (accepted: dispatch shape)
  01 fib_recursive     355.8M -> 362.4M  +1.8%

Card: CLJW-NUMERIC-SEQ-PERF (20261021235636-3459e0ce).
agent, locking, concurrency_stress, the java.io family, reducers, repl,
json_fs, tokenizer_long_input, extend_via_metadata and bfs_queue_gc now run
in the shared suite image. shutdown-agents cases stay in a child cljw since
the call is process-global. Divergence pins repointed to the suites;
e2e ceiling 322 -> 305.

Smell-audited: 0: no smells; scripts deleted with their run_all rows and fixtures
… a suite

gc_torture, http_client, http_server, load_file_source, repl_classpath,
stdin_read_line, vm_error_loc_sidecar, wasm_polyglot and eval_budget each
assert something only a fresh cljw process shows (startup env, a bound
port, stdin, rendered stderr, exit codes, external toolchains), so they
carry the e2e-process-boundary marker with the reason. eval_budget's
in-process cljw.eval/with-budget cases move to suites/eval_budget_test.
Counted scripts 305 -> 296.

Smell-audited: 0: no smells; each marker reason checked against the script body
phase16_wasm_component, _component_multiexport and _component_budget and
the catchability half of phase16_wasm_ffi become deftests:
wasm_component_test, wasm_component_multiexport_test (the ADR-0135 echo
table, one assertion per row), wasm_component_budget_test and
wasm_ffi_catch_test. wasm_ffi keeps only the exit-time leak report and is
marked a process boundary. Probe fixtures deleted, debt refs repointed.
Counted scripts 296 -> 292.

Smell-audited: 0: no smells; every probe marker kept as an assertion message
The linear-memory probe and the engine-selection probe become
wasm_memory_test and wasm_engine_select_test, one assertion per probe
label. scripts/run_wasm_gate.sh now also runs every wasm_*_test suite so
the wasm-only quick path keeps its coverage. Counted scripts 292 -> 290.

Smell-audited: 0: no smells; run_wasm_gate exercised against the suites
test/run_all.sh: both sides deleted rows for migrated scripts (phase15
set_bang/stm_dosync by #89, phase16 locking/agent/concurrency_stress here);
none of the five scripts exists after the merge, so neither side's rows
stay. check_e2e_count.sh baseline is the combined count: 231 (18 exempt by
marker). check_e2e_reach and check_e2e_dup pass.

Smell-audited: 0: merge of two deletions plus the recomputed ceiling
Each node of a one-coll map/filter/take/iterate chain allocated an
interpreted arity-0 closure and re-entered eval to run its body. Over an
unchunked source such as (range) = (iterate inc 0) that was one closure
and one eval re-entry per element per layer.

A LazySeq can now hold a builtin step plus two operands
(lazy_seq.allocNative). The steps in lang/primitive/lazy_step.zig build
the same nodes as the clj bodies: chunk-cons over a chunked source via
the O-032 drains, cons plus the next node otherwise. filter skips
rejected elements and empty chunks within one realization. iterate does
not call f for its head, as clj's Iterate. take keeps the interpreted
body for a non-integer n.

lazy_chain (3 reps): 1208 ms -> ~270 ms, bb 141 ms.
Realization counts are pinned against bb in lazy_step_test.clj.
VM and tree_walk give the same results under --compare.

Smell-audited: 1: one native-step slot on LazySeq instead of a node type per producer
lazy_chain 13.75x -> 2.28x bb on the cherry-pick of #91 onto v1.15.4
staging (bench/vs_bb -n 3, load 12.6).

Smell-audited: 1: changelog and bench row only, no code
Smell-audited: 1: changelog only, no code
zwasm d77568a0 (BuddhiLW/zwasm fix/wasi-p2-adapter-imports, upstream
zwasm/zwasm#634): a component built with the wasi_snapshot_preview1
adapter no longer fails wasm/load-component with UnknownImport. The
adapter's env::memory re-export resolves, and filesystem-error-code plus
the 0.2 clock resolution rows are served. This pins a fix branch only
until fork #7 merges (fork Actions are disabled); the merge has the same
tree, so the .hash carries over.
perf(lazy): native lazy nodes for map/filter/take/iterate (lazy_chain 13.75x -> 2.28x bb)
A vector's TailNode carried an inline [32]Value (a 272-byte cell for a
2-vector) and an ArrayMap an inline [16]Value (152 bytes for a 2-key map).
Both now keep their slots as trailing storage in the same GC cell
(allocWithTail, the ADR-0184 shape), sized to len / 2*count and read
through items() / entries(). Every persistent change copies the cell
anyway, so the reserve bought nothing.

200k retained {:id i :v [i (inc i)]}: last_live 97.2 -> 29.6 MB
(486 -> 148 B per entry). gc_churn: bytes_alloc 497 -> 175 MB, peak RSS
353 -> 160 MB. Instructions per literal are unchanged (perf stat,
pinned): the remaining time gap to bb is VM dispatch, recorded in O-091.

Smell-audited: 1: removes a fixed-capacity reserve that no persistent op used; one cell per node kept, no new seam
A String paid two allocations: the GC cell and a separate gpa.dupe of its
bytes, freed again by the per-tag finaliser at sweep. perf of
(count (str i)) in a loop showed libc malloc/free at about 12% of samples.

Payloads up to page_heap.max_small_bytes - @sizeof(String) now live in the
tail of the String's own cell (allocWithTail). A storage enum in a former
padding byte tells the finaliser and ownedBytes which arm applies; larger
payloads keep the owned infra block. The inline alloc runs inside a
fabrication region, since byte_view may be the tail of an unrooted String.

Pinned user instructions: str loop 2790 -> 2300 per iteration over the
empty loop; vs_bb string_ops 5959M -> 5219M, gc_churn 8369M -> 8011M.

Smell-audited: 1: stale module doc that argued against inline storage rewritten; no address-adjacency test, explicit storage tag instead
perf(eval): inline f64 arith/compare in the VM intrinsics (O-065) + numeric seq bench cells
Splits gc_churn into the retained build, the full churn loop, the small
map+vector literal, (str i) and the empty recur loop, timed per piece.
Runs on cljw and bb alike. The measurements behind CLJW-PERF-SMALL-LITERAL,
CLJW-PERF-STR-1ARG and CLJW-PERF-RECUR-LOOP came from it.

Smell-audited: 1: bench probe only, no runtime code
perf(collection): size vector tails and array maps to their contents
perf(string): store short string payloads inline in the GC cell
threshold = max(floor, last_live) instead of max(floor, 2 * last_live).
Peak committed heap is about live + threshold, so the 2x growth factor
held a large live set near 3x itself in RSS. Shrinking the retained empty
pages instead does not lower the peak (the next cycle maps them again,
pages_released 89 -> 11117), so the growth factor is the lever. Adds an
adaptiveThreshold unit test and a heap-return suite case (peak committed
under 2.5x live; base measures 3.0x, new 2.0x).

Smell-audited: 1: one-line policy change in gc_trigger, no new knob, retention budget untouched, three doc comments updated
The bench-vs-bb job restores the gate's x86_64-linux .zig-cache read-only.
That cache can hold a WriteFiles manifest (stub_wf, which writes
bootstrap_sources_stub.zig for cache_gen) whose o/<digest> output dir is
gone. Zig then fails with "failed to check cache ... file_hash
FileNotFound" instead of rebuilding. It hit #99 twice on 2026-10-24, from
a cache saved by a green gate run, so the save-side guard in ci.yml (never
save a cancelled job) does not cover this case.

The job never saves the cache, so the fix is local: on a failed build,
drop .zig-cache and build cold once.

Smell-audited: 1: read-only consumer recovers locally; no save-side change without a reproducer
…line fastBinaryNumber

Recovered work-in-progress from the previous session (CLJW-PERF-RECUR-LOOP).

Smell-audited: 1: WIP recovery, no new smells; cold ops dispatched via toCold prong
BuddhiLW and others added 28 commits November 11, 2026 11:31
A jolt --library build exports jolt_library_init and jolt_lookup only;
the export! entry points are function pointers, not dynamic symbols.
cljw runs the init (221 to 495 ms) and the lookups by name today. An
interim C shim in the example dir re-exports the looked-up pointers by
name, so add, mandel and the hive C ABI run now, and hive_call.clj
drives the jolt library unchanged. The call-by-address form is written
in jolt_lib.clj and reported pending CLJW-FFI-CALL-ADDRESS.

build.sh extracts the Chez kernel the prebuilt jolt embeds and links a
chez front end, because jolt 0.8.19 --library otherwise fails on a
missing scheme.h and a missing chez executable.
Best-of-N harness across cljw, bb and jolt. Findings in hive memory
20261111113312-4b8b6ce6.
cljw.ffi resolved a C function only by (lib, name), so a function pointer
a C API hands back (a vtable slot, a plugin registry, jolt_lookup) was
unreachable. (ffi/function-at addr arg-types ret-type) turns an integer
code address into an ordinary Clojure fn with the same types and limits
as function.

A separate fn, not an overload of function: the :closed rule stays on
the one constructor whose code a library owns. New leaves -prepare-at and
-invoke-at take no library; a Function records in state[2] whether a
library owns it, and -invoke-at refuses an owned one. No library owns an
address, so close does not reach it and the caller keeps the code loaded.
A NULL address (0 or nil) throws {:ffi/error :symbol :address addr} at
creation and is never called; a bad signature throws :signature with
:address in place of :symbol. No new error kind.

The errors-as-data builders move to ffi/errors.zig, which keeps
surface.zig under the 400-line ceiling.

Fixture: get_adder, get_halver and get_null_fn return pointers to static
callees, so only the returned address reaches them.

Smell-audited: 2: separate constructor keeps the lifetime rule single-owner; error builders extracted to their own module instead of growing surface.zig
…leased, not absent

Two passages said there is no wasm32 build. ADR-0193 landed one in August
and CI compiles it on every Linux run. State what the build is (single-
threaded, no network, no FFI, no embedded engine) and that no release
ships it.
jolt_lookup hands back function pointers, and ffi/function-at (merged from
feat/ffi-call-address) calls them, so jolt_lib.clj now runs the init, the
lookups, add, the mandel kernel and the hive C ABI against libdemo.so with
no C in between. A missing export is address 0 and function-at refuses it
with {:ffi/error :symbol :address 0}.

Measured over five alternating runs on a loaded host: the 60k-call mandel
grid takes 407 to 502 ms through function-at and 385 to 531 ms through the
shim, so the shim (shim.c, libdemo_named.so and its build step) is deleted
and the example has one path. README states the numbers with date and
versions; landscape.md points at the example.

Smell-audited: 1: one call path; the interim shim is removed rather than kept beside it
…nd memory budgets

Builds cljw for wasm32-wasi (ReleaseSmall, 3,692,268 B) and runs it from a
native cljw through wasm/run. Records size, startup, the confinement results
(fuel trap, memory.grow refusal, no file or env authority) and the limits of
the single-threaded build. The guest needs one preopened directory because it
reads ./deps.edn at startup and WASI resolves no relative path without one.
…(ADR-0208)

check_accepted_divergences.sh now requires a `check` on every AD row and
evaluates its expr through one portable driver, scripts/ad_check_render.clj:
on the built cljw (must render the recorded `cljw`, one process per row) and,
when the Clojure CLI is on PATH, on Clojure 1.12.4 with TZ Asia/Tokyo and
en-US (must render `clj`). A row whose two recorded renderings are equal
fails as STALE. Without clojure the clj half prints a boxed SKIPPED banner;
AD_CHECK_REQUIRE_CLJ=1 makes the skip fail. The step moves after build_cljw
and joins SMOKE_CORE, since a src change can converge a divergence.

Smell-audited: 2: ADR-0208, DA fork run, disposition recorded in the ADR
…7 (ADR-0208)

Run on cljw v1.15.3 and Clojure 1.12.4: 61 executable checks, 8 prose.
No row is STALE. AD-065's JVM claim was false: a running future's
(.cancel f false) answers [true true] on the JVM too; the real divergence is
that the JVM lets the task finish. AD-077 records that cljw accepts an ^int
fn parameter hint where clj refuses it (F-004, F-005, ADR-0059).

Smell-audited: 1: data backfill under ADR-0208
The Linux gate installed ripgrep with apt-get. On 2026-11-11 the runner
could not resolve its Ubuntu mirror and six consecutive runs across five
branches failed in that step before any build. Fetch the release tarball
from GitHub and verify its SHA-256, the way the same step already
installs babashka. The pin lives in .github/versions.lock.
ci: install ripgrep from a checksum-pinned release, not apt
docs(examples): cljw hosting cljw as a wasm32-wasi guest
docs(polyglot): run hosts.cljc on jolt, a fourth Clojure host
AD ledger: every accepted divergence carries an executable check, re-run on cljw and clj (ADR-0208)
feat(ffi): call a function pointer with ffi/function-at
bench(numeric): fib30 and loop10m probes with a best-of-5 runner
…hangelog for them

The README never mentioned cljw.ffi. It now has a native-library section with
function-at and the jolt_lookup route, names jolt as the fourth host of the
host-free .cljc, and points at the cljw-as-guest example. The changelog gains
the entries for the work landed by #117 to #123.
docs(ffi): cljw calls a Clojure library compiled by jolt
docs(readme): cljw.ffi, the jolt library route, four hosts, and the changelog
Release: cljw.ffi function-at, jolt library route, cljw as a Wasm guest, AD staleness gate
@BuddhiLW BuddhiLW changed the title Release v1.15.5 to v1.15.7 from BuddhiLW/main Release v1.15.5 to v1.15.8 from BuddhiLW/main Nov 11, 2026
@BuddhiLW
BuddhiLW merged commit 02525c6 into clojurewasm:main Nov 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant