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:
parent
221df5af1c
commit
29a9441f12
17
FIX.org
17
FIX.org
@ -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
|
||||||
|
|||||||
93
lib/check.ml
93
lib/check.ml
@ -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
|
||||||
|
|||||||
11
lib/emit.ml
11
lib/emit.ml
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 =
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
127
test/dyn_ops.c
127
test/dyn_ops.c
@ -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
111
test/programs/dyn-view.flan
Normal 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))))
|
||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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", [];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user