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

Commit 48f5d83

Browse files
committed
Scalar def lands; the oracle unwraps what eval wraps; (def y y) refuses
0022 D verbatim: one mutable global per var, declared null, assigned at eval; a redef reuses its global; a def is a top-level statement only — in expression position, or as the entry's last form, or without init, it is a loud out-of-slice error, because a var value and an Unbound sentinel have no representation to compare. Two findings from the independent critique (Check #6), fixed here: the init now emits before the var's global registers, so the self-referential (def y y) — which silently returned 2 where the JVM's Unbound sentinel is truthy and returns 1 — hits the unresolved-var error instead; and status.md's entry count and mode-divergence claim contradicted their own paragraph (29, not 21; divergence starts at calls to def'd fns, not at def). The oracle records the cause when eval wraps a top-level throw in Compiler$CompilerException — the same triage clojure.main performs — found by def-redef-throws, the entry that exercises trap row 1 through a def init. Mode divergence has still not started; fn is next. Claude-Session: https://claude.ai/code/session_01XF5Hfq4Ca2N2XYEzWQQuHt
1 parent 4b6173d commit 48f5d83

5 files changed

Lines changed: 70 additions & 11 deletions

File tree

‎corpus/s3.edn‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,12 @@
4545
{:id "let-shadow" :forms [(let [x 1] (let [x (+ x 1)] (* x 10)))]}
4646
{:id "let-sequential" :forms [(let [x 2 y (+ x 3)] (* x y))]}
4747
{:id "top-level-forms" :forms [(+ 1 1) (+ 2 2)]}
48+
{:id "def-ref" :forms [(def x 5) (+ x 1)]}
49+
{:id "def-uses-def" :forms [(def x 5) (def y (+ x 3)) (+ x y)]}
50+
{:id "def-redef" :forms [(def x 1) (def x 2) x]}
51+
;; A throwing re-def: both lanes abort the entry at form 2, and row 1
52+
;; licenses the comparison. That the old root survives (0022 D, the
53+
;; sibling's recorded bug) is unobservable until try/catch exists —
54+
;; claimed nowhere until it does.
55+
{:id "def-redef-throws" :forms [(def x 5) (def x (quot 1 0)) x]}
4856
{:id "trap-div-zero" :forms [(quot 1 0)]}]

‎doc/design/0023-first-vertical-slice.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ representation for everything the slice touches.
5656
compare with `=`. An oracle value that `pr-str`→`read-string` cannot
5757
round-trip is a corpus lint error (`0022` B.4) — this falls out of the
5858
mechanism rather than being a separate linter.
59+
- **The oracle records what the program observably throws**, not what
60+
`eval` wraps it in: a throw during a top-level form (a `def` init, per
61+
the first `def` entries) surfaces as `Compiler$CompilerException`, and
62+
the oracle unwraps to its cause — the same triage `clojure.main`
63+
performs before showing a user the error. Found by the harness itself
64+
the day `def` landed, not designed in advance.
5965

6066
### The representation (the new pins)
6167

‎doc/status.md‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,22 @@ _Short by design, and printed at every session start — so findings live in
2020
the watcher — waits for a compiler to serve, so:
2121
3. **S3's first vertical slice is landed** (`0022` decided the shape,
2222
`0023` the harness and representation): the differential oracle is in
23-
the gate — `corpus/s3.edn` (21 scalar entries) runs through real
23+
the gate — `corpus/s3.edn` (29 scalar entries) runs through real
2424
`clojure` (fresh namespace per entry) and through
2525
`cljwit.analyze` → `cljwit.emit` → `wasm-tools` → **wasmtime and V8,
26-
in both modes** (which emit identical bytes until `def` lands — stated,
27-
not hidden), with `corpus/trap_table.edn` row 1
26+
in both modes** (which emit identical bytes until calls to def'd fns
27+
land — stated, not hidden), with `corpus/trap_table.edn` row 1
2828
(`ArithmeticException` ↔ division trap) exercised. Representation
2929
pinned in `0023`: values are `(ref null eq)`, fixnums i31, nil = null,
3030
false/true = singleton structs, overflow arm `unreachable`. Still
3131
open, each with its benchmark named (`0022`): the dev-loop output
3232
format (before the nREPL unit), the boxed-i64 lane (before the
33-
emitter grows past i31), the throw representation. Next unit:
34-
**`loop`/`recur` entries and emission** (a block and a `br`, forces
35-
nothing open), then `def` — which starts the real dev/prod divergence.
33+
emitter grows past i31), the throw representation. `loop`/`recur` and
34+
scalar `def` are in the corpus (29 entries); mode divergence has still
35+
not started — it begins with *calls to* def'd fns (direct linking,
36+
`0004`), so the next unit is **`fn`**: `0022` D's full contract —
37+
multi-arity ships with it, varargs carry an explicit rest-mode — and
38+
the first depth-sensitive entry brings the trap table its stack row.
3639

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

