flan/test/programs/dyn-view.flan
Joseph Ferano 29a9441f12 Typed containers into dyn as views — M2 item 3
A (Vec T), a slice or a fixed array crossing into dyn no longer refuses; it
is a view, one word in the box, over the container's own storage. Reads box
the element on the way out; writes tag-check the dyn value's tag against the
element type on the way in and trap, by name, on a mismatch, never coercing
or silently storing.

The open question the decision left — whether the descriptor points at the
container or snapshots pointer and length beside it — is settled by kind. A
Vec view holds the address of the Vec's own header (flan_rt.c's flan_vec,
restated in flan_dyn.c under the file's standing "if either table changes,
change both" rule) and reads ptr and len live on every operation, so a push
that reallocates cannot leave it stale: flan_vec_grow overwrites that same
header in place, and there is nothing captured at the crossing for the
growth to invalidate. A slice and a fixed array cannot grow, so a flat view
snapshots data and length once; pointing it at the value's own slot instead
would be worse, since a slot's lifetime is not the slice's.

The element set is i64, f64 and bool, not everything box already handles
typed-to-dyn. A string element's dyn form is a pointer into the collector's
heap, and a typed container's storage is arena or stack memory the collector
never scans — a wider set would let a write plant a live reference nothing
ever traces, which no care at the write site closes. (Vec string) and a
typed (Map K V) keep the "does not cross into dyn yet" refusal, now for that
reason.

flan_dyn.c gains a fourth object kind, OBJ_VIEW, and flan_dyn_len/at/set_at/
push and the printer each grow one branch for it beside the existing vec
one. A view's own stale-container check is the runtime's own spelling
(flan_trap, park-and-inspect) rather than flan_rt.c's rt_die, per the
duplicity doctrine; growing a Vec through a view calls flan_rt.c's own
flan_vec_push rather than re-implementing doubling and allocator adoption a
second time. (set (at target i) x) against a dyn target — a plain dyn vec or
a view alike — was a hole in the base dyn milestone rather than something
item 3 introduced; it is wired to flan_dyn_set_at here because a view's
writes needed it to exist at all.

Both backends: emit.ml and x86.ml both already passed a Vec or a Map to a
runtime call by address rather than by value; a fixed array crossing into a
view needed the same arm added in both, for the same reason — a copy would
view the copy and never see a write to the caller's own array.

test/dyn_ops.c drives the runtime directly with a hand-built Vec header and
a plain C array, ahead of any compiler involvement: reads, writes on both
element kinds, the tag-check refusal on every element kind, the range
refusal, and the push that grows and moves a hand-built header out from
under the view watching it. test_flan.ml turns the old "does not cross into
dyn yet" refusal into acceptances for Vec/slice/array, keeps it for a string
element and for Map, and adds the element-restriction refusal by name.
test/programs/dyn-view.flan is the compiler-level survey: a Vec view mutated
through both sides including the grow-and-move case, a fixed array's and a
slice's views, a bool Vec's view, and its own two trapping modes for the
acceptance rows to run against. test_sanitize.ml carries the survey's happy
path; test_dyn.ml's new refusals are the runtime's own.
2026-09-20 09:19:17 +07:00

112 lines
4.3 KiB
Plaintext

;;;; M2 item 3: a typed container crossing into dyn is a VIEW, not a copy.
;;;;
;;;; [as-dyn]'s parameter is unannotated dyn and its argument is a typed
;;;; (Vec i64), a fixed array or a slice — the box happens at the call, on the
;;;; caller's own value, which is what makes [dv] below the SAME storage [v]
;;;; is and not a copy of it. (Boxing a value AFTER passing it through an
;;;; ordinary by-value parameter would view that parameter's own copy instead
;;;; — value semantics, not a hole in this feature — so every view here is
;;;; taken where the container already lives.)
;;;;
;;;; Mode 0 is the survey: a read through the view boxes the element
;;;; correctly, a write through either side is seen through the other, and a
;;;; push through the view — which can only mean the Vec case, since neither
;;;; a slice nor a fixed array can grow — moves the Vec's backing storage and
;;;; the typed side still sees the grown length and the new element. That
;;;; last one is the design's central claim: the view's descriptor points AT
;;;; the Vec's own header rather than snapshotting its pointer and length, so
;;;; there is no snapshot for the growth to invalidate.
;;;;
;;;; Modes 1 and 2 are the two traps a view can throw: an index outside its
;;;; length, and a write whose dyn tag does not match the element type the
;;;; view was built over. Both come from the runtime, by name, and both end
;;;; the process — a survey program can show at most one trap, so each gets
;;;; its own mode the way test/programs/bounds.flan's do.
(defn as-dyn [d dyn] dyn d)
(defn main [args [string]] i32
(let [n (i32 (bytes->i64 (bytes (at args 1))))]
(cond
(= n 0)
(do
;; A (Vec i64) view.
(let [v (vec-new i64)]
(push v 10)
(push v 20)
(push v 30)
(let [dv (as-dyn v)]
(print dv)
(print "\n")
;; Write through the view, read through the typed side.
(set (at dv 1) 999)
(print (at v 1))
(print "\n")
;; Write through the typed side, read through the view.
(set (at v 2) 777)
(print (at dv 2))
(print "\n")
;; Grow through the view. flan_vec_grow reallocates v's backing
;; storage and overwrites v's own header in place, which is the
;; same header the view points at — so the typed side, asked
;; afterwards, already agrees with the push it never made itself.
(push dv 40)
(print (len v))
(print "\n")
(print (at v 3))
(print "\n")))
;; A fixed array's view: nothing here can grow, so a snapshot taken
;; once at the crossing is sound — there is no move to go stale over.
(let [a (array 4 i64)]
(set (at a 0) 1)
(set (at a 1) 2)
(set (at a 2) 3)
(set (at a 3) 4)
(let [da (as-dyn a)]
(print da)
(print "\n")
(set (at da 0) 100)
(print (at a 0))
(print "\n")
(set (at a 3) 400)
(print (at da 3))
(print "\n")))
;; A slice's view, over f64 elements, and a bool Vec's view — the
;; other two of the three element kinds a view can hold.
(let [a2 (array 3 f64)]
(set (at a2 0) 1.5)
(set (at a2 1) 2.5)
(set (at a2 2) 3.5)
(let [ds (as-dyn (slice a2 0 3))]
(print ds)
(print "\n")
(set (at ds 0) 9.5)
(print (at a2 0))
(print "\n")))
(let [bv (vec-new bool)]
(push bv true)
(push bv false)
(let [db (as-dyn bv)]
(print db)
(print "\n")
(set (at db 1) true)
(print (at bv 1))
(print "\n")))
0)
(= n 1)
;; Out of range. The runtime's own message names the length.
(do (let [v (vec-new i64)]
(push v 1)
(let [dv (as-dyn v)]
(print (at dv 5))))
0)
(= n 2)
;; Wrong type on write: a text where the view holds i64. Tag-checked
;; and refused, never coerced and never silently stored.
(do (let [v (vec-new i64)]
(push v 1)
(let [dv (as-dyn v)]
(set (at dv 0) "nope")))
0)
:else (do (println "?") 1))))