Skip to content

Commit 3fa87f4

Browse files
committed
fix(gate): the two new scripts obey the epipe + portable-timeout rules
Smell-audited: 1: both failures are mine, and both are disciplines the project already had a check for — the lesson is the process one recorded below, not a new rule. `check_entrypoint_surface.sh` fed `grep` into `head -1` (the SIGPIPE shape `check_epipe_head.sh` exists to catch; `sed -n 1p` reads to EOF instead), and `entrypoint_eval_parity.sh` used bare `timeout`, which hosted macOS runners do not ship — it now uses the `run_bounded` helper the http e2e steps use. PROCESS NOTE: 304b4a4 was pushed on the strength of a positive + negative manual check, without a full gate, because it "only touched a script". That is what turned a local red into a red push CI. Script-only is not a gate exemption; the tiering in ADR-0107 is smoke-vs-full, not code-vs-script. Also records the arc in the ledger: D-374's discharge claimed the top-level-`do` unroll was "routed through ALL form-loading paths" and listed five, but eval_session.zig and builder.zig kept their own loops — coverage LISTED, not probed (the D-177 false-positive-discharge class). Corrected in place, and the claim is mechanical now rather than narrative. handover trimmed to the wind-down sequence; CHANGELOG carries the fix.
1 parent fd5f044 commit 3fa87f4

5 files changed

Lines changed: 70 additions & 50 deletions

File tree

