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.
112 lines
4.3 KiB
Plaintext
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))))
|