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

Commit 4f0b170

Browse files
committed
The boxed lane ships: longs past i31 compile, and the oracle killed fib(92)
0025 implemented: $unbox takes either representation, $box is the canonicalizing guard (fits-i31 -> i31, else a fresh $BoxI64 — a boxed small integer cannot exist), literals beyond i31 emit boxed, and the entry unwraps through $unbox so boxed results print. Overflow arms are per-op unreachable — add/sub by sign rules, mul by division with the -1 x MIN case tested before the division that would itself trap — and stay out of contract until the throw representation lands. quot wraps at MIN/-1 exactly like JVM long division, pinned by its own entry. Twelve new corpus lines, including the stop-condition domain run iteratively at both ends: fib(46), the first value off i31, and fib(91) — not 92, because this loop shape computes fib(n+1) into b and real clojure throws "long overflow" one step ahead of the value. The differential oracle caught that when the entry first said 92, which is the second time today it corrected its own author. 0023's literal-outside-i31 compile error is amended in place: it lasted exactly as long as the lane was unmeasured, which was the point. Claude-Session: https://claude.ai/code/session_01XF5Hfq4Ca2N2XYEzWQQuHt
1 parent b170077 commit 4f0b170

4 files changed

Lines changed: 118 additions & 27 deletions

File tree

corpus/s3.edn

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,32 @@
9393
;; so redefinition works in both modes.
9494
{:id "fn-redef-marked" :forms [(def ^:redef f (fn [] 1))
9595
(def ^:redef f (fn [] 2))
96-
(f)]}]
96+
(f)]}
97+
;; --- the boxed-i64 lane (0025): longs outside i31 are $BoxI64 and
98+
;; fixnums are canonical. True 64-bit overflow stays out of contract
99+
;; until the throw representation lands.
100+
{:id "box-lit" :forms [2147483648]}
101+
{:id "box-lit-long-max" :forms [9223372036854775807]}
102+
{:id "box-lit-long-min" :forms [-9223372036854775808]}
103+
{:id "box-cross-up" :forms [(+ 1073741823 1)]}
104+
;; Comes back under the i31 boundary: exercises the canonicalizing arm.
105+
{:id "box-canon-down" :forms [(- (+ 1073741823 1) 1)]}
106+
{:id "box-mul" :forms [(* 100000 100000)]}
107+
{:id "box-quot-down" :forms [(quot 10000000000 100000)]}
108+
;; JVM long division wraps silently at MIN / -1; so do we (0025).
109+
{:id "box-quot-min-neg1" :forms [(quot -9223372036854775808 -1)]}
110+
{:id "box-lt" :forms [(if (< 2147483648 2147483649) 1 2)]}
111+
{:id "box-mixed-lt" :forms [(if (< 5 2147483648) 1 2)]}
112+
;; The stop-condition domain (0022 C, iteratively): fib(46) is the
113+
;; first value off i31. The upper entry is 91, not 92: this loop shape
114+
;; computes fib(n+1) into b on the last iteration, and at n = 92 real
115+
;; clojure throws "long overflow" one step ahead of the returned value
116+
;; — the oracle caught exactly that when this entry first said 92.
117+
{:id "box-fib-46" :forms [(defn fib-iter [n]
118+
(loop [i n a 0 b 1]
119+
(if (< 0 i) (recur (- i 1) b (+ a b)) a)))
120+
(fib-iter 46)]}
121+
{:id "box-fib-91" :forms [(defn fib-iter [n]
122+
(loop [i n a 0 b 1]
123+
(if (< 0 i) (recur (- i 1) b (+ a b)) a)))
124+
(fib-iter 91)]}]

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,16 @@ representation for everything the slice touches.
6868
1. **Every Clojure value is `(ref null eq)`.** One uniform type; no
6969
unboxing pass in the slice. Refinement is a prod-mode optimisation
7070
later, per binaryen's lowering guidance already cited in `0022` A.
71-
2. **Fixnums are `i31`** — B3's measured substrate, verbatim. A literal
72-
outside i31's signed 31-bit range is a **compile error** in the slice:
73-
that value belongs to the boxed-i64 lane, which is open (`0022` C)
74-
precisely because it is unmeasured. Runtime arithmetic computes in
75-
i64 and re-boxes through one guard; a result outside i31 hits
71+
2. **Fixnums are `i31`** — B3's measured substrate, verbatim. *(Amended
72+
the same day: the rest of this point described the slice before B8
73+
ran. `0025` measured the boxed lane, decided canonical fixnums, and
74+
the emitter now boxes longs outside i31 as `$BoxI64` — the compile
75+
error and the `unreachable` re-box arm below lasted exactly as long
76+
as the lane was unmeasured, which was the point.)* A literal
77+
outside i31's signed 31-bit range was a **compile error** in the
78+
slice: that value belonged to the boxed-i64 lane, open (`0022` C)
79+
precisely because it was unmeasured. Runtime arithmetic computes in
80+
i64 and re-boxes through one guard; a result outside i31 hit
7681
`(unreachable)` — the same shape B3 measured, and the same honesty:
7782
the slow path exists and no corpus entry may reach it yet. The
7883
corresponding **corpus authoring rule covers intermediates and

