Typed containers into dyn as views — M2 item 3

A (Vec T), a slice or a fixed array crossing into dyn no longer refuses; it
is a view, one word in the box, over the container's own storage. Reads box
the element on the way out; writes tag-check the dyn value's tag against the
element type on the way in and trap, by name, on a mismatch, never coercing
or silently storing.

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

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

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

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

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

17
FIX.org
View File

@ -420,7 +420,22 @@ rename. typed-flan branch freezes the static language pre-dyn.
Left open until the lane is built: whether the descriptor points at the Left open until the lane is built: whether the descriptor points at the
container or is a fattened slice stored beside it. That only bites if the container or is a fattened slice stored beside it. That only bites if the
container can grow and move, which would leave a push through dyn holding container can grow and move, which would leave a push through dyn holding
a stale pointer. a stale pointer. — LANDED. Settled: a Vec view holds the address of the
Vec's own header and reads its ptr/len live on every operation, so a push
that reallocates cannot go stale — there is no snapshot to invalidate,
because flan_vec_grow overwrites that same header in place. A slice and a
fixed array cannot grow, so a flat view snapshots pointer and length once,
which is sound for both and is not the weaker half of an asymmetric
choice — pointing a flat view at its own value's slot instead would be
worse, since a slot's lifetime is not the slice's. The element set is i64,
f64 and bool only: a string element's dyn form is a pointer into the
collector's heap, and a typed container's storage is memory the collector
never scans, so a wider set would let a write plant a live reference
nothing traces. (Vec string) and (Map K V) keep the refusal [box] already
gave every container. Both backends, runtime/flan_dyn.c and .h, checker
tests, an acceptance row per backend, and a survey program
(dyn-view.flan) proving the view against both a growing Vec and a fixed
array/slice, plus its own two trap modes.
4. nil: arrives with maps. nil <-> None at (Option T) boundaries, trap at 4. nil: arrives with maps. nil <-> None at (Option T) boundaries, trap at
bare T, (Some nil) unconstructible. — LANDED, 3c1fb1b. The bare-T trap is bare T, (Some nil) unconstructible. — LANDED, 3c1fb1b. The bare-T trap is
split: a literal nil the checker can see is refused at compile time, in split: a literal nil the checker can see is refused at compile time, in

View File

