From 956c34ee0608ab1b2eab6e6e219af56fc2e1614a Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 12 Aug 2026 10:04:58 -0300 Subject: [PATCH 001/466] docs: ClojureWasm continues as a maintained fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream clojurewasm/ClojureWasm stopped at v1.10.0, its final release, and invited forks under EPL-2.0. Development continues here from that exact tree: same gate, same ADR record, same release workflow. Nothing in the runtime changes with this commit. The README notice is replaced rather than deleted — a reader arriving from a stale link needs to learn both that upstream stopped and where it went, and the credit for the runtime itself belongs to @chaploud, so it is now stated in its own section instead of a footer. What moves with the handover: - Issues and Pull Requests are OPEN here. Upstream had both disabled for essentially its whole life, so contributions arrived as branches to cherry-pick; CONTRIBUTING already described an open queue and is now simply true. - The Homebrew tap is buddhilw/tap (`brew install buddhilw/tap/cljw`); clojurewasm/tap is no longer bumped. - README badges, CONTRIBUTING, SECURITY, the PR template and all four issue templates point at BuddhiLW/ClojureWasm. Private vulnerability reports reach @BuddhiLW. - The 1.x line continues from v1.10.0. Links to cw-playground and cw-serverless-demo are left pointing at the upstream org: those deployments are live and are not ours to reassign. --- .dev/handover.md | 29 +++++++++++-------- .github/CONTRIBUTING.md | 4 +-- .github/ISSUE_TEMPLATE/bug.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 4 +-- .github/ISSUE_TEMPLATE/divergence.yml | 2 +- .github/ISSUE_TEMPLATE/missing.yml | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/SECURITY.md | 6 ++-- CHANGELOG.md | 15 ++++++++++ README.md | 41 +++++++++++++++++---------- 10 files changed, 69 insertions(+), 38 deletions(-) diff --git a/.dev/handover.md b/.dev/handover.md index cf81d84ba..fa2c896f0 100644 --- a/.dev/handover.md +++ b/.dev/handover.md @@ -15,18 +15,23 @@ (`278587f6`, 2026-08-12, FINAL — full WASI 0.3 + the `libzwasm.a` C-API export fix; neither reaches cljw's Zig embedding surface. Tagging zwasm is user-only, its ADR-0156). Latest release: - **v1.10.0** (2026-08-12) — **the FINAL release**. CHANGELOG is the - release-history SSOT. -- **First task on resume**: nothing is pending. The wind-down is DONE: - README declares the project unmaintained, v1.10.0 is released (artifacts - verified by download + checksum + running the macOS binary), the homebrew - tap is bumped, cw-playground and cw-serverless-demo are redeployed on it - and live, cw-arcade is re-verified (32 tests / 153 assertions; it pins - nothing by design), and the announcement is Discussion #15 (pin it in the - web UI — the API has no pinDiscussion). The PERF - CAMPAIGN (D-450) is **stopped, not paused** — findings stay in the row as - a record. -- **Unreleased on main**: nothing. CHANGELOG `[1.10.0]` is the SSOT. + **v1.10.0** (2026-08-12) — upstream's final release, and this fork's + starting point. CHANGELOG is the release-history SSOT. +- **This repository is a MAINTAINED FORK.** Upstream + `clojurewasm/ClojureWasm` (chaploud) stopped at v1.10.0 and invited forks + under EPL-2.0; `BuddhiLW/ClojureWasm` continues from that tree. + `origin` = upstream (read-only, archived-in-practice), `buddhilw` = the + maintained remote. What changed with the handover: Issues and PRs are OPEN + here (upstream disabled both — the "branches to cherry-pick" workflow in + CONTRIBUTING no longer applies), the homebrew tap is + `BuddhiLW/homebrew-tap` (`brew install buddhilw/tap/cljw`), and the `1.x` + line continues from v1.10.0. The PERF CAMPAIGN (D-450) stays **stopped** + — findings remain in the row as a record. +- **First task on resume**: cut **v1.10.1** — a no-feature release whose only + job is to prove the fork's release path end to end (tag → `release.yml` + artifacts on this repo → tap formula bump → `brew install buddhilw/tap/cljw`). +- **Unreleased on main**: the fork-handover docs. CHANGELOG `[Unreleased]` + is the SSOT. - **Release-process hazards** (both live in `.github/workflows/release.yml`): cutting the GitHub release BY HAND before the tag workflow runs breaks the artifact upload (zwasm #160), and the workflow's `gh release view || gh diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c022307de..75307cd6c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -14,7 +14,7 @@ that has not been written down yet — and finding them from the outside is wort more than anything else, because a project cannot easily see its own blind spots. -There is a [Clojure divergence issue template](https://github.com/clojurewasm/ClojureWasm/issues/new?template=divergence.yml) +There is a [Clojure divergence issue template](https://github.com/BuddhiLW/ClojureWasm/issues/new?template=divergence.yml) that asks for the three things that make such a report actionable: the expression, what `cljw` prints, and what a JVM REPL prints. @@ -82,7 +82,7 @@ something is the way it is. ## Getting in touch Open-ended questions belong in -[GitHub Discussions](https://github.com/clojurewasm/ClojureWasm/discussions); +[GitHub Discussions](https://github.com/BuddhiLW/ClojureWasm/discussions); the wider Clojure community also gathers on the [Clojurians Slack](https://clojurians.slack.com). diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 07f93ddd3..29b524eae 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -12,7 +12,7 @@ body: Crashes and hangs are the highest-priority reports here. A crash in the WebAssembly FFI or the sandbox may be a security issue — in that case - please use [private vulnerability reporting](https://github.com/clojurewasm/ClojureWasm/security/advisories/new) + please use [private vulnerability reporting](https://github.com/BuddhiLW/ClojureWasm/security/advisories/new) rather than a public issue. - type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 79b8fce9f..cd6b714c1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,12 +3,12 @@ blank_issues_enabled: false contact_links: - name: 💬 Questions, ideas, "is this supposed to work?" - url: https://github.com/clojurewasm/ClojureWasm/discussions + url: https://github.com/BuddhiLW/ClojureWasm/discussions about: >- Anything open-ended belongs in Discussions rather than an issue — usage questions, design conversations, showing what you built. - name: 🔒 Report a security vulnerability (private) - url: https://github.com/clojurewasm/ClojureWasm/security/advisories/new + url: https://github.com/BuddhiLW/ClojureWasm/security/advisories/new about: >- Do NOT report security problems in public. Use private vulnerability reporting — see SECURITY.md for scope and what to include. diff --git a/.github/ISSUE_TEMPLATE/divergence.yml b/.github/ISSUE_TEMPLATE/divergence.yml index a677e0d32..77302cff4 100644 --- a/.github/ISSUE_TEMPLATE/divergence.yml +++ b/.github/ISSUE_TEMPLATE/divergence.yml @@ -12,7 +12,7 @@ body: down yet. Before filing, it is worth a glance at - [`docs/clojure_vs_clojurewasm.md`](https://github.com/clojurewasm/ClojureWasm/blob/main/docs/clojure_vs_clojurewasm.md) + [`docs/clojure_vs_clojurewasm.md`](https://github.com/BuddhiLW/ClojureWasm/blob/main/docs/clojure_vs_clojurewasm.md) — some differences are deliberate and already recorded there. If yours is listed, no need to file; if you think the reasoning is wrong, a Discussion is the better place for that. diff --git a/.github/ISSUE_TEMPLATE/missing.yml b/.github/ISSUE_TEMPLATE/missing.yml index 30f6753d1..eaf3f62e0 100644 --- a/.github/ISSUE_TEMPLATE/missing.yml +++ b/.github/ISSUE_TEMPLATE/missing.yml @@ -10,7 +10,7 @@ body: namespaces, but not all of it, and some of it never will be — anything that requires the JVM class system (`gen-class`, `proxy` against real Java base classes, `bean`'s reflective variant) is permanently out of - scope, recorded in [`data/compat_tiers.yaml`](https://github.com/clojurewasm/ClojureWasm/blob/main/data/compat_tiers.yaml). + scope, recorded in [`data/compat_tiers.yaml`](https://github.com/BuddhiLW/ClojureWasm/blob/main/data/compat_tiers.yaml). Telling us *what you were trying to do* matters more than the var name — often there is a supported path, and if there is not, the use case is diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 843ae3985..0da43ceb4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,7 +4,7 @@ ClojureWasm is a small, resource-limited project and cannot review external PRs right now. Please close this PR and instead start a thread in Discussions: - https://github.com/clojurewasm/ClojureWasm/discussions + https://github.com/BuddhiLW/ClojureWasm/discussions and mention @chaploud. Thanks for understanding — we do want to hear about behaviour that differs from JVM Clojure, just through Discussions while PRs diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 13dca4b85..1b2954a1c 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -19,18 +19,18 @@ is no backport branch. | newest `1.x` release | ✅ | | any older release | ❌ | -If you are on Homebrew, `brew upgrade clojurewasm/tap/cljw` puts you on the +If you are on Homebrew, `brew upgrade buddhilw/tap/cljw` puts you on the supported line. ## Reporting a vulnerability **Please do not open a public issue or Discussion for security problems.** -Report privately via GitHub's **[Private Vulnerability Reporting](https://github.com/clojurewasm/ClojureWasm/security/advisories/new)** +Report privately via GitHub's **[Private Vulnerability Reporting](https://github.com/BuddhiLW/ClojureWasm/security/advisories/new)** (the "Report a vulnerability" button under the repository's *Security* tab). If that is unavailable to you, open a minimal public Discussion asking a maintainer to reach out — **without any exploit detail** — and mention -`@chaploud`. +`@BuddhiLW`. Please include, where possible: diff --git a/CHANGELOG.md b/CHANGELOG.md index 06f0645cf..08e39802f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces. ## [Unreleased] +### Changed + +- **Maintenance moved to a fork.** Upstream + [clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm) + stopped at v1.10.0, its final release, and invited forks under EPL-2.0. + Development continues in + [BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm) from that + exact tree — same gate, same ADR record, same release workflow. Nothing in + the runtime changed. What moved: Issues and Pull Requests are open here + (upstream disabled both, so contributions arrived as branches to + cherry-pick), the Homebrew tap is now `buddhilw/tap` + (`brew install buddhilw/tap/cljw`), and README / CONTRIBUTING / SECURITY / + issue templates point at this repository. The `1.x` line continues from + v1.10.0. + ## [1.10.0] - 2026-08-12 **The final release.** ClojureWasm is no longer maintained — a from-scratch diff --git a/README.md b/README.md index 2bf26b18a..9fafa97df 100644 --- a/README.md +++ b/README.md @@ -9,25 +9,28 @@

- CI + CI Zig 0.16.0 Clojure runtime License: EPL 2.0

> [!IMPORTANT] -> **ClojureWasm is no longer maintained.** A from-scratch Clojure runtime -> turned out to be more than one person can sustain, so I am stopping here -> while it still works. The code stays up under EPL-2.0 — fork it and build on -> it, no permission needed. -> [Discussions](https://github.com/clojurewasm/ClojureWasm/discussions) remain -> open, but may go unanswered. +> **This is the maintained continuation of ClojureWasm.** The original +> project by [@chaploud](https://github.com/chaploud) — +> [clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm) — +> stopped at **v1.10.0**, its final release, and its author invited forks +> under EPL-2.0. Development continues here from that exact tree, with the +> gate, the ADR record and the release process intact. > -> I believe ClojureWasm demonstrated what a from-scratch Clojure can be: a -> single small static binary, with a WebAssembly FFI. The hardest parts — and -> why this needs more than a solo maintainer — were keeping up JVM-Clojure -> compatibility (Java interop above all) and performance. Thanks to everyone -> who used it, reported differences, and contributed fixes. +> [Issues](https://github.com/BuddhiLW/ClojureWasm/issues), Pull Requests and +> [Discussions](https://github.com/BuddhiLW/ClojureWasm/discussions) are open. +> **The single most useful thing you can report is Clojure code that behaves +> differently here than on the JVM** — there is an issue template for exactly +> that. +> +> Installs and version pins move to this repository: `brew install +> buddhilw/tap/cljw`, and releases continue the `1.x` line from v1.10.0. ## What it is @@ -64,7 +67,7 @@ still use what other languages have already built. **Homebrew** (macOS arm64 / Linux x86_64): ```sh -brew install clojurewasm/tap/cljw +brew install buddhilw/tap/cljw ``` The `cljw` binary is not code-signed. Homebrew installs it without a Gatekeeper @@ -76,7 +79,7 @@ xattr -d com.apple.quarantine "$(which cljw)" ``` Or grab a binary straight from the -[Releases](https://github.com/clojurewasm/ClojureWasm/releases) page. Building +[Releases](https://github.com/BuddhiLW/ClojureWasm/releases) page. Building from source is in [Quickstart](#quickstart) below. ## Quickstart @@ -160,6 +163,14 @@ from a Clojure one. Eclipse Public License 2.0 — see [LICENSE](./LICENSE) and [NOTICE](./legal/NOTICE). +## Credit + +ClojureWasm was created and written by [Shota Kudo +(@chaploud)](https://github.com/chaploud), who took it from nothing to a +working JVM-free Clojure runtime with a WebAssembly FFI over the life of +[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm), through +v1.10.0. Everything this fork ships is built on that work. + --- -Developed in spare time alongside a day job. +Maintained as a fork since v1.10.0. Developed in spare time. From 59150f5809fd27af0cfe7d32e1c89960fcd14808 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 12 Aug 2026 10:07:12 -0300 Subject: [PATCH 002/466] =?UTF-8?q?release:=20v1.10.1=20=E2=80=94=20prove?= =?UTF-8?q?=20the=20fork's=20release=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No runtime change: the binary is v1.10.0's, rebuilt under a new version string. The release exists to exercise the distribution path that the handover moved, end to end — tag on this repository, artifacts built by this repository's release.yml, and a buddhilw/tap formula pointing at them. Cutting it now, while the tree is known-good and the diff is a version string, means any breakage is unambiguously in the path and not in the code. The alternative is to discover a broken release path on the first release that also carries a fix. Smell-audited: 0: clean — version-string bump, no code --- CHANGELOG.md | 9 +++++++++ build.zig.zon | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08e39802f..a7465e85d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces. ## [Unreleased] +## [1.10.1] - 2026-08-12 + +**The first release of the maintained fork.** It carries no runtime change — +the binary is v1.10.0's, rebuilt — and exists to prove the fork's own release +path end to end: tag on `BuddhiLW/ClojureWasm`, artifacts built by this +repository's `release.yml`, and a `buddhilw/tap` formula pointing at them. A +release cut against a distribution path nobody has exercised is a guess; this +one makes the guess cheap to check. + ### Changed - **Maintenance moved to a fork.** Upstream diff --git a/build.zig.zon b/build.zig.zon index d7439889c..d584ccd2a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .cljw, - .version = "1.10.0", + .version = "1.10.1", .fingerprint = 0x1869d207073beffa, .minimum_zig_version = "0.16.0", .dependencies = .{ From 621de41473cd48a2d5008b2c249232cc133a4721 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 12 Aug 2026 10:18:49 -0300 Subject: [PATCH 003/466] ci(release): bump the homebrew tap from the release that built it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `brew install buddhilw/tap/cljw` was pointing at a formula edited by hand after each release, which is a step that gets skipped and a checksum that gets mistyped. A `tap` job now rewrites Formula/cljw.rb in BuddhiLW/homebrew-tap once both matrix legs have uploaded. Three details that are not incidental: - It runs `needs: build`, so the formula can never name an asset that the release does not carry yet. - The checksums come from the `.sha256` sidecars the build wrote and uploaded, not from re-hashing a fresh download. The formula therefore pins the bytes that were published; a re-hash would pin whatever the job happened to fetch. - Auth is a deploy key on the tap repository (secret TAP_DEPLOY_KEY), not a PAT. Write access reaches that one repository and nothing else in the account, and revoking it is one click on the tap. The formula deliberately carries no `version` line — brew scans the version from the URL, and `brew audit --strict` fails a formula that states it twice. Verified end to end on v1.10.1 by hand before automating: both artifacts downloaded, both sidecar checksums recomputed and matched, the Linux binary run (`--version` reports 1.10.1), then `brew install buddhilw/tap/cljw`, `brew test` and `brew audit --strict` green. --- .github/workflows/release.yml | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 954f807d6..d26036f4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,3 +78,71 @@ jobs: gh release view "$TAG_NAME" >/dev/null 2>&1 || \ gh release create "$TAG_NAME" --title "$TAG_NAME" --generate-notes gh release upload "$TAG_NAME" dist/* --clobber + + # Point `brew install buddhilw/tap/cljw` at the release this run just built. + # Runs after BOTH matrix legs so the formula never names an asset that does + # not exist yet; the checksums come from the .sha256 sidecars the build wrote, + # not from a re-hash here, so the formula pins exactly what was uploaded. + # + # Auth is a deploy key on BuddhiLW/homebrew-tap (secret TAP_DEPLOY_KEY), not a + # PAT: write access reaches that one repository and nothing else in the account. + tap: + name: bump homebrew tap + needs: build + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-22.04 + timeout-minutes: 10 + steps: + - name: Read the release checksums + env: + GH_TOKEN: ${{ github.token }} + TAG_NAME: ${{ github.ref_name }} + GH_REPO: ${{ github.repository }} + shell: bash + run: | + set -euo pipefail + mkdir -p sums && cd sums + gh release download "$TAG_NAME" --pattern '*.sha256' --clobber + # `shasum -a 256 FILE` writes " "; keep the sum alone. + mac=$(awk '{print $1}' cljw-macos-aarch64.tar.gz.sha256) + lin=$(awk '{print $1}' cljw-linux-x86_64.tar.gz.sha256) + test -n "$mac" && test -n "$lin" + { + echo "MAC_SHA256=$mac" + echo "LINUX_SHA256=$lin" + } >> "$GITHUB_ENV" + + - name: Check out the tap + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: BuddhiLW/homebrew-tap + ssh-key: ${{ secrets.TAP_DEPLOY_KEY }} + path: tap + + - name: Write and push the formula + env: + TAG_NAME: ${{ github.ref_name }} + shell: bash + working-directory: tap + run: | + set -euo pipefail + python3 - <<'PY' + import os, pathlib, re + p = pathlib.Path("Formula/cljw.rb") + s = p.read_text() + # The formula carries no `version` line: brew scans it from the URL, + # and `brew audit --strict` rejects stating it twice. + s = re.sub(r'/download/v[^/]+/', '/download/%s/' % os.environ["TAG_NAME"], s) + # Each sha256 sits under its own url line, so replace positionally. + for host, sha in (("macos-aarch64", os.environ["MAC_SHA256"]), + ("linux-x86_64", os.environ["LINUX_SHA256"])): + s = re.sub(r'(cljw-%s\.tar\.gz"\n sha256 ")[0-9a-f]*(")' % host, + lambda m: m.group(1) + sha + m.group(2), s) + p.write_text(s) + PY + grep -q "REPLACE_" Formula/cljw.rb && { echo "formula still carries a placeholder"; exit 1; } + git diff --quiet && { echo "formula already current for $TAG_NAME"; exit 0; } + git config user.name "clojurewasm-release" + git config user.email "clojurewasm-release@users.noreply.github.com" + git commit -am "cljw $TAG_NAME" + git push From 837d1f708dcaef9a9e1af72567909bbaff765eaa Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 12 Aug 2026 10:21:47 -0300 Subject: [PATCH 004/466] docs(handover): the fork's release path is proven; workflows need a dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.10.1 is out and `brew install buddhilw/tap/cljw` works, so the resume contract no longer asks for it. The fork-default Actions behaviour is recorded as a HAZARD rather than a note, because it fails in the direction that looks fine: an absent CI run reads as a passing one, and a pushed `v*` tag produces no release at all. Every run so far came from an explicit `gh workflow run`. Two bullets were false-for-this-repo rather than merely stale, so they are corrected, not carried: Issues and PRs are open here, and D-565's residuals (7)/(8) point into upstream's gitignored `private/notes/`, which did not come with the fork — unreachable, not pending. Upstream's wind-down is compressed to the one line a resuming reader still needs, which pays for the new bullets against the 100-line cap. --- .dev/handover.md | 62 +++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/.dev/handover.md b/.dev/handover.md index fa2c896f0..b1041bad3 100644 --- a/.dev/handover.md +++ b/.dev/handover.md @@ -21,23 +21,29 @@ `clojurewasm/ClojureWasm` (chaploud) stopped at v1.10.0 and invited forks under EPL-2.0; `BuddhiLW/ClojureWasm` continues from that tree. `origin` = upstream (read-only, archived-in-practice), `buddhilw` = the - maintained remote. What changed with the handover: Issues and PRs are OPEN - here (upstream disabled both — the "branches to cherry-pick" workflow in - CONTRIBUTING no longer applies), the homebrew tap is - `BuddhiLW/homebrew-tap` (`brew install buddhilw/tap/cljw`), and the `1.x` - line continues from v1.10.0. The PERF CAMPAIGN (D-450) stays **stopped** - — findings remain in the row as a record. -- **First task on resume**: cut **v1.10.1** — a no-feature release whose only - job is to prove the fork's release path end to end (tag → `release.yml` - artifacts on this repo → tap formula bump → `brew install buddhilw/tap/cljw`). -- **Unreleased on main**: the fork-handover docs. CHANGELOG `[Unreleased]` - is the SSOT. + maintained remote. Changed with the handover: Issues and PRs are OPEN here + (so CONTRIBUTING's "branches to cherry-pick" no longer applies), the tap is + `BuddhiLW/homebrew-tap` (`brew install buddhilw/tap/cljw`), and `1.x` + continues from v1.10.0. PERF CAMPAIGN (D-450) stays **stopped** — findings + remain in the row as a record. +- **Release path: PROVEN on v1.10.1** (2026-08-12) — artifacts built by this + repo, both sidecar checksums recomputed and matched, the Linux binary run + (`--version` = 1.10.1), `brew install buddhilw/tap/cljw` + `brew test` + + `brew audit --strict` green. The tap bump is now a `tap` job in + `release.yml` (deploy key, secret `TAP_DEPLOY_KEY`); v1.10.1's formula was + written by hand, so **v1.10.2 is the first release that exercises the + automated bump** — watch that job. +- **HAZARD — workflows do not fire on push/tag here.** GitHub's fork default: + every run so far came from `gh workflow run --ref `. Until the + Actions tab's enable button is pressed once, "no run appeared" does NOT mean + "CI passed", and a `v*` tag will not build a release by itself. Dispatch + explicitly, or press it and delete this bullet. +- **Unreleased on main**: nothing. CHANGELOG `[1.10.1]` is the SSOT. - **Release-process hazards** (both live in `.github/workflows/release.yml`): cutting the GitHub release BY HAND before the tag workflow runs breaks the - artifact upload (zwasm #160), and the workflow's `gh release view || gh - release create` is TOCTOU across the two matrix legs — if they reach it - within the same instant, the loser fails on "already exists" under - `set -e`. v1.10.0 missed it by 47 s. Never hit, never fixed. + artifact upload (zwasm #160), and `gh release view || gh release create` is + TOCTOU across the two matrix legs — simultaneous, the loser dies on "already + exists" under `set -e`. v1.10.0 missed it by 47 s. Never hit, never fixed. - **Forbidden**: bare `zig build test` without `-Dwasm`; bare `zig build` for a probe (use ReleaseSafe). The FULL gate runs `--serial-e2e`, ALONE; never a concurrent build during a gate. `.claude/**` edits + cross-repo publishes may @@ -47,19 +53,15 @@ ## Current state (details = CHANGELOG + git log) -- **Issues and PRs are DISABLED and always were** (deliberate — the - maintainer cannot service them); **Discussions is the one channel**, and - outside contributors send branches to cherry-pick. CONTRIBUTING exempts - them from the loop's own conventions, so a contributed commit is taken - as-is with authorship preserved and any missing `Smell-audited:` line - amended in. Debug tooling: `scripts/check_*.sh` headers are each their - own SSOT — read the script, not a list here. -- **2026-08-12: the README declares ClojureWasm no longer maintained** - (`1c8a9cbf`); the Sponsors badge, README footer line, and `.github/FUNDING.yml` - are gone with it. Issues/PRs were always disabled — Discussions is the one - channel. Wind-down context + the surveyed wording/transfer research: - memory `clojurewasm-wind-down-plan`, `private/notes/2026-08-11-{sunset-wording, - transfer-risk}-survey.md`. +- **Issues, PRs and Discussions are all OPEN here** (they were disabled + upstream, so contributions used to arrive as branches to cherry-pick). + CONTRIBUTING exempts outside contributors from the loop's own conventions: + a contributed commit is taken as-is with authorship preserved and any + missing `Smell-audited:` line amended in. Debug tooling: + `scripts/check_*.sh` headers are each their own SSOT — read the script. +- Upstream's own wind-down (`1c8a9cbf` README notice, Sponsors + FUNDING.yml + removed, Discussion #15) is history now; `956c34ee` replaced that notice + with the fork's. - 2026-08-11/12: Discussions #12/#13/#14 (BuddhiLW) answered in code — three fix branches cherry-picked with authorship preserved, each one's CLASS then swept and fenced. #12 repr-misread → 3 more live bugs (multimethod isa? at 9 @@ -74,8 +76,8 @@ - **D-565** — external-contributor reproducibility sweep (Discussion #11). Items (1)-(6) DISCHARGED 2026-08-04; the ledger row carries what each was. - Residual: (7) survey-workflow setup notes, (8) handover pointing at - gitignored `private/notes/` recipes. Companion: zwasm D-526. + Residuals (7)/(8) both point into upstream's gitignored `private/notes/`, + which did not come with the fork — treat as unreachable, not pending. - **Perf campaign (§9.2.S) — PAUSED** (D-520 / D-386 / D-005/006). **D-513** item (1) `clojure.core.reducers` is the only remaining piece (repl + var :doc From 2a51e4fe2106bf9b73a2211e314e703d9c44a66c Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 12 Aug 2026 10:52:06 -0300 Subject: [PATCH 005/466] docs(adr): open the golden, property and mutation test layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0021 named 8 deferred layers and gave each a phase that would open it. Golden ("Phase 7+"), Property ("Phase 8+") and Fuzz ("Phase 6+") never opened; the phases passed and the layers did not. What the suite has instead is genuinely good — 1,249 inline unit tests, ~400 e2e scripts, a dual-backend differential oracle, a clj corpus — with two structural gaps that the fork inherits and should not keep: Nothing checks whole-program observable output. An e2e case asserts a chosen substring of a chosen stream, so it can only fail for the thing its author predicted. The error renderer prints a source excerpt with a caret under the offending form and no test compares that rendering as a whole. Nothing checks the suite itself. 120 of 265 source files carry no inline test, and coverage would answer the wrong question anyway: not whether a line ran under test, but whether changing it makes a test fail. ADR-0186 opens Layer 6 (golden snapshots), Layer 7 (properties over generated input) and Layer 8 (mutation, a measurement OF the layers, run on demand and never in the per-commit gate). Fuzz stays deferred: fuzzing asks whether it crashes, properties ask whether it is right, and the second question is both cheaper here and the one this codebase's bugs have been answering wrong. The layer tables in test/README.md and the taxonomy rule are updated in the same commit, because "where does this test go" is the question those two documents exist to answer and a stale answer sends the next test to the wrong layer. --- .claude/rules/test_taxonomy.md | 10 +- .../0186_golden_property_mutation_layers.md | 132 ++++++++++++++++++ test/README.md | 19 ++- 3 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 .dev/decisions/0186_golden_property_mutation_layers.md diff --git a/.claude/rules/test_taxonomy.md b/.claude/rules/test_taxonomy.md index 627f10b31..fd060bde1 100644 --- a/.claude/rules/test_taxonomy.md +++ b/.claude/rules/test_taxonomy.md @@ -19,6 +19,9 @@ Choose the layer before writing the test. | 3 | Differential | `test/diff/cases.yaml` | TreeWalk and VM must produce equal Value for the same source. ADR-0005 / 0022. | | 4 | Bench (on demand) | `bench/compare_langs.sh` + `bench/run_bench.sh` | Perf measurement — run on demand, NOT a gate layer (the `quick.sh` auto-baseline was retired 2026-06-11). | | 5 | Conformance | `test/clj/` (Phase 11+) | Adapted upstream Clojure JVM test. | +| 6 | Golden | `test/golden/cases/*.clj` + `.expected` | A whole program's stdout + stderr + exit code must not change. ADR-0186. | +| 7 | Property | `src/testing/prop_*.zig` | A law over GENERATED input, checked against a model or an inverse. ADR-0186. | +| 8 | Mutation | `scripts/mutation/` (on demand, never gated) | Not a test — a measurement of whether the other layers constrain a line. ADR-0186. | ## Why @@ -43,9 +46,14 @@ Ask in order: 4. Is it a performance measurement? → Layer 4 (run on demand via `bench/`, not added to the gate). 5. Is it an upstream Clojure test port (Phase 11+)? → Layer 5. +6. Is the thing to protect everything the program PRINTED, rather than one + asserted substring? → Layer 6 (`test/golden/`, `run.sh --update` to record). +7. Does it state a law that should hold for EVERY input — an inverse, an + algebraic identity, agreement with a naive model? → Layer 7. If none fit, the test is in a future layer (defer or open the -future layer's directory). +future layer's directory). Layer 8 is not a place to put a test: it is the +sweep that tells you which lines the layers above are not constraining. ### Naming diff --git a/.dev/decisions/0186_golden_property_mutation_layers.md b/.dev/decisions/0186_golden_property_mutation_layers.md new file mode 100644 index 000000000..be45b373e --- /dev/null +++ b/.dev/decisions/0186_golden_property_mutation_layers.md @@ -0,0 +1,132 @@ +# 0186 — Open the golden, property and mutation test layers + +- **Status**: Accepted +- **Date**: 2026-08-12 +- **Author**: Pedro (BuddhiLW) +- **Tags**: test, taxonomy, golden, property, mutation, fork-foundation + +## Context + +ADR-0021 committed to 5 layers and named 8 more as deferred, each with a phase +that would open it. Golden ("Phase 7+"), Property ("Phase 8+") and Fuzz +("Phase 6+") never opened; the phases passed and the layers did not. What the +suite has instead is 1,249 inline unit tests, ~400 e2e shell scripts, a +dual-backend differential oracle and a clj corpus — a large, genuinely good +suite with two structural gaps. + +**Nothing checks whole-program observable output.** An e2e case asserts a +chosen substring of a chosen stream, so it can only fail for the thing its +author thought to assert. The error renderer prints a source excerpt with a +caret under the offending form; no test compares that rendering as a whole, so +a regression that drops the excerpt, moves a message from stderr to stdout, or +changes an exit code passes every assertion written today. + +**Nothing checks the suite itself.** 120 of 265 source files carry no inline +test. Coverage is unmeasured, and coverage would be the wrong measure anyway: +the question is not whether a line executed under test but whether changing it +makes a test fail. That question has one honest answer — change the line and +see. + +The fork inherits maintenance from a solo author whose confidence came partly +from an autonomous loop with written rails. A new maintainer does not inherit +that context. What transfers is a suite that fails when behaviour changes, and +evidence about where it would not. + +## Decision + +Open three layers, numbered as continuations of the ADR-0021 table. + +**Layer 6 — Golden snapshot** (`test/golden/`). A case is a whole program; its +snapshot is stdout, stderr and exit code in one file. `run.sh --update` +regenerates, and the resulting diff is the review surface. Determinism comes +from explicit normalisation (repo path, hex addresses, pids, durations, version +string); a case that still varies does not belong in the layer. Runs every +gate, after `build_cljw`. + +**Layer 7 — Property** (`test/prop/`). Properties are stated over generated +input rather than over examples, with a fixed default seed so the gate is +deterministic, and an overridable one so a sweep can explore. A failing case +shrinks before it is reported. The oracles available here are the ones the +codebase already trusts elsewhere: an algebraic law (`conj` then `pop` is +identity), a round-trip (read then print then read), and cross-implementation +agreement (a persistent collection against a naive array model). + +**Layer 8 — Mutation** (`scripts/mutation/`). Not a test layer but a +measurement OF the layers: mutate one source construct, rebuild, run the unit +suite, and record whether anything failed. A surviving mutant names a specific +line whose behaviour no test constrains. It runs on demand and in a scheduled +sweep, never in the per-commit gate — a rebuild per mutant is minutes, and the +output is a work-list, not a pass/fail. + +Mutation runs in a **throwaway git worktree**, never in the working checkout. +The harness rewrites source files; pointing it at a checkout someone is editing +would destroy uncommitted work, and a mutant left behind by an interrupted run +would be indistinguishable from a real edit. + +## Alternatives considered + +### Alternative A — Extend the e2e layer instead of adding golden + +- **Sketch**: keep writing `phase_.sh` scripts with explicit asserts + for the outputs that currently go unchecked. +- **Why rejected**: it reproduces the gap it is meant to close. An assert + encodes what its author predicted could break; the failures that hurt are the + unpredicted ones. A snapshot has no prediction in it. The two are + complementary, not competing — e2e stays the layer for "this specific thing + must hold", golden becomes the layer for "nothing about this changed". + +### Alternative B — Line coverage instead of mutation + +- **Sketch**: build with coverage instrumentation, report percentage per module, + set a floor. +- **Why rejected**: it measures execution, not constraint. A line executed by a + test that asserts nothing about its effect counts as covered, and a suite + tuned to a coverage floor drifts toward tests that execute rather than tests + that check. Mutation asks the question coverage is a proxy for. + +### Alternative C — Fuzzing (the layer ADR-0021 actually named) instead of property tests + +- **Sketch**: open `fuzz/`, run a coverage-guided fuzzer against the reader and + the evaluator, look for crashes. +- **Why rejected**: not rejected, deferred. Fuzzing answers "does it crash", + properties answer "is it right"; a runtime that never crashes on malformed + input can still return the wrong number. Properties are also the cheaper + first move, since they need no new toolchain. `fuzz/` stays open in ADR-0021's + deferred list. + +### Alternative D — Mutation in the per-commit gate + +- **Sketch**: sample a handful of mutants per commit, fail the gate on a + survivor. +- **Why rejected**: a rebuild per mutant puts it minutes past the gate's budget, + and a survivor is not a defect — it is a question about a missing test, which + is work to schedule rather than a commit to block. Gating it would also make + the seed load-bearing: the same commit would pass or fail depending on which + mutants were drawn. + +## Consequences + +- **Positive**: a rendering change fails the gate without anyone having + predicted its shape; the untested regions of the tree become a named, + ranked work-list instead of an impression; the layers ADR-0021 promised exist. +- **Negative**: snapshots have to be read when they change, and a reviewer who + regenerates without reading converts the layer into a rubber stamp. The + mutation harness is a source-rewriting tool, which is a foot-gun kept safe + only by the worktree rule above. +- **Neutral / follow-ups**: the mutation sweep's first report will name modules + with no inline test at all; those become debt rows, not a single task. + `fuzz/` remains deferred. + +## Affected files + +- `.dev/decisions/0186_golden_property_mutation_layers.md` (this file) +- `test/golden/run.sh`, `test/golden/cases/**` +- `test/prop/**` +- `scripts/mutation/**` +- `test/run_all.sh` (the `golden` and `prop` steps) +- `test/README.md`, `.claude/rules/test_taxonomy.md` (the layer table) + +## Revision history + +- 2026-08-12: Status: Proposed -> Accepted. Layers 6 and 7 land with the ADR; + Layer 8's harness lands beside them and its first sweep report follows. diff --git a/test/README.md b/test/README.md index 6872120da..004603d67 100644 --- a/test/README.md +++ b/test/README.md @@ -11,6 +11,9 @@ | 3 | Differential | `test/diff/` | `Evaluator.compare()` | Phase 4 | | 4 | Bench (on demand) | `bench/compare_langs.sh` / `run_bench.sh` | bash + `cljw` | on demand (not gated) | | 5 | Conformance | `test/clj/` (Phase 11+) | `clojure.test` | Phase 11 | +| 6 | Golden snapshot | `test/golden/` | `test/golden/run.sh` | ADR-0186 | +| 7 | Property | `src/testing/prop_*.zig` (see `test/prop/README.md`) | `zig build test` | ADR-0186 | +| 8 | Mutation | `scripts/mutation/` | `scripts/mutation/run.sh` | ADR-0186 (on demand) | ## "Where does this test go?" @@ -22,6 +25,12 @@ - **Performance measurement**: Layer 4 — run on demand via `bench/` (no longer part of the gate as of 2026-06-11). - **Upstream Clojure JVM test (port)**: Layer 5 (Phase 11+). +- **Everything a program printed must stay the same**: Layer 6 — a whole-program + stdout/stderr/exit snapshot, for when the thing to protect is the output as a + whole rather than one asserted substring. +- **A law that must hold for every input, not just the chosen ones**: Layer 7. +- **"Does any test actually constrain this line?"**: Layer 8 — not a test, a + measurement of the others. On demand; never in the per-commit gate. ## Running @@ -35,12 +44,12 @@ bash bench/compare_langs.sh --cold # Layer 4: cross-language perf (on demand) `test/run_all.sh` accepts `--skip ` / `--only ` / `--list` flags (per ADR-0024 run_step pattern). -## Future layers (deferred per ADR-0021) +## Future layers (still deferred) -`test/integration/` (Phase 5+), `test/golden/` (Phase 7+), -`test/prop/` (Phase 8+), `fuzz/` (Phase 6+), `test/clj/` (Phase 11+). -These directories are created when the corresponding phase opens, -not as empty placeholders. +`test/integration/` (Phase 5+) and `fuzz/` (Phase 6+) remain unopened. +Golden, property and mutation opened with ADR-0186; `test/clj/` opened at +Phase 11. Directories are created when a layer opens, not as empty +placeholders. ## Conventions From e941759bd87787d1e0507119e8d93d97a7cc26fe Mon Sep 17 00:00:00 2001 From: Pedro Gomes Branquinho Date: Wed, 12 Aug 2026 10:52:23 -0300 Subject: [PATCH 006/466] test(golden): snapshot whole-program output for 20 programs (Layer 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A case is a program; its snapshot is stdout, stderr and exit code in one file. `run.sh --update` regenerates and the diff is the review surface. The 20 opening cases were chosen where a regression would be invisible to the existing layers rather than where writing a test is easy: the printed form of every scalar and collection, the error renderer's source excerpt and caret for six error classes, laziness ordering, destructuring, dispatch (multimethod / protocol / record), exception unwinding order, atoms and delays, transducers, metadata, the numeric tower's promotion boundary, and the Wasm FFI calling a real .wasm module. Every snapshot was generated and then READ before being committed. Two are worth knowing about: - `error_ex_info` records that a thrown ex-info prints "Exception / boom" and DROPS its ex-data, with no source location or trace — where the other five error classes all render an excerpt. The snapshot pins what cljw does today, not what it should do; it becomes the regression test the moment that is fixed. - `numeric_tower` pins AD-008 (Long overflow auto-promotes to BigInt where clj throws). A snapshot over an accepted divergence is exactly where one belongs: the divergence is a decision, and a decision that drifts silently is no longer a decision. Determinism is enforced by normalisation — repo path, hex addresses, pids, durations, version string — not by hoping. CLJW_BIN lets the suite run against a binary built elsewhere (a clean worktree, a release artifact, a mutant), which is how these snapshots were verified to reproduce byte-identically against the published v1.10.1 artifact rather than only against the local build. Wired into the gate after build_cljw: it needs the fresh binary, it is cheaper than the e2e suite, and its failures are more legible. --- test/golden/cases/core_string_set_walk.clj | 5 + .../cases/core_string_set_walk.expected | 8 ++ test/golden/cases/destructuring.clj | 6 + test/golden/cases/destructuring.expected | 7 ++ test/golden/cases/error_arity.clj | 3 + test/golden/cases/error_arity.expected | 15 +++ test/golden/cases/error_div_zero.clj | 2 + test/golden/cases/error_div_zero.expected | 11 ++ test/golden/cases/error_ex_info.clj | 2 + test/golden/cases/error_ex_info.expected | 7 ++ test/golden/cases/error_missing_ns.clj | 2 + test/golden/cases/error_missing_ns.expected | 11 ++ test/golden/cases/error_reader.clj | 2 + test/golden/cases/error_reader.expected | 11 ++ test/golden/cases/error_type.clj | 2 + test/golden/cases/error_type.expected | 11 ++ test/golden/cases/error_unresolved_symbol.clj | 3 + .../cases/error_unresolved_symbol.expected | 12 ++ test/golden/cases/exceptions_control.clj | 11 ++ test/golden/cases/exceptions_control.expected | 11 ++ test/golden/cases/metadata_vars.clj | 6 + test/golden/cases/metadata_vars.expected | 8 ++ test/golden/cases/numeric_tower.clj | 6 + test/golden/cases/numeric_tower.expected | 9 ++ test/golden/cases/polymorphism.clj | 17 +++ test/golden/cases/polymorphism.expected | 8 ++ test/golden/cases/printer_collections.clj | 9 ++ .../golden/cases/printer_collections.expected | 17 +++ test/golden/cases/printer_opaque.clj | 7 ++ test/golden/cases/printer_opaque.expected | 9 ++ test/golden/cases/printer_scalars.clj | 6 + test/golden/cases/printer_scalars.expected | 21 ++++ test/golden/cases/seq_laziness.clj | 6 + test/golden/cases/seq_laziness.expected | 10 ++ test/golden/cases/state_atoms.clj | 8 ++ test/golden/cases/state_atoms.expected | 10 ++ test/golden/cases/transducers_seq.clj | 8 ++ test/golden/cases/transducers_seq.expected | 12 ++ test/golden/cases/wasm_ffi.clj | 5 + test/golden/cases/wasm_ffi.expected | 6 + test/golden/run.sh | 119 ++++++++++++++++++ test/run_all.sh | 7 ++ 42 files changed, 456 insertions(+) create mode 100644 test/golden/cases/core_string_set_walk.clj create mode 100644 test/golden/cases/core_string_set_walk.expected create mode 100644 test/golden/cases/destructuring.clj create mode 100644 test/golden/cases/destructuring.expected create mode 100644 test/golden/cases/error_arity.clj create mode 100644 test/golden/cases/error_arity.expected create mode 100644 test/golden/cases/error_div_zero.clj create mode 100644 test/golden/cases/error_div_zero.expected create mode 100644 test/golden/cases/error_ex_info.clj create mode 100644 test/golden/cases/error_ex_info.expected create mode 100644 test/golden/cases/error_missing_ns.clj create mode 100644 test/golden/cases/error_missing_ns.expected create mode 100644 test/golden/cases/error_reader.clj create mode 100644 test/golden/cases/error_reader.expected create mode 100644 test/golden/cases/error_type.clj create mode 100644 test/golden/cases/error_type.expected create mode 100644 test/golden/cases/error_unresolved_symbol.clj create mode 100644 test/golden/cases/error_unresolved_symbol.expected create mode 100644 test/golden/cases/exceptions_control.clj create mode 100644 test/golden/cases/exceptions_control.expected create mode 100644 test/golden/cases/metadata_vars.clj create mode 100644 test/golden/cases/metadata_vars.expected create mode 100644 test/golden/cases/numeric_tower.clj create mode 100644 test/golden/cases/numeric_tower.expected create mode 100644 test/golden/cases/polymorphism.clj create mode 100644 test/golden/cases/polymorphism.expected create mode 100644 test/golden/cases/printer_collections.clj create mode 100644 test/golden/cases/printer_collections.expected create mode 100644 test/golden/cases/printer_opaque.clj create mode 100644 test/golden/cases/printer_opaque.expected create mode 100644 test/golden/cases/printer_scalars.clj create mode 100644 test/golden/cases/printer_scalars.expected create mode 100644 test/golden/cases/seq_laziness.clj create mode 100644 test/golden/cases/seq_laziness.expected create mode 100644 test/golden/cases/state_atoms.clj create mode 100644 test/golden/cases/state_atoms.expected create mode 100644 test/golden/cases/transducers_seq.clj create mode 100644 test/golden/cases/transducers_seq.expected create mode 100644 test/golden/cases/wasm_ffi.clj create mode 100644 test/golden/cases/wasm_ffi.expected create mode 100755 test/golden/run.sh diff --git a/test/golden/cases/core_string_set_walk.clj b/test/golden/cases/core_string_set_walk.clj new file mode 100644 index 000000000..2c570c6dd --- /dev/null +++ b/test/golden/cases/core_string_set_walk.clj @@ -0,0 +1,5 @@ +(require '[clojure.string :as str] '[clojure.set :as set] '[clojure.walk :as walk]) +(prn (str/split "a,b,,c" #",")) +(prn (str/join "-" ["x" "y"]) (str/upper-case "ab") (str/trim " p ") (str/replace "aXa" "X" "-")) +(prn (set/union #{1 2} #{2 3}) (set/difference #{1 2 3} #{2}) (set/intersection #{1 2} #{2 3})) +(prn (walk/postwalk (fn [x] (if (number? x) (* 10 x) x)) {:a [1 {:b 2}]})) diff --git a/test/golden/cases/core_string_set_walk.expected b/test/golden/cases/core_string_set_walk.expected new file mode 100644 index 000000000..1f6dfe6b9 --- /dev/null +++ b/test/golden/cases/core_string_set_walk.expected @@ -0,0 +1,8 @@ +--- exit --- +0 +--- stdout --- +["a" "b" "" "c"] +"x-y" "AB" "p" "a-a" +#{1 2 3} #{1 3} #{2} +{:a [10 {:b 20}]} +--- stderr --- diff --git a/test/golden/cases/destructuring.clj b/test/golden/cases/destructuring.clj new file mode 100644 index 000000000..e344a833f --- /dev/null +++ b/test/golden/cases/destructuring.clj @@ -0,0 +1,6 @@ +(let [[a b & rest :as all] [1 2 3 4] + {:keys [x y] :or {y 99} :as m} {:x 10}] + (println a b rest all) + (println x y m)) +(defn g [{:keys [k]} & more] [k more]) +(prn (g {:k :v} 1 2)) diff --git a/test/golden/cases/destructuring.expected b/test/golden/cases/destructuring.expected new file mode 100644 index 000000000..8bba0348f --- /dev/null +++ b/test/golden/cases/destructuring.expected @@ -0,0 +1,7 @@ +--- exit --- +0 +--- stdout --- +1 2 (3 4) [1 2 3 4] +10 99 {:x 10} +[:v (1 2)] +--- stderr --- diff --git a/test/golden/cases/error_arity.clj b/test/golden/cases/error_arity.clj new file mode 100644 index 000000000..390361b1b --- /dev/null +++ b/test/golden/cases/error_arity.clj @@ -0,0 +1,3 @@ +(defn f [a b] (+ a b)) +(println "before") +(f 1) diff --git a/test/golden/cases/error_arity.expected b/test/golden/cases/error_arity.expected new file mode 100644 index 000000000..f8bd28a4d --- /dev/null +++ b/test/golden/cases/error_arity.expected @@ -0,0 +1,15 @@ +--- exit --- +1 +--- stdout --- +before +--- stderr --- +Arity error at /test/golden/cases/error_arity.clj:3:0 + Wrong number of args (1) passed to fn, expected 2 + + 1 | (defn f [a b] (+ a b)) + 2 | (println "before") + 3 | (f 1) + ^--- Wrong number of args (1) passed to fn, expected 2 + +Trace: + user/f (/test/golden/cases/error_arity.clj:3) diff --git a/test/golden/cases/error_div_zero.clj b/test/golden/cases/error_div_zero.clj new file mode 100644 index 000000000..7cc6180e8 --- /dev/null +++ b/test/golden/cases/error_div_zero.clj @@ -0,0 +1,2 @@ +(println "before") +(/ 1 0) diff --git a/test/golden/cases/error_div_zero.expected b/test/golden/cases/error_div_zero.expected new file mode 100644 index 000000000..0ab5a9802 --- /dev/null +++ b/test/golden/cases/error_div_zero.expected @@ -0,0 +1,11 @@ +--- exit --- +1 +--- stdout --- +before +--- stderr --- +Arithmetic error at /test/golden/cases/error_div_zero.clj:2:5 + Divide by zero + + 1 | (println "before") + 2 | (/ 1 0) + ^--- Divide by zero diff --git a/test/golden/cases/error_ex_info.clj b/test/golden/cases/error_ex_info.clj new file mode 100644 index 000000000..e8218aa98 --- /dev/null +++ b/test/golden/cases/error_ex_info.clj @@ -0,0 +1,2 @@ +(println "before") +(throw (ex-info "boom" {:code 42 :nested {:k [1 2]}})) diff --git a/test/golden/cases/error_ex_info.expected b/test/golden/cases/error_ex_info.expected new file mode 100644 index 000000000..7429fb071 --- /dev/null +++ b/test/golden/cases/error_ex_info.expected @@ -0,0 +1,7 @@ +--- exit --- +1 +--- stdout --- +before +--- stderr --- +Exception + boom diff --git a/test/golden/cases/error_missing_ns.clj b/test/golden/cases/error_missing_ns.clj new file mode 100644 index 000000000..f782739d1 --- /dev/null +++ b/test/golden/cases/error_missing_ns.clj @@ -0,0 +1,2 @@ +(println "before") +(require '[no.such.namespace :as n]) diff --git a/test/golden/cases/error_missing_ns.expected b/test/golden/cases/error_missing_ns.expected new file mode 100644 index 000000000..f33367623 --- /dev/null +++ b/test/golden/cases/error_missing_ns.expected @@ -0,0 +1,11 @@ +--- exit --- +1 +--- stdout --- +before +--- stderr --- +Name error at /test/golden/cases/error_missing_ns.clj:2:10 + Could not locate 'no.such.namespace' on the require resolver + + 1 | (println "before") + 2 | (require '[no.such.namespace :as n]) + ^--- Could not locate 'no.such.namespace' on the require resolver diff --git a/test/golden/cases/error_reader.clj b/test/golden/cases/error_reader.clj new file mode 100644 index 000000000..a4f319562 --- /dev/null +++ b/test/golden/cases/error_reader.clj @@ -0,0 +1,2 @@ +(println "before") +(let [x 1] (println x) diff --git a/test/golden/cases/error_reader.expected b/test/golden/cases/error_reader.expected new file mode 100644 index 000000000..4a35fc891 --- /dev/null +++ b/test/golden/cases/error_reader.expected @@ -0,0 +1,11 @@ +--- exit --- +1 +--- stdout --- +before +--- stderr --- +Syntax error at /test/golden/cases/error_reader.clj:2:0 + Unmatched delimiter; reached EOF before ')' + + 1 | (println "before") + 2 | (let [x 1] (println x) + ^--- Unmatched delimiter; reached EOF before ')' diff --git a/test/golden/cases/error_type.clj b/test/golden/cases/error_type.clj new file mode 100644 index 000000000..9dfe7dad9 --- /dev/null +++ b/test/golden/cases/error_type.clj @@ -0,0 +1,2 @@ +(println "before") +(+ 1 "not a number") diff --git a/test/golden/cases/error_type.expected b/test/golden/cases/error_type.expected new file mode 100644 index 000000000..f788e1fb1 --- /dev/null +++ b/test/golden/cases/error_type.expected @@ -0,0 +1,11 @@ +--- exit --- +1 +--- stdout --- +before +--- stderr --- +Type error at /test/golden/cases/error_type.clj:2:5 + +: expected number, got string + + 1 | (println "before") + 2 | (+ 1 "not a number") + ^--- +: expected number, got string diff --git a/test/golden/cases/error_unresolved_symbol.clj b/test/golden/cases/error_unresolved_symbol.clj new file mode 100644 index 000000000..8a8f72306 --- /dev/null +++ b/test/golden/cases/error_unresolved_symbol.clj @@ -0,0 +1,3 @@ +(println "before") +(no-such-fn 1 2) +(println "after") diff --git a/test/golden/cases/error_unresolved_symbol.expected b/test/golden/cases/error_unresolved_symbol.expected new file mode 100644 index 000000000..786b2b261 --- /dev/null +++ b/test/golden/cases/error_unresolved_symbol.expected @@ -0,0 +1,12 @@ +--- exit --- +1 +--- stdout --- +before +--- stderr --- +Name error at /test/golden/cases/error_unresolved_symbol.clj:2:1 + Unable to resolve symbol: 'no-such-fn' + + 1 | (println "before") + 2 | (no-such-fn 1 2) + ^--- Unable to resolve symbol: 'no-such-fn' + 3 | (println "after") diff --git a/test/golden/cases/exceptions_control.clj b/test/golden/cases/exceptions_control.clj new file mode 100644 index 000000000..45a178ddf --- /dev/null +++ b/test/golden/cases/exceptions_control.clj @@ -0,0 +1,11 @@ +;; The order side effects happen in when an exception unwinds is the contract. +(prn (try 1 (catch Exception e :caught) (finally (println "finally-1")))) +(prn (try (throw (ex-info "x" {:a 1})) + (catch clojure.lang.ExceptionInfo e [:info (ex-message e) (ex-data e)]) + (finally (println "finally-2")))) +(prn (try (/ 1 0) (catch ArithmeticException e (.getMessage e)))) +(prn (try (throw (RuntimeException. "plain")) (catch Exception e (str "caught: " (.getMessage e))))) +;; A rethrow must not lose the original data. +(prn (try (try (throw (ex-info "inner" {:depth 1})) + (catch Exception e (throw e))) + (catch Exception e (ex-data e)))) diff --git a/test/golden/cases/exceptions_control.expected b/test/golden/cases/exceptions_control.expected new file mode 100644 index 000000000..ab1c498ba --- /dev/null +++ b/test/golden/cases/exceptions_control.expected @@ -0,0 +1,11 @@ +--- exit --- +0 +--- stdout --- +finally-1 +1 +finally-2 +[:info "x" {:a 1}] +"Divide by zero" +"caught: plain" +{:depth 1} +--- stderr --- diff --git a/test/golden/cases/metadata_vars.clj b/test/golden/cases/metadata_vars.clj new file mode 100644 index 000000000..6a2d6f712 --- /dev/null +++ b/test/golden/cases/metadata_vars.clj @@ -0,0 +1,6 @@ +(def ^{:doc "documented" :private false :custom :m} x 1) +(prn (select-keys (meta #'x) [:doc :custom :name])) +(prn (meta (with-meta [1 2] {:k :v}))) +(prn (:tag (meta (with-meta 'sym {:tag 'String})))) +;; Metadata must not participate in equality. +(prn (= (with-meta [1] {:a 1}) (with-meta [1] {:b 2}))) diff --git a/test/golden/cases/metadata_vars.expected b/test/golden/cases/metadata_vars.expected new file mode 100644 index 000000000..fe2cd7e70 --- /dev/null +++ b/test/golden/cases/metadata_vars.expected @@ -0,0 +1,8 @@ +--- exit --- +0 +--- stdout --- +{:doc "documented", :custom :m, :name x} +{:k :v} +String +true +--- stderr --- diff --git a/test/golden/cases/numeric_tower.clj b/test/golden/cases/numeric_tower.clj new file mode 100644 index 000000000..a04d984cd --- /dev/null +++ b/test/golden/cases/numeric_tower.clj @@ -0,0 +1,6 @@ +;; Where an integer stops being an integer, and what the boundary prints as. +(prn (+ 1 2) (- 1 2) (* 3 4) (/ 6 3) (/ 7 2) (quot 7 2) (rem 7 2) (mod -7 2)) +(prn (+ 1 2.0) (* 2 0.5) (== 1 1.0) (= 1 1.0)) +(prn (inc 9007199254740992) (* 1000000000000 1000000000000)) +(prn (Integer/MAX_VALUE) (inc Integer/MAX_VALUE)) +(prn (double 1/3) (float? 1.0) (integer? 1) (ratio? 1/3)) diff --git a/test/golden/cases/numeric_tower.expected b/test/golden/cases/numeric_tower.expected new file mode 100644 index 000000000..f934df7e0 --- /dev/null +++ b/test/golden/cases/numeric_tower.expected @@ -0,0 +1,9 @@ +--- exit --- +0 +--- stdout --- +3 -1 12 2 7/2 3 1 1 +3.0 1.0 true false +9007199254740993 1000000000000000000000000N +2147483647 2147483648 +0.3333333333333333 true true true +--- stderr --- diff --git a/test/golden/cases/polymorphism.clj b/test/golden/cases/polymorphism.clj new file mode 100644 index 000000000..69b6002f6 --- /dev/null +++ b/test/golden/cases/polymorphism.clj @@ -0,0 +1,17 @@ +;; Dispatch: multimethods, protocols, records. The printed forms of a record and +;; of a protocol-satisfying value are part of the user-visible surface. +(defmulti area :shape) +(defmethod area :circle [{:keys [r]}] (* 3 r r)) +(defmethod area :rect [{:keys [w h]}] (* w h)) +(defmethod area :default [_] :unknown) +(prn (area {:shape :circle :r 2}) (area {:shape :rect :w 2 :h 3}) (area {:shape :blob})) + +(defprotocol Greet (greet [this] [this loud?])) +(defrecord Person [name] + Greet + (greet [this] (str "hi " (:name this))) + (greet [this loud?] (if loud? "HI" (greet this)))) +(def p (->Person "ada")) +(prn (greet p) (greet p true)) +(prn (:name p) (map->Person {:name "bob"})) +(prn (satisfies? Greet p) (instance? Person p)) diff --git a/test/golden/cases/polymorphism.expected b/test/golden/cases/polymorphism.expected new file mode 100644 index 000000000..87455f60b --- /dev/null +++ b/test/golden/cases/polymorphism.expected @@ -0,0 +1,8 @@ +--- exit --- +0 +--- stdout --- +12 6 :unknown +"hi ada" "HI" +"ada" #user.Person{:name "bob"} +true true +--- stderr --- diff --git a/test/golden/cases/printer_collections.clj b/test/golden/cases/printer_collections.clj new file mode 100644 index 000000000..83ac7a66d --- /dev/null +++ b/test/golden/cases/printer_collections.clj @@ -0,0 +1,9 @@ +;; Collection printing, including nesting, empties, and a map's entry form. +(doseq [v ['() [] {} #{} + '(1 2 3) [1 2 3] {:a 1 :b 2} #{1 2 3} + [nil [nil [nil]]] + {:xs [1 2] :m {:k #{:v}}} + (range 5) + (map inc [1 2 3]) + (seq "abc")]] + (prn v)) diff --git a/test/golden/cases/printer_collections.expected b/test/golden/cases/printer_collections.expected new file mode 100644 index 000000000..c80bb270c --- /dev/null +++ b/test/golden/cases/printer_collections.expected @@ -0,0 +1,17 @@ +--- exit --- +0 +--- stdout --- +() +[] +{} +#{} +(1 2 3) +[1 2 3] +{:a 1, :b 2} +#{1 2 3} +[nil [nil [nil]]] +{:xs [1 2], :m {:k #{:v}}} +(0 1 2 3 4) +(2 3 4) +(\a \b \c) +--- stderr --- diff --git a/test/golden/cases/printer_opaque.clj b/test/golden/cases/printer_opaque.clj new file mode 100644 index 000000000..f146ccc24 --- /dev/null +++ b/test/golden/cases/printer_opaque.clj @@ -0,0 +1,7 @@ +;; Values whose printed form is identity-bearing or type-bearing. These are the +;; ones most likely to drift silently when an internal representation changes. +(prn (type 1) (type 1.0) (type "s") (type :k) (type 's) (type nil)) +(prn (type []) (type '()) (type {}) (type #{}) (type (range 3)) (type (map inc [1]))) +(prn (type #"re") (type (atom 1)) (type #'prn) (type (fn []))) +(println (boolean (re-find #"a+" "caaat"))) +(prn (re-find #"a+" "caaat") (re-seq #"[0-9]+" "a1b22c333")) diff --git a/test/golden/cases/printer_opaque.expected b/test/golden/cases/printer_opaque.expected new file mode 100644 index 000000000..bb414c891 --- /dev/null +++ b/test/golden/cases/printer_opaque.expected @@ -0,0 +1,9 @@ +--- exit --- +0 +--- stdout --- +Long Double String Keyword Symbol nil +PersistentVector PersistentList PersistentArrayMap PersistentHashSet LongRange LazySeq +Pattern Atom Var Fn +true +"aaa" ("1" "22" "333") +--- stderr --- diff --git a/test/golden/cases/printer_scalars.clj b/test/golden/cases/printer_scalars.clj new file mode 100644 index 000000000..64dc33885 --- /dev/null +++ b/test/golden/cases/printer_scalars.clj @@ -0,0 +1,6 @@ +;; How every scalar prints, and how `pr` differs from `println`. +;; A change in any of these lines is a change in what every user sees. +(doseq [v [nil true false 0 -1 42 3.14 1/3 \a \newline "s" "with \"quote\"" :kw :ns/kw 'sym 'ns/sym]] + (pr v) + (print " | ") + (println v)) diff --git a/test/golden/cases/printer_scalars.expected b/test/golden/cases/printer_scalars.expected new file mode 100644 index 000000000..538986173 --- /dev/null +++ b/test/golden/cases/printer_scalars.expected @@ -0,0 +1,21 @@ +--- exit --- +0 +--- stdout --- +nil | nil +true | true +false | false +0 | 0 +-1 | -1 +42 | 42 +3.14 | 3.14 +1/3 | 1/3 +\a | a +\newline | + +"s" | s +"with \"quote\"" | with "quote" +:kw | :kw +:ns/kw | :ns/kw +sym | sym +ns/sym | ns/sym +--- stderr --- diff --git a/test/golden/cases/seq_laziness.clj b/test/golden/cases/seq_laziness.clj new file mode 100644 index 000000000..ba5bf9b62 --- /dev/null +++ b/test/golden/cases/seq_laziness.clj @@ -0,0 +1,6 @@ +;; Laziness is observable through side effects; the ORDER of these lines is +;; the contract, not just the final value. +(def xs (map (fn [x] (println "realising" x) (* x x)) [1 2 3])) +(println "defined") +(println "first" (first xs)) +(println "all" (doall xs)) diff --git a/test/golden/cases/seq_laziness.expected b/test/golden/cases/seq_laziness.expected new file mode 100644 index 000000000..4e994f65d --- /dev/null +++ b/test/golden/cases/seq_laziness.expected @@ -0,0 +1,10 @@ +--- exit --- +0 +--- stdout --- +defined +realising 1 +first 1 +realising 2 +realising 3 +all (1 4 9) +--- stderr --- diff --git a/test/golden/cases/state_atoms.clj b/test/golden/cases/state_atoms.clj new file mode 100644 index 000000000..c8fc5af0c --- /dev/null +++ b/test/golden/cases/state_atoms.clj @@ -0,0 +1,8 @@ +(def a (atom 0)) +(prn (swap! a inc) (swap! a + 10) @a (reset! a :done) @a) +(def v (volatile! 1)) +(prn (vswap! v inc) @v) +(def d (delay (do (println "forced once") 42))) +(prn (force d) (force d) (realized? d)) +(prn @(future (+ 1 2))) +(let [p (promise)] (deliver p :val) (prn @p)) diff --git a/test/golden/cases/state_atoms.expected b/test/golden/cases/state_atoms.expected new file mode 100644 index 000000000..e4b028107 --- /dev/null +++ b/test/golden/cases/state_atoms.expected @@ -0,0 +1,10 @@ +--- exit --- +0 +--- stdout --- +1 11 11 :done :done +2 2 +forced once +42 42 true +3 +:val +--- stderr --- diff --git a/test/golden/cases/transducers_seq.clj b/test/golden/cases/transducers_seq.clj new file mode 100644 index 000000000..997e0eee2 --- /dev/null +++ b/test/golden/cases/transducers_seq.clj @@ -0,0 +1,8 @@ +(prn (into [] (comp (map inc) (filter even?) (take 3)) (range 20))) +(prn (transduce (map inc) + 0 [1 2 3])) +(prn (sequence (mapcat range) [1 2 3])) +(prn (eduction (filter odd?) (range 10))) +(prn (reduce-kv (fn [acc k v] (assoc acc v k)) {} {:a 1 :b 2})) +(prn (group-by odd? (range 6)) (frequencies "aabbbc") (partition-by odd? [1 3 2 4 5])) +(prn (sort [3 1 2]) (sort-by - [3 1 2]) (sort compare ["b" "a"])) +(prn (take 3 (iterate #(* 2 %) 1)) (take 4 (cycle [:a :b])) (take 3 (repeat :x))) diff --git a/test/golden/cases/transducers_seq.expected b/test/golden/cases/transducers_seq.expected new file mode 100644 index 000000000..8a731b466 --- /dev/null +++ b/test/golden/cases/transducers_seq.expected @@ -0,0 +1,12 @@ +--- exit --- +0 +--- stdout --- +[2 4 6] +9 +(0 0 1 0 1 2) +(1 3 5 7 9) +{1 :a, 2 :b} +{false [0 2 4], true [1 3 5]} {\a 2, \b 3, \c 1} ((1 3) (2 4) (5)) +(1 2 3) (3 2 1) ("a" "b") +(1 2 4) (:a :b :a :b) (:x :x :x) +--- stderr --- diff --git a/test/golden/cases/wasm_ffi.clj b/test/golden/cases/wasm_ffi.clj new file mode 100644 index 000000000..de5bad3ae --- /dev/null +++ b/test/golden/cases/wasm_ffi.clj @@ -0,0 +1,5 @@ +;; The headline feature: a module compiled from another language, called like a +;; function. If this snapshot changes, the FFI's user-visible surface changed. +(def m (wasm/load "docs/examples/wasm/add.wasm")) +(prn (wasm/call m "add" 40 2)) +(prn (wasm/call m "add" -1 1)) diff --git a/test/golden/cases/wasm_ffi.expected b/test/golden/cases/wasm_ffi.expected new file mode 100644 index 000000000..a8d1ea971 --- /dev/null +++ b/test/golden/cases/wasm_ffi.expected @@ -0,0 +1,6 @@ +--- exit --- +0 +--- stdout --- +42 +0 +--- stderr --- diff --git a/test/golden/run.sh b/test/golden/run.sh new file mode 100755 index 000000000..c92970746 --- /dev/null +++ b/test/golden/run.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# test/golden/run.sh — Layer 6 (Golden snapshot), opened per ADR-0186. +# +# A golden case is a WHOLE PROGRAM plus everything the process produced for it: +# stdout, stderr and exit code, in one file. That is the difference from Layer 2 +# (e2e), which asserts a chosen substring of a chosen stream: an e2e case can +# only fail for the thing its author thought to assert, while a snapshot fails +# for any change in what the user sees — a reworded error, a dropped newline, a +# value that starts printing its metadata, a warning that appears on stderr. +# +# That sensitivity is the point and also the cost: a snapshot suite that is +# tedious to update gets updated carelessly. So `--update` regenerates, and the +# diff it produces is the review surface. A reviewer reads the diff, not the +# file. +# +# Determinism is enforced by NORMALISATION, not by hoping: the repo path, hex +# addresses, pids, durations and the version string are rewritten to fixed +# tokens before comparison (see normalise()). A case that still varies run to +# run does not belong in this layer. +# +# Usage: +# bash test/golden/run.sh # verify every case +# bash test/golden/run.sh --update # regenerate every .expected +# bash test/golden/run.sh --only printer # substring-filter case names +# CLJW_SKIP_BUILD=1 bash test/golden/run.sh +# +# Adding a case: drop `cases/.clj` in, run with --update, READ the +# generated `.expected`, commit both. A snapshot committed without being read +# is a record of a bug as readily as of a behaviour. +set -euo pipefail +cd "$(dirname "$0")/../.." +ROOT=$(pwd) +# CLJW_BIN lets a caller point the suite at a binary built elsewhere — a clean +# git worktree, a downloaded release artifact, a mutant build. Without it the +# suite tests this checkout, which is what the gate wants. +BIN="${CLJW_BIN:-$ROOT/zig-out/bin/cljw}" +CASE_DIR="$ROOT/test/golden/cases" + +UPDATE=0 +ONLY="" +while [ $# -gt 0 ]; do + case "$1" in + --update) UPDATE=1 ;; + --only) ONLY="$2"; shift ;; + *) echo "unknown flag: $1" >&2; exit 2 ;; + esac + shift +done + +[ -n "${CLJW_SKIP_BUILD:-}" ] || zig build -Dwasm -Doptimize="${CLJW_OPT:-ReleaseSafe}" >/dev/null +[ -x "$BIN" ] || { echo "golden: no binary at $BIN" >&2; exit 1; } + +# Rewrite everything that legitimately differs between two runs of the same +# build, and between two machines. Anything not rewritten here is a promise +# that it is stable. +normalise() { + sed -e "s|$ROOT||g" \ + -e 's|0x[0-9a-f]\{4,\}|0xADDR|g' \ + -e 's|ClojureWasm v[0-9][0-9.]*|ClojureWasm v|g' \ + -e 's|[0-9]\{1,\}\.[0-9]\{1,\}\(ms\|s\|us\|µs\)|