|
2 | 2 | "AST -> WAT text, the batch emitter (`doc/design/0022` A; the value |
3 | 3 | representation is pinned in `0023`, fn's in `0024`). |
4 | 4 |
|
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. |
10 | 13 |
|
11 | 14 | fn (`0024`): closures are subtypes of a `$Fn` base struct carrying one |
12 | 15 | nullable typed-function-ref slot per arity; each capture signature is |
|
64 | 67 | :number (cond |
65 | 68 | (not (integer? val)) |
66 | 69 | (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) |
69 | 74 | :else (format "(ref.i31 (i32.const %d))" val)) |
70 | 75 | :bool (if val "(global.get $true)" "(global.get $false)") |
71 | 76 | :nil "(ref.null eq)" |
|
366 | 371 |
|
367 | 372 | (def ^:private runtime |
368 | 373 | " (type $Unit (struct)) |
| 374 | + (type $BoxI64 (struct (field $v i64))) |
369 | 375 | (global $false (ref $Unit) (struct.new $Unit)) |
370 | 376 | (global $true (ref $Unit) (struct.new $Unit)) |
371 | 377 | (func $truthy (param $v (ref null eq)) (result i32) |
372 | 378 | (i32.eqz (i32.or (ref.is_null (local.get $v)) |
373 | 379 | (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). |
374 | 382 | (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. |
376 | 388 | (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) |
380 | 391 | (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. |
383 | 396 | (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))) |
385 | 406 | (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))) |
387 | 416 | (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. |
389 | 433 | (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)))))) |
391 | 440 | (func $lt (param $a (ref null eq)) (param $b (ref null eq)) (result (ref eq)) |
392 | 441 | (if (result (ref eq)) |
393 | 442 | (i64.lt_s (call $unbox (local.get $a)) (call $unbox (local.get $b))) |
|
452 | 501 | " (func $entry (export \"entry\") (result i64)\n" |
453 | 502 | (str/join (map #(str " " % "\n") @(:local-decls ctx))) |
454 | 503 | (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