@ -1441,6 +1441,39 @@ let no_dyn_yet loc ~into t extra =
"%s does not cross into %s yet%s" "%s does not cross into %s yet%s"
(Types.to_string t) (if into then "dyn" else "a written type") extra (Types.to_string 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 [unboxable] above 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) =
mk loc (Types.Int Types.I32) (Tast.Int (k, Types.I32))
let view_not_yet loc (container : Types.t) (elem : Types.t) =
no_dyn_yet loc ~into:true container
(Printf.sprintf
". A container view at this milestone holds i64, f64 or bool \
elements a %s element's dyn form is a pointer into the \
collector's heap, and a typed container's storage is memory the \
collector never scans, so a write through the view could plant a \
pointer where nothing will ever trace it"
(Types.to_string elem))
let box loc (e : Tast.expr) : Tast.expr = let box 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
@ -1465,11 +1498,35 @@ let box loc (e : Tast.expr) : Tast.expr =
nothing a dyn could hold. The absent dyn value is nil, which is a \ nothing a dyn could hold. The absent dyn value is nil, which is a \
literal here: write nil" literal here: write nil"
| Types.Never -> e | Types.Never -> e
| Types.Vec _ | Types.Map _ | Types.Slice _ | Types.Array _ -> (* A view, not a copy: the box holds one word naming where the elements
live and what one of them is, and every read or write goes straight
through to the container's own storage see runtime/flan_dyn.h's
view section for the whole of the argument, including why the
descriptor points AT the container (a Vec's own header address)
rather than snapshotting its ptr+len. That is what makes a push
through the view safe even though a Vec can grow and move: there is
no snapshot for the growth to invalidate. A slice and a fixed array
cannot grow, so a snapshot taken once at the crossing is sound for
both, and they share [flan_dyn_view_flat]. *)
| Types.Vec elem ->
(match view_elem elem with
| Some k -> dyn "flan_dyn_view_vec" [ e; view_elem_lit loc k ]
| None -> view_not_yet loc e.Tast.ty elem)
| Types.Slice elem ->
(match view_elem elem with
| Some k -> dyn "flan_dyn_view_flat" [ e; view_elem_lit loc k ]
| None -> view_not_yet loc e.Tast.ty elem)
| Types.Array (n, elem) ->
(match view_elem elem with
| Some k ->
dyn "flan_dyn_view_flat"
[ e; mk loc dyn_i64 (Tast.Int (n, Types.I64)); view_elem_lit loc k ]
| None -> view_not_yet loc e.Tast.ty elem)
| Types.Map _ ->
no_dyn_yet loc ~into:true e.Tast.ty no_dyn_yet loc ~into:true e.Tast.ty
". The dyn container at this milestone is the runtime's own, from \ ". The dyn container at this milestone is the runtime's own, from \
(vec-new dyn); a typed container has a representation the dyn runtime \ (map-new dyn); a typed (Map K V) has a representation the dyn \
cannot walk" runtime cannot walk"
(* [Option] is on this list in name only: [expect] intercepts it before (* [Option] is on this list in name only: [expect] intercepts it before
[box] ever sees one [box_option] is the real answer, M2 item 4 so [box] ever sees one [box_option] is the real answer, M2 item 4 so
this arm only fires for a direct caller that hands [box] an Option this arm only fires for a direct caller that hands [box] an Option
@ -2078,6 +2135,36 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
(match ctx.defers with (match ctx.defers with
| [] -> r | [] -> r
| ds -> mk loc Types.Never (Tast.Do (ds @ [ r ]))) | ds -> mk loc Types.Never (Tast.Do (ds @ [ r ])))
(* (set (at target i) x) against a dyn target — a dyn vec from (vec-new
dyn), or a typed container's own view (M2 item 3) is a call and not a
place: [flan_dyn_set_at] tag-checks [x]'s dyn tag against what the vec
or the view holds and traps on a mismatch, which is not a memory write
[Tast.Set] could express through a pointer. [target] is checked once,
here, and handed to [vec_at]/[indexed] unchecked in the [else] branch
below rather than re-checked by [check_place] checking it twice would
evaluate a target with a side effect twice. A dyn target indexed more
than once nothing in this milestone builds one still falls to the
ordinary [Ast.Set] arm below, and [indexed] refuses it by name. *)
| Ast.Set (Ast.Pindex (target, [ idx ]), v) ->
let target = check ctx target in
if target.Tast.ty = Types.Dyn then
let i = check ctx ~want:Types.Dyn idx in
let v = check ctx ~want:Types.Dyn v in
expect loc ~want
(rt loc Types.Unit "flan_dyn_set_at" [ target; i; v ])
else begin
let p, pty =
match target.Tast.ty with
| Types.Vec _ ->
let pp, ty = vec_at ctx loc target [ idx ] in
Tast.Pderef pp, ty
| _ ->
let iidx, ty = indexed ctx target [ idx ] in
Tast.Pindex (target, iidx), ty
in
let v = check ctx ~want:pty v in
expect loc ~want (mk loc Types.Unit (Tast.Set (p, v)))
end
| Ast.Set (p, v) -> | Ast.Set (p, v) ->
let p, pty = check_place ctx loc p in let p, pty = check_place ctx loc p in
let v = check ctx ~want:pty v in let v = check ctx ~want:pty v in

View File

@ -2640,6 +2640,15 @@ 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
address for the same reason a Vec or a Map does here the
view reads through it live, and passing the value would hand
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 ]
| t -> [ ll t ^ " " ^ value f a ]) | t -> [ ll t ^ " " ^ value f a ])
args) args)
in in
@ -3387,6 +3396,8 @@ declare i32 @flan_dyn_need_bool(i64)
; builtin. ; builtin.
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 i64 @flan_dyn_view_vec(ptr, i32)
declare i64 @flan_dyn_view_flat(ptr, i64, 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 void @flan_dyn_root_pop(i64) declare void @flan_dyn_root_pop(i64)

View File

@ -1461,6 +1461,13 @@ let classify_c (l : loc) (t : Types.t) =
| Types.String | Types.Slice _ -> [ Aint (l, Types.Ptr Types.Unit); Alen l ] | Types.String | Types.Slice _ -> [ Aint (l, Types.Ptr 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
the same reason: the view reads through it live and a copy would leave
the caller's own array unseen by later writes through the view. Every
[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 ]
| _ 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)
| _ when is_float t -> [ Aflt (l, t) ] | _ when is_float t -> [ Aflt (l, t) ]
@ -2708,7 +2715,7 @@ and call_native f ~sym ?(chan = false) ~(args : Tast.expr list) ~rty dst =
List.map List.map
(fun (a : Tast.expr) -> (fun (a : Tast.expr) ->
(match a.Tast.ty with (match a.Tast.ty with
| Types.Vec _ | Types.Map _ -> lvalue f a | Types.Vec _ | Types.Map _ | Types.Array _ -> lvalue f a
| _ -> eval f a), a.Tast.ty) | _ -> eval f a), a.Tast.ty)
args args
in in

View File

@ -55,6 +55,16 @@ void flan_write_stdout(const uint8_t *p, int64_t n);
* only one of. So flan_rt.c exports a thin wrapper and this calls it. */ * only one of. So flan_rt.c exports a thin wrapper and this calls it. */
_Noreturn void flan_trap(const uint8_t *name, int64_t namelen); _Noreturn void flan_trap(const uint8_t *name, int64_t namelen);
/* Growing a Vec through a dyn view borrows flan_rt.c's own growth: doubling,
* allocator adoption and the epoch check all live in [flan_vec_push], and
* re-implementing any of that here would be a second copy of logic the
* duplicity doctrine (docs/SPIKE-DUPLICITY.md) says belongs on one side only.
* [v] is declared [void *] rather than [flan_vec *] so this file need not
* name flan_rt.c's type; the two structs' layouts must agree, which is
* [flan_dyn_vec_hdr] below, restated for the same reason [flan_desc] is. */
int8_t flan_vec_push(void *v, const void *elem, int64_t size, int64_t align,
const uint8_t *loc, int64_t loclen);
/* ── The representation ──────────────────────────────────────────────── /* ── The representation ────────────────────────────────────────────────
* *
* NaN-boxed, in a word. A double is *itself*: the 2^64 minus a NaN's worth of * NaN-boxed, in a word. A double is *itself*: the 2^64 minus a NaN's worth of
@ -126,6 +136,11 @@ typedef struct flan_desc {
* already opens with, never a memcmp. */ * already opens with, never a memcmp. */
#define BOX_KW 4u #define BOX_KW 4u
/* Restated from flan_dyn.h — a view's element kind. */
#define FLAN_VIEW_I64 0
#define FLAN_VIEW_F64 1
#define FLAN_VIEW_BOOL 2
/* Spelled as a negated positive rather than as a shift of -1: shifting a /* Spelled as a negated positive rather than as a shift of -1: shifting a
* negative value left is undefined, and this file is swept by UBSan. */ * negative value left is undefined, and this file is swept by UBSan. */
#define DYN_INT_MAX (((int64_t)1 << 47) - 1) #define DYN_INT_MAX (((int64_t)1 << 47) - 1)
@ -164,6 +179,32 @@ static inline flan_dyn dyn_make(unsigned tag, uint64_t payload) {
#define OBJ_VEC 1 #define OBJ_VEC 1
#define OBJ_INT 2 /* an i64 too wide for the payload */ #define OBJ_INT 2 /* an i64 too wide for the payload */
#define OBJ_MAP 3 /* keys and values interleaved: k0 v0 k1 v1 ... */ #define OBJ_MAP 3 /* keys and values interleaved: k0 v0 k1 v1 ... */
#define OBJ_VIEW 4 /* a typed container crossing into dyn as a view */
/* flan_vec, restated. This file must not name flan_rt.c's [flan_vec] — see
* the "if either table changes, change both" note above [flan_vec_push]
* so a view over a [(Vec T)] is built from an address whose first five words
* this mirrors exactly. Only [ptr], [len] and [epoch]/[alloc] are ever read
* through it; nothing here writes one. */
typedef struct flan_dyn_vec_hdr {
void *ptr;
int64_t len;
int64_t cap;
void *alloc;
int64_t epoch;
} flan_dyn_vec_hdr;
/* flan_allocator's prefix, far enough to read the one word a stale-container
* check needs. The struct has more fields after [epoch]; this file never
* touches them; and the alignment of a leading same-typed prefix is the same
* in any translation unit that agrees on the field order, which is the
* "change both" this comment is the other half of. */
typedef struct flan_dyn_alloc_hdr {
void *proc;
void *data;
uint32_t caps;
uint64_t epoch;
} flan_dyn_alloc_hdr;
typedef struct flan_obj { typedef struct flan_obj {
struct flan_obj *next; /* every object ever allocated, newest first */ struct flan_obj *next; /* every object ever allocated, newest first */
@ -179,6 +220,15 @@ 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
allocate and does not own. [is_vec] set means [base] is a
[flan_dyn_vec_hdr *] and [len] here is unused the live length is
read from the header on every operation, which is the whole of why a
Vec growing through the view cannot go stale. [is_vec] clear means
[base] is the first element's address and [len] is the snapshot taken
at the crossing, for a slice or a fixed array, neither of which moves.
[elem] is one of FLAN_VIEW_I64/F64/BOOL. */
struct { void *base; int64_t len; int32_t elem; int32_t is_vec; } view;
/* OBJ_TEXT's bytes trail the header; see [obj_text_bytes]. */ /* OBJ_TEXT's bytes trail the header; see [obj_text_bytes]. */
} u; } u;
} flan_obj; } flan_obj;
@ -309,7 +359,11 @@ int32_t flan_dyn_tag(flan_dyn v) {
if (o == NULL) return FLAN_DYN_TAG_NIL; if (o == NULL) return FLAN_DYN_TAG_NIL;
switch (o->kind) { switch (o->kind) {
case OBJ_TEXT: return FLAN_DYN_TAG_TEXT; case OBJ_TEXT: return FLAN_DYN_TAG_TEXT;
/* A view answers the same tag a heap vec does: from a dyn program's
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. */
case OBJ_VEC: return FLAN_DYN_TAG_VEC; case OBJ_VEC: return FLAN_DYN_TAG_VEC;
case OBJ_VIEW: return 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;
} }
@ -403,6 +457,13 @@ static void emit_escaped(const uint8_t *p, int64_t n) {
static int64_t dyn_int_value(flan_dyn v); /* forward: both int shapes */ static int64_t dyn_int_value(flan_dyn v); /* forward: both int shapes */
static double dyn_num_value(flan_dyn v); static double dyn_num_value(flan_dyn v);
/* forward: the view helpers, needed by [render] and [say_render] above where
* they are defined, alongside the container operations below */
static int64_t view_len(const char *op, flan_dyn v, flan_obj *o);
static void *view_base(flan_obj *o);
static flan_dyn view_box(int32_t elem, const uint8_t *p);
static int64_t view_elem_size(int32_t elem);
static void render(flan_dyn v, int depth, int nested) { static void render(flan_dyn v, int depth, int nested) {
char buf[64]; char buf[64];
int32_t t = flan_dyn_tag(v); int32_t t = flan_dyn_tag(v);
@ -461,10 +522,16 @@ static void render(flan_dyn v, int depth, int nested) {
} }
default: { default: {
flan_obj *o = dyn_obj(v); flan_obj *o = dyn_obj(v);
int64_t i; int64_t i, n = o->kind == OBJ_VIEW ? view_len("print", v, o) : o->len;
emit("["); emit("[");
for (i = 0; i < o->len; i++) { for (i = 0; i < n; i++) {
emit(" "); emit(" ");
if (o->kind == OBJ_VIEW)
render(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(o->u.v.items[i], depth + 1, 1); render(o->u.v.items[i], depth + 1, 1);
} }
emit("]"); emit("]");
@ -551,14 +618,21 @@ 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; int64_t i, n = o->kind == OBJ_VIEW ? view_len("print", v, o) : o->len;
if (depth >= 2) { say_puts(s, "[...]"); return; } if (depth >= 2) { say_puts(s, "[...]"); return; }
say_puts(s, "["); say_puts(s, "[");
for (i = 0; i < o->len && s->n < s->cap - 8; i++) { for (i = 0; i < n && s->n < s->cap - 8; i++) {
say_puts(s, " "); say_puts(s, " ");
if (o->kind == OBJ_VIEW)
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_render(s, o->u.v.items[i], depth + 1);
} }
say_puts(s, i < o->len ? " ...]" : "]"); say_puts(s, i < n ? " ...]" : "]");
return; return;
} }
} }
@ -1255,9 +1329,128 @@ static inline int is_map(flan_dyn v) {
return flan_dyn_tag(v) == FLAN_DYN_TAG_MAP; return flan_dyn_tag(v) == FLAN_DYN_TAG_MAP;
} }
/* ── Typed containers as views ─────────────────────────────────────────
*
* Every entry point below already dispatches on [flan_dyn_tag], which does
* not distinguish a view from a heap vec see [flan_dyn_tag]'s switch so
* [flan_dyn_len], [flan_dyn_at], [flan_dyn_set_at], [flan_dyn_push] and the
* printer each add one branch for [OBJ_VIEW] beside the existing [OBJ_VEC]
* one. What follows is that branch's machinery. */
static int64_t view_elem_size(int32_t elem) {
return elem == FLAN_VIEW_BOOL ? 1 : 8;
}
/* The stale-container check flan_rt.c's [flan_vec_check] runs for a typed
* Vec, restated for a view's own trap rather than reused: the duplicity
* 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]
* 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. */
static void view_vec_check(const char *op, flan_dyn v, flan_dyn_vec_hdr *h) {
if (h->alloc) {
flan_dyn_alloc_hdr *a = (flan_dyn_alloc_hdr *)h->alloc;
if ((int64_t)a->epoch != h->epoch) {
char sv[SAY_MAX];
say(sv, SAY_MAX, v);
fflush(stdout);
fprintf(stderr,
"dyn %s: this view's container's allocator was released — the "
"Vec was made at epoch %lld and the allocator is at %lld now "
"— %s\n",
op, (long long)h->epoch, (long long)(int64_t)a->epoch, sv);
flan_trap((const uint8_t *)"DynRange", 8);
}
}
}
/* [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
* the snapshot for a flat one. */
static int64_t view_len(const char *op, flan_dyn v, flan_obj *o) {
if (o->u.view.is_vec) {
flan_dyn_vec_hdr *h = (flan_dyn_vec_hdr *)o->u.view.base;
view_vec_check(op, v, h);
return h->len;
}
return o->u.view.len;
}
static void *view_base(flan_obj *o) {
if (o->u.view.is_vec) return ((flan_dyn_vec_hdr *)o->u.view.base)->ptr;
return o->u.view.base;
}
/* Reads box the element on the way out — the runtime already knows how to
* box an i64, an f64 or a bool, so this is that, from raw bytes rather than
* from a C value already in hand. */
static flan_dyn view_box(int32_t elem, const uint8_t *p) {
switch (elem) {
case FLAN_VIEW_I64: { int64_t x; memcpy(&x, p, 8); return flan_dyn_from_i64(x); }
case FLAN_VIEW_F64: { double x; memcpy(&x, p, 8); return flan_dyn_from_f64(x); }
default: { uint8_t b = *p; return flan_dyn_from_bool(b); }
}
}
/* Writes tag-check on the way in: the dyn value's tag must be the one this
* view's element type wants, or this traps by name and never coerces or
* truncates a mismatched value into the slot. [v] is the view, for the
* sentence's container half; [x] is the value that was refused. */
static void view_unbox(const char *op, flan_dyn v, int32_t elem, flan_dyn x,
uint8_t *p) {
switch (elem) {
case FLAN_VIEW_I64: {
int64_t n;
if (flan_dyn_tag(x) != FLAN_DYN_TAG_INT)
trap2(TYPE_TRAP, op, "this view's elements are int", v, x);
n = dyn_int_value(x);
memcpy(p, &n, 8);
return;
}
case FLAN_VIEW_F64: {
double d;
if (flan_dyn_tag(x) != FLAN_DYN_TAG_FLOAT)
trap2(TYPE_TRAP, op, "this view's elements are float", v, x);
d = dyn_num_value(x);
memcpy(p, &d, 8);
return;
}
default: {
uint8_t b;
if (flan_dyn_tag(x) != FLAN_DYN_TAG_BOOL)
trap2(TYPE_TRAP, op, "this view's elements are bool", v, x);
b = dyn_payload(x) ? 1 : 0;
*p = b;
return;
}
}
}
flan_dyn flan_dyn_view_vec(void *hdr, int32_t elem) {
flan_obj *o = gc_alloc(OBJ_VIEW, 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_obj *o = gc_alloc(OBJ_VIEW, 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) {
if (is_text(v) || is_vec(v) || is_map(v)) if (is_text(v) || is_map(v)) return flan_dyn_from_i64(dyn_obj(v)->len);
return flan_dyn_from_i64(dyn_obj(v)->len); if (is_vec(v)) {
flan_obj *o = dyn_obj(v);
if (o->kind == OBJ_VIEW) return flan_dyn_from_i64(view_len("len", v, o));
return flan_dyn_from_i64(o->len);
}
trap1(TYPE_TRAP, "len", "only a text, a vec or a map has one", v); trap1(TYPE_TRAP, "len", "only a text, a vec or a map has one", v);
} }
@ -1280,6 +1473,12 @@ flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i) {
trap2(TYPE_TRAP, "at", "only a text or a vec is indexed", v, i); trap2(TYPE_TRAP, "at", "only a text or a vec is indexed", v, i);
k = need_index("at", v, i); k = need_index("at", v, i);
o = dyn_obj(v); o = dyn_obj(v);
if (o->kind == OBJ_VIEW) {
int64_t len = view_len("at", v, o);
if (k < 0 || k >= len) trap_range("at", v, k, len);
return view_box(o->u.view.elem,
(const uint8_t *)view_base(o) + k * view_elem_size(o->u.view.elem));
}
if (k < 0 || k >= o->len) trap_range("at", v, k, o->len); if (k < 0 || k >= o->len) trap_range("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]);
return o->u.v.items[k]; return o->u.v.items[k];
@ -1288,13 +1487,20 @@ flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i) {
void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x) { void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x) {
int64_t k; int64_t k;
flan_obj *o; flan_obj *o;
(void)x;
if (is_text(v)) if (is_text(v))
trap2(TYPE_TRAP, "set-at", "a text is immutable — build another one", v, i); trap2(TYPE_TRAP, "set-at", "a text is immutable — build another one", v, i);
if (!is_vec(v)) if (!is_vec(v))
trap2(TYPE_TRAP, "set-at", "only a vec is assigned into", v, i); trap2(TYPE_TRAP, "set-at", "only a vec is assigned into", v, i);
k = need_index("set-at", v, i); k = need_index("set-at", v, i);
o = dyn_obj(v); o = dyn_obj(v);
if (o->kind == OBJ_VIEW) {
int64_t len = view_len("set-at", v, o);
uint8_t *p;
if (k < 0 || k >= len) trap_range("set-at", v, k, len);
p = (uint8_t *)view_base(o) + k * view_elem_size(o->u.view.elem);
view_unbox("set-at", v, o->u.view.elem, x, p);
return;
}
if (k < 0 || k >= o->len) trap_range("set-at", v, k, o->len); if (k < 0 || k >= o->len) trap_range("set-at", v, k, o->len);
o->u.v.items[k] = x; o->u.v.items[k] = x;
} }
@ -1307,6 +1513,20 @@ void flan_dyn_push(flan_dyn v, flan_dyn x) {
trap2(TYPE_TRAP, "push", "only a vec is pushed to", v, x); trap2(TYPE_TRAP, "push", "only a vec is pushed to", v, x);
} }
o = dyn_obj(v); o = dyn_obj(v);
if (o->kind == OBJ_VIEW) {
uint8_t buf[8];
static const uint8_t push_loc[] = "(dyn push)";
int64_t size;
if (!o->u.view.is_vec)
trap2(TYPE_TRAP, "push",
"this view is a slice or an array and cannot grow", v, x);
size = view_elem_size(o->u.view.elem);
view_unbox("push", v, o->u.view.elem, x, buf);
if (!flan_vec_push(o->u.view.base, buf, size, size, push_loc,
(int64_t)sizeof(push_loc) - 1))
trap_oom(size);
return;
}
if (o->len == o->u.v.cap) { if (o->len == o->u.v.cap) {
int64_t cap = o->u.v.cap ? o->u.v.cap * 2 : 8; int64_t cap = o->u.v.cap ? o->u.v.cap * 2 : 8;
flan_dyn *items = flan_dyn *items =

View File

@ -145,6 +145,60 @@ uint8_t flan_dyn_need_bool(flan_dyn v);
int32_t flan_dyn_is_nil(flan_dyn v); int32_t flan_dyn_is_nil(flan_dyn v);
flan_dyn flan_dyn_need_not_nil(flan_dyn v); flan_dyn flan_dyn_need_not_nil(flan_dyn v);
/* ── Typed containers as views — M2 item 3 ─────────────────────────────
*
* A [(Vec T)], a [T] slice, or a fixed [n T] array crossing into dyn is a
* VIEW, not a copy: the box holds a small heap record naming where the
* elements live and what one of them is, and every read or write goes
* straight through to the container's own storage. [flan_dyn_at] boxes an
* element on the way out; [flan_dyn_set_at] tag-checks the dyn value it is
* given against the element type on the way in and traps, by [flan_trap],
* on a mismatch never a silent coercion.
*
* T is restricted to i64, f64 and bool exactly the set [flan_dyn_need_i64]
* and friends already treat as crossing the typed boundary both ways. That
* is not an arbitrary cut: the excluded case that matters is a string
* 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
* and move (a push may reallocate), a slice and a fixed array cannot.
*
* [flan_dyn_view_vec] takes the address of the Vec's own header the
* struct [flan_vec] in flan_rt.c, restated in flan_dyn.c under the same
* "if either table changes, change both" rule this whole boundary already
* lives under. That address is the Vec's home: a local's slot, a global, a
* field, fixed for as long as the Vec exists. 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 crossing sound for a slice and for a fixed array because neither
* 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_F64 1
#define FLAN_VIEW_BOOL 2
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);
/* ── The collector ───────────────────────────────────────────────────── /* ── The collector ─────────────────────────────────────────────────────
* *
* Mark-sweep, precise, and never moving. [flan_gc_init] is idempotent, and the * Mark-sweep, precise, and never moving. [flan_gc_init] is idempotent, and the

View File

@ -32,6 +32,8 @@
#include "flan_dyn.h" #include "flan_dyn.h"
void flan_rt_init(int32_t argc, char **argv); void flan_rt_init(int32_t argc, char **argv);
void flan_vec_free(void *v, int64_t size, int64_t align, const uint8_t *loc,
int64_t loclen);
static int failures; static int failures;
@ -347,6 +349,123 @@ static void ops(void) {
(void)s; (void)s;
} }
/* A typed container's own header, restated a third time — flan_rt.c's
* [flan_vec], flan_dyn.c's [flan_dyn_vec_hdr], and this. The three must
* agree on layout, and this file is exactly the mechanism that makes a
* disagreement a compile or link error rather than a silent corruption:
* there is no [flan_vec_init] to call from here (flan_rt.c is not linked
* against this main), so the header is built by hand, the same five words
* [flan_vec_grow] would leave behind after a few pushes. */
typedef struct {
void *ptr;
int64_t len;
int64_t cap;
void *alloc;
int64_t epoch;
} hand_vec;
/* The runtime's half of M2 item 3: a typed container crossing into dyn as a
* view, driven directly with no compiler in the loop [flan_dyn_view_vec]
* and [flan_dyn_view_flat] built by hand over a [hand_vec] and a plain
* array, exactly as the checker's [box] will build them over a real [(Vec
* i64)] and a real [[4]i64]. */
static void view(void) {
int64_t buf[4] = { 10, 20, 30, 40 };
flan_dyn flat = flan_dyn_nil(), vv = flan_dyn_nil();
flan_dyn_root_push(&flat);
flan_dyn_root_push(&vv);
/* A flat view over a fixed array: reads box, writes tag-check, and the
storage really is the array's own a write through the view is read
back through the C array with no call into this file at all. */
flat = flan_dyn_view_flat(buf, 4, FLAN_VIEW_I64);
check(flan_dyn_tag(flat) == FLAN_DYN_TAG_VEC, "a view tags as a vec");
check(num(flan_dyn_len(flat)) == 4, "flat view len");
check(num(flan_dyn_at(flat, flan_dyn_from_i64(2))) == 30, "flat view at");
flan_dyn_set_at(flat, flan_dyn_from_i64(2), flan_dyn_from_i64(99));
check(buf[2] == 99, "flat view write reaches the array");
buf[3] = 7;
check(num(flan_dyn_at(flat, flan_dyn_from_i64(3))) == 7,
"the array's own write reaches the view — it is not a copy");
prints(flat, "[ 10 20 99 7]");
/* A vec view: points at the header's own address, so a push that grows
and moves it is seen on the very next read there is no snapshot to
go stale. */
{
hand_vec hv;
hv.ptr = NULL; hv.len = 0; hv.cap = 0; hv.alloc = NULL; hv.epoch = 0;
vv = flan_dyn_view_vec(&hv, FLAN_VIEW_I64);
check(num(flan_dyn_len(vv)) == 0, "vec view starts empty");
{
int i;
for (i = 0; i < 20; i++) flan_dyn_push(vv, flan_dyn_from_i64(i));
}
check(num(flan_dyn_len(vv)) == 20, "vec view len after growth");
check(num(flan_dyn_at(vv, flan_dyn_from_i64(0))) == 0,
"first element survived the growth and the move");
check(num(flan_dyn_at(vv, flan_dyn_from_i64(19))) == 19,
"pushed element reachable after the header's ptr moved");
/* [hv]'s own fields moved under the view's feet, by construction — the
view never captured [hv.ptr]; it captured [&hv]. */
check(hv.len == 20 && hv.cap >= 20, "the hand-built header itself grew");
flan_dyn_set_at(vv, flan_dyn_from_i64(0), flan_dyn_from_i64(-1));
check(((int64_t *)hv.ptr)[0] == -1, "write through the view reaches hv");
flan_vec_free(&hv, 8, 8, (const uint8_t *)"view", 4);
}
/* A bool view and a float view, so the element-tag dispatch is exercised
on all three kinds and not only i64. */
{
uint8_t bools[2] = { 1, 0 };
double floats[2] = { 1.5, -2.0 };
flan_dyn bv = flan_dyn_view_flat(bools, 2, FLAN_VIEW_BOOL);
flan_dyn fv = flan_dyn_view_flat(floats, 2, FLAN_VIEW_F64);
check(truth(flan_dyn_at(bv, flan_dyn_from_i64(0))), "bool view at true");
check(!truth(flan_dyn_at(bv, flan_dyn_from_i64(1))), "bool view at false");
flan_dyn_set_at(bv, flan_dyn_from_i64(1), flan_dyn_from_bool(1));
check(bools[1] == 1, "bool view write");
check(flan_dyn_need_f64(flan_dyn_at(fv, flan_dyn_from_i64(0))) == 1.5,
"float view at");
flan_dyn_set_at(fv, flan_dyn_from_i64(0), flan_dyn_from_f64(3.25));
check(floats[0] == 3.25, "float view write");
}
flan_dyn_root_pop(2);
printf(failures == 0 ? "view ok\n" : "view failed\n");
}
/* Every wrong way to use a view: out of range, a mismatched write on each of
* the three element kinds, and a push against a fixed-size (flat) view. Each
* is its own mode because each ends the process. */
static void refuse_view(const char *what) {
static int64_t buf[2] = { 1, 2 };
flan_dyn v;
if (strcmp(what, "range") == 0) {
v = flan_dyn_view_flat(buf, 2, FLAN_VIEW_I64);
(void)flan_dyn_at(v, flan_dyn_from_i64(2));
} else if (strcmp(what, "wrongwrite") == 0) {
v = flan_dyn_view_flat(buf, 2, FLAN_VIEW_I64);
flan_dyn_set_at(v, flan_dyn_from_i64(0), text("nope"));
} else if (strcmp(what, "wrongbool") == 0) {
static uint8_t bb[1];
v = flan_dyn_view_flat(bb, 1, FLAN_VIEW_BOOL);
flan_dyn_set_at(v, flan_dyn_from_i64(0), flan_dyn_from_i64(1));
} else if (strcmp(what, "wrongfloat") == 0) {
static double ff[1];
v = flan_dyn_view_flat(ff, 1, FLAN_VIEW_F64);
flan_dyn_set_at(v, flan_dyn_from_i64(0), flan_dyn_from_i64(1));
} else if (strcmp(what, "flatpush") == 0) {
v = flan_dyn_view_flat(buf, 2, FLAN_VIEW_I64);
flan_dyn_push(v, flan_dyn_from_i64(9));
} else {
printf("no such refusal: %s\n", what);
exit(2);
}
printf("did not trap\n");
exit(3);
}
/* ── The collector ─────────────────────────────────────────────────────*/ /* ── The collector ─────────────────────────────────────────────────────*/
/* Allocate a great many, hold a few, and assert the heap does not grow. The /* Allocate a great many, hold a few, and assert the heap does not grow. The
@ -734,7 +853,15 @@ int main(int argc, char **argv) {
if (strcmp(argv[1], "sharing") == 0) { sharing(); return 0; } if (strcmp(argv[1], "sharing") == 0) { sharing(); return 0; }
if (strcmp(argv[1], "unrooted") == 0) { unrooted(); return 0; } if (strcmp(argv[1], "unrooted") == 0) { unrooted(); return 0; }
if (strcmp(argv[1], "desc") == 0) { desc(); return 0; } if (strcmp(argv[1], "desc") == 0) { desc(); return 0; }
if (strcmp(argv[1], "view") == 0) {
view();
return failures == 0 ? 0 : 1;
}
if (strncmp(argv[1], "refuse:", 7) == 0) { refuse(argv[1] + 7); return 0; } if (strncmp(argv[1], "refuse:", 7) == 0) { refuse(argv[1] + 7); return 0; }
if (strncmp(argv[1], "refuseview:", 11) == 0) {
refuse_view(argv[1] + 11);
return 0;
}
printf("no such mode: %s\n", argv[1]); printf("no such mode: %s\n", argv[1]);
return 2; return 2;
} }

111
test/programs/dyn-view.flan Normal file
View File

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

View File

@ -3699,6 +3699,57 @@ level "1"
some_nil ~opt:"-O0" (); some_nil ~opt:"-O0" ();
some_nil ~x86:true (); some_nil ~x86:true ();
(* ── Typed containers into dyn as views, M2 item 3 ────────────────
programs/dyn-view.flan takes its mode from argv, the way bounds.flan
does, because a survey and a trap cannot share a process: mode 0 is
the survey proper (a Vec view, a fixed-array view, a slice view and a
bool Vec's view, each written through one side and read through the
other, plus a push through the Vec view that grows and moves it), and
modes 1 and 2 are the two ways a view refuses out of range, and a
write whose dyn tag does not match the element type. The expected
text for mode 0 was captured from the running program. *)
let dyn_view_out =
"[ 10 20 30]\n999\n777\n4\n40\n\
[ 1 2 3 4]\n100\n400\n\
[ 1.5 2.5 3.5]\n9.5\n\
[ true false]\ntrue\n"
in
let dyn_view ?opt ?x86 () =
let exe = compile ?opt ?x86 "programs/dyn-view.flan" in
let name suffix =
"dyn: a typed container's view" ^ suffix
^ (match opt with Some o -> ", " ^ o | None -> "")
^ (match x86 with Some true -> ", --x86" | _ -> "")
in
let code, text = run exe (Some "0") in
if code <> 0 || text <> dyn_view_out then begin
incr failures;
Printf.printf
"FAIL %s\n got: %S (exit %d)\n wanted: %S (exit 0)\n"
(name "") text code dyn_view_out
end;
let code, text = run exe (Some "1") in
if code <> 134 || not (contains text "index 5 is out of bounds")
then begin
incr failures;
Printf.printf
"FAIL %s\n got: %S (exit %d)\n wanted a range trap \
(exit 134)\n" (name ", out of range") text code
end;
let code, text = run exe (Some "2") in
if code <> 134 || not (contains text "this view's elements are int")
then begin
incr failures;
Printf.printf
"FAIL %s\n got: %S (exit %d)\n wanted a tag-check \
trap (exit 134)\n" (name ", wrong-type write") text code
end;
(try Sys.remove exe with Sys_error _ -> ())
in
dyn_view ();
dyn_view ~opt:"-O0" ();
dyn_view ~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
@ -3772,7 +3823,7 @@ level "1"
[ "programs/dyn-basic.flan"; "programs/dyn-vec.flan"; [ "programs/dyn-basic.flan"; "programs/dyn-vec.flan";
"programs/dyn-struct.flan"; "programs/dyn-struct.flan";
"programs/dyn-global.flan"; "programs/dyn-boundary.flan"; "programs/dyn-global.flan"; "programs/dyn-boundary.flan";
"programs/dyn-defer.flan"; "programs/dyn-defer.flan"; "programs/dyn-view.flan";
(* Included for the same reason every dyn program is, though this (* Included for the same reason every dyn program is, though this
check cannot see the slot M2 item 4 actually added: [%dx]/[%ax] check cannot see the slot M2 item 4 actually added: [%dx]/[%ax]
are the pool-ran-dry fallback for a temporary [root_plan] COUNTED are the pool-ran-dry fallback for a temporary [root_plan] COUNTED

View File

@ -20,11 +20,18 @@
nested a chain of vecs sixty-four deep, traced through one root nested a chain of vecs sixty-four deep, traced through one root
sharing one object held three times written through one path and read sharing one object held three times written through one path and read
through another, and swept once when the last goes through another, and swept once when the last goes
view M2 item 3: a typed container's view, driven directly over a
hand-built flan_vec header and a plain C array the runtime
half of "typed containers into dyn as views", with no
compiler in the loop
refuse:* twenty-four refusals, one process each, asserted on the sentence refuse:* twenty-four refusals, one process each, asserted on the sentence
as well as on the status: a process that died some other way is as well as on the status: a process that died some other way is
not the guard firing, and the exit code cannot tell them apart not the guard firing, and the exit code cannot tell them apart
refuseview:* five more refusals, the view's own: out of range and a
mismatched write on each of the three element kinds, and a
push against a flat (slice or array) view
One binary, built once, run twenty-nine times. The build is the expensive One binary, built once, run thirty-five times. The build is the expensive
part and the runs are milliseconds, which is what keeps this inside part and the runs are milliseconds, which is what keeps this inside
`dune test` rather than behind an alias. *) `dune test` rather than behind an alias. *)
@ -133,6 +140,19 @@ let () =
fail "interior sharing\n got: %S (exit %d)\n wanted: %S" fail "interior sharing\n got: %S (exit %d)\n wanted: %S"
out code want_sh; out code want_sh;
(* M2 item 3, the runtime's half: a flat view over a fixed C array (reads
box, writes tag-check, and a write through the view is the array's own
write and vice versa proving it is a view and not a copy), and a
Vec view over a hand-built header, pushed through twenty times so the
header's own [ptr] moves under it the case that says the descriptor
pointing AT the header rather than snapshotting it is what survives a
growth. And a bool view and a float view, so the element dispatch is
exercised on all three kinds dyn_ops.c's [view] carries. *)
let code, out, err = run "view" in
if code <> 0 || out <> "view ok\n" then
fail "a typed container's view\n got: %S (exit %d, err %S)"
out code err;
(* Every refusal. The pair is (mode, a phrase the sentence must contain); (* Every refusal. The pair is (mode, a phrase the sentence must contain);
the phrase is chosen to be the part that says *which* mistake it was, the phrase is chosen to be the part that says *which* mistake it was,
so a message that named the wrong operation or the wrong tag would not so a message that named the wrong operation or the wrong tag would not
@ -176,11 +196,34 @@ let () =
fail "%s did not say %S; it said %S" mode phrase err) fail "%s did not say %S; it said %S" mode phrase err)
refusals; refusals;
(* The view's own refusals: an index outside it, a write whose dyn tag
does not match the element the view holds once per element kind, so
the tag-check is asserted on int, on float and on bool separately and
not only on the one this file happens to build first and a push
against a flat (slice or array) view, which cannot grow by
construction and says so rather than corrupting whatever follows it
in memory. *)
let view_refusals =
[ ("range", "index 2 is out of bounds for vec of length 2");
("wrongwrite", "this view's elements are int");
("wrongbool", "this view's elements are bool");
("wrongfloat", "this view's elements are float");
("flatpush", "this view is a slice or an array and cannot grow") ]
in
List.iter
(fun (mode, phrase) ->
let code, out, err = run ("refuseview:" ^ mode) in
if code = 0 then
fail "%s returned rather than trapping: %S" mode out
else if not (has err phrase) then
fail "%s did not say %S; it said %S" mode phrase err)
view_refusals;
(try Sys.remove exe with Sys_error _ -> ()); (try Sys.remove exe with Sys_error _ -> ());
(* A line on the way out, because a test that says nothing when it passes (* A line on the way out, because a test that says nothing when it passes
is a test nobody can tell from a test that did not run. *) is a test nobody can tell from a test that did not run. *)
if !failures = 0 then if !failures = 0 then
Printf.printf " ok the dyn runtime: %d refusals and six runs\n" Printf.printf " ok the dyn runtime: %d refusals and seven runs\n"
(List.length refusals) (List.length refusals + List.length view_refusals)
else exit 1 else exit 1
| _ -> print_endline "SKIP test_dyn: no clang" | _ -> print_endline "SKIP test_dyn: no clang"

View File

@ -874,14 +874,41 @@ let () =
rejects_check "a parameter named after a type" "(defn f [i64 x] ())" rejects_check "a parameter named after a type" "(defn f [i64 x] ())"
~needle:"cannot also be this parameter's name"; ~needle:"cannot also be this parameter's name";
(* The three "not yet" refusals, each by name and each for its own reason. (* M2 item 3 lifted the container-into-dyn refusal: a [(Vec T)], a slice or
a fixed array with an i64/f64/bool element now crosses as a VIEW rather
A typed container does not box: [(Vec i64)] has a representation the dyn than refusing. A [(Map K V)] still refuses it rides a different
runtime cannot walk, and the heterogeneous container at this milestone is representation this milestone does not give a view and so does any
the runtime's own from [(vec-new dyn)]. *) container whose element is outside the three the view can hold. *)
rejects_check "a typed container boxed into dyn" accepts "a typed Vec boxed into dyn is a view, not a refusal"
"(defn take [d dyn] i32 1)\n\ "(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [v (vec-new i64)] (take v)))" (defn main [] i32 (let [v (vec-new i64)] (take v)))";
accepts "a slice boxed into dyn is a view"
"(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [xs (array 3 i64)] (take (slice xs 0 3))))";
accepts "a fixed array boxed into dyn is a view"
"(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [a (array 4 i64)] (take a)))";
accepts "a bool Vec's view"
"(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [v (vec-new bool)] (take v)))";
accepts "an f64 Vec's view"
"(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [v (vec-new f64)] (take v)))";
(* The element restriction is still refused, and by name: a string element
would need a dyn string's own boxing, whose payload is a pointer into
the collector's heap, planted where nothing will ever trace it. *)
rejects_check "a Vec of strings does not view into dyn yet"
"(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [v (vec-new string)] (take v)))"
~needle:"does not cross into dyn yet";
rejects_check "an i32 element is not one of the view's three"
"(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [v (vec-new i32)] (take v)))"
~needle:"does not cross into dyn yet";
(* A typed (Map K V) is unrelated to item 3 and keeps its own refusal. *)
rejects_check "a typed Map still refuses into dyn"
"(defn take [d dyn] i32 1)\n\
(defn main [] i32 (let [m (map-new i64 i64)] (take m)))"
~needle:"does not cross into dyn yet"; ~needle:"does not cross into dyn yet";
(* 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

View File

@ -213,6 +213,17 @@ let corpus =
ASan's build being asked to trap the same way the plain build does. *) ASan's build being asked to trap the same way the plain build does. *)
"programs/nil-option.flan", []; "programs/nil-option.flan", [];
"programs/some-nil.flan", []; "programs/some-nil.flan", [];
(* M2 item 3: a typed container's view. Mode 0, the survey — the modes
that trap are exercised as C refusals in test_dyn.ml's [refuseview:*]
instead, the same split [bounds.flan]'s "0" argument makes above. A
view's storage is a plain array or a Vec's own malloc block, neither
one this collector allocates, so there is nothing here for ASan to
catch that the runtime tests above did not already exercise directly
this row is about the *compiler* lane: the address the checker
hands the runtime at the crossing, and whether a push through the
view that grows and moves the Vec leaves anything for ASan's
use-after-free detection to find. *)
"programs/dyn-view.flan", [ "0" ];
"../spike/x86/p13-dyn-collect.flan", []; "../spike/x86/p13-dyn-collect.flan", [];
"programs/sand-headless.flan", []; "programs/sand-headless.flan", [];
"programs/signedness.flan", []; "programs/signedness.flan", [];