‎src/cljwit/emit.clj‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@
2626
(swap! local-decls conj (format "(local %s (ref null eq))" nm))
2727
nm))
2828

29+
(defn- global-for!
30+
"The module global backing a def'd var — one per var, declared null and
31+
assigned at eval (`0022` D: declare at analysis, assign at eval)."
32+
[{:keys [counter globals global-decls]} v]
33+
(or (get @globals v)
34+
(let [nm (str "$g" (swap! counter inc))]
35+
(swap! global-decls conj (format "(global %s (mut (ref null eq)) (ref.null eq))" nm))
36+
(swap! globals assoc v nm)
37+
nm)))
38+
2939
(declare emit-expr)
3040

3141
(defn- emit-const [{:keys [type val] :as ast}]
@@ -136,9 +146,27 @@
136146
:local (if-let [wl (get-in ctx [:locals (:name ast)])]
137147
(format "(local.get %s)" wl)
138148
(out-of-slice! (str "unresolved local " (:name ast)) ast))
149+
:var (if-let [g (get @(:globals ctx) (:var ast))]
150+
(format "(global.get %s)" g)
151+
(out-of-slice! (str "var " (:var ast) " has no global — only def'd vars are referenceable") ast))
152+
:def (out-of-slice! "def in expression position — its value is a var, which has no representation yet" ast)
139153
:invoke (emit-invoke ctx ast)
140154
(out-of-slice! (str "op " (:op ast)) ast)))
141155

156+
(defn- emit-top-level
157+
"A top-level statement: a def becomes a `global.set`; anything else is
158+
an expression whose value is dropped."
159+
[ctx ast]
160+
(if (= :def (:op ast))
161+
(if-let [init (:init ast)]
162+
;; The init is emitted before the var's global registers: a
163+
;; self-referential `(def y y)` must hit the loud unresolved-var
164+
;; error, not silently read the null the global was declared with.
165+
(let [init-wat (emit-expr ctx init)]
166+
(format "(global.set %s %s)" (global-for! ctx (:var ast)) init-wat))
167+
(out-of-slice! "def without init — an unbound var has no edn representation" ast))
168+
(format "(drop %s)" (emit-expr ctx ast))))
169+
142170
(def ^:private runtime
143171
" (type $Unit (struct))
144172
(global $false (ref $Unit) (struct.new $Unit))
@@ -179,13 +207,19 @@
179207
(`0023`)."
180208
[asts mode]
181209
{:pre [(#{:dev :prod} mode) (seq asts)]}
182-
(let [ctx {:locals {} :counter (atom 0) :local-decls (atom [])}
183-
stmts (mapv #(emit-expr ctx %) (butlast asts))
184-
ret (emit-expr ctx (last asts))]
210+
(let [ctx {:locals {} :counter (atom 0) :local-decls (atom [])
211+
:globals (atom {}) :global-decls (atom [])}
212+
stmts (mapv #(emit-top-level ctx %) (butlast asts))
213+
;; The last form is the entry's result, so it cannot be a def —
214+
;; a def's value is the var itself, which is not a scalar.
215+
ret (if (= :def (:op (last asts)))
216+
(out-of-slice! "a def cannot be an entry's last form — its value is a var" (last asts))
217+
(emit-expr ctx (last asts)))]
185218
(str "(module\n"
186219
(format " ;; mode %s — no divergence in the slice yet (0023)\n" (name mode))
187220
runtime
221+
(str/join (map #(str " " % "\n") @(:global-decls ctx)))
188222
" (func $entry (export \"entry\") (result i64)\n"
189223
(str/join (map #(str " " % "\n") @(:local-decls ctx)))
190-
(str/join (map #(format " (drop %s)\n" %) stmts))
224+
(str/join (map #(str " " % "\n") stmts))
191225
(format " (i64.extend_i32_s (i31.get_s (ref.cast (ref i31) %s)))))\n" ret))))

‎test/cljwit/corpus_test.clj‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,15 @@
5050
(refer-clojure)
5151
{:value (reduce (fn [_ f] (eval f)) nil forms)}
5252
(catch Throwable t
53-
{:throw (.getName (class t)) :message (.getMessage t)})
53+
;; `eval` wraps a throw during a top-level form (a def init, say)
54+
;; in Compiler$CompilerException; what the program observably
55+
;; throws under `clojure` is the cause — clojure.main's own
56+
;; ex-triage reports the cause's class — so the oracle records it.
57+
(let [t (if (and (instance? clojure.lang.Compiler$CompilerException t)
58+
(.getCause t))
59+
(.getCause t)
60+
t)]
61+
{:throw (.getName (class t)) :message (.getMessage t)}))
5462
(finally (remove-ns ns-sym))))))
5563

5664
(defn- assemble!

0 commit comments

Comments
 (0)