flan/spike/js/p2-value-copies.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

88 lines
3.7 KiB
Plaintext

;;;; The evidence for lib/js.ml's value-semantics decision.
;;;;
;;;; A Flan struct and a fixed array are values: binding one copies it, and
;;;; emit.ml spends a memcpy at every such site. A JS object assigns by
;;;; reference, so an object mapping that does nothing aliases where the LLVM
;;;; build copied — and the divergence is invisible until something mutates a
;;;; copy, which is what every line below does.
;;;;
;;;; test/programs/values.flan already pins the two simplest cases. This goes
;;;; past them, to the ones a shallow copy would pass and a wrong one would
;;;; not: a struct inside a struct, a struct returned out of a function, an
;;;; element read out of an array of structs, and a global.
;;;;
;;;; Fable answers the same question the other way for F# structs on its JS
;;;; backend — it inserts no clone at all, and its Rust backend does — so this
;;;; is the property that says which of the two this dialect chose.
(defstruct Point [x i32 y i32])
(defstruct Box [lo Point hi Point n i32])
(defvar gp Point)
;; A parameter is a copy: writing to it must not reach the caller's value.
(defn bump [p Point] Point
(set (.x p) (+ (.x p) 100))
p)
;; A returned struct is a copy of whatever it names, not a second name for it.
(defn origin-of [b Box] Point
(.lo b))
(defn show [p Point] ()
(print (.x p)) (print " ") (println (.y p)))
(defn main [] i32
;; ── A let binding copies ────────────────────────────────────────
(let [a (Point {.x 1 .y 2})
b a]
(set (.x b) 99)
(show a) ; 1 2 — a must not have moved
(show b)) ; 99 2
;; ── A parameter copies ──────────────────────────────────────────
(let [a (Point {.x 1 .y 2})
c (bump a)]
(show a) ; 1 2
(show c)) ; 101 2
;; ── A nested struct copies with its container ───────────────────
(let [bx (Box {.lo (Point {.x 1 .y 1}) .hi (Point {.x 9 .y 9}) .n 3})
cy bx]
(set (.x (.lo cy)) 42)
(set (.n cy) 7)
(print (.x (.lo bx))) (print " ") (println (.n bx)) ; 1 3
(print (.x (.lo cy))) (print " ") (println (.n cy))) ; 42 7
;; ── A field read is a copy, not a view ──────────────────────────
(let [bx (Box {.lo (Point {.x 1 .y 1}) .hi (Point {.x 9 .y 9}) .n 3})
p (origin-of bx)]
(set (.x p) 55)
(print (.x (.lo bx))) (print " ") (println (.x p))) ; 1 55
;; ── A global copies both ways ───────────────────────────────────
(set gp (Point {.x 4 .y 5}))
(let [g gp]
(set (.x g) 0)
(show gp) ; 4 5
(show g)) ; 0 5
;; ── A fixed array is a value too ────────────────────────────────
(let [xs [1 2 3]
ys xs]
(set (at ys 0) 77)
(print (at xs 0)) (print " ") (println (at ys 0))) ; 1 77
;; ── An array of structs copies its elements ─────────────────────
;; An element read is a value too, so mutating what came out of one must
;; reach neither array. A shallow copy of the outer array would leave both
;; naming the same element object, and this would print 8 8 8.
(let [ps [(Point {.x 1 .y 1}) (Point {.x 2 .y 2})]
qs ps
e (at qs 0)]
(set (.x e) 8)
(print (.x (at ps 0))) (print " ")
(print (.x (at qs 0))) (print " ")
(println (.x e))) ; 1 1 8
0)