flan/spike/js/p1-int-semantics.flan
Joseph Ferano dfe0296479 The sweep is the evidence: 24 match, 0 differ, 71 refused by name
spike/js/survey.sh is the x86 sweep's shape with one deliberate difference in
what it counts. That backend is behind, so a refusal there is a regression and
its strict mode fails on one. This is a dialect, so a refusal is the design
working -- a pointer, an allocator, a Map, the FFI and conditions are refused
permanently and correctly. What fails the @js alias is a DIFFER, which is a
wrong answer, and a CRASH, which is JS this backend emitted and node would not
run.

Two probes carry the decisions the corpus does not reach. p1-int-semantics
prints wrapping at all eight widths, a multiply past 2^53, truncating division
with a negative operand, shifts whose count is out of range, bitwise over a
u32, f32 that is not a double, and the conversions both ways -- 35 lines, all
identical to the LLVM build. It found two real bugs: >>> binds tighter than &
in JavaScript, so a bit-and on a u32 answered -1; and a 64-bit value through
Number() rounds to 53 bits before it can be truncated, so (i32 i64hi) answered
0 where it must answer -1.

p2-value-copies goes past values.flan to the cases a shallow copy would pass:
a struct inside a struct, a struct returned out of a function, an element read
out of an array of structs, and a global.

Also fixed, and all three were found by the sweep rather than by reading: a
unit-typed call in statement position was compiled to an expression nobody
emitted, so (load-xs) silently did not happen; an arrow body that starts with
a brace is a block, so a zeroed array of structs was a syntax error; and a
bounds message must carry the index expression's location, not the form's,
because that is the one emit.ml passes to check_at.

Render reads an Option's tag as field 0 and a union's as field 0, which is the
LLVM layout and not this one, so both are answered here rather than refused.
fdefers is dropped rather than refused: nothing in the dialect can start a
transfer, so the transfer exit path is unreachable, and refusing it would have
refused every program that writes a plain defer.
2026-09-17 22:05:25 +07:00

107 lines
4.3 KiB
Plaintext

;;;; The evidence for lib/js.ml's integer decisions, run by the survey.
;;;;
;;;; JavaScript has one number type and it is a double. Flan has eight integer
;;;; types and they wrap. Every line below is a place where the two disagree
;;;; unless the backend normalises, and the survey diffs this program's output
;;;; against the LLVM build's byte for byte — so a wrong answer here is a
;;;; DIFFER and not a discussion.
;;;;
;;;; Everything goes through a global rather than a literal, for the reason
;;;; arith.flan gives: a literal operand is exactly what a constant folder
;;;; removes, and a folded program does not contain the code being tested.
(defvar i8hi i8 127)
(defvar i16hi i16 32767)
(defvar i32hi i32 2147483647)
(defvar u8hi u8 255)
(defvar u16hi u16 65535)
(defvar u32hi u32 4294967295)
(defvar i64hi i64 9223372036854775807)
(defvar u64zero u64 0)
(defvar one8 i8 1)
(defvar one16 i16 1)
(defvar one32 i32 1)
(defvar oneu8 u8 1)
(defvar oneu16 u16 1)
(defvar oneu32 u32 1)
(defvar one64 i64 1)
(defvar oneu64 u64 1)
(defvar big32 i32 123456789)
(defvar big64 i64 1234567890123)
(defvar three i32 3)
(defvar three64 i64 3)
(defvar seven i32 7)
(defvar negsev i32 -7)
(defvar shift i32 33)
(defvar shift8 i8 9)
(defvar f32one f32 1.0)
(defvar f32big f32 16777217.0)
(defn main [] i32
;; ── Wrapping at every width ──────────────────────────────────────
;; The top of the type plus one. A JS number would answer 128, 32768,
;; 2147483648, 256, 65536, 4294967296 and never wrap at all.
(println (+ i8hi one8))
(println (+ i16hi one16))
(println (+ i32hi one32))
(println (+ u8hi oneu8))
(println (+ u16hi oneu16))
(println (+ u32hi oneu32))
(println (+ i64hi one64))
(println (- u64zero oneu64)) ; the top of u64, which no literal spells
;; A multiply whose exact product passes 2^53, where a * b in JS is not
;; exact and Math.imul is. 123456789 * 123456789 = 15241578750190521, which
;; is above 2^53, so a double rounds it before the truncation can happen.
(println (* big32 big32))
(println (* big64 big64))
;; ── Truncating division, and the sign of a remainder ─────────────
;; C truncates toward zero and so does JS's %, but (a / b) | 0 is only the
;; same answer below 2^31, which is why the backend uses Math.trunc.
(println (/ seven three))
(println (/ negsev three))
(println (% seven three))
(println (% negsev three))
(println (/ big64 three64))
;; ── Shifts, with the count masked to the operand's width ─────────
;; 33 is not a legal shift count for a 32-bit type, and the checker only
;; rejects a *literal* out of range, so this is the computed case emit.ml
;; masks and this backend has to mask too. JS masks a 32-bit shift itself
;; and has no 8- or 16-bit shift at all, which is why the mask is written
;; out rather than relied on.
(println (<< one32 shift))
(println (<< one8 shift8))
(println (>> i32hi one32))
(println (>> (- (i32 0) i32hi) one32)) ; arithmetic: signed stays signed
(println (>> u32hi oneu32)) ; logical: unsigned stays unsigned
(println (<< one64 (i64 65)))
;; ── Bitwise over an unsigned 32-bit value ────────────────────────
;; JS bitwise operators produce a *signed* 32-bit result, so every one of
;; these needs the >>> 0 back.
(println (bit-and u32hi u32hi))
(println (bit-or u32hi oneu32))
(println (bit-xor u32hi oneu32))
;; ── f32 is not a double ──────────────────────────────────────────
;; 16777217 is the first integer a float cannot hold, so this prints
;; 16777216 under a real f32 and 16777218 under a double.
(println (+ f32big f32one))
(println (/ (f32 1.0) (f32 3.0)))
(println (/ (f64 1.0) (f64 3.0)))
;; ── The conversions, both ways ───────────────────────────────────
(println (i8 i32hi))
(println (u8 (- (i32 0) one32)))
(println (i32 i64hi))
(println (i64 big32))
(println (u64 i64hi))
(println (f64 i64hi))
(println (i32 (f64 2.9)))
(println (i32 (f64 -2.9)))
0)