‎.dev/debt.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3158,7 +3158,7 @@ discharged:
31583158
barrier: "DISCHARGED 2026-06-17 — cl-format is COMPLETE on the minimal cl-run (no upstream architecture rewrite needed). chunk1 ~C/~&, chunk2 ~P/~*/~T, chunk3 ~$/~E/~G (full CLtL Steele), chunk4a ~[~;~] conditional (nesting-aware via cl-close-nested + cl-clauses), chunk4b ~<~;~> justification (full ~mincol,colinc,minpad,padchar< + :/@ + ~^ drop). All clj-oracle byte-matched; e2e phase14_cl_format 82 cases + clj_corpus/cl_format_float.txt 13 pins. Remaining gaps are documented divergences that RAISE cleanly: the `V`/`#` runtime-valued directive params (D-458) and the ~<…~:;…~> pretty-print column mode (needs a column-tracking writer cljw's pr-alias pprint lacks). cljw pprint.clj stayed a minimal cl-run reimpl throughout — the prior 'ADR-level full port' assessment was wrong."
31593159
last_reviewed: "2026-06-17"
31603160
- id: "D-374"
3161-
status: "DISCHARGED 2026-06-16 (gaps/bugs sweep). cljw now unrolls a top-level `(do …)`: each child is analyzed+evaluated IN SEQUENCE, so an effect in an earlier child (`(import …)` / `(defmacro …)`) is visible to a later child's ANALYSIS — `(do (defmacro m [] 42) (m))` → 42 (was a 'macro Var not callable' analyze error), `(do (import …) (instance? … x))` matches clj. Impl: driver.zig `topLevelDoChildren(form)` detector + `evalTopLevelForm` (recursive; do's value = last child, empty `(do)`→nil) routed through ALL form-loading paths — runner.zig (`-e`/main), loader.zig (file load/require; per-leaf chunk to the `cljw build` sink so replay order is preserved), evaluator.zig (`--compare` dual-backend oracle, backend-parameterized twin), driver.evalValue (so `(eval '(do …))` unrolls), bootstrap.zig (consistency; no-op for hand-written core). e2e phase14_eval (5 new cases: defmacro-then-use / def-then-use / value-is-last / nested-do / eval-of-do) + smoke green (diff oracle ×2 + corpus 3206 + zlinter). RESIDUAL (narrow, not re-filed): a MACRO expanding to a top-level `(do …)` is not unrolled (only a LITERAL `do` head is matched); clj macroexpands first. No real case hits it (declare→do-of-defs needs no effect-sequencing). ORIGINAL: open (2026-06-10, surfaced by the D-373 corpus). cljw does NOT unroll a top-level `(do …)` — it analyzes the whole form before evaluating, so a side-effecting form and a later form that depends on its effect cannot share one compilation unit. Concretely: `(do (import 'java.io.BufferedReader) (instance? BufferedReader x))` → cljw NameError (BufferedReader resolved at analyze time, before the import effect ran); clj → true (clj treats each top-level-`do` child as if a separate top-level form, compiling+evaluating in sequence). This was MASKED for instance? until D-373: the old macro quoted the class symbol so it resolved at EVAL time (after import). The REALISTIC forms — `(ns … (:import …))` then use, OR separate top-level forms — both work in cljw and match clj (verified). Only the single-`-e`-`(do (import …) (use …))` bundle diverges. Removed the artifact corpus line (io_stream_class.txt:25) that depended on the old late-binding; the divergence is a bug scheduled for fix when top-level-do-unrolling lands."
3161+
status: "CORRECTION 2026-08-12 (Discussion #14): the discharge below claimed the unroll was 'routed through ALL form-loading paths' and listed five. It was not — `src/app/eval_session.zig` (backing BOTH `cljw repl` and `cljw nrepl`) and `src/app/builder.zig` (two loops, backing `cljw build` and the bundled-.clj AOT) each kept their own analyze-then-eval loop, so a top-level `(do (require …) (use …))` failed in the REPL, over nREPL, and at build time while `-e` worked. An external user hit it (three entry points diverging) rather than the gate. This is the D-177 false-positive-discharge class — coverage was LISTED, not probed. Now genuinely closed: eval_session routes through `driver.evalTopLevelForm` (74db35d0, contributor fix), builder enumerates via the new `driver.topLevelLeaves` (fd5f0449), and the claim is now MECHANICAL rather than narrative — `scripts/check_entrypoint_surface.sh` fails if any of the four form-stream sites stops calling the shared driver, and `test/e2e/entrypoint_eval_parity.sh` diffs 8 programs across all six entry points with no expected values written down. ORIGINAL DISCHARGE (2026-06-16, gaps/bugs sweep). cljw now unrolls a top-level `(do …)`: each child is analyzed+evaluated IN SEQUENCE, so an effect in an earlier child (`(import …)` / `(defmacro …)`) is visible to a later child's ANALYSIS — `(do (defmacro m [] 42) (m))` → 42 (was a 'macro Var not callable' analyze error), `(do (import …) (instance? … x))` matches clj. Impl: driver.zig `topLevelDoChildren(form)` detector + `evalTopLevelForm` (recursive; do's value = last child, empty `(do)`→nil) routed through ALL form-loading paths — runner.zig (`-e`/main), loader.zig (file load/require; per-leaf chunk to the `cljw build` sink so replay order is preserved), evaluator.zig (`--compare` dual-backend oracle, backend-parameterized twin), driver.evalValue (so `(eval '(do …))` unrolls), bootstrap.zig (consistency; no-op for hand-written core). e2e phase14_eval (5 new cases: defmacro-then-use / def-then-use / value-is-last / nested-do / eval-of-do) + smoke green (diff oracle ×2 + corpus 3206 + zlinter). RESIDUAL (narrow, not re-filed): a MACRO expanding to a top-level `(do …)` is not unrolled (only a LITERAL `do` head is matched); clj macroexpands first. No real case hits it (declare→do-of-defs needs no effect-sequencing). ORIGINAL: open (2026-06-10, surfaced by the D-373 corpus). cljw does NOT unroll a top-level `(do …)` — it analyzes the whole form before evaluating, so a side-effecting form and a later form that depends on its effect cannot share one compilation unit. Concretely: `(do (import 'java.io.BufferedReader) (instance? BufferedReader x))` → cljw NameError (BufferedReader resolved at analyze time, before the import effect ran); clj → true (clj treats each top-level-`do` child as if a separate top-level form, compiling+evaluating in sequence). This was MASKED for instance? until D-373: the old macro quoted the class symbol so it resolved at EVAL time (after import). The REALISTIC forms — `(ns … (:import …))` then use, OR separate top-level forms — both work in cljw and match clj (verified). Only the single-`-e`-`(do (import …) (use …))` bundle diverges. Removed the artifact corpus line (io_stream_class.txt:25) that depended on the old late-binding; the divergence is a bug scheduled for fix when top-level-do-unrolling lands."
31623162
category: "eval-semantics"
31633163
barrier: |-
31643164
cljw lacks top-level-`do` unrolling (clj compiles+evals each child of a

‎.dev/handover.md‎

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,18 @@
1818
lookbehind / `\A\z\Z` / class algebra, multidim aget/aset, and the GC
1919
stops taxing long-running programs 2-4x — D-571/573/446/447 discharged).
2020
CHANGELOG is the release-history SSOT.
21-
- **First task on resume**: the PERF CAMPAIGN continues — D-450's status
22-
head is the live brief. State 2026-08-06: ADR-0184 LANDED (Functions are
23-
collectable GC cells; the 15.5M-immortal-closure leak is dead), rush-hour
24-
17.9 s → **12.3 s** (clj gap 17x → 11.5x), GC share ~7% (inside the
25-
ADR-0183 target). O-055 budget-TLV hoist also landed; TraceRef +
26-
threshold-multiplier experiments REFUTED (recorded in D-450 — do not
27-
re-run). Next step is a RE-PROFILE (the 2026-08-05 attribution is stale)
28-
via `sample` on the BFS (`cd ~/Documents/MyProducts/cw-arcade/apps/
29-
rush-hour && cljw -e "(require '[rush-hour.generator :as g])
30-
(g/generate 7 :medium)"`, `-Dprofile` build), then the lever stack:
31-
bindCallFrame copies, ADR-0184 Alt 3 template/closure split, Alt 1
32-
epoch-mark, D-386 dispatch. Residual rows: D-574 reify-TD churn, D-575
33-
worker fetched-fn class. The 9-target fastest-script list stays a
34-
regression fence. ALSO fixed en route: nightly CI red (speed-scaled test
35-
bound — budget_thread_ownership promise-gated; `run_remote_ubuntu.sh
36-
--parity` reproduces the nightly config), re-extend-never-wins clj
37-
parity defect, and the misplaced D-450 re-anchor block (was on D-343).
38-
Release-process note worth keeping: **cutting the GitHub release by hand
39-
before the tag workflow runs breaks the artifact upload**. zwasm's workflow
40-
did an unconditional `gh release create`, which failed on "already exists"
41-
and took the upload down with it, so v2.4.1 had notes and no binaries until
42-
the release was deleted and the workflow re-run. Fixed to `view || create`
43-
then `upload --clobber` (zwasm #160), matching what cljw's has always done.
44-
NOTE on history hygiene: commit `673b082b`'s message describes only the
45-
gate-parity fix, but its diff ALSO carries the zwasm tag re-pin, `wasm/run`'s
46-
`:timeout-ms` axis and D-570's discharge — they were in the tree when the
47-
gate went green and rode along. Not amended (already pushed); the v1.8.0
48-
CHANGELOG was written from the diff rather than that message.
49-
- **Unreleased on main**: nothing. v1.8.0 (2026-08-04) carried the whole
50-
audit-drain day; CHANGELOG is the SSOT for what it contained.
21+
- **First task on resume**: the wind-down sequence, NOT a feature unit. In
22+
order: zwasm 2.5.0 (user-tagged) → bump the `.zwasm` pin + full gate →
23+
cut the final cljw release tag → point cw-arcade / cw-playground /
24+
cw-serverless-demo at that tag instead of a PATH `cljw` → post the
25+
Discussions announcement (Announcements, pinned; the README already
26+
carries the notice). The PERF CAMPAIGN (D-450, rush-hour 12.3 s, next
27+
step was a re-profile) is **stopped, not paused** — its findings stay in
28+
the row as a record. Release-process note that still applies: cutting a
29+
GitHub release BY HAND before the tag workflow runs breaks the artifact
30+
upload (zwasm #160 — use `view || create` then `upload --clobber`).
31+
- **Unreleased on main**: the Discussion #12/#13/#14 fixes + the two new
32+
gates. CHANGELOG `[Unreleased]` is the SSOT for what they were.
5133
- **Forbidden**: bare `zig build test` without `-Dwasm`; bare `zig build` for a
5234
probe (use ReleaseSafe). The FULL gate runs `--serial-e2e`, ALONE; never a
5335
concurrent build during a gate. `.claude/**` edits + cross-repo publishes may
@@ -57,17 +39,28 @@
5739

5840
## Current state (details = CHANGELOG + git log)
5941

60-
- **Issues and PRs are OPEN**; CONTRIBUTING exempts outside contributors from
61-
the loop's own conventions. Debug tooling: `scripts/check_*.sh` headers are
62-
each their own SSOT — read the script, not a list here.
63-
- 2026-08-11: Discussions #12/#13 (BuddhiLW) answered in code — both fix
64-
branches cherry-picked with authorship preserved; the repr-misread class
65-
audited and 3 more live bugs fixed (multimethod isa? at 9 defmethods, http
66-
client/server 9-entry headers, error_render context); `cljw repl` gained the
67-
shared `[-cp][-A]` surface (D-322 residual closed). Two new smoke-core
68-
gates fence both classes: `check_repr_decode.sh` + `check_entrypoint_surface.sh`.
69-
Endgame row = D-576. Reply drafts (posting is user-gated):
70-
`private/notes/2026-08-11-discussion-replies.md`.
42+
- **Issues and PRs are DISABLED and always were** (deliberate — the
43+
maintainer cannot service them); **Discussions is the one channel**, and
44+
outside contributors send branches to cherry-pick. CONTRIBUTING exempts
45+
them from the loop's own conventions, so a contributed commit is taken
46+
as-is with authorship preserved and any missing `Smell-audited:` line
47+
amended in. Debug tooling: `scripts/check_*.sh` headers are each their
48+
own SSOT — read the script, not a list here.
49+
- **2026-08-12: the README declares ClojureWasm no longer maintained**
50+
(`1c8a9cbf`); the Sponsors badge, README footer line, and `.github/FUNDING.yml`
51+
are gone with it. Issues/PRs were always disabled — Discussions is the one
52+
channel. Wind-down context + the surveyed wording/transfer research:
53+
memory `clojurewasm-wind-down-plan`, `private/notes/2026-08-11-{sunset-wording,
54+
transfer-risk}-survey.md`.
55+
- 2026-08-11/12: Discussions #12/#13/#14 (BuddhiLW) answered in code — three
56+
fix branches cherry-picked with authorship preserved, each one's CLASS then
57+
swept and fenced. #12 repr-misread → 3 more live bugs (multimethod isa? at 9
58+
defmethods, http client/server 9-entry headers, error_render context) +
59+
`check_repr_decode.sh`; #13/#14 entry-point divergence → `cljw repl`'s
60+
`[-cp][-A]` surface, `cljw build`'s missing top-level-`do` unroll,
61+
`check_entrypoint_surface.sh` + `test/e2e/entrypoint_eval_parity.sh` (a
62+
6-entry-point differential oracle). D-374's discharge was a listed-not-probed
63+
claim; corrected in place. Endgame row = D-576.
7164

7265
## Standing units (tracked in .dev/debt.yaml)
7366

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces.
99

1010
### Fixed
1111

12+
- **A top-level `(do …)` that requires before it uses now works in every
13+
entry point.** `(do (require '[clojure.string :as s]) (s/upper-case
14+
"ok"))` returned `"OK"` from `cljw -e` but raised `No namespace: 's'`
15+
over nREPL and in `cljw repl`, and failed to compile under `cljw build`:
16+
the top-level-`do` unroll, which analyses and evaluates each child in
17+
sequence so an earlier `require` or `defmacro` is visible to a later
18+
child, had only ever been wired into the script path. Reported,
19+
diagnosed and fixed for the REPL engine by @BuddhiLW (Discussion #14;
20+
commit cherry-picked with authorship preserved); the `cljw build` half
21+
was found by auditing the other callers. A new differential oracle
22+
(`test/e2e/entrypoint_eval_parity.sh`) now runs each program through
23+
`-e`, a script file, stdin, `repl`, `nrepl`, and `build`+run and
24+
requires all six to agree, so this class of entry-point divergence
25+
fails the gate instead of reaching users.
26+
1227
- **A set literal of 9+ elements no longer crashes macroexpansion**
1328
(segfault / index-out-of-bounds / bogus "cannot be re-analysed" —
1429
three faces of one misread). The analyzer decoded the set's backing

‎scripts/check_entrypoint_surface.sh‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ done
8787
# #13, 2026-08-11 confirmation reply). A help line that stops advertising -cp
8888
# silently breaks that capability detection even if the flag still works.
8989
for sc in "${EVAL_SUBCOMMANDS[@]}"; do
90-
help_line=$(grep -F "\\ $sc " "$CLI" | head -1 || true)
90+
# `sed -n 1p` rather than `head -1`: head closes the pipe on its Nth line,
91+
# and grep takes SIGPIPE (check_epipe_head.sh gates this).
92+
help_line=$(grep -F "\\ $sc " "$CLI" | sed -n 1p || true)
9193
if [ -z "$help_line" ] || ! printf '%s' "$help_line" | grep -q -- '-cp'; then
9294
echo "check_entrypoint_surface: --help line for '$sc' is missing or does not advertise -cp."
9395
echo " External integrations probe \`cljw --help\` for -cp capability detection."

‎test/e2e/entrypoint_eval_parity.sh‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ BIN="$ROOT/zig-out/bin/cljw"
4040

4141
fail() { echo "FAIL $1" >&2; exit 1; }
4242

43+
# Portable bounded run: GNU `timeout`, else coreutils `gtimeout`, else
44+
# unbounded (hosted mac runners ship neither) — the same helper the http e2e
45+
# steps use; check_portable_timeout.sh gates the bare-`timeout` form.
46+
run_bounded() {
47+
local secs="$1"; shift
48+
if command -v timeout >/dev/null 2>&1; then timeout "$secs" "$@"
49+
elif command -v gtimeout >/dev/null 2>&1; then gtimeout "$secs" "$@"
50+
else "$@"; fi
51+
}
52+
4353
WORK=$(mktemp -d)
4454
trap 'rm -rf "$WORK"' EXIT
4555

@@ -71,38 +81,38 @@ PROGRAMS=(
7181
# part of what is under test.
7282

7383
run_e_flag() {
74-
(cd "$WORK" && CLJW_PATH=src timeout 30 "$BIN" -e "$1" 2>&1)
84+
(cd "$WORK" && CLJW_PATH=src run_bounded 30 "$BIN" -e "$1" 2>&1)
7585
}
7686

7787
run_file() {
7888
printf '%s\n' "$1" > "$WORK/prog.clj"
79-
(cd "$WORK" && CLJW_PATH=src timeout 30 "$BIN" prog.clj 2>&1)
89+
(cd "$WORK" && CLJW_PATH=src run_bounded 30 "$BIN" prog.clj 2>&1)
8090
}
8191

8292
run_stdin() {
83-
(cd "$WORK" && printf '%s\n' "$1" | CLJW_PATH=src timeout 30 "$BIN" - 2>&1)
93+
(cd "$WORK" && printf '%s\n' "$1" | CLJW_PATH=src run_bounded 30 "$BIN" - 2>&1)
8494
}
8595

8696
run_repl() {
8797
# The REPL echoes a banner and `user=>` prompts around the output; strip
8898
# them so the comparison is on the program's own output.
89-
(cd "$WORK" && printf '%s\n' "$1" | timeout 30 "$BIN" repl -cp src 2>&1) \
99+
(cd "$WORK" && printf '%s\n' "$1" | run_bounded 30 "$BIN" repl -cp src 2>&1) \
90100
| sed -e 's/^user=> //' -e '/^Wasm REPL/d' -e '/^user=>$/d' \
91101
| grep -v '^$' || true
92102
}
93103

94104
run_build() {
95105
printf '%s\n' "$1" > "$WORK/prog.clj"
96-
(cd "$WORK" && timeout 180 "$BIN" build prog.clj -o prog_bin -cp src >/dev/null 2>&1) || {
106+
(cd "$WORK" && run_bounded 180 "$BIN" build prog.clj -o prog_bin -cp src >/dev/null 2>&1) || {
97107
echo "BUILD-FAILED"; return 0
98108
}
99-
(cd "$WORK" && timeout 30 ./prog_bin 2>&1)
109+
(cd "$WORK" && run_bounded 30 ./prog_bin 2>&1)
100110
}
101111

102112
run_nrepl() {
103113
local port=$(( 19000 + (RANDOM % 900) ))
104114
rm -f "$WORK/.nrepl-port"
105-
( cd "$WORK" && timeout 60 "$BIN" nrepl --port "$port" -cp src >/dev/null 2>&1 ) &
115+
( cd "$WORK" && run_bounded 60 "$BIN" nrepl --port "$port" -cp src >/dev/null 2>&1 ) &
106116
local pid=$!
107117
local deadline=$((SECONDS + 15))
108118
while [[ ! -f "$WORK/.nrepl-port" ]] && [[ $SECONDS -lt $deadline ]]; do sleep 0.1; done

0 commit comments

Comments
 (0)