doc/status.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,16 @@ _Short by design, and printed at every session start — so findings live in
4141
value fitting i31 must not exist; the probe measured free). Still
4242
open: the dev-loop output format (before the nREPL unit) and the
4343
throw representation (its check is priced, its taken arm is not).
44-
Next unit: **the boxed lane in the emitter**`$box` grows its
45-
boxed arm, ops unbox both representations, literals beyond i31
46-
compile — with its corpus entries in the same commit.
44+
**The boxed lane is in the emitter** (57 corpus entries): longs
45+
beyond i31 compile, fixnums canonicalize through `$box`, `quot`
46+
wraps at MIN∕−1 like the JVM, and the corpus runs the stop-condition
47+
domain iteratively — fib(46) and fib(91); the 92 entry died at the
48+
oracle's hands because that loop shape computes one step ahead, and
49+
the corpus comment records it. Next unit: **the dev-loop output
50+
format** (`0009`'s precondition, `0022` A's candidate list —
51+
assembling near the engine, a persistent assembler, batching,
52+
TeaVM's writer), because it gates S2's nREPL unit and is the larger
53+
of the two open decisions; the throw representation follows it.
4754

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

src/cljwit/emit.clj

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
"AST -> WAT text, the batch emitter (`doc/design/0022` A; the value
33
representation is pinned in `0023`, fn's in `0024`).
44
5-
Every value is `(ref null eq)`; fixnums are i31; nil is the null
6-
reference; false and true are singleton struct globals distinct by
7-
identity. Arithmetic computes in i64 and re-boxes through one guard
8-
whose failure arm is `unreachable` — the boxed-i64 lane is open
9-
(`0022` C) and no corpus entry may reach it.
5+
Every value is `(ref null eq)`; fixnums are i31 and canonical — a
6+
boxed value that fits i31 must not exist (`0025`); longs outside i31
7+
are `$BoxI64` structs; nil is the null reference; false and true are
8+
singleton struct globals distinct by identity. Arithmetic unboxes
9+
either representation, computes in i64, and re-boxes through the one
10+
canonicalizing guard. The overflow arms are `(unreachable)` — the
11+
throw representation is the open decision (`0022` C, `0025` §4) and
12+
no corpus entry may reach them.
1013
1114
fn (`0024`): closures are subtypes of a `$Fn` base struct carrying one
1215
nullable typed-function-ref slot per arity; each capture signature is
@@ -64,8 +67,10 @@
6467
:number (cond
6568
(not (integer? val))
6669
(out-of-slice! (str "non-integer literal " (pr-str val)) ast)
67-
(or (< val i31-min) (> val i31-max))
68-
(out-of-slice! (str "literal " val " outside i31 — the boxed lane is open (0022 C)") ast)
70+
(not (instance? Long val))
71+
(out-of-slice! (str "integer literal beyond long: " (pr-str val)) ast)
72+
(or (< ^long val i31-min) (> ^long val i31-max))
73+
(format "(struct.new $BoxI64 (i64.const %d))" val)
6974
:else (format "(ref.i31 (i32.const %d))" val))
7075
:bool (if val "(global.get $true)" "(global.get $false)")
7176
:nil "(ref.null eq)"
@@ -366,28 +371,72 @@
366371

367372
(def ^:private runtime
368373
" (type $Unit (struct))
374+
(type $BoxI64 (struct (field $v i64)))
369375
(global $false (ref $Unit) (struct.new $Unit))
370376
(global $true (ref $Unit) (struct.new $Unit))
371377
(func $truthy (param $v (ref null eq)) (result i32)
372378
(i32.eqz (i32.or (ref.is_null (local.get $v))
373379
(ref.eq (local.get $v) (global.get $false)))))
380+
;; Either numeric representation -> i64. A non-number traps the cast
381+
;; (out-of-contract until an exception representation exists).
374382
(func $unbox (param $v (ref null eq)) (result i64)
375-
(i64.extend_i32_s (i31.get_s (ref.cast (ref i31) (local.get $v)))))
383+
(if (result i64) (ref.test (ref i31) (local.get $v))
384+
(then (i64.extend_i32_s (i31.get_s (ref.cast (ref i31) (local.get $v)))))
385+
(else (struct.get $BoxI64 $v (ref.cast (ref $BoxI64) (local.get $v))))))
386+
;; The canonicalizing guard (0025): fits i31 -> i31, else a fresh box.
387+
;; A boxed value that fits i31 must not exist.
376388
(func $box (param $v i64) (result (ref eq))
377-
;; 31-bit signed range check; the failure arm is the boxed-i64 lane,
378-
;; open and unmeasured (0022 C) — no corpus entry may reach it.
379-
(if (i64.ne (local.get $v)
389+
(if (result (ref eq))
390+
(i64.eq (local.get $v)
380391
(i64.shr_s (i64.shl (local.get $v) (i64.const 33)) (i64.const 33)))
381-
(then (unreachable)))
382-
(ref.i31 (i32.wrap_i64 (local.get $v))))
392+
(then (ref.i31 (i32.wrap_i64 (local.get $v))))
393+
(else (struct.new $BoxI64 (local.get $v)))))
394+
;; Overflow arms are the open throw representation (0022 C, 0025 §4);
395+
;; signed-overflow checks per Hacker's Delight sign rules.
383396
(func $add (param $a (ref null eq)) (param $b (ref null eq)) (result (ref eq))
384-
(call $box (i64.add (call $unbox (local.get $a)) (call $unbox (local.get $b)))))
397+
(local $x i64) (local $y i64) (local $t i64)
398+
(local.set $x (call $unbox (local.get $a)))
399+
(local.set $y (call $unbox (local.get $b)))
400+
(local.set $t (i64.add (local.get $x) (local.get $y)))
401+
(if (i64.lt_s (i64.and (i64.xor (local.get $x) (local.get $t))
402+
(i64.xor (local.get $y) (local.get $t)))
403+
(i64.const 0))
404+
(then (unreachable)))
405+
(call $box (local.get $t)))
385406
(func $sub (param $a (ref null eq)) (param $b (ref null eq)) (result (ref eq))
386-
(call $box (i64.sub (call $unbox (local.get $a)) (call $unbox (local.get $b)))))
407+
(local $x i64) (local $y i64) (local $t i64)
408+
(local.set $x (call $unbox (local.get $a)))
409+
(local.set $y (call $unbox (local.get $b)))
410+
(local.set $t (i64.sub (local.get $x) (local.get $y)))
411+
(if (i64.lt_s (i64.and (i64.xor (local.get $x) (local.get $y))
412+
(i64.xor (local.get $x) (local.get $t)))
413+
(i64.const 0))
414+
(then (unreachable)))
415+
(call $box (local.get $t)))
387416
(func $mul (param $a (ref null eq)) (param $b (ref null eq)) (result (ref eq))
388-
(call $box (i64.mul (call $unbox (local.get $a)) (call $unbox (local.get $b)))))
417+
(local $x i64) (local $y i64) (local $t i64)
418+
(local.set $x (call $unbox (local.get $a)))
419+
(local.set $y (call $unbox (local.get $b)))
420+
(local.set $t (i64.mul (local.get $x) (local.get $y)))
421+
;; The a = -1, b = MIN case must be tested before the division below
422+
;; would itself trap on MIN / -1.
423+
(if (i64.ne (local.get $x) (i64.const 0))
424+
(then
425+
(if (i32.and (i64.eq (local.get $x) (i64.const -1))
426+
(i64.eq (local.get $y) (i64.const -9223372036854775808)))
427+
(then (unreachable)))
428+
(if (i64.ne (i64.div_s (local.get $t) (local.get $x)) (local.get $y))
429+
(then (unreachable)))))
430+
(call $box (local.get $t)))
431+
;; quot: y = -1 wraps like the JVM's long division (MIN / -1 is MIN,
432+
;; silently); y = 0 takes the native division trap, trap-table row 1.
389433
(func $quot (param $a (ref null eq)) (param $b (ref null eq)) (result (ref eq))
390-
(call $box (i64.div_s (call $unbox (local.get $a)) (call $unbox (local.get $b)))))
434+
(local $x i64) (local $y i64)
435+
(local.set $x (call $unbox (local.get $a)))
436+
(local.set $y (call $unbox (local.get $b)))
437+
(if (result (ref eq)) (i64.eq (local.get $y) (i64.const -1))
438+
(then (call $box (i64.sub (i64.const 0) (local.get $x))))
439+
(else (call $box (i64.div_s (local.get $x) (local.get $y))))))
391440
(func $lt (param $a (ref null eq)) (param $b (ref null eq)) (result (ref eq))
392441
(if (result (ref eq))
393442
(i64.lt_s (call $unbox (local.get $a)) (call $unbox (local.get $b)))
@@ -452,4 +501,6 @@
452501
" (func $entry (export \"entry\") (result i64)\n"
453502
(str/join (map #(str " " % "\n") @(:local-decls ctx)))
454503
(str/join (map #(str " " % "\n") stmts))
455-
(format " (i64.extend_i32_s (i31.get_s (ref.cast (ref i31) %s)))))\n" ret))))
504+
;; $unbox accepts both numeric representations; a non-number
505+
;; result traps its cast — 0022 B.3's scalar rule, mechanically.
506+
(format " (call $unbox %s)))\n" ret))))

0 commit comments

Comments
 (0)