Any typed container of numbers, bools, str, structs, arrays, slices or Vecs crosses into dyn as a view from any storage, and a dev build traps on a view whose frame returned or whose block was released.

This commit is contained in:
Joseph Ferano 2026-09-26 06:10:11 +07:00
parent f9dadce5d0
commit 7953376e4e
12 changed files with 1319 additions and 455 deletions

View File

@ -354,7 +354,23 @@ type env = {
mutable guard_next : bool; mutable guard_next : bool;
} }
let new_env () = { (* The struct table [box] reads a struct's fields from when it describes one
for a view: [box] is called from places that hold no [ctx], and there is
one table per program. Set by [new_env]. *)
let view_structs : (string, Tast.structure) Hashtbl.t ref = ref (Hashtbl.create 1)
(* The global whose initialiser is being checked, and its form. A view taken
there of storage the initialiser itself built is gone the moment the
initialiser returns, so it is refused rather than left to the dev check:
nothing could ever read it. *)
let view_global_init : (string * Ast.reinit) option ref = ref None
let rec new_env () =
let env = new_env_record () in
view_structs := env.structs;
env
and new_env_record () = {
structs = Hashtbl.create 16; structs = Hashtbl.create 16;
datas = Hashtbl.create 16; datas = Hashtbl.create 16;
unions = Hashtbl.create 16; unions = Hashtbl.create 16;
@ -3612,28 +3628,48 @@ let no_dyn_yet loc ~into t extra =
"%s does not cross into %s yet%s" "%s does not cross into %s yet%s"
(tyname loc t) (if into then "dyn" else "a written type") extra (tyname loc t) (if into then "dyn" else "a written type") extra
(* M2 item 3: a typed container crossing into dyn as a view. The element set
is exactly the unboxable scalars — i64, f64, bool — and that is not a smaller
version of the same cut for the same reason: every other element type
would need [box] to run on IT too, and a string element's dyn form is a
pointer into the collector's heap, while a typed container's storage is
arena or stack memory the collector never scans. Writing that pointer
into memory nobody roots is a live reference the collector could free out
from under — the hazard runtime/flan_dyn.h's view section states at
length — and i64/f64/bool carry no pointer, so a view restricted to them
cannot manufacture it. It is a compile-time refusal here rather than a
run-time one because the element type is exactly what the checker already
knows at the crossing. The FLAN_VIEW_* constants are runtime/flan_dyn.h's;
this is the compiler's one copy of the same table. *)
let view_elem (t : Types.t) : int64 option =
match t with
| Types.Int Types.I64 -> Some 0L (* FLAN_VIEW_I64 *)
| Types.Float Types.F64 -> Some 1L (* FLAN_VIEW_F64 *)
| Types.Bool -> Some 2L (* FLAN_VIEW_BOOL *)
| _ -> None
let view_elem_lit loc (k : int64) = (* A typed container crossing into dyn is a view, and the runtime needs to
mk loc (Types.Int Types.I32) (Tast.Int (k, Types.I32)) know what one element is: its descriptor, a prefix code runtime/flan_dyn.c
documents beside [desc_lay] and reads offsets out of by C's layout rule.
Every number, bool, str, struct of those, and fixed array, slice or Vec of
those can be described. [Error t] names the first type inside that cannot:
a dyn, a pointer, a function, an Option, a map, an enum or a data type.
None of those is refused for want of a descriptor letter — each is one a
dyn value cannot be read out of or written into without a meaning
nobody has decided. A str is read as a copy and never written, since a
dyn text is a collector pointer and typed storage is never scanned; a
[const] slice is refused because a dyn view can be written through. *)
let rec view_desc (t : Types.t) : (string, Types.t) result =
let ( let* ) = Result.bind in
match t with
| Types.Int k ->
Ok (match k with
| Types.I8 -> "b" | Types.U8 -> "B" | Types.I16 -> "h" | Types.U16 -> "H"
| Types.I32 -> "i" | Types.U32 -> "I" | Types.I64 -> "l" | Types.U64 -> "L")
| Types.Float Types.F32 -> Ok "f"
| Types.Float Types.F64 -> Ok "d"
| Types.Bool -> Ok "?"
| Types.String -> Ok "t"
| Types.Array (n, e) ->
let* d = view_desc e in
Ok (Printf.sprintf "a%Ld;%s" n d)
| Types.Slice (Types.Mut, e) -> let* d = view_desc e in Ok ("s" ^ d)
| Types.Vec e -> let* d = view_desc e in Ok ("v" ^ d)
| Types.Named n ->
(match Hashtbl.find_opt !view_structs n with
| None -> Error t
| Some st ->
let* fs =
List.fold_left
(fun acc (fl : Tast.field) ->
let* acc = acc in
let* d = view_desc fl.Tast.fty in
Ok ((fl.Tast.fname ^ ";" ^ d) :: acc))
(Ok []) st.Tast.fields
in
Ok ("{" ^ n ^ ";" ^ String.concat "" (List.rev fs) ^ "}"))
| _ -> Error t
(* A fix is spelled in the syntax of the file the mistake is in: the checker (* A fix is spelled in the syntax of the file the mistake is in: the checker
sees one AST for both, so the location's file is the only thing left that sees one AST for both, so the location's file is the only thing left that
@ -3711,99 +3747,48 @@ let view_refusal kind loc (e : Tast.expr) reason =
Loc.failk kind loc "%s, and a dyn value is wanted here. %s. %s" subject Loc.failk kind loc "%s, and a dyn value is wanted here. %s. %s" subject
reason fix reason fix
let view_not_yet loc (e : Tast.expr) (elem : Types.t) = let view_not_yet loc (e : Tast.expr) (inner : Types.t) =
view_refusal "check/dyn-not-yet" loc e view_refusal "check/dyn-not-yet" loc e
(Printf.sprintf (Printf.sprintf
"A dyn value can see into a typed container only when its elements \ "A dyn value sees into numbers, bools, str and structs, and arrays, \
are i64, f64 or bool, and these are %s" slices and Vecs of those; a %s is none of these"
(tyname loc elem)) (tyname loc inner))
(* M2 item 3's second guard, added on review: a view's descriptor holds an (* Whether a view's storage is the current function's own frame, which is
address into the container's own storage, chased fresh on every what the runtime's dev check needs to be told: it then records this
operation, which is what makes a Vec's growth safe — but it is also what activation and traps if the view is used after the call returns. A local,
makes a *dangling* container's storage a live hazard nothing catches a parameter (copied into the frame, an array parameter too), a field or
until somebody reads through the view. A view returned from the function an array element of one, a slice cut directly from a local array, and a
whose frame the Vec lived in, stashed in a global and read after that temporary [box] has bound to a slot of its own are all the frame's. A
frame is gone, or left behind when a condition transfer unwinds it, are slice's data, and anything reached through a [Ptr] or a global, is not:
all stack-use-after-return once box stopped refusing containers outright there the runtime looks the address up in the allocation registry instead,
— reachable now for the first time, not a pre-existing hole this lane and storage the registry does not know — a global, or a caller's local
merely inherited. seen through a slice parameter — is not checked. *)
let rec frame_root (e : Tast.expr) : bool =
On the dynamic side Flan aims where Clojure and Common Lisp are: holding let rec all_array ty = function
a value should not hand you garbage. Treating a view as a bare pointer and | [] -> true
calling the lifetime the programmer's problem is the Odin answer, and | _ :: rest ->
neither Odin nor C stops it — this guard is the trade going the other way, (match ty with Types.Array (_, elem) -> all_array elem rest | _ -> false)
refused rather than merely documented. in
What it is NOT is a proof. runtime/flan_dyn.h states the actual property:
the view is exactly as stale-safe as the thing it is a view of, no more
and no less. This guard narrows what a view can be taken of; it does not
make the underlying storage outlive anything. A global [[T]] slice whose
data was cut from a frame that has since returned still passes here, and
reading through the view then reads a dead frame. So this is a guard that
closes the routes the checker can see, not a guarantee that a dyn value
never dangles.
[permanent_root] asks whether an expression's own address — the one a
view's pointer will chase — is guaranteed to outlive every frame, which is
true of exactly one thing at this milestone: a global. A field of a
permanent value is permanent at the same fixed offset from it, and so is
an element of a permanent *array* — both are still inside the permanent
value's own storage. An element of a permanent *slice* is not: a slice is
ptr+len, so a global [[T]] holds only the two words, and the storage they
point at can be a frame that has already gone. The [At] arm below is where
that distinction is made, and it is made per index rather than once: an
[(at g i j)] is a single node carrying the whole index list, so the arm
steps the list the way [indexed] does and an array level at every step is
what it demands. Reading only the target's type would settle level zero
and let a slice at any later level through — which it did, and the
accepted program printed a returned frame's contents. A slice built directly from
[(slice T lo hi)] inherits the
permanence of the [T] it was cut from — unwrapped here because that is
the one shape still carrying the trace back to it; once a slice has been
bound to a name the trace is gone and it is refused; the spelling that
keeps it is to view the slice expression directly, the way this file's
own survey program does.
Everything else — a local, a parameter, a temporary, anything reached
through a [Ptr] — answers false. A [Ptr] is refused rather than trusted
because a heap-allocated block and a frame slot are the same type: a
[(Ptr (Vec i64))] taken from a heap allocation would be sound to view, but
the same type is what [(addr some-local)] answers too, and the checker
cannot tell the two apart. Admitting one admits the other, which is the
whole hazard this guard exists to close — so until a Flan type exists
that says "durably heap-owned" and a [Ptr] does not, a container reached
through one is refused rather than guessed at. An arena-held container is
not a separate case: an arena changes where a Vec's *elements* live, never
where its own header — the value a name is bound to — lives, so a Vec
grown from an arena is exactly as permanent as the binding that holds it,
already covered by the cases above. *)
let rec permanent_root (e : Tast.expr) : bool =
match e.Tast.e with match e.Tast.e with
| Tast.Global _ -> true | Tast.Local _ -> (match e.Tast.ty with Types.Slice _ -> false | _ -> true)
| Tast.Field (target, _) -> permanent_root target | Tast.Field (target, _) ->
(match target.Tast.ty with Types.Named _ -> frame_root target | _ -> false)
| Tast.Prim (Tast.At, target :: idx) -> | Tast.Prim (Tast.At, target :: idx) ->
(* [(at g i j)] is ONE node carrying every index, so the target's own type all_array target.Tast.ty idx && frame_root target
is only level zero and asking about it alone misses a slice reached at | Tast.Prim (Tast.Slice, [ target; _; _ ]) ->
any later level. Step the list the way [indexed] does — that walk is (match target.Tast.ty with Types.Array _ -> frame_root target | _ -> false)
the definition of which levels exist — and require every level stepped
to be an array. *)
let rec all_array ty = function
| [] -> true
| _ :: rest ->
(match ty with
| Types.Array (_, elem) -> all_array elem rest
| _ -> false)
in
all_array target.Tast.ty idx && permanent_root target
| Tast.Prim (Tast.Slice, [ target; _; _ ]) -> permanent_root target
| _ -> false | _ -> false
let view_not_permanent loc (e : Tast.expr) = (* Whether [e] names storage that already has an address, so a view can
view_refusal "check/dyn-view-lifetime" loc e point at it; anything else is a temporary [box] binds to a slot first. *)
"A dyn value can see into a typed container only when it is a global: a \ let rec view_place (e : Tast.expr) : bool =
local, a parameter or a temporary can be gone while the dyn value still \ match e.Tast.e with
points at it" | Tast.Local _ | Tast.Global _ | Tast.Deref _ -> true
| Tast.Field (target, _) ->
(match target.Tast.ty with Types.Named _ -> view_place target | _ -> true)
| Tast.Prim (Tast.At, _ :: _ :: _) -> true
| _ -> false
(* A value handed out of [f] that points into [f]'s own frame: returned (the (* A value handed out of [f] that points into [f]'s own frame: returned (the
last form's tails, or a [return]), or stored into a global or a field or last form's tails, or a [return]), or stored into a global or a field or
@ -4062,7 +4047,7 @@ let refuse_frame_escapes (f : Tast.fn) =
if returns then if returns then
match List.rev f.Tast.body with x :: _ -> tails x | [] -> () match List.rev f.Tast.body with x :: _ -> tails x | [] -> ()
let box loc (e : Tast.expr) : Tast.expr = let box ?ctx loc (e : Tast.expr) : Tast.expr =
let dyn sym args = rt loc Types.Dyn sym args in let dyn sym args = rt loc Types.Dyn sym args in
match e.Tast.ty with match e.Tast.ty with
| Types.Dyn -> e | Types.Dyn -> e
@ -4084,28 +4069,70 @@ let box loc (e : Tast.expr) : Tast.expr =
Loc.failk "check/dyn-unit" loc Loc.failk "check/dyn-unit" loc
"() does not box into dyn. The absent dyn value is nil — write nil" "() does not box into dyn. The absent dyn value is nil — write nil"
| Types.Never -> e | Types.Never -> e
(* A view, not a copy: the box holds one word naming where the elements (* A view, not a copy: the box holds a small record naming where the
live and what one of them is, and every read or write goes straight storage is and what one element is (its descriptor, [view_desc]), and
through to the container's own storage — see runtime/flan_dyn.h's every read or write goes straight through to the container's own
view section for the whole of the argument, including why the storage — see runtime/flan_dyn.h's view section. A Vec's view points AT
descriptor points AT the container (a Vec's own header address) the Vec's header and reads its pointer and length live, so a push
rather than snapshotting its ptr+len. That is what makes a push through the view cannot go stale; a slice and a fixed array cannot grow,
through the view safe even though a Vec can grow and move: there is so a snapshot taken at the crossing is sound for both. A struct's view
no snapshot for the growth to invalidate. A slice and a fixed array is a map-like value: (get p :x), (set (get p :x) v), (put p :x v).
cannot grow, so a snapshot taken once at the crossing is sound for
both, and they share [flan_dyn_view_flat]. *) A container, array or struct is handed over by address. One that is not
(* The element check runs before the lifetime one in all three arms, and a place already — a call's result — is bound to a slot of its own
the order is load-bearing rather than incidental: the lifetime message first, so the view points at storage that lives as long as the frame
says a global can be seen into, and for an element type no view can rather than at a temporary the next statement reuses. [frame_root] then
carry — a string, an i32 — a global is refused too, so the wrong order says whether the storage is this frame's, for the runtime's dev check. *)
hands the programmer a reason that is false for their case. Whichever | Types.Vec _ | Types.Array _ | Types.Named _ | Types.Slice (Types.Mut, _)
refusal is unconditional wins. *) when (match e.Tast.ty with
| Types.Vec elem -> | Types.Named n -> Hashtbl.mem !view_structs n
(match view_elem elem with | _ -> true) ->
| None -> view_not_yet loc e elem let desc_of t =
| Some k -> match view_desc t with
if not (permanent_root e) then view_not_permanent loc e | Ok d -> mk loc Types.String (Tast.Str d)
else dyn "flan_dyn_view_vec" [ e; view_elem_lit loc k ]) | Error inner -> view_not_yet loc e inner
in
let i32 n = mk loc (Types.Int Types.I32) (Tast.Int (n, Types.I32)) in
let i64 n = mk loc dyn_i64 (Tast.Int (n, Types.I64)) in
let bind, e =
match ctx, e.Tast.ty with
| Some ctx, (Types.Vec _ | Types.Array _ | Types.Named _)
when not (view_place e) ->
let sl = fresh_slot ctx e.Tast.ty in
Some (sl, e), mk loc e.Tast.ty (Tast.Local sl)
| _ -> None, e
in
(match !view_global_init with
| Some (g, kind) when frame_root e ->
let fln = fln_source loc in
let ty = tyname loc e.Tast.ty in
Loc.failk "check/dyn-view-lifetime" loc
"%s is a dyn global, and its initialiser builds a %s that is gone \
once the initialiser returns, so a dyn view of it would outlive \
it. Give %s its type, as in %s"
g ty g
(let form =
match kind with Ast.Every -> "def" | Ast.Once -> "defonce" in
if fln then
Printf.sprintf "%s %s: %s = ..."
(match kind with Ast.Every -> "def" | Ast.Once -> "once") g ty
else Printf.sprintf "(%s %s %s ...)" form g ty)
| _ -> ());
let here = i32 (if frame_root e then 1L else 0L) in
let view =
match e.Tast.ty with
| Types.Slice (_, elem) ->
dyn "flan_dyn_view_slice" [ e; desc_of elem; here ]
| Types.Vec elem ->
dyn "flan_dyn_view_at" [ addr_of loc e; i64 0L; desc_of elem; i32 1L; here ]
| Types.Array (n, elem) ->
dyn "flan_dyn_view_at" [ addr_of loc e; i64 n; desc_of elem; i32 0L; here ]
| t ->
dyn "flan_dyn_view_at" [ addr_of loc e; i64 0L; desc_of t; i32 2L; here ]
in
(match bind with
| None -> view
| Some b -> mk loc Types.Dyn (Tast.Let ([ b ], [ view ])))
(* A dyn view is written through by (set (at d i) x), and nothing on the (* A dyn view is written through by (set (at d i) x), and nothing on the
dyn side can tell a read-only one apart, so a [[const T]] does not dyn side can tell a read-only one apart, so a [[const T]] does not
cross. *) cross. *)
@ -4115,20 +4142,6 @@ let box loc (e : Tast.expr) : Tast.expr =
[const %s] can only be read. A dyn view is taken of the writable \ [const %s] can only be read. A dyn view is taken of the writable \
storage it came from" storage it came from"
(tyname loc e.Tast.ty) (tyname loc elem) (tyname loc e.Tast.ty) (tyname loc elem)
| Types.Slice (Types.Mut, elem) ->
(match view_elem elem with
| None -> view_not_yet loc e elem
| Some k ->
if not (permanent_root e) then view_not_permanent loc e
else dyn "flan_dyn_view_flat" [ e; view_elem_lit loc k ])
| Types.Array (n, elem) ->
(match view_elem elem with
| None -> view_not_yet loc e elem
| Some k ->
if not (permanent_root e) then view_not_permanent loc e
else
dyn "flan_dyn_view_flat"
[ e; mk loc dyn_i64 (Tast.Int (n, Types.I64)); view_elem_lit loc k ])
(* No view for a map yet, and the suggestion is the *literal* rather than a (* No view for a map yet, and the suggestion is the *literal* rather than a
constructor call: there is no [(map-new dyn)] — [map_new_types] wants a constructor call: there is no [(map-new dyn)] — [map_new_types] wants a
key and a value, and [map_type] refuses dyn as a key — so naming one key and a value, and [map_type] refuses dyn as a key — so naming one
@ -4144,7 +4157,7 @@ let box loc (e : Tast.expr) : Tast.expr =
mis-lowering. *) mis-lowering. *)
| Types.Named _ | Types.Enum _ | Types.Option _ | Types.Ptr _ | Types.Named _ | Types.Enum _ | Types.Option _ | Types.Ptr _
| Types.Alloc | Types.Fn _ | Types.CFn _ | Types.Var _ | Types.Len _ | Types.Alloc | Types.Fn _ | Types.CFn _ | Types.Var _ | Types.Len _
| Types.LArray _ -> | Types.LArray _ | Types.Vec _ | Types.Array _ | Types.Slice _ ->
no_dyn_yet loc ~into:true e.Tast.ty "" no_dyn_yet loc ~into:true e.Tast.ty ""
(* Every dyn an expectation opened ([expect]'s dyn arm), by the node that (* Every dyn an expectation opened ([expect]'s dyn arm), by the node that
@ -4549,7 +4562,7 @@ let expect ctx loc ~want (got : Tast.expr) =
match w, got.Tast.ty with match w, got.Tast.ty with
| Types.Dyn, Types.Dyn -> got | Types.Dyn, Types.Dyn -> got
| Types.Dyn, Types.Option t -> box_option ctx loc t got | Types.Dyn, Types.Option t -> box_option ctx loc t got
| Types.Dyn, _ -> box loc got | Types.Dyn, _ -> box ~ctx loc got
| Types.Option t, Types.Dyn when not (is_nil_lit got) -> | Types.Option t, Types.Dyn when not (is_nil_lit got) ->
let opened = unbox_option ctx loc t got in let opened = unbox_option ctx loc t got in
Opened.replace opened_by_want opened got; Opened.replace opened_by_want opened got;
@ -16458,7 +16471,11 @@ let check_global env (d : Ast.decl) : Tast.global option =
{ Tast.e = Tast.Uninit ty; ty; loc = d.Ast.dloc } { Tast.e = Tast.Uninit ty; ty; loc = d.Ast.dloc }
| Ast.Init v -> | Ast.Init v ->
let c = ctx () in let c = ctx () in
let v = check c ~want:ty v in let v =
view_global_init := Some (n, kind);
Fun.protect ~finally:(fun () -> view_global_init := None)
(fun () -> check c ~want:ty v)
in
if Tast.const_init v && not lift_always then v if Tast.const_init v && not lift_always then v
else lift_ginit c d.Ast.dloc n ty v else lift_ginit c d.Ast.dloc n ty v
(* [settle_defvars] turned every one of these into a [Zeroed] or an (* [settle_defvars] turned every one of these into a [Zeroed] or an
@ -17768,7 +17785,7 @@ let memory_class (sym : string) (args : Tast.expr list) =
| "flan_dyn_map_new_class" -> | "flan_dyn_map_new_class" ->
gc "allocates: a class instance is a dyn map on the collector's heap, \ gc "allocates: a class instance is a dyn map on the collector's heap, \
with the class's name in its header" with the class's name in its header"
| "flan_dyn_view_vec" | "flan_dyn_view_flat" -> | "flan_dyn_view_slice" | "flan_dyn_view_at" ->
gc "allocates: a typed container crossing into dyn takes a view record \ gc "allocates: a typed container crossing into dyn takes a view record \
on the collector's heap — the elements are not copied, the record is" on the collector's heap — the elements are not copied, the record is"
| "flan_dyn_from_i64" when (match args with [ x ] -> int_may_spill x | _ -> true) -> | "flan_dyn_from_i64" when (match args with [ x ] -> int_may_spill x | _ -> true) ->

View File

@ -250,7 +250,8 @@ module Rt = struct
before the call. Null until the first. *) before the call. Null until the first. *)
let flanframe = let flanframe =
{ sname = "flanframe"; { sname = "flanframe";
fields = [ "prev", Ptr; "info", Ptr; "slots", Ptr; "at", Ptr ] } fields = [ "prev", Ptr; "info", Ptr; "slots", Ptr; "at", Ptr;
"serial", I64 ] }
let align_up n a = (n + a - 1) / a * a let align_up n a = (n + a - 1) / a * a
@ -4045,14 +4046,9 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
Passing the header by value here would hand the runtime a Passing the header by value here would hand the runtime a
copy to grow and leave the caller's untouched. *) copy to grow and leave the caller's untouched. *)
| Types.Vec _ | Types.Map _ -> [ "ptr " ^ addr f a ] | Types.Vec _ | Types.Map _ -> [ "ptr " ^ addr f a ]
(* A fixed array crossing into a dyn view (M2 item 3) needs its (* A fixed array crosses by address, as a Vec or a Map does: a
address for the same reason a Vec or a Map does here — the runtime entry point that takes one reads it in place. (A dyn
view reads through it live, and passing the value would hand view is handed an explicit [addr_of] by check.ml's [box].) *)
the runtime a copy nothing writes back through. Every other
[Rt] caller of an array argument is [flan_dyn_view_flat],
which takes the address and never mutates the array's shape,
so this is not the move-only argument Vec/Map's comment is
about — it is simply the only way to view rather than copy. *)
| Types.Array _ -> [ "ptr " ^ addr f a ] | Types.Array _ -> [ "ptr " ^ addr f a ]
| t -> [ ll t ^ " " ^ value f a ]) | t -> [ ll t ^ " " ^ value f a ])
args) args)
@ -4478,6 +4474,13 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
"%%frame.a = getelementptr inbounds %%flanframe, ptr %%frame, i32 0, i32 %d" "%%frame.a = getelementptr inbounds %%flanframe, ptr %%frame, i32 0, i32 %d"
(Rt.index Rt.flanframe "at"); (Rt.index Rt.flanframe "at");
"store ptr null, ptr %frame.a"; "store ptr null, ptr %frame.a";
(* Zeroed at every push: a dyn view of a local claims a number here
(runtime/flan_dev.c, [flan_dev_frame_claim]), and the next call
to land at this address must not inherit it. *)
Printf.sprintf
"%%frame.n = getelementptr inbounds %%flanframe, ptr %%frame, i32 0, i32 %d"
(Rt.index Rt.flanframe "serial");
"store i64 0, ptr %frame.n";
"store ptr %frame, ptr @flan_frame_head" ]; "store ptr %frame, ptr @flan_frame_head" ];
f.frame <- Some prev; f.frame <- Some prev;
(* The parameters are bound before the body starts, so they are recorded (* The parameters are bound before the body starts, so they are recorded
@ -5059,8 +5062,8 @@ declare i32 @flan_dyn_cast_kind(i64, ptr, i64, ptr, i64, i32)
declare i32 @flan_dyn_is_nil(i64) declare i32 @flan_dyn_is_nil(i64)
declare i64 @flan_dyn_need_not_nil(i64) declare i64 @flan_dyn_need_not_nil(i64)
declare i32 @flan_dyn_truthy(i64) declare i32 @flan_dyn_truthy(i64)
declare i64 @flan_dyn_view_vec(ptr, i32) declare i64 @flan_dyn_view_slice(ptr, i64, ptr, i64, i32)
declare i64 @flan_dyn_view_flat(ptr, i64, i32) declare i64 @flan_dyn_view_at(ptr, i64, ptr, i64, i32, i32)
declare void @flan_dyn_root_push(ptr) declare void @flan_dyn_root_push(ptr)
declare void @flan_dyn_root_push_desc(ptr, ptr) declare void @flan_dyn_root_push_desc(ptr, ptr)
declare ptr @flan_dyn_env_new(i64, ptr) declare ptr @flan_dyn_env_new(i64, ptr)

View File

@ -1602,12 +1602,9 @@ let classify_c (l : loc) (t : Types.t) =
| Types.String | Types.Slice _ -> [ Aint (l, Types.Ptr (Types.Mut, Types.Unit)); Alen l ] | Types.String | Types.Slice _ -> [ Aint (l, Types.Ptr (Types.Mut, Types.Unit)); Alen l ]
| Types.Unit | Types.Never -> [] | Types.Unit | Types.Never -> []
| Types.Vec _ | Types.Map _ -> [ Aptr l ] | Types.Vec _ | Types.Map _ -> [ Aptr l ]
(* A fixed array crossing into a dyn view (M2 item 3) needs its address for (* A fixed array crosses by address, as a Vec or a Map does: a runtime
the same reason: the view reads through it live and a copy would leave entry point that takes one reads it in place. (A dyn view is handed an
the caller's own array unseen by later writes through the view. Every explicit [addr_of] by check.ml's [box].) *)
[Rt] call that takes an array argument is [flan_dyn_view_flat], which
never mutates the array's shape, so this is not the move-only case
Vec/Map is. *)
| Types.Array _ -> [ Aptr l ] | Types.Array _ -> [ Aptr l ]
| _ when is_agg t -> | _ when is_agg t ->
unsupported "aggregate %s across the C boundary" (Types.to_string t) unsupported "aggregate %s across the C boundary" (Types.to_string t)
@ -4216,6 +4213,12 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
store_int f.b store_int f.b
~src:rax ~mm:(Frame (fr + Emit.Rt.field Emit.Rt.flanframe "at")) ~src:rax ~mm:(Frame (fr + Emit.Rt.field Emit.Rt.flanframe "at"))
~size:8; ~size:8;
(* Zeroed at every push, as emit.ml's is: a dyn view of a local claims a
number here, and the next call to land at this address must not
inherit it. *)
store_int f.b
~src:rax ~mm:(Frame (fr + Emit.Rt.field Emit.Rt.flanframe "serial"))
~size:8;
lea f.b ~dst:rax ~mm:(Frame fr); lea f.b ~dst:rax ~mm:(Frame fr);
store_int f.b ~src:rax ~mm:(lmem f head ~scratch:r11) ~size:8; store_int f.b ~src:rax ~mm:(lmem f head ~scratch:r11) ~size:8;
(* The parameters are bound before the body starts, so they are recorded (* The parameters are bound before the body starts, so they are recorded

View File

@ -1111,6 +1111,11 @@ typedef struct flan_frame {
* from that call since — which is why [flan_dev_frame_at_loc] is read for * from that call since — which is why [flan_dev_frame_at_loc] is read for
* the outer frames only. */ * the outer frames only. */
const char *at; const char *at;
/* Zero at the push, and given a number from [flan_dev_frame_claim] the
* first time a dyn view is taken of this frame's storage. It is what tells
* this activation from the next call to land at the same address, which a
* view kept past the return would otherwise take for its own. */
uint64_t serial;
} flan_frame; } flan_frame;
/* The compiler names this symbol directly. A redefinition module reaches it /* The compiler names this symbol directly. A redefinition module reaches it
@ -1134,6 +1139,35 @@ void flan_dev_frames_reset(void) { flan_frame_head = NULL; }
void *flan_dev_frames_mark(void) { return flan_frame_head; } void *flan_dev_frames_mark(void) { return flan_frame_head; }
void flan_dev_frames_restore(void *head) { flan_frame_head = (flan_frame *)head; } void flan_dev_frames_restore(void *head) { flan_frame_head = (flan_frame *)head; }
/* A dyn view of a local (flan_dyn.c, [view_make]): the frame's serial, made
* on first use, and the function's name for the sentence a stale view prints
* — copied out now, because by then the frame is dead stack. */
static uint64_t flan_frame_serials;
uint64_t flan_dev_frame_claim(void *frame, const char **name,
int64_t *namelen) {
flan_frame *f = frame;
*name = "?";
*namelen = 1;
if (f == NULL) return 0;
if (f->info != NULL) {
*name = f->info->name;
*namelen = f->info->namelen;
}
if (f->serial == 0) f->serial = ++flan_frame_serials;
return f->serial;
}
/* Still on the chain, and still the same activation. The walk is from the
* innermost frame out and is the depth of the stack at worst; dead stack
* keeps its old bytes, so the serial alone cannot say the frame is gone. */
int32_t flan_dev_frame_alive(const void *frame, uint64_t serial) {
const flan_frame *f;
for (f = flan_frame_head; f != NULL; f = f->prev)
if (f == frame) return f->serial == serial;
return 0;
}
/* [i] counts from the innermost. NULL past the end, which is how a caller /* [i] counts from the innermost. NULL past the end, which is how a caller
* learns the depth without a second walk. */ * learns the depth without a second walk. */
void *flan_dev_frame_at(int32_t i) { void *flan_dev_frame_at(int32_t i) {
@ -1775,6 +1809,50 @@ int32_t flan_dev_reg_live(const void *p) {
return (int32_t)(e != NULL && e->died == 0 ? 1 : 0); return (int32_t)(e != NULL && e->died == 0 ? 1 : 0);
} }
/* A dyn view's storage (flan_dyn.c, [view_make]): the smallest live block
* holding [p], by base and sequence, so the view can ask later whether that
* same note is still alive. The smallest, because an arena's own region can
* be a block too, and it outlives the allocation inside it that a free-all
* ends. 0 when no live block holds [p] — a global, a frame, C memory — and
* always in a release build. A scan, once per crossing, in a dev build. */
int32_t flan_dev_reg_claim(const void *p, uintptr_t *base, int64_t *seq,
const char **type, int64_t *typelen) {
uintptr_t a = (uintptr_t)p;
flan_reg_entry *best = NULL;
int64_t i;
if (!flan_reg_on || a == 0) return 0;
for (i = 0; i < FLAN_REG_CAP; i++) {
flan_reg_entry *e = &flan_reg[i];
if (e->base == 0 || e->died != 0) continue;
if (a < e->base || a >= e->base + (uintptr_t)e->bytes) continue;
if (best == NULL || e->bytes < best->bytes) best = e;
}
if (best == NULL) return 0;
*base = best->base;
*seq = best->seq;
*type = best->type;
*typelen = best->typelen;
return 1;
}
/* Is the note [flan_dev_reg_claim] found still there and alive? The probe a
* free takes, keyed on the base: an entry is dropped only when its address is
* handed out again, and the new note has a new sequence. A compaction moves
* entries but keeps both. */
int32_t flan_dev_reg_alive(uintptr_t base, int64_t seq) {
size_t s;
int64_t probe;
if (!flan_reg_on || base == 0) return 1;
s = flan_reg_slot(base);
for (probe = 0; probe < FLAN_REG_CAP; probe++) {
size_t j = (s + (size_t)probe) & (FLAN_REG_CAP - 1);
if (flan_reg[j].base == 0) return 0;
if (flan_reg[j].base != base || flan_reg[j].seq != seq) continue;
return flan_reg[j].died == 0;
}
return 0;
}
/* What was at this address, in words, for the branch that may not follow it. /* What was at this address, in words, for the branch that may not follow it.
* Emitted straight into the result buffer rather than returned: the caller is * Emitted straight into the result buffer rather than returned: the caller is
* a render thunk, which has no allocator, and every other piece of a rendering * a render thunk, which has no allocator, and every other piece of a rendering

View File

@ -355,15 +355,17 @@ typedef struct flan_obj {
entries and [cap] counting entries too. Sharing the arm is what lets the entries and [cap] counting entries too. Sharing the arm is what lets the
marker and the sweep treat the two kinds with one load and a doubled marker and the sweep treat the two kinds with one load and a doubled
count rather than a second field to keep in step. */ count rather than a second field to keep in step. */
/* OBJ_VIEW: a typed container's elements, native words this file did not /* OBJ_VIEW: typed storage this file did not allocate and does not own.
allocate and does not own. [is_vec] set means [base] is a The shape is in the header's [gen], which only a class instance
[flan_dyn_vec_hdr *] and [len] here is unused — the live length is otherwise uses: VIEW_VEC means [base] is a [flan_dyn_vec_hdr *] whose
read from the header on every operation, which is the whole of why a length is read live, which is why a Vec growing through the view
Vec growing through the view cannot go stale. [is_vec] clear means cannot go stale; VIEW_FLAT means [base] is the first element and the
[base] is the first element's address and [len] is the snapshot taken header's [len] the count taken at the crossing, for a slice or a fixed
at the crossing, for a slice or a fixed array, neither of which moves. array; VIEW_STRUCT means [base] is one struct. [desc] is the element's
[elem] is one of FLAN_VIEW_I64/F64/BOOL. */ descriptor, or the struct's. [nul] is always NULL and sits where a
struct { void *base; int64_t len; int32_t elem; int32_t is_vec; } view; map's [klass] does, so a struct view answers "no class" to every class
question without a branch. A [view_guard] trails the header. */
struct { void *base; const uint8_t *desc; void *nul; } view;
/* OBJ_ENV: the descriptor the environment's bytes are marked through, /* OBJ_ENV: the descriptor the environment's bytes are marked through,
or NULL when it holds nothing the collector follows. [len] is the or NULL when it holds nothing the collector follows. [len] is the
byte count, and the bytes trail the header as a text's do. */ byte count, and the bytes trail the header as a text's do. */
@ -520,7 +522,9 @@ int32_t flan_dyn_tag(flan_dyn v) {
side there is nothing to tell them apart by, which is the point of a side there is nothing to tell them apart by, which is the point of a
view being indistinguishable rather than a fourth kind of vec. */ view being indistinguishable rather than a fourth kind of vec. */
case OBJ_VEC: return FLAN_DYN_TAG_VEC; case OBJ_VEC: return FLAN_DYN_TAG_VEC;
case OBJ_VIEW: return FLAN_DYN_TAG_VEC; /* ...and a struct's view answers a map's, [gen] being its shape
(VIEW_STRUCT, below). */
case OBJ_VIEW: return o->gen == 2 ? FLAN_DYN_TAG_MAP : FLAN_DYN_TAG_VEC;
case OBJ_MAP: return FLAN_DYN_TAG_MAP; case OBJ_MAP: return FLAN_DYN_TAG_MAP;
default: return FLAN_DYN_TAG_INT; default: return FLAN_DYN_TAG_INT;
} }
@ -631,9 +635,11 @@ static double dyn_num_value(flan_dyn v);
* they are defined, alongside the container operations below */ * they are defined, alongside the container operations below */
static int64_t view_len(const uint8_t *loc, int64_t loclen, const char *op, static int64_t view_len(const uint8_t *loc, int64_t loclen, const char *op,
flan_obj *o); flan_obj *o);
static void *view_base(flan_obj *o); /* A struct view's fields, for the map arms of the printers. */
static flan_dyn view_box(int32_t elem, const uint8_t *p); static int64_t view_nfields(flan_obj *o);
static int64_t view_elem_size(int32_t elem); static flan_dyn view_field_key(flan_obj *o, int64_t i);
static flan_dyn view_field_val(flan_obj *o, int64_t i);
static void view_struct_name(flan_obj *o, const char **name, int64_t *len);
/* forward: needed by [dyn_equal] below, defined alongside the view helpers /* forward: needed by [dyn_equal] below, defined alongside the view helpers
* further down — a length and an element reader that answer correctly * further down — a length and an element reader that answer correctly
@ -687,6 +693,24 @@ static void render(dyn_sink w, flan_dyn v, int depth, int nested) {
case FLAN_DYN_TAG_MAP: { case FLAN_DYN_TAG_MAP: {
flan_obj *o = dyn_obj(v); flan_obj *o = dyn_obj(v);
int64_t i; int64_t i;
/* A struct's view prints as the class instance it most resembles: its
* type's name as the shape tag, then its fields in declaration order. */
if (o->kind == OBJ_VIEW) {
const char *nm;
int64_t nl, n = view_nfields(o);
view_struct_name(o, &nm, &nl);
emit(w, "#");
emit_n(w, (const uint8_t *)nm, nl);
emit(w, "{");
for (i = 0; i < n; i++) {
if (i > 0) emit(w, " ");
render(w, view_field_key(o, i), depth + 1, 1);
emit(w, " ");
render(w, view_field_val(o, i), depth + 1, 1);
}
emit(w, "}");
return;
}
/* A class instance prints its shape tag in front, Clojure's own spelling /* A class instance prints its shape tag in front, Clojure's own spelling
* for a record: #point{:x 1 :y 2}. The tag is not an entry, so it is * for a record: #point{:x 1 :y 2}. The tag is not an entry, so it is
* written here or it is not written at all. */ * written here or it is not written at all. */
@ -706,17 +730,11 @@ static void render(dyn_sink w, flan_dyn v, int depth, int nested) {
} }
default: { default: {
flan_obj *o = dyn_obj(v); flan_obj *o = dyn_obj(v);
int64_t i, n = o->kind == OBJ_VIEW ? view_len(NULL, 0, "print", o) : o->len; int64_t i, n = vecish_len(o);
emit(w, "["); emit(w, "[");
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) {
if (i > 0) emit(w, " "); if (i > 0) emit(w, " ");
if (o->kind == OBJ_VIEW) render(w, vecish_at(o, i), depth + 1, 1);
render(w, view_box(o->u.view.elem,
(const uint8_t *)view_base(o)
+ i * view_elem_size(o->u.view.elem)),
depth + 1, 1);
else
render(w, o->u.v.items[i], depth + 1, 1);
} }
emit(w, "]"); emit(w, "]");
return; return;
@ -802,6 +820,27 @@ static void say_render(sayer *s, flan_dyn v, int depth) {
flan_obj *o = dyn_obj(v); flan_obj *o = dyn_obj(v);
int64_t i; int64_t i;
if (depth >= 2) { say_puts(s, "{...}"); return; } if (depth >= 2) { say_puts(s, "{...}"); return; }
if (o->kind == OBJ_VIEW) {
const char *nm;
int64_t nl, n = view_nfields(o), j;
view_struct_name(o, &nm, &nl);
say_puts(s, "#");
for (j = 0; j < nl && s->n < s->cap - 8; j++) {
char c[2];
c[0] = nm[j];
c[1] = '\0';
say_puts(s, c);
}
say_puts(s, "{");
for (i = 0; i < n && s->n < s->cap - 8; i++) {
if (i > 0) say_puts(s, " ");
say_render(s, view_field_key(o, i), depth + 1);
say_puts(s, " ");
say_render(s, view_field_val(o, i), depth + 1);
}
say_puts(s, i == n ? "}" : i > 0 ? " ...}" : "...}");
return;
}
/* The same tag [render] writes, so a trap sentence naming an instance /* The same tag [render] writes, so a trap sentence naming an instance
* says which class it was. Truncated with the rest when the buffer is * says which class it was. Truncated with the rest when the buffer is
* short: [say] is a 96-byte sentence, not a printer. */ * short: [say] is a 96-byte sentence, not a printer. */
@ -827,19 +866,12 @@ static void say_render(sayer *s, flan_dyn v, int depth) {
} }
default: { default: {
flan_obj *o = dyn_obj(v); flan_obj *o = dyn_obj(v);
int64_t i, n = o->kind == OBJ_VIEW ? view_len(NULL, 0, "print", o) : o->len; int64_t i, n = vecish_len(o);
if (depth >= 2) { say_puts(s, "[...]"); return; } if (depth >= 2) { say_puts(s, "[...]"); return; }
say_puts(s, "["); say_puts(s, "[");
for (i = 0; i < n && s->n < s->cap - 8; i++) { for (i = 0; i < n && s->n < s->cap - 8; i++) {
if (i > 0) say_puts(s, " "); if (i > 0) say_puts(s, " ");
if (o->kind == OBJ_VIEW) say_render(s, vecish_at(o, i), depth + 1);
say_render(s,
view_box(o->u.view.elem,
(const uint8_t *)view_base(o)
+ i * view_elem_size(o->u.view.elem)),
depth + 1);
else
say_render(s, o->u.v.items[i], depth + 1);
} }
say_puts(s, i == n ? "]" : i > 0 ? " ...]" : "...]"); say_puts(s, i == n ? "]" : i > 0 ? " ...]" : "...]");
return; return;
@ -2810,6 +2842,23 @@ static int dyn_equal(flan_dyn a, flan_dyn b, int depth) {
int64_t i, j; int64_t i, j;
if (x == y) return 1; if (x == y) return 1;
if (depth >= EQ_DEPTH) return 0; if (depth >= EQ_DEPTH) return 0;
/* A struct's view is equal to another view of the same struct type with
equal fields, and to nothing else — the answer an instance gets beside
a plain map, for the same reason: the type's name is its shape tag. */
if (x->kind == OBJ_VIEW || y->kind == OBJ_VIEW) {
const char *xn, *yn;
int64_t xl, yl, n;
if (x->kind != OBJ_VIEW || y->kind != OBJ_VIEW) return 0;
view_struct_name(x, &xn, &xl);
view_struct_name(y, &yn, &yl);
if (xl != yl || memcmp(xn, yn, (size_t)xl) != 0) return 0;
n = view_nfields(x);
if (n != view_nfields(y)) return 0;
for (i = 0; i < n; i++)
if (!dyn_equal(view_field_val(x, i), view_field_val(y, i), depth + 1))
return 0;
return 1;
}
/* Two instances of one class built either side of a redefinition hold /* Two instances of one class built either side of a redefinition hold
different key sets, and comparing those key sets would answer "not different key sets, and comparing those key sets would answer "not
equal" about a difference the class no longer has. So both are brought equal" about a difference the class no longer has. So both are brought
@ -2860,14 +2909,273 @@ static inline int is_map(flan_dyn v) {
/* ── Typed containers as views ───────────────────────────────────────── /* ── Typed containers as views ─────────────────────────────────────────
* *
* Every entry point below already dispatches on [flan_dyn_tag], which does * Every entry point below already dispatches on [flan_dyn_tag]. A view over
* not distinguish a view from a heap vec — see [flan_dyn_tag]'s switch — so * a Vec, a slice or a fixed array answers the vec tag and a view over a
* [flan_dyn_len], [flan_dyn_at], [flan_dyn_set_at], [flan_dyn_push] and the * struct answers the map tag, so [flan_dyn_len], [flan_dyn_at],
* printer each add one branch for [OBJ_VIEW] beside the existing [OBJ_VEC] * [flan_dyn_set_at], [flan_dyn_push], the map operations and the printers
* one. What follows is that branch's machinery. */ * each add one branch for [OBJ_VIEW]. What follows is that branch's
* machinery.
*
* What one element is comes from a descriptor the compiler writes into
* read-only data (lib/check.ml, [view_desc]), a prefix code:
*
* b B h H i I l L i8 u8 i16 u16 i32 u32 i64 u64
* f d ? f32 f64 bool
* t str (read as a copy; never written from here)
* a<n>;T a fixed [n T]
* sT a slice [T]
* vT a (Vec T)
* {Name;f1;T1f2;T2} a struct, its fields in declaration order
*
* Offsets are computed here by C's rule, the one Emit.lay spells: every
* scalar aligned to its size, a struct to its strictest field and padded to
* it, an array adding no padding of its own. test/programs/dyn-view-any.flan
* reads back a struct whose fields sit at offsets only that rule gets right,
* on both backends.
*
* A view never stores a collector pointer into typed storage: a read boxes
* (or copies, for a str), and a write unboxes a number or a bool. That is
* why a str element is read-only from here, and why an aggregate element is
* written through its own view rather than replaced whole. */
static int64_t view_elem_size(int32_t elem) { static int64_t desc_int(const uint8_t **p) {
return elem == FLAN_VIEW_BOOL ? 1 : 8; int64_t n = 0;
while (**p >= '0' && **p <= '9') { n = n * 10 + (**p - '0'); (*p)++; }
if (**p == ';') (*p)++;
return n;
}
/* Past a name and its ';'. */
static const uint8_t *desc_name_end(const uint8_t *d) {
while (*d != ';') d++;
return d + 1;
}
static const uint8_t *desc_skip(const uint8_t *d) {
switch (*d) {
case 'a': d++; desc_int(&d); return desc_skip(d);
case 's': case 'v': return desc_skip(d + 1);
case '{':
d = desc_name_end(d + 1);
while (*d != '}') d = desc_skip(desc_name_end(d));
return d + 1;
default: return d + 1;
}
}
static int64_t align_to(int64_t n, int64_t a) { return (n + a - 1) / a * a; }
static void desc_lay(const uint8_t *d, int64_t *size, int64_t *align) {
switch (*d) {
case 'b': case 'B': case '?': *size = 1; *align = 1; return;
case 'h': case 'H': *size = 2; *align = 2; return;
case 'i': case 'I': case 'f': *size = 4; *align = 4; return;
case 't': case 's': *size = 16; *align = 8; return;
case 'v': *size = 40; *align = 8; return;
case 'a': {
int64_t n, s, a;
d++;
n = desc_int(&d);
desc_lay(d, &s, &a);
*size = n * s;
*align = a;
return;
}
case '{': {
int64_t off = 0, al = 1, s, a;
d = desc_name_end(d + 1);
while (*d != '}') {
d = desc_name_end(d);
desc_lay(d, &s, &a);
if (a < 1) a = 1;
off = align_to(off, a) + s;
if (a > al) al = a;
d = desc_skip(d);
}
*size = align_to(off, al);
*align = al;
return;
}
default: *size = 8; *align = 8; return;
}
}
static int64_t desc_size(const uint8_t *d) {
int64_t s, a;
desc_lay(d, &s, &a);
return s;
}
/* A struct descriptor's fields, one at a time: [desc_fields] answers where
* the first one starts, and each [desc_next] answers a field's name, offset
* and type and steps past it, 0 at the closing brace. [*off] is the running
* offset. */
static const uint8_t *desc_fields(const uint8_t *d, int64_t *off) {
*off = 0;
return desc_name_end(d + 1);
}
static int desc_next(const uint8_t **at, int64_t *off, const uint8_t **name,
int64_t *namelen, int64_t *foff, const uint8_t **fty) {
const uint8_t *d = *at;
int64_t s, a;
if (*d == '}') return 0;
*name = d;
while (*d != ';') d++;
*namelen = (int64_t)(d - *name);
d++;
desc_lay(d, &s, &a);
if (a < 1) a = 1;
*foff = align_to(*off, a);
*off = *foff + s;
*fty = d;
*at = desc_skip(d);
return 1;
}
/* The field keyword [k] names, or NULL. */
static const uint8_t *desc_field(const uint8_t *d, flan_dyn k, int64_t *foff) {
const uint8_t *at, *name, *fty;
int64_t off, namelen;
kw_entry *kw;
if (flan_dyn_tag(k) != FLAN_DYN_TAG_KEYWORD) return NULL;
kw = dyn_kw(k);
at = desc_fields(d, &off);
while (desc_next(&at, &off, &name, &namelen, foff, &fty))
if (namelen == kw->len && memcmp(name, kw_bytes(kw), (size_t)namelen) == 0)
return fty;
return NULL;
}
static int64_t desc_nfields(const uint8_t *d) {
const uint8_t *at, *name, *fty;
int64_t off, namelen, foff, n = 0;
at = desc_fields(d, &off);
while (desc_next(&at, &off, &name, &namelen, &foff, &fty)) n++;
return n;
}
/* The Flan spelling of a descriptor's type, for a sentence. */
static void desc_spell(const uint8_t *d, char *buf, size_t cap) {
static const char scalars[] = "bBhHiIlLfd?t";
static const char *const words[] = { "i8", "u8", "i16", "u16", "i32", "u32",
"i64", "u64", "f32", "f64", "bool",
"str" };
const char *w;
char inner[96];
if (cap == 0) return;
buf[0] = '\0';
if (*d != '\0' && (w = strchr(scalars, *d)) != NULL) {
snprintf(buf, cap, "%s", words[w - scalars]);
return;
}
switch (*d) {
case 'a': {
int64_t n;
d++;
n = desc_int(&d);
desc_spell(d, inner, sizeof inner);
snprintf(buf, cap, "[%lld %s]", (long long)n, inner);
return;
}
case 's':
desc_spell(d + 1, inner, sizeof inner);
snprintf(buf, cap, "[%s]", inner);
return;
case 'v':
desc_spell(d + 1, inner, sizeof inner);
snprintf(buf, cap, "(Vec %s)", inner);
return;
case '{': {
const uint8_t *e = desc_name_end(d + 1);
snprintf(buf, cap, "%.*s", (int)(e - d - 2), (const char *)d + 1);
return;
}
default: snprintf(buf, cap, "?"); return;
}
}
/* What a dev build remembers of a view's storage at the crossing, and checks
* before every read or write through it (runtime/flan_dev.c keeps both
* tables):
*
* a frame the shadow frame the storage belongs to and the serial that
* activation was given. Live while that frame is still on the
* chain with the same serial: a later call landing at the same
* address gets a different one.
* a block the registry entry of the heap or arena block holding the
* storage, by base and note sequence. Live while the entry is,
* which a free, a free-all, an arena's destroy and a Vec's
* growth (for the block it left) all end.
*
* A release build keeps neither table, so a view there records nothing and
* checks nothing. Storage neither table knows — a global, rodata, C memory,
* a caller's local reached through a slice parameter — is not checked. */
typedef struct view_guard {
const void *frame; /* NULL when no frame is checked */
uint64_t serial;
const char *fname; /* whose frame, for the sentence */
int64_t fnamelen;
uintptr_t rbase; /* 0 when no block is checked */
int64_t rseq;
const char *rtype;
int64_t rtypelen;
} view_guard;
uint64_t flan_dev_frame_claim(void *frame, const char **name, int64_t *namelen);
int32_t flan_dev_frame_alive(const void *frame, uint64_t serial);
int32_t flan_dev_reg_claim(const void *p, uintptr_t *base, int64_t *seq,
const char **type, int64_t *typelen);
int32_t flan_dev_reg_alive(uintptr_t base, int64_t seq);
extern void *flan_frame_head;
#define VIEW_FLAT 0 /* [base] is the first element, [o->len] the count */
#define VIEW_VEC 1 /* [base] is a Vec's header, read live */
#define VIEW_STRUCT 2 /* [base] is the struct, [desc] the struct's own */
static inline view_guard *view_g(flan_obj *o) { return (view_guard *)(o + 1); }
static inline int view_shape(flan_obj *o) { return (int)o->gen; }
static void guard_block(view_guard *g, const void *p) {
g->rbase = 0;
if (p != NULL)
flan_dev_reg_claim(p, &g->rbase, &g->rseq, &g->rtype, &g->rtypelen);
}
/* A new view record, its guard empty. */
static flan_obj *view_new(void *base, int64_t len, const uint8_t *desc,
int shape) {
flan_obj *o = gc_alloc(OBJ_VIEW, (int64_t)sizeof(view_guard));
o->u.view.base = base;
o->u.view.desc = desc;
o->u.view.nul = NULL;
o->gen = (uint32_t)shape;
o->len = len;
memset(view_g(o), 0, sizeof(view_guard));
return o;
}
/* The stale-storage check. The sentence never renders the view: it has just
* been found to point at storage that is gone, and rendering reads it. */
static void view_guard_check(const uint8_t *loc, int64_t loclen,
const char *op, flan_obj *o) {
view_guard *g = view_g(o);
if (g->frame != NULL && !flan_dev_frame_alive(g->frame, g->serial)) {
flan_say(loc, loclen,
"dyn %s: this view points into a local of %.*s, and that call "
"has returned. A view of a local lasts as long as the call that "
"made it",
op, (int)g->fnamelen, g->fname);
flan_trap((const uint8_t *)"DynStale", 8);
}
if (g->rbase != 0 && !flan_dev_reg_alive(g->rbase, g->rseq)) {
flan_say(loc, loclen,
"dyn %s: this view's storage, a block of %.*s, has been "
"released — freed, cleared by free-all, or left behind when a "
"Vec grew. Take the view again after the change",
op, (int)g->rtypelen, g->rtype);
flan_trap((const uint8_t *)"DynStale", 8);
}
} }
/* The stale-container check flan_rt.c's [flan_vec_check] runs for a typed /* The stale-container check flan_rt.c's [flan_vec_check] runs for a typed
@ -2875,17 +3183,8 @@ static int64_t view_elem_size(int32_t elem) {
* doctrine's dyn side gets its own spelling (docs/SPIKE-DUPLICITY.md), and a * doctrine's dyn side gets its own spelling (docs/SPIKE-DUPLICITY.md), and a
* dyn program that hits this wants the same park-and-inspect [flan_trap] * dyn program that hits this wants the same park-and-inspect [flan_trap]
* gives every other dyn mistake, not the typed side's [rt_die]. A Vec with * gives every other dyn mistake, not the typed side's [rt_die]. A Vec with
* no allocator yet — one nobody has pushed to — has nothing to check. * no allocator yet — one nobody has pushed to — has nothing to check. Like
* * the guard's, the sentence never renders the view. */
* The message never renders the view it just declared unsafe to read —
* review's second finding, and it was not a decoration this dropped for
* safety's sake, it was a real infinite recursion: [say] on a view calls
* [say_render]'s view branch, which calls [view_len], which calls back in
* here, unconditionally, because the epoch is still stale. Every render of
* this same view would hit the same check and take the same branch, so
* nothing about depth or a visited set closes it — the fix is that a
* stale-container check must never read the container it has just refused
* to trust, not even to describe it in the sentence explaining why. */
static void view_vec_check(const uint8_t *loc, int64_t loclen, const char *op, static void view_vec_check(const uint8_t *loc, int64_t loclen, const char *op,
flan_dyn_vec_hdr *h) { flan_dyn_vec_hdr *h) {
if (h->alloc) { if (h->alloc) {
@ -2902,104 +3201,300 @@ static void view_vec_check(const uint8_t *loc, int64_t loclen, const char *op,
/* [len] and [base], read live for a Vec view (so a push that grows and /* [len] and [base], read live for a Vec view (so a push that grows and
* moves the underlying Vec is seen the very next operation) and read from * moves the underlying Vec is seen the very next operation) and read from
* the snapshot for a flat one. */ * the snapshot for a flat one. [view_len] checks the guard first. */
static int64_t view_len(const uint8_t *loc, int64_t loclen, const char *op, static int64_t view_len(const uint8_t *loc, int64_t loclen, const char *op,
flan_obj *o) { flan_obj *o) {
if (o->u.view.is_vec) { view_guard_check(loc, loclen, op, o);
if (view_shape(o) == VIEW_VEC) {
flan_dyn_vec_hdr *h = (flan_dyn_vec_hdr *)o->u.view.base; flan_dyn_vec_hdr *h = (flan_dyn_vec_hdr *)o->u.view.base;
view_vec_check(loc, loclen, op, h); view_vec_check(loc, loclen, op, h);
return h->len; return h->len;
} }
return o->u.view.len; return o->len;
} }
static void *view_base(flan_obj *o) { static void *view_base(flan_obj *o) {
if (o->u.view.is_vec) return ((flan_dyn_vec_hdr *)o->u.view.base)->ptr; if (view_shape(o) == VIEW_VEC) return ((flan_dyn_vec_hdr *)o->u.view.base)->ptr;
return o->u.view.base; return o->u.view.base;
} }
/* Reads box the element on the way out — the runtime already knows how to /* A view of the aggregate element at [p], inside [parent]'s storage. An
* box an i64, an f64 or a bool, so this is that, from raw bytes rather than * element of a Vec is checked against the Vec's block, since the Vec's
* from a C value already in hand. */ * growth is what would leave it behind; any other inline element shares its
static flan_dyn view_box(int32_t elem, const uint8_t *p) { * parent's guard. A slice element's data is somewhere else, so it is looked
switch (elem) { * up afresh. */
case FLAN_VIEW_I64: { int64_t x; memcpy(&x, p, 8); return flan_dyn_from_i64(x); } static flan_dyn view_child(flan_obj *parent, const uint8_t *d, uint8_t *p) {
case FLAN_VIEW_F64: { double x; memcpy(&x, p, 8); return flan_dyn_from_f64(x); } flan_obj *c;
default: { uint8_t b = *p; return flan_dyn_from_bool(b); } switch (*d) {
case 's': {
void *data;
int64_t n;
memcpy(&data, p, 8);
memcpy(&n, p + 8, 8);
c = view_new(data, n, d + 1, VIEW_FLAT);
guard_block(view_g(c), data);
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)c);
} }
case 'a': {
const uint8_t *e = d + 1;
int64_t n = desc_int(&e);
c = view_new(p, n, e, VIEW_FLAT);
break;
}
case 'v': c = view_new(p, 0, d + 1, VIEW_VEC); break;
default: c = view_new(p, 0, d, VIEW_STRUCT); break;
}
if (view_shape(parent) == VIEW_VEC) guard_block(view_g(c), p);
else *view_g(c) = *view_g(parent);
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)c);
}
/* One element, boxed on the way out. A number widens to a dyn int or float,
* except a u64 above the largest i64, which has no dyn int to become. */
static flan_dyn view_read(const uint8_t *loc, int64_t loclen, const char *op,
flan_obj *parent, const uint8_t *d, uint8_t *p) {
switch (*d) {
case 'b': { int8_t x; memcpy(&x, p, 1); return flan_dyn_from_i64(x); }
case 'B': { uint8_t x; memcpy(&x, p, 1); return flan_dyn_from_i64(x); }
case 'h': { int16_t x; memcpy(&x, p, 2); return flan_dyn_from_i64(x); }
case 'H': { uint16_t x; memcpy(&x, p, 2); return flan_dyn_from_i64(x); }
case 'i': { int32_t x; memcpy(&x, p, 4); return flan_dyn_from_i64(x); }
case 'I': { uint32_t x; memcpy(&x, p, 4); return flan_dyn_from_i64(x); }
case 'l': { int64_t x; memcpy(&x, p, 8); return flan_dyn_from_i64(x); }
case 'L': {
uint64_t x;
memcpy(&x, p, 8);
if (x > (uint64_t)INT64_MAX) {
flan_say(loc, loclen,
"dyn %s: this u64 element is %llu, above the largest dyn int "
"(9223372036854775807), so it has no dyn value",
op, (unsigned long long)x);
flan_trap((const uint8_t *)"DynRange", 8);
}
return flan_dyn_from_i64((int64_t)x);
}
case 'f': { float x; memcpy(&x, p, 4); return flan_dyn_from_f64((double)x); }
case 'd': { double x; memcpy(&x, p, 8); return flan_dyn_from_f64(x); }
case '?': return flan_dyn_from_bool(*p ? 1 : 0);
case 't': {
const uint8_t *s;
int64_t n;
memcpy(&s, p, 8);
memcpy(&n, p + 8, 8);
return flan_dyn_from_bytes(s, n);
}
default: return view_child(parent, d, p);
}
}
/* The range each integer element holds. */
static int int_range(uint8_t c, int64_t *lo, int64_t *hi) {
switch (c) {
case 'b': *lo = INT8_MIN; *hi = INT8_MAX; return 1;
case 'B': *lo = 0; *hi = UINT8_MAX; return 1;
case 'h': *lo = INT16_MIN; *hi = INT16_MAX; return 1;
case 'H': *lo = 0; *hi = UINT16_MAX; return 1;
case 'i': *lo = INT32_MIN; *hi = INT32_MAX; return 1;
case 'I': *lo = 0; *hi = UINT32_MAX; return 1;
case 'l': *lo = INT64_MIN; *hi = INT64_MAX; return 1;
case 'L': *lo = 0; *hi = INT64_MAX; return 1;
default: return 0;
}
}
/* One element, unboxed on the way in. The dyn value's tag must be the one
* the element's type wants, or this traps by name and never coerces a
* mismatched value into the slot; an int the element's width cannot hold
* traps too, naming both. An int is not a float here: it is refused, not
* converted, as every write through a view has been. A float into an f32
* narrows, as (f32 x) does. [v] is the view and [x] the value, for the
* sentence. */
static void view_write(const uint8_t *loc, int64_t loclen, const char *op,
flan_dyn v, const uint8_t *d, flan_dyn x, uint8_t *p) {
char ty[128];
int64_t lo, hi;
if (int_range(*d, &lo, &hi)) {
int64_t n;
if (flan_dyn_tag(x) != FLAN_DYN_TAG_INT)
trap2(loc, loclen, TYPE_TRAP, op, "this view's elements are int", v, x);
n = dyn_int_value(x);
if (n < lo || n > hi) {
desc_spell(d, ty, sizeof ty);
if (*d == 'L')
flan_say(loc, loclen,
"dyn %s: %lld does not fit a u64 element, which holds no "
"negative number", op, (long long)n);
else
flan_say(loc, loclen,
"dyn %s: %lld does not fit a %s element, which holds %lld "
"to %lld", op, (long long)n, ty, (long long)lo, (long long)hi);
flan_trap((const uint8_t *)"DynRange", 8);
}
switch (*d) {
case 'b': case 'B': { uint8_t b = (uint8_t)n; memcpy(p, &b, 1); return; }
case 'h': case 'H': { uint16_t h = (uint16_t)n; memcpy(p, &h, 2); return; }
case 'i': case 'I': { uint32_t w = (uint32_t)n; memcpy(p, &w, 4); return; }
default: memcpy(p, &n, 8); return;
}
}
switch (*d) {
case 'f': case 'd': {
double f;
if (flan_dyn_tag(x) != FLAN_DYN_TAG_FLOAT)
trap2(loc, loclen, TYPE_TRAP, op, "this view's elements are float", v, x);
f = dyn_num_value(x);
if (*d == 'f') { float g = (float)f; memcpy(p, &g, 4); }
else memcpy(p, &f, 8);
return;
}
case '?':
if (flan_dyn_tag(x) != FLAN_DYN_TAG_BOOL)
trap2(loc, loclen, TYPE_TRAP, op, "this view's elements are bool", v, x);
*p = dyn_payload(x) ? 1 : 0;
return;
case 't':
trap2(loc, loclen, TYPE_TRAP, op,
"a str element is read-only through a dyn view", v, x);
default: {
char sx[SAY_MAX];
desc_spell(d, ty, sizeof ty);
say(sx, SAY_MAX, x);
flan_say(loc, loclen,
"dyn %s: this element is a %s, and a dyn view does not replace "
"it whole — write into its own elements or fields instead of "
"storing %s",
op, ty, sx);
flan_trap((const uint8_t *)"DynType", 7);
}
}
}
/* Element [i] of a vec-shaped view. The caller has checked the bounds. */
static uint8_t *view_elem_at(flan_obj *o, int64_t i) {
return (uint8_t *)view_base(o) + i * desc_size(o->u.view.desc);
} }
/* A length and an element reader that answer correctly whether [o] is an /* A length and an element reader that answer correctly whether [o] is an
* ordinary heap vec (OBJ_VEC, elements are dyn words) or a view over a * ordinary heap vec (OBJ_VEC, elements are dyn words) or a view over a
* typed container (OBJ_VIEW, elements are native bytes boxed on the way * typed container (OBJ_VIEW, native bytes boxed on the way out) — the pair
* out) — the pair [dyn_equal]'s VEC arm needs so that a view compares * [dyn_equal]'s VEC arm and the printers need. */
* correctly against another view and against an ordinary vec alike. Reading
* [o->len]/[o->u.v.items] directly, the way that arm used to, answers 0 and
* garbage for a view: nothing sets [len] for OBJ_VIEW, and its elements
* alias [u.view.base] reinterpreted as dyn words rather than the native
* bytes they are. */
static int64_t vecish_len(flan_obj *o) { static int64_t vecish_len(flan_obj *o) {
return o->kind == OBJ_VIEW ? view_len(NULL, 0, "=", o) : o->len; return o->kind == OBJ_VIEW ? view_len(NULL, 0, "=", o) : o->len;
} }
static flan_dyn vecish_at(flan_obj *o, int64_t i) { static flan_dyn vecish_at(flan_obj *o, int64_t i) {
if (o->kind == OBJ_VIEW) if (o->kind == OBJ_VIEW)
return view_box(o->u.view.elem, return view_read(NULL, 0, "print", o, o->u.view.desc, view_elem_at(o, i));
(const uint8_t *)view_base(o)
+ i * view_elem_size(o->u.view.elem));
return o->u.v.items[i]; return o->u.v.items[i];
} }
/* Writes tag-check on the way in: the dyn value's tag must be the one this /* A struct view's field [k], or a trap naming the fields there are: a
* view's element type wants, or this traps by name and never coerces or * struct's shape is fixed, so a key it does not have is a mistake rather
* truncates a mismatched value into the slot. [v] is the view, for the * than an absence. */
* sentence's container half; [x] is the value that was refused. */ static uint8_t *view_field(const uint8_t *loc, int64_t loclen, const char *op,
static void view_unbox(const uint8_t *loc, int64_t loclen, const char *op, flan_obj *o, flan_dyn k, const uint8_t **fty) {
flan_dyn v, int32_t elem, flan_dyn x, uint8_t *p) { int64_t off;
switch (elem) { const uint8_t *d = o->u.view.desc;
case FLAN_VIEW_I64: { view_guard_check(loc, loclen, op, o);
int64_t n; *fty = desc_field(d, k, &off);
if (flan_dyn_tag(x) != FLAN_DYN_TAG_INT) if (*fty == NULL) {
trap2(loc, loclen, TYPE_TRAP, op, "this view's elements are int", v, x); char sk[SAY_MAX], nm[96];
n = dyn_int_value(x); const uint8_t *at, *name, *t;
memcpy(p, &n, 8); int64_t o2, namelen, foff;
return; say(sk, SAY_MAX, k);
} desc_spell(d, nm, sizeof nm);
case FLAN_VIEW_F64: { said_len = 0;
double d; said_add("dyn %s: a %s has no field %s. Its fields are", op, nm, sk);
if (flan_dyn_tag(x) != FLAN_DYN_TAG_FLOAT) at = desc_fields(d, &o2);
trap2(loc, loclen, TYPE_TRAP, op, "this view's elements are float", v, x); while (desc_next(&at, &o2, &name, &namelen, &foff, &t))
d = dyn_num_value(x); said_add(" :%.*s", (int)namelen, (const char *)name);
memcpy(p, &d, 8); flan_say(loc, loclen, "%s", said_buf);
return; flan_trap((const uint8_t *)"DynType", 7);
}
default: {
uint8_t b;
if (flan_dyn_tag(x) != FLAN_DYN_TAG_BOOL)
trap2(loc, loclen, TYPE_TRAP, op, "this view's elements are bool", v, x);
b = dyn_payload(x) ? 1 : 0;
*p = b;
return;
}
} }
return (uint8_t *)o->u.view.base + off;
}
/* The crossing. [base] is the container's address (a Vec's header, the
* array's or the struct's first byte) or a slice's data, [len] a flat
* view's element count, [desc] the element's descriptor (the struct's own,
* for a struct view). [here] is the checker's word that the storage is the
* calling function's own frame; otherwise a dev build looks the address up
* in the allocation registry. */
static flan_dyn view_make(void *base, int64_t len, const uint8_t *desc,
int shape, int32_t here) {
flan_obj *o = view_new(base, len, desc, shape);
view_guard *g = view_g(o);
if (here) {
if (flan_frame_head != NULL) {
g->frame = flan_frame_head;
g->serial =
flan_dev_frame_claim(flan_frame_head, &g->fname, &g->fnamelen);
}
} else
guard_block(g, base);
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
}
flan_dyn flan_dyn_view_slice(void *data, int64_t len, const uint8_t *desc,
int64_t desclen, int32_t here) {
(void)desclen;
return view_make(data, len, desc, VIEW_FLAT, here);
}
flan_dyn flan_dyn_view_at(void *addr, int64_t len, const uint8_t *desc,
int64_t desclen, int32_t shape, int32_t here) {
(void)desclen;
return view_make(addr, len, desc, shape, here);
}
/* A struct view's fields by position, for the printers and equality. */
static int64_t view_nfields(flan_obj *o) {
view_guard_check(NULL, 0, "print", o);
return desc_nfields(o->u.view.desc);
}
static int view_nth(flan_obj *o, int64_t i, const uint8_t **name,
int64_t *namelen, int64_t *foff, const uint8_t **fty) {
int64_t off;
const uint8_t *at = desc_fields(o->u.view.desc, &off);
while (desc_next(&at, &off, name, namelen, foff, fty))
if (i-- == 0) return 1;
return 0;
}
static flan_dyn view_field_key(flan_obj *o, int64_t i) {
const uint8_t *name, *fty;
int64_t namelen, foff;
if (!view_nth(o, i, &name, &namelen, &foff, &fty)) return flan_dyn_nil();
return flan_dyn_kw(name, namelen);
}
static flan_dyn view_field_val(flan_obj *o, int64_t i) {
const uint8_t *name, *fty;
int64_t namelen, foff;
if (!view_nth(o, i, &name, &namelen, &foff, &fty)) return flan_dyn_nil();
return view_read(NULL, 0, "print", o, fty, (uint8_t *)o->u.view.base + foff);
}
static void view_struct_name(flan_obj *o, const char **name, int64_t *len) {
const uint8_t *d = o->u.view.desc;
const uint8_t *e = desc_name_end(d + 1);
*name = (const char *)d + 1;
*len = (int64_t)(e - d - 2);
}
/* The first ABI, kept for test/dyn_ops.c: FLAN_VIEW_I64/F64/BOOL. */
static const uint8_t *old_elem_desc(int32_t elem) {
return (const uint8_t *)(elem == FLAN_VIEW_I64 ? "l"
: elem == FLAN_VIEW_F64 ? "d" : "?");
} }
flan_dyn flan_dyn_view_vec(void *hdr, int32_t elem) { flan_dyn flan_dyn_view_vec(void *hdr, int32_t elem) {
flan_obj *o = gc_alloc(OBJ_VIEW, 0); return view_make(hdr, 0, old_elem_desc(elem), VIEW_VEC, 0);
o->u.view.base = hdr;
o->u.view.len = 0;
o->u.view.elem = elem;
o->u.view.is_vec = 1;
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
} }
flan_dyn flan_dyn_view_flat(void *data, int64_t len, int32_t elem) { flan_dyn flan_dyn_view_flat(void *data, int64_t len, int32_t elem) {
flan_obj *o = gc_alloc(OBJ_VIEW, 0); return view_make(data, len, old_elem_desc(elem), VIEW_FLAT, 0);
o->u.view.base = data;
o->u.view.len = len;
o->u.view.elem = elem;
o->u.view.is_vec = 0;
return dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
} }
flan_dyn flan_dyn_len(flan_dyn v) { flan_dyn flan_dyn_len(flan_dyn v) {
@ -3009,6 +3504,10 @@ flan_dyn flan_dyn_len(flan_dyn v) {
reason [get] is. */ reason [get] is. */
if (is_map(v)) { if (is_map(v)) {
flan_obj *o = dyn_obj(v); flan_obj *o = dyn_obj(v);
if (o->kind == OBJ_VIEW) {
view_guard_check(NULL, 0, "length", o);
return flan_dyn_from_i64(view_nfields(o));
}
class_sync(o); class_sync(o);
return flan_dyn_from_i64(o->len); return flan_dyn_from_i64(o->len);
} }
@ -3044,8 +3543,7 @@ flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i, const uint8_t *loc,
if (o->kind == OBJ_VIEW) { if (o->kind == OBJ_VIEW) {
int64_t len = view_len(loc, loclen, "at", o); int64_t len = view_len(loc, loclen, "at", o);
if (k < 0 || k >= len) trap_range(loc, loclen, "at", v, k, len); if (k < 0 || k >= len) trap_range(loc, loclen, "at", v, k, len);
return view_box(o->u.view.elem, return view_read(loc, loclen, "at", o, o->u.view.desc, view_elem_at(o, k));
(const uint8_t *)view_base(o) + k * view_elem_size(o->u.view.elem));
} }
if (k < 0 || k >= o->len) trap_range(loc, loclen, "at", v, k, o->len); if (k < 0 || k >= o->len) trap_range(loc, loclen, "at", v, k, o->len);
if (o->kind == OBJ_TEXT) return flan_dyn_from_i64(obj_text_bytes(o)[k]); if (o->kind == OBJ_TEXT) return flan_dyn_from_i64(obj_text_bytes(o)[k]);
@ -3092,10 +3590,8 @@ void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x, const uint8_t *loc,
o = dyn_obj(v); o = dyn_obj(v);
if (o->kind == OBJ_VIEW) { if (o->kind == OBJ_VIEW) {
int64_t len = view_len(loc, loclen, "set-at", o); int64_t len = view_len(loc, loclen, "set-at", o);
uint8_t *p;
if (k < 0 || k >= len) trap_range(loc, loclen, "set-at", v, k, len); if (k < 0 || k >= len) trap_range(loc, loclen, "set-at", v, k, len);
p = (uint8_t *)view_base(o) + k * view_elem_size(o->u.view.elem); view_write(loc, loclen, "set-at", v, o->u.view.desc, x, view_elem_at(o, k));
view_unbox(loc, loclen, "set-at", v, o->u.view.elem, x, p);
return; return;
} }
if (k < 0 || k >= o->len) trap_range(loc, loclen, "set-at", v, k, o->len); if (k < 0 || k >= o->len) trap_range(loc, loclen, "set-at", v, k, o->len);
@ -3111,20 +3607,24 @@ void flan_dyn_push(flan_dyn v, flan_dyn x, const uint8_t *loc, int64_t loclen) {
} }
o = dyn_obj(v); o = dyn_obj(v);
if (o->kind == OBJ_VIEW) { if (o->kind == OBJ_VIEW) {
uint8_t buf[8]; uint8_t buf[16];
/* The typed Vec's own traps print a site too, and without one from the /* The typed Vec's own traps print a site too, and without one from the
* caller the best this can name is the operation. */ * caller the best this can name is the operation. */
static const uint8_t push_loc[] = "(dyn push)"; static const uint8_t push_loc[] = "(dyn push)";
const uint8_t *site = loc != NULL && loclen > 0 ? loc : push_loc; const uint8_t *site = loc != NULL && loclen > 0 ? loc : push_loc;
int64_t sitelen = loc != NULL && loclen > 0 ? loclen int64_t sitelen = loc != NULL && loclen > 0 ? loclen
: (int64_t)sizeof(push_loc) - 1; : (int64_t)sizeof(push_loc) - 1;
int64_t size; int64_t size, align;
if (!o->u.view.is_vec) if (view_shape(o) != VIEW_VEC)
trap2(loc, loclen, TYPE_TRAP, "push", trap2(loc, loclen, TYPE_TRAP, "push",
"this view is a slice or an array and cannot grow", v, x); "this view is a slice or an array and cannot grow", v, x);
size = view_elem_size(o->u.view.elem); view_len(loc, loclen, "push", o);
view_unbox(loc, loclen, "push", v, o->u.view.elem, x, buf); desc_lay(o->u.view.desc, &size, &align);
if (!flan_vec_push(o->u.view.base, buf, size, size, site, sitelen)) /* Only a number or a bool is ever written, and [view_write] refuses the
rest before a byte of [buf] is used. */
memset(buf, 0, sizeof buf);
view_write(loc, loclen, "push", v, o->u.view.desc, x, buf);
if (!flan_vec_push(o->u.view.base, buf, size, align, site, sitelen))
trap_oom(loc, loclen, size); trap_oom(loc, loclen, size);
return; return;
} }
@ -3187,12 +3687,22 @@ static flan_obj *want_map(const char *op, flan_dyn m, flan_dyn k) {
flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k) { flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k) {
flan_obj *o = want_map("get", m, k); flan_obj *o = want_map("get", m, k);
if (o->kind == OBJ_VIEW) {
const uint8_t *fty;
uint8_t *p = view_field(NULL, 0, "get", o, k, &fty);
return view_read(NULL, 0, "get", o, fty, p);
}
int64_t i = map_find(o, k); int64_t i = map_find(o, k);
return i < 0 ? flan_dyn_nil() : o->u.v.items[i * 2 + 1]; return i < 0 ? flan_dyn_nil() : o->u.v.items[i * 2 + 1];
} }
flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k) { flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k) {
flan_obj *o = want_map("has-key?", m, k); flan_obj *o = want_map("has-key?", m, k);
if (o->kind == OBJ_VIEW) {
int64_t off;
view_guard_check(NULL, 0, "has-key?", o);
return flan_dyn_from_bool(desc_field(o->u.view.desc, k, &off) != NULL);
}
return flan_dyn_from_bool(map_find(o, k) >= 0); return flan_dyn_from_bool(map_find(o, k) >= 0);
} }
@ -3294,6 +3804,12 @@ void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v,
class_entry *e; class_entry *e;
int64_t j; int64_t j;
flan_dyn out; flan_dyn out;
if (is_map(m) && dyn_obj(m)->kind == OBJ_VIEW) {
const uint8_t *fty;
uint8_t *p = view_field(loc, loclen, "set", dyn_obj(m), k, &fty);
view_write(loc, loclen, "set", m, fty, v, p);
return;
}
if (!is_map(m) || dyn_obj(m)->u.v.klass == NULL) { if (!is_map(m) || dyn_obj(m)->u.v.klass == NULL) {
char sm[SAY_MAX]; char sm[SAY_MAX];
say(sm, SAY_MAX, m); say(sm, SAY_MAX, m);
@ -3335,6 +3851,12 @@ void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc,
class_entry *e; class_entry *e;
if (!is_map(m)) trap2(NULL, 0, TYPE_TRAP, "put", "only a map answers it", m, k); if (!is_map(m)) trap2(NULL, 0, TYPE_TRAP, "put", "only a map answers it", m, k);
o = dyn_obj(m); o = dyn_obj(m);
if (o->kind == OBJ_VIEW) {
const uint8_t *fty;
uint8_t *p = view_field(loc, loclen, "put", o, k, &fty);
view_write(loc, loclen, "put", m, fty, v, p);
return;
}
e = class_sync(o); e = class_sync(o);
/* A map with no class, and a class with no typed slot, stop at the test. */ /* A map with no class, and a class with no typed slot, stop at the test. */
if (e != NULL && e->typed) v = check_slot(loc, loclen, BY_PUT, o, e, m, k, v); if (e != NULL && e->typed) v = check_slot(loc, loclen, BY_PUT, o, e, m, k, v);

View File

@ -301,56 +301,40 @@ flan_dyn flan_dyn_need_not_nil(flan_dyn v);
* "", an empty vec, an empty map, and any keyword. Never traps. */ * "", an empty vec, an empty map, and any keyword. Never traps. */
uint8_t flan_dyn_truthy(flan_dyn v); uint8_t flan_dyn_truthy(flan_dyn v);
/* ── Typed containers as views — M2 item 3 ───────────────────────────── /* ── Typed containers as views ────────────────────────────────────────
* *
* A [(Vec T)], a [T] slice, or a fixed [n T] array crossing into dyn is a * A [(Vec T)], a [T] slice, a fixed [n T] array or a struct crossing into
* VIEW, not a copy: the box holds a small heap record naming where the * dyn is a VIEW, not a copy: the box holds a small heap record naming where
* elements live and what one of them is, and every read or write goes * the storage is and a descriptor of one element (flan_dyn.c documents the
* straight through to the container's own storage. [flan_dyn_at] boxes an * code beside [desc_lay]), and every read or write goes straight through to
* element on the way out; [flan_dyn_set_at] tag-checks the dyn value it is * the container's own storage. A read boxes the element — every number
* given against the element type on the way in and traps, by [flan_trap], * widens, an aggregate element answers a view of its own, a str is copied
* on a mismatch — never a silent coercion. * into a text — and a write tag-checks and range-checks the dyn value
* against the element type and traps, by [flan_trap], rather than coerce.
* A struct's view answers the map tag: [get], [put] and (set (get p :k) v)
* reach its fields.
* *
* T is restricted to i64, f64 and bool — exactly the set [flan_dyn_need_i64] * No collector pointer is ever written into typed storage, which the
* and friends already treat as crossing the typed boundary both ways. That * collector never scans: that is why a str element is read-only from dyn and
* is not an arbitrary cut: the excluded case that matters is a string * an aggregate element is written through its own view, never replaced.
* element, whose dyn form is a pointer into this collector's heap, while a
* typed container's storage is arena or stack memory the collector never
* scans. Writing such a pointer into that memory would be a live reference
* nothing ever traces — a use-after-free the collector cannot see coming,
* not a bug in this file but a hazard the type admits. i64, f64 and bool
* carry no such pointer, so a view restricted to them cannot manufacture
* it. [box] in lib/check.ml keeps the "does not cross into dyn yet" refusal
* for every other element type, and this paragraph is why.
* *
* Two kinds, because the containers split exactly here: a [(Vec T)] can grow * [flan_dyn_view_at] with shape 1 takes the address of a Vec's own header —
* and move (a push may reallocate), a slice and a fixed array cannot. * the struct [flan_vec] in flan_rt.c, restated in flan_dyn.c under the same
* "if either table changes, change both" rule — and every operation re-reads
* its [ptr] and [len], so a push that grows and moves the Vec is never seen
* as stale. A slice (flan_dyn_view_slice) and a fixed array (shape 0) are
* snapshotted at the crossing, sound because neither moves; a struct is
* shape 2.
* *
* [flan_dyn_view_vec] takes the address of the Vec's own header — the * The storage may be anywhere. [here] is the compiler's word that it is the
* struct [flan_vec] in flan_rt.c, restated in flan_dyn.c under the same * calling function's own frame; a dev build then records that activation
* "if either table changes, change both" rule this whole boundary already * (runtime/flan_dev.c's shadow frame and its serial), and otherwise looks
* lives under. That address is the Vec's home, fixed for as long as the Vec * the address up in its allocation registry, and every later operation
* exists — but "as long as the Vec exists" is the whole of the guarantee, * traps with DynStale when the frame has returned or the block has been
* which is why [permanent_root] in lib/check.ml admits only storage that * released. A release build records and checks nothing.
* outlives every frame: a global, a field or an array element of one, or a
* slice cut from one at the crossing. A local's slot is a home too, and it
* is precisely the one that is refused. Every operation re-reads that
* header's [ptr] and [len] fresh, so a push that grows and moves the Vec is
* never seen as stale — [flan_vec_grow] overwrites the SAME header's [ptr]
* field in place, and there is no snapshot anywhere to go stale. That is
* what makes the failure the open design question worried about
* (a push through dyn holding a dangling pointer) impossible rather than
* merely unlikely: there is nothing captured at the crossing for a later
* push to invalidate.
* *
* [flan_dyn_view_flat] takes a data address and a length captured once, at * The FLAN_VIEW_* entry points below are the first ABI, kept for
* the crossing — sound for a slice and for a fixed array because neither * test/dyn_ops.c; the compiler calls the two after them.
* ever moves or grows. Note the asymmetry is not an oversight: pointing
* *this* case at the value's own slot instead would be worse than a
* snapshot, because a slot's lifetime is not the slice's, and a slice taken
* from a Vec is already one push away from dangling on its own account
* (flan_vec_grow's own comment says so) — the view is exactly as
* stale-safe as the thing it is a view of, no more and no less.
*/ */
#define FLAN_VIEW_I64 0 #define FLAN_VIEW_I64 0
#define FLAN_VIEW_F64 1 #define FLAN_VIEW_F64 1
@ -358,6 +342,10 @@ uint8_t flan_dyn_truthy(flan_dyn v);
flan_dyn flan_dyn_view_vec(void *hdr, int32_t elem); flan_dyn flan_dyn_view_vec(void *hdr, int32_t elem);
flan_dyn flan_dyn_view_flat(void *data, int64_t len, int32_t elem); flan_dyn flan_dyn_view_flat(void *data, int64_t len, int32_t elem);
flan_dyn flan_dyn_view_slice(void *data, int64_t len, const uint8_t *desc,
int64_t desclen, int32_t here);
flan_dyn flan_dyn_view_at(void *addr, int64_t len, const uint8_t *desc,
int64_t desclen, int32_t shape, int32_t here);
/* ── The collector ───────────────────────────────────────────────────── /* ── The collector ─────────────────────────────────────────────────────
* *

View File

@ -0,0 +1,211 @@
;;;; Any typed container crosses into dyn as a view: every element type and
;;;; any storage. Mode 0 is the survey; the others are one trap each, since a
;;;; trap ends the process. test_acceptance.ml runs it on both backends, and
;;;; the stale-view modes under --dev only: a release build keeps no record of
;;;; frames or blocks, so it neither checks nor promises anything there.
;; Unannotated parameters and returns are dyn: every call below boxes its
;; typed argument into a view at the call.
(defn show [label d] ()
(print label)
(print " ")
(println d))
(defn bump-all [d] ()
(dotimes [i (length d)]
(set (at d i) (+ (at d i) 1))))
(defn keep [d] dyn d)
;; Field offsets only C's layout rule gets right: a u8, then an i64 aligned
;; to 8, an f32, a bool, three u16 at 2, an i32 at 4.
(defstruct Mix [a u8 b i64 c f32 d bool e [3 u16] f i32])
(defstruct Point [x f32 y i32])
(defstruct Named [name str id u32])
(defn make-point [] Point (Point {.x 1.5 .y 2}))
(defn param-array [xs [4 i32]] i32
;; A parameter array is this frame's copy.
(bump-all xs)
(at xs 3))
(defn param-slice [xs [i64]] ()
(bump-all xs))
(defonce held dyn nil)
;; A view of a local, kept past the call that owns the local.
(defn leak-local [] ()
(let [a [1 2 3]]
(set held (keep a))))
(defn clobber [] i64
(let [b [(i64 7) 8 9 10 11 12]]
(+ (at b 0) (at b 5))))
(defn main [args [str]] i32
(let [n (i32 (bytes->i64 (bytes-view (at args 1))))]
(cond
(= n 0)
(do
;; Every integer width, a local array each: read widens, write stores.
(let [a [(i8 -1) -2 -3]
b [(u8 250) 251 252]
c [(i16 -300) 300]
d [(u16 60000) 1]
e [(i32 -70000) 70000]
f [(u32 4000000000) 1]
g [(i64 -5) 5]
h [(u64 9000000000000000000) 1]]
(bump-all a) (bump-all b) (bump-all c) (bump-all d)
(bump-all e) (bump-all f) (bump-all g) (bump-all h)
(show "i8" a) (show "u8" b) (show "i16" c) (show "u16" d)
(show "i32" e) (show "u32" f) (show "i64" g) (show "u64" h)
(println (at b 2)))
;; f32: read widens, write narrows.
(let [fs [(f32 0.5) 1.25]
dv (keep fs)]
(set (at dv 0) 2.75)
(set (at dv 1) 3.1)
(show "f32" dv)
(println (at fs 0)))
;; bool, through a slice cut from a local array.
(let [bs [true false true]]
(let [dv (keep (slice bs 0 3))]
(set (at dv 1) true)
(show "bool" dv)
(println (at bs 1))))
;; The author's case: a [u8] from bytes, sorted in place by dyn code.
(let [text (bytes "INSERTIONSORT")]
(let [d (keep text)]
(dotimes [i (length d)]
(let [j i]
(while (and (> j 0) (< (at d j) (at d (- j 1))))
(let [t (at d j)]
(set (at d j) (at d (- j 1)))
(set (at d (- j 1)) t))
(set j (- j 1))))))
(println (str text)))
;; A struct: a map-like view, written through get and put.
(let [p (Point {.x 0.5 .y 3})
dp (keep p)]
(show "point" dp)
(set (get dp :y) 40)
(put dp :x 9.5)
(println (.y p))
(println (.x p))
(println (get dp :x))
(println (length dp))
(println (has-key? dp :y)))
;; Every field of a padded struct reads back.
(let [m (Mix {.a 7 .b -9000000000 .c 2.5 .d true .e [1 2 3] .f -4})]
(show "mix" (keep m))
(set (at (get (keep m) :e) 2) 65535)
(println (at (.e m) 2)))
;; A str field reads as text.
(let [nm (Named {.name "ada" .id 7})]
(show "named" (keep nm)))
;; Nested arrays: a view of a view.
(let [grid [[(i32 1) 2 3] [4 5 6]]
dg (keep grid)]
(set (at (at dg 1) 2) 60)
(show "grid" dg)
(println (at (at grid 1) 2)))
;; An array of structs.
(let [ps [(Point {.x 1.0 .y 1}) (Point {.x 2.0 .y 2})]
dp (keep ps)]
(set (get (at dp 1) :y) 20)
(show "points" dp)
(println (.y (at ps 1))))
;; A Vec in a local: a push through the view grows the typed Vec.
(let [v (vec-new i32)]
(push v 1)
(let [dv (keep v)]
(push dv 2)
(push dv 3)
(show "vec" dv))
(println (length v)))
;; A Vec of structs.
(let [v (vec-new Point)]
(push v (Point {.x 0.0 .y 5}))
(let [dv (keep v)]
(set (get (at dv 0) :y) 6))
(println (.y (at v 0))))
;; Parameters: an array parameter is the callee's copy; a slice
;; parameter sees the caller's elements.
(let [xs [(i32 1) 2 3 4]]
(println (param-array xs))
(println (at xs 3)))
(let [ys [(i64 1) 2]]
(param-slice (slice ys 0 2))
(println (at ys 1)))
;; A temporary: a struct returned by a call.
(show "temp" (keep (make-point)))
;; A heap slice, and an arena's Vec.
(let [c (clone (slice [(i64 1) 2 3] 0 3))]
(bump-all c)
(println (at c 2)))
(let [ar (arena-new 4096)]
(with-allocator ar
(let [v (vec-new i16)]
(push v 1)
(push v 2)
(bump-all v)
(println (at v 1))))
(arena-destroy ar))
;; Two views of equal structs are equal.
(let [p (Point {.x 1.0 .y 2})
q (Point {.x 1.0 .y 2})]
(println (= (keep p) (keep q))))
0)
;; A value an element's width cannot hold.
(= n 1)
(let [b [(u8 1) 2]]
(set (at (keep b) 0) 300)
0)
;; A u64 above the largest dyn int.
(= n 2)
(let [h [(u64 18000000000000000000)]]
(println (at (keep h) 0))
0)
;; A field a struct does not have.
(= n 3)
(let [p (Point {.x 1.0 .y 2})]
(println (get (keep p) :z))
0)
;; A view of a local, used after its call returned: stale in a dev build.
(= n 4)
(do (leak-local)
(println (clobber))
(println (at held 0))
0)
;; A slice of a Vec's block, used after the Vec grew and moved.
(= n 5)
(let [v (vec-new i64)]
(push v 1)
(let [d (keep (slice v))]
(dotimes [i 100] (push v i))
(println (at d 0)))
0)
;; A heap slice used after it was freed.
(= n 6)
(let [c (clone (slice [(i64 1) 2 3] 0 3))
d (keep c)]
(free c)
(println (at d 0))
0)
;; An arena's storage used after free-all.
(= n 7)
(let [ar (arena-new 4096)
v (with-allocator ar (clone (slice [(i32 4) 5] 0 2)))
d (keep v)]
(free-all ar)
(println (at d 1))
0)
;; A str element is read-only through a view.
(= n 8)
(let [nm (Named {.name "ada" .id 7})]
(put (keep nm) :name "bob")
0)
:else (do (println "?") 1))))

View File

@ -5,24 +5,8 @@
;;;; caller's own value, which is what makes [dv] below the SAME storage [v] ;;;; caller's own value, which is what makes [dv] below the SAME storage [v]
;;;; is and not a copy of it. ;;;; is and not a copy of it.
;;;; ;;;;
;;;; Every container viewed below is a GLOBAL, and that is not incidental to ;;;; Every container viewed below is a global; dyn-view-any.flan covers
;;;; this program — it is the lifetime guard review added after the first ;;;; the other storage and element types.
;;;; landing: a view's descriptor chases the container's own address on every
;;;; operation, which is what makes a Vec's growth safe, but it is also what
;;;; makes a DANGLING container's address a live hazard. box refuses a Vec, a
;;;; slice or a fixed array whose storage is not known to outlive the view —
;;;; a local's, a parameter's, a temporary's — and a global's is the one
;;;; storage this milestone can prove permanent: fixed in .data for the
;;;; process — as is a field of one, and an ELEMENT of one when the global
;;;; is an array, whose elements sit inside its own storage. An element of a
;;;; global SLICE is not: the slice is ptr+len and says nothing about where
;;;; the data is — and that holds at every index of a multi-index (at g i j),
;;;; not just the first, so one slice level anywhere in the walk refuses.
;;;; test_flan.ml's checker tests carry the refusal side of this (a local
;;;; Vec, a Vec parameter, a Vec behind a Ptr, a slice rebound to a local, an
;;;; element of a global slice, and an element reached through a slice at a
;;;; later index level); this program is the acceptance side, over storage
;;;; the guard allows.
;;;; ;;;;
;;;; Mode 0 is the survey: a read through the view boxes the element ;;;; 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 ;;;; correctly, a write through either side is seen through the other, and a

View File

@ -5838,6 +5838,74 @@ level "1"
dyn_view ~opt:"-O0" (); dyn_view ~opt:"-O0" ();
dyn_view ~x86:true (); dyn_view ~x86:true ();
(* ── Any typed container crosses as a view ─────────────────────────
programs/dyn-view-any.flan: every integer width, f32, bool, str, a
struct and a padded one, nested arrays, arrays and Vecs of structs,
over locals, parameters, a temporary, a heap slice and an arena's Vec
(mode 0); then one trap per mode. The traps a release build cannot
see — a view kept past its frame, a slice kept past its Vec's growth,
past a free, past a free-all — run under --dev only, on both
backends. The survey's text was captured from the running program and
is the same in all five builds. *)
let any_out =
"i8 [0 -1 -2]\nu8 [251 252 253]\ni16 [-299 301]\nu16 [60001 2]\n\
i32 [-69999 70001]\nu32 [4000000001 2]\ni64 [-4 6]\n\
u64 [9000000000000000001 2]\n253\n\
f32 [2.75 3.1]\n2.75\n\
bool [true true true]\ntrue\n\
EIINNOORRSSTT\n\
point #Point{:x 0.5 :y 3}\n40\n9.5\n9.5\n2\ntrue\n\
mix #Mix{:a 7 :b -9000000000 :c 2.5 :d true :e [1 2 3] :f -4}\n65535\n\
named #Named{:name \"ada\" :id 7}\n\
grid [[1 2 3] [4 5 60]]\n60\n\
points [#Point{:x 1 :y 1} #Point{:x 2 :y 20}]\n20\n\
vec [1 2 3]\n3\n6\n5\n4\n3\n\
temp #Point{:x 1.5 :y 2}\n4\n3\ntrue\n"
in
let any_traps =
[ ("1", "300 does not fit a u8 element, which holds 0 to 255");
("2", "this u64 element is 18000000000000000000, above the largest \
dyn int");
("3", "a Point has no field :z. Its fields are :x :y");
("8", "a str element is read-only through a dyn view") ]
and any_stale =
[ ("4", "this view points into a local of leak-local, and that call \
has returned");
("5", "this view's storage, a block of i64, has been released");
("6", "this view's storage, a block of i64, has been released");
("7", "this view's storage, a block of i32, has been released") ]
in
let dyn_view_any ?opt ?(x86 = false) ?(dev = false) () =
let exe = compile ?opt ~x86 ~dev "programs/dyn-view-any.flan" in
let name what =
"dyn: any container's view" ^ what
^ (match opt with Some o -> ", " ^ o | None -> "")
^ (if x86 then ", --x86" else "") ^ (if dev then ", --dev" else "")
in
let code, text = run exe (Some "0") in
if code <> 0 || text <> any_out then begin
incr failures;
Printf.printf "FAIL %s\n got: %S (exit %d)\n wanted: %S\n"
(name "") text code any_out
end;
List.iter
(fun (mode, needle) ->
let code, text = run exe (Some mode) in
if code <> 134 || not (contains text needle) then begin
incr failures;
Printf.printf
"FAIL %s\n got: %S (exit %d)\n wanted a trap \
saying %S\n" (name (", mode " ^ mode)) text code needle
end)
(any_traps @ if dev then any_stale else []);
(try Sys.remove exe with Sys_error _ -> ())
in
dyn_view_any ();
dyn_view_any ~opt:"-O0" ();
dyn_view_any ~x86:true ();
dyn_view_any ~dev:true ();
dyn_view_any ~dev:true ~x86:true ();
(* The root count, which is the part of this feature the runs above cannot (* The root count, which is the part of this feature the runs above cannot
check — and the reason has outlived the stub it was first written check — and the reason has outlived the stub it was first written
about. flan_dyn.c's trigger has a one-megabyte floor, and not one about. flan_dyn.c's trigger has a one-megabyte floor, and not one

View File

@ -1525,105 +1525,80 @@ let () =
"(defonce v (Vec f64) (vec-new f64))\n\ "(defonce v (Vec f64) (vec-new f64))\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (take v))"; (defn main [] i32 (take v))";
(* The element restriction is still refused, and by name: a string element (* Every element a view can describe crosses: every number, bool, str,
would need a dyn string's own boxing, whose payload is a pointer into struct, and arrays, slices and Vecs of those. What cannot be described
the collector's heap, planted where nothing will ever trace it. *) is refused by name — a pointer, an Option, a function, a map, an enum
rejects_check "a Vec of strings does not view into dyn yet" or a data type inside the container. *)
accepts "a Vec of strings views into dyn, read-only"
"(defonce v (Vec str) (vec-new str))\n\ "(defonce v (Vec str) (vec-new str))\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (take v))" (defn main [] i32 (take v))";
~needle:"only when its elements are i64, f64 or bool"; accepts "an i32 element views into dyn"
rejects_check "an i32 element is not one of the view's three"
"(defonce v (Vec i32) (vec-new i32))\n\ "(defonce v (Vec i32) (vec-new i32))\n\
(defn take [d dyn] i32 1)\n\
(defn main [] i32 (take v))";
rejects_check "a Vec of pointers does not view into dyn"
"(defonce v (Vec (Ptr i64)) (vec-new (Ptr i64)))\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (take v))" (defn main [] i32 (take v))"
~needle:"only when its elements are i64, f64 or bool"; ~needle:"a (Ptr i64) is none of these";
rejects_check "a struct with an Option field names the field's type"
"(defstruct Maybe [x (Option i64)])\n\
(defn take [d dyn] i32 1)\n\
(defn main [] i32 (take (Maybe {.x None})))"
~needle:"a (Option i64) is none of these";
(* A typed (Map K V) is unrelated to item 3 and keeps its own refusal. *) (* A typed (Map K V) is unrelated to item 3 and keeps its own refusal. *)
rejects_check "a typed Map still refuses into dyn" rejects_check "a typed Map still refuses into dyn"
"(defonce m (Map i64 i64) (map-new i64 i64))\n\ "(defonce m (Map i64 i64) (map-new i64 i64))\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (take m))" (defn main [] i32 (take m))"
~needle:"does not cross into dyn yet"; ~needle:"does not cross into dyn yet";
(* Which of the two refusals wins when both apply. A LOCAL (Vec string) (* Any storage: a local, a parameter, a temporary, a slice bound to a
fails the lifetime guard and the element check both, and the element local, an element of a global slice, and a container behind a pointer.
one has to be the one that speaks: the lifetime message names A dev build checks each against its frame or its block at run time
(defonce g ...) as the spelling that works, and for a string element (test_acceptance.ml, dyn-view-any.flan); nothing is refused here. *)
the global spelling is refused too, so the other order would hand back accepts "a local Vec views into dyn"
advice that fails when taken. *)
rejects_check "a local Vec of strings gets the element refusal, not the \
lifetime one"
"(defn take [d dyn] i32 1)\n\ "(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [v (vec-new str)] (take v)))" (defn main [] i32 (let [v (vec-new i64)] (take v)))";
~needle:"only when its elements are i64, f64 or bool"; accepts "a Vec parameter views into dyn"
(* ── The lifetime guard, added on review ─────────────────────────
A local, a parameter and a temporary all answer false to
[permanent_root], and each gets the same message rather than "cannot be
indexed" or some other accident of which path noticed. *)
rejects_check "a local Vec does not view into dyn — its frame ends"
"(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [v (vec-new i64)] (take v)))"
~needle:"only when it is a global";
rejects_check "a Vec parameter does not view into dyn"
"(defn take [d dyn] i32 1)\n\ "(defn take [d dyn] i32 1)\n\
(defn give [v (Vec i64)] i32 (take v))\n\ (defn give [v (Vec i64)] i32 (take v))\n\
(defn main [] i32 0)" (defn main [] i32 0)";
~needle:"only when it is a global"; accepts "a fixed array local views into dyn"
rejects_check "a fixed array local does not view into dyn"
"(defn take [d dyn] i32 1)\n\ "(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [a (array 4 i64)] (take a)))" (defn main [] i32 (let [a (array 4 i64)] (take a)))";
~needle:"only when it is a global"; accepts "a slice cut from a global inline views into dyn"
(* A slice cut from a global is permanent; the same slice expression
rebound to a local first loses the trace back to it and is refused —
conservative rather than wrong, and the message says what does work. *)
accepts "a slice cut from a global inline is still permanent"
"(defonce xs [3 i64])\n\ "(defonce xs [3 i64])\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (take (slice xs 0 3)))"; (defn main [] i32 (take (slice xs 0 3)))";
rejects_check "a slice rebound to a local loses the trace and is refused" accepts "a slice bound to a local views into dyn"
"(defonce xs [3 i64])\n\ "(defonce xs [3 i64])\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [s (slice xs 0 3)] (take s)))" (defn main [] i32 (let [s (slice xs 0 3)] (take s)))";
~needle:"only when it is a global"; accepts "an element of a global array views into dyn"
(* An element of a global is permanent only when the global is an ARRAY.
An array's elements are inside the global's own storage; a slice's are
not — a global [[T]] holds ptr+len and nothing more, and what they
point at may be a frame that has already returned. The refusal row
below is one word different from the acceptance row above it, which is
the point: it is the [At] arm's demand for an array at the level being
indexed and nothing else deciding. Before that guard the refusal row
compiled and segfaulted with no diagnostic at all. *)
accepts "an element of a global array is permanent"
"(defonce rows [2 (Vec i64)])\n\ "(defonce rows [2 (Vec i64)])\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (take (at rows 0)))"; (defn main [] i32 (take (at rows 0)))";
rejects_check "an element of a global slice is not permanent" accepts "an element of a global slice views into dyn"
"(defonce sv [(Vec i64)])\n\ "(defonce sv [(Vec i64)])\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (take (at sv 0)))" (defn main [] i32 (take (at sv 0)))";
~needle:"only when it is a global"; accepts "an element of a global array of arrays views into dyn"
(* [(at g i j)] is ONE typed node holding both indices, not two nested
ones, so a guard that reads the target's type alone sees level zero and
nothing after it. These two rows pin the multi-index spelling on both
sides: every level an array is permanent, and a slice at ANY level is
not — including the second, which the one-level guard accepted and
which then printed a dead frame's contents with exit 0. *)
accepts "an element of a global array of arrays is permanent"
"(defonce rows [2 [3 (Vec i64)]])\n\ "(defonce rows [2 [3 (Vec i64)]])\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (take (at rows 0 1)))"; (defn main [] i32 (take (at rows 0 1)))";
rejects_check "an element reached through a slice level is not permanent" accepts "an element reached through a slice level views into dyn"
"(defonce g [2 [[3 i64]]])\n\ "(defonce g [2 [[3 i64]]])\n\
(defn take [d dyn] i32 1)\n\ (defn take [d dyn] i32 1)\n\
(defn main [] i32 (take (at g 0 1)))" (defn main [] i32 (take (at g 0 1)))";
~needle:"only when it is a global"; accepts "a Vec behind a Ptr views into dyn"
(* A Vec behind a Ptr is refused even though some Ptrs really are
heap-durable — the checker cannot tell this one from a Ptr taken off a
local, and admitting one admits the other. *)
rejects_check "a Vec behind a Ptr does not view into dyn"
"(defn take [d dyn] i32 1)\n\ "(defn take [d dyn] i32 1)\n\
(defn use [p (Ptr (Vec i64))] i32 (take (deref p)))\n\ (defn use [p (Ptr (Vec i64))] i32 (take (deref p)))\n\
(defn main [] i32 0)" (defn main [] i32 0)";
~needle:"only when it is a global"; accepts "a struct views into dyn"
"(defstruct P [x f32 y u8])\n\
(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [p (P {.x 1.0 .y 2})] (take p)))";
(* A bracket *literal* is not a typed container yet, and where a dyn is (* A bracket *literal* is not a typed container yet, and where a dyn is
wanted it builds the runtime's own vec instead — the lowering the map wanted it builds the runtime's own vec instead — the lowering the map
literal's values ride on, and what makes {:xs [1 2]} mean what it literal's values ride on, and what makes {:xs [1 2]} mean what it
@ -3700,12 +3675,10 @@ let () =
The three-element spelling does not mean this, and could not. A defonce The three-element spelling does not mean this, and could not. A defonce
whose third element is not a type is a *dyn* global by the 2026-09-20 whose third element is not a type is a *dyn* global by the 2026-09-20
rule, and a typed fixed array crosses into dyn only as a view of storage rule, and a typed fixed array crosses into dyn as a view of its
that outlives the view. A freshly built array is a temporary, so the view storage. A freshly built array is the initialiser's temporary, gone when
lifetime guard refuses it — and where the elements are an array rather the initialiser returns, so a view of it is refused there by name with
than one of the three scalar widths a view carries, the element refusal the typed spelling as the fix. *)
gets there first. Both refusals are the ones any other temporary gets;
neither was written for this form. *)
defvar_reading "a typed array-fill global is computed, not zeroed" defvar_reading "a typed array-fill global is computed, not zeroed"
"(defconst rows 2) (defconst cols 3)\n\ "(defconst rows 2) (defconst cols 3)\n\
(defonce grid [rows [cols u8]] (array-fill [rows cols] 255))\n\ (defonce grid [rows [cols u8]] (array-fill [rows cols] 255))\n\
@ -3713,10 +3686,11 @@ let () =
"grid" ~ty:"[2 [3 u8]]" ~zeroed:false; "grid" ~ty:"[2 [3 u8]]" ~zeroed:false;
rejects_check "a three-element array-fill defonce is the dyn reading" rejects_check "a three-element array-fill defonce is the dyn reading"
"(defonce xs (array-fill [3] (i64 1))) (defn f [] ())" "(defonce xs (array-fill [3] (i64 1))) (defn f [] ())"
~needle:"only when it is a global"; ~needle:"xs is a dyn global, and its initialiser builds a [3 i64] that is \
rejects_check "and its element type is asked about first" gone once the initialiser returns";
rejects_check "and the fix it names is the typed spelling"
"(defonce grid (array-fill [2 3] 255)) (defn f [] ())" "(defonce grid (array-fill [2 3] 255)) (defn f [] ())"
~needle:"only when its elements are i64, f64 or bool"; ~needle:"as in (defonce grid [2 [3 i32]] ...)";
(* A defconst is not a second path to it: its value is what the linker (* A defconst is not a second path to it: its value is what the linker
writes into the image, and a fill is a loop. *) writes into the image, and a fill is a loop. *)
rejects_check "array-fill is not a constant's value" rejects_check "array-fill is not a constant's value"
@ -7664,14 +7638,18 @@ let () =
infers "the names the element type of a mixed literal" "(the [dyn] [1 2.5])" "[2 dyn]"; infers "the names the element type of a mixed literal" "(the [dyn] [1 2.5])" "[2 dyn]";
infers "the with a slice type gives the literal's array type" infers "the with a slice type gives the literal's array type"
"(the [f32] [1 2.5])" "[2 f32]"; "(the [f32] [1 2.5])" "[2 f32]";
(match checked "(defstruct P [x i32]) (defn main [] i32 (let [a [(P 1) 2]] 0))" with (* A struct crosses into dyn as a view, so a struct beside a number is a dyn
| _ -> check "a struct beside a number is refused" false vector; a pointer has no dyn form, so it is refused against the first. *)
accepts "a struct beside a number is a dyn vector"
"(defstruct P [x i32]) (defn main [] i32 (let [a [(P 1) 2]] (println a) 0))";
(match checked "(defn main [] i32 (let [x 1 a [(addr x) 2]] 0))" with
| _ -> check "a pointer beside a number is refused" false
| exception Loc.Error d -> | exception Loc.Error d ->
check "elements that cannot become a dyn are refused against the first" check "elements that cannot become a dyn are refused against the first"
(contains d.Loc.dmsg "expected P, found the integer literal 2" (contains d.Loc.dmsg "expected (Ptr i32), found the integer literal 2"
&& List.exists && List.exists
(fun (n : Loc.note) -> (fun (n : Loc.note) ->
contains n.Loc.nmsg "this array's first element is P") contains n.Loc.nmsg "this array's first element is (Ptr i32)")
d.Loc.notes)); d.Loc.notes));
(match checked "(defn g [x $t] i32 (let [a [x 1]] 0))" with (match checked "(defn g [x $t] i32 (let [a [x 1]] 0))" with
| _ -> check "a type variable beside a literal is refused" false | _ -> check "a type variable beside a literal is refused" false

View File

@ -262,6 +262,10 @@ let corpus =
view that grows and moves the Vec leaves anything for ASan's view that grows and moves the Vec leaves anything for ASan's
use-after-free detection to find. *) use-after-free detection to find. *)
"programs/dyn-view.flan", [ "0" ]; "programs/dyn-view.flan", [ "0" ];
(* Views of every element type over locals, parameters, a temporary, a
heap slice and an arena's Vec: the offsets the runtime computes from
a descriptor, read and written under ASan. *)
"programs/dyn-view-any.flan", [ "0" ];
"programs/x86-p13-dyn-collect.flan", []; "programs/x86-p13-dyn-collect.flan", [];
"programs/sand-headless.flan", []; "programs/sand-headless.flan", [];
"programs/signedness.flan", []; "programs/signedness.flan", [];

View File

@ -930,46 +930,54 @@ let checks name text =
let () = let () =
let poke_fln = "fn poke(coll) -> dyn\n coll[0] = 99\n coll\n\n" in let poke_fln = "fn poke(coll) -> dyn\n coll[0] = 99\n coll\n\n" in
let poke_flan = "(defn poke [coll] dyn (set (at coll 0) 99) coll)\n" in let poke_flan = "(defn poke [coll] dyn (set (at coll 0) 99) coll)\n" in
(* A typed local is not a global, so no dyn value may see into it. *) (* A container of pointers has no dyn view, and the refusal's subject and
fix follow what the container is: a local, a temporary, a parameter. *)
refused "view-local.fln" refused "view-local.fln"
(poke_fln ^ "fn main() -> ()\n let a: [4 i64] = [6 2 4 9]\n poke(a)\n") (poke_fln ^ "fn main() -> ()\n let a = vec-new(Ptr(i64))\n poke(a)\n")
[ "a is a [4 i64], and a dyn value is wanted here"; [ "a is a Vec(Ptr(i64)), and a dyn value is wanted here";
"a local, a parameter or a temporary"; "a Ptr(i64) is none of these";
"as in let a: dyn = [...]" ]; "as in let a: dyn = [...]" ];
refused "view-local.flan" refused "view-local.flan"
(poke_flan ^ "(defn main [] () (let [a (array 4 i64)] (poke a)))\n") (poke_flan ^ "(defn main [] () (let [a (vec-new (Ptr i64))] (poke a)))\n")
[ "a is a [4 i64]"; "as in (let [a (the dyn [...])] ...)" ]; [ "a is a (Vec (Ptr i64))"; "as in (let [a (the dyn [...])] ...)" ];
refused "view-temp.flan" refused "view-temp.flan"
(poke_flan ^ "(defn main [] () (poke (array 4 i64)))\n") (poke_flan ^ "(defn main [] () (poke (vec-new (Ptr i64))))\n")
[ "This is a [4 i64]"; "as in (the dyn [...])" ]; [ "This is a (Vec (Ptr i64))"; "as in (the dyn [...])" ];
(* An unannotated literal is [4 i32], whose elements no view carries. *) (* Any storage and any number crosses: a local [4 i32] is a view. *)
refused "view-elem.fln" checks "view-elem.fln"
(poke_fln ^ "fn main() -> ()\n let d = [6 2 4 9]\n poke(d)\n") (poke_fln ^ "fn main() -> ()\n let d = [6 2 4 9]\n poke(d)\n");
[ "d is a [4 i32]"; "only when its elements are i64, f64 or bool, and these are i32";
"as in let d: dyn = [...]" ];
(* A parameter is made by the caller, so its fix is its declaration. *) (* A parameter is made by the caller, so its fix is its declaration. *)
refused "view-param.fln" refused "view-param.fln"
"fn take(d) -> i32 = 1\n\nfn give(n: i32, v: [4 i64]) -> i32\n take(v)\n\n\ "fn take(d) -> i32 = 1\n\nfn give(n: i32, v: [Ptr(i64)]) -> i32\n take(v)\n\n\
fn main() -> i32 = 0\n" fn main() -> i32 = 0\n"
[ "v is a [4 i64] parameter"; "Declare v as dyn in give's parameters: v: dyn" ]; [ "v is a [Ptr(i64)] parameter"; "Declare v as dyn in give's parameters: v: dyn" ];
refused "view-param.flan" refused "view-param.flan"
"(defn take [d dyn] i32 1)\n(defn give [n i32 v (Vec i64)] i32 (take v))\n\ "(defn take [d dyn] i32 1)\n(defn give [n i32 v (Vec (Ptr i64))] i32 (take v))\n\
(defn main [] i32 0)\n" (defn main [] i32 0)\n"
[ "v is a (Vec i64) parameter"; "Declare v as dyn in give's parameters: v dyn" ]; [ "v is a (Vec (Ptr i64)) parameter"; "Declare v as dyn in give's parameters: v dyn" ];
checks "view-param-fix.fln" checks "view-param-fix.fln"
"fn take(d) -> i32 = 1\n\nfn give(n: i32, v: dyn) -> i32\n take(v)\n\n\ "fn take(d) -> i32 = 1\n\nfn give(n: i32, v: dyn) -> i32\n take(v)\n\n\
fn main() -> i32 = 0\n"; fn main() -> i32 = 0\n";
(* A global's fix redefines it, in the form it was defined with. *) (* A global's fix redefines it, in the form it was defined with. *)
let show_flan = "(defn show [d dyn] i32 1)\n" in let show_flan = "(defn show [d dyn] i32 1)\n" in
refused "view-global.flan" refused "view-global.flan"
("(defonce gs [2 i32] [1 2])\n" ^ show_flan ^ "(defn main [] i32 (show gs))\n") ("(defonce gs (Vec (Ptr i32)) (vec-new (Ptr i32)))\n" ^ show_flan
[ "gs is a [2 i32]"; "as in (defonce gs dyn [...])" ]; ^ "(defn main [] i32 (show gs))\n")
[ "gs is a (Vec (Ptr i32))"; "as in (defonce gs dyn [...])" ];
refused "view-global-def.flan" refused "view-global-def.flan"
("(def gs [2 i32] [1 2])\n" ^ show_flan ^ "(defn main [] i32 (show gs))\n") ("(def gs (Vec (Ptr i32)) (vec-new (Ptr i32)))\n" ^ show_flan
^ "(defn main [] i32 (show gs))\n")
[ "as in (def gs dyn [...])" ]; [ "as in (def gs dyn [...])" ];
refused "view-global.fln" refused "view-global.fln"
"once gs: [2 i32] = [1 2]\n\nfn show(d) -> i32 = 1\n\nfn main() -> i32 = show(gs)\n" "once gs: Vec(Ptr(i32)) = vec-new(Ptr(i32))\n\nfn show(d) -> i32 = 1\n\n\
[ "gs is a [2 i32]"; "as in once gs: dyn = [...]" ]; fn main() -> i32 = show(gs)\n"
[ "gs is a Vec(Ptr(i32))"; "as in once gs: dyn = [...]" ];
checks "view-global-typed.flan"
("(defonce gs [2 i32] [1 2])\n" ^ show_flan ^ "(defn main [] i32 (show gs))\n");
(* A dyn global's initialiser cannot view what it builds itself. *)
refused "view-global-init.fln"
"once xs = array-fill([3], i64(1))\n\nfn main() -> i32 = 0\n"
[ "xs is a dyn global"; "as in once xs: [3 i64] = ..." ];
checks "view-global-fix.flan" checks "view-global-fix.flan"
("(defonce gs dyn [1 2])\n(def hs dyn [1 2])\n" ^ show_flan ("(defonce gs dyn [1 2])\n(def hs dyn [1 2])\n" ^ show_flan
^ "(defn main [] i32 (show gs) (show hs))\n"); ^ "(defn main [] i32 (show gs) (show hs))\n");