A dyn text becomes a str as its own bytes, a dyn view becomes its own storage, and a dyn vec or map becomes a checked copy in a [const T], a fixed array or a struct, while a writable [T] is never a copy.

This commit is contained in:
Joseph Ferano 2026-09-26 11:48:01 +07:00
parent 75fe42d310
commit 6d8ced2ba3
10 changed files with 767 additions and 6 deletions

View File

@ -28,6 +28,12 @@ class's float slot's rule), and a u64 above the largest i64 traps when read. A v
storage its own dyn global's initialiser built is refused. Rules out copying at the
crossing, a dyn big int for u64, and any check in a release build.
** DONE A dyn value crosses into a str, a slice, an array or a struct
CLOSED: [2026-09-26]
A writable [T] is never a copy: a plain dyn vec into one traps and names [const T], so a
write can never miss the vec. A str from a text is its own bytes, live at least until the
next free-temp. Rules out copy-in/copy-out at a call, and rooting the text in the crossing's frame.
** NEXT Dyn unless annotated
Decided 2026-09-26, replacing the plain rule: number, bool and char literals are typed,
their type inferred from their uses inside the function (never across functions); an

View File

@ -3639,8 +3639,11 @@ let no_dyn_yet loc ~into t extra =
dyn value cannot be read out of or written into without a meaning
nobody has decided. A str is read as a copy and never written, since a
dyn text is a collector pointer and typed storage is never scanned; a
[const] slice is refused because a dyn view can be written through. *)
let rec view_desc structs (t : Types.t) : (string, Types.t) result =
[const] slice is refused because a dyn view can be written through. One
is described only [~into] a written type ([into_typed]), as [c]. *)
let rec view_desc ?(into = false) structs (t : Types.t)
: (string, Types.t) result =
let view_desc = view_desc ~into in
let ( let* ) = Result.bind in
match t with
| Types.Int k ->
@ -3655,6 +3658,8 @@ let rec view_desc structs (t : Types.t) : (string, Types.t) result =
let* d = view_desc structs e in
Ok (Printf.sprintf "a%Ld;%s" n d)
| Types.Slice (Types.Mut, e) -> let* d = view_desc structs e in Ok ("s" ^ d)
| Types.Slice (Types.Const, e) when into ->
let* d = view_desc structs e in Ok ("c" ^ d)
| Types.Vec e -> let* d = view_desc structs e in Ok ("v" ^ d)
| Types.Named n ->
(match Hashtbl.find_opt structs n with
@ -4215,6 +4220,48 @@ let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr =
then "f64" else "i64"))
| _ -> no_dyn_yet loc ~into:false want ""
(* A dyn where a str, a slice, a fixed array or a struct was written:
[flan_dyn_need_as] with the written type's descriptor, answering through
a slot of that type. The runtime decides, since only it can see what the
dyn holds: a text is a str's own bytes, a view of the wanted elements is
their own storage, a vec or a map is a checked copy, and a [T] that can be
written through is never a copy — a write through one would not reach the
dyn vec, so the answer would differ with the vec typed or dyn. Its comment
in runtime/flan_dyn.c says where the copies live and how long a str from a
text lasts.
A fixed array or a struct that holds a Vec is refused: from a view it
would be a second copy of an owning header. *)
let into_typed ctx loc (want : Types.t) (got : Tast.expr) : Tast.expr =
let structs = ctx.env.structs in
let rec owns (t : Types.t) =
match t with
| Types.Vec _ -> true
| Types.Array (_, e) -> owns e
| Types.Named n ->
(match Hashtbl.find_opt structs n with
| Some st -> List.exists (fun (f : Tast.field) -> owns f.Tast.fty) st.Tast.fields
| None -> false)
| _ -> false
in
match view_desc ~into:true structs want with
| Error inner ->
no_dyn_yet loc ~into:false want
(if Types.equal inner want then ""
else Printf.sprintf " — a dyn value has no %s to become" (tyname loc inner))
| Ok _ when owns want ->
no_dyn_yet loc ~into:false want " — it holds a Vec, which owns its storage"
| Ok d ->
let ds = fresh_slot ctx Types.Dyn and out = fresh_slot ctx want in
let local s ty = mk loc ty (Tast.Local s) in
mk loc want
(Tast.Let
([ (ds, got); (out, mk loc want (Tast.Zero want)) ],
[ rt loc Types.Unit "flan_dyn_need_as"
[ local ds Types.Dyn; mk loc Types.String (Tast.Str d);
addr_of loc (local out want); here loc ];
local out want ]))
(* ── A numeric cast written on a dyn ─────────────────────────────────
*
TODO.org, "A numeric cast opens a dyn box".
@ -4581,6 +4628,14 @@ let expect ctx loc ~want (got : Tast.expr) =
or to dyn itself; wrap the type in Option, or keep the value dyn"
(tyname loc w)
| _, Types.Dyn when Types.fits ~expected:w ~actual:Types.Dyn -> got
| (Types.String | Types.Slice _ | Types.Array _), Types.Dyn ->
let opened = into_typed ctx loc w got in
Opened.replace opened_by_want opened got;
opened
| Types.Named n, Types.Dyn when Hashtbl.mem ctx.env.structs n ->
let opened = into_typed ctx loc w got in
Opened.replace opened_by_want opened got;
opened
| _, Types.Dyn ->
let opened = unbox loc w got in
Opened.replace opened_by_want opened got;

View File

@ -5079,6 +5079,7 @@ declare i64 @flan_dyn_need_not_nil(i64)
declare i32 @flan_dyn_truthy(i64)
declare i64 @flan_dyn_view_slice(ptr, i64, ptr, i64, i32)
declare i64 @flan_dyn_view_at(ptr, i64, ptr, i64, i32, i32)
declare void @flan_dyn_need_as(i64, ptr, i64, ptr, ptr, i64)
declare void @flan_dyn_root_push(ptr)
declare void @flan_dyn_root_push_desc(ptr, ptr)
declare ptr @flan_dyn_env_new(i64, ptr)

View File

@ -1525,9 +1525,71 @@ static void mark_desc(char *base, const flan_desc *d) {
}
}
/* ── Texts a str was taken from ────────────────────────────────────────
*
* A dyn text crossing into a str ([flan_dyn_need_as]) is not copied: the str
* is the text's own bytes, which never move, since this collector never
* moves anything. What could happen is a free — a str in a typed local,
* field or Vec is not a root, and typed storage is never scanned — so the
* crossing pins the text here, keyed by the temp arena's stamp, and every
* collection marks the pins whose stamp is still live. The str then lasts
* as long as the text is reachable from dyn or until the next free-temp,
* whichever is later: the lifetime i64->bytes's text already has, and text
* kept longer is cloned, as there.
*
* Rooting the text in the crossing's frame would be shorter than that and
* wrong for a str returned or stored, and a pin for good would keep every
* text a frame loop ever crossed. A copy into the temp arena would be sound
* too; this is the same lifetime without the copy. */
const void *flan_temp_stamp(uint64_t *inc, uint64_t *epoch);
int32_t flan_temp_stamp_live(const void *a, uint64_t inc, uint64_t epoch);
typedef struct text_pin {
flan_obj *o;
const void *a;
uint64_t inc, epoch;
} text_pin;
static text_pin *pins;
static int64_t pins_n, pins_cap;
static void pins_prune(void) {
int64_t i, k = 0;
for (i = 0; i < pins_n; i++)
if (flan_temp_stamp_live(pins[i].a, pins[i].inc, pins[i].epoch))
pins[k++] = pins[i];
pins_n = k;
}
static void pin_text(flan_obj *o) {
uint64_t inc, epoch;
const void *a = flan_temp_stamp(&inc, &epoch);
text_pin *last = pins_n > 0 ? &pins[pins_n - 1] : NULL;
if (last != NULL && last->o == o && last->a == a && last->inc == inc
&& last->epoch == epoch)
return;
if (pins_n == pins_cap) {
pins_prune();
if (pins_n == pins_cap) {
int64_t cap = pins_cap ? pins_cap * 2 : 64;
text_pin *p = (text_pin *)realloc(pins, (size_t)cap * sizeof *p);
if (p == NULL) trap_oom(NULL, 0, cap * (int64_t)sizeof *p);
pins = p;
pins_cap = cap;
}
}
pins[pins_n].o = o;
pins[pins_n].a = a;
pins[pins_n].inc = inc;
pins[pins_n].epoch = epoch;
pins_n++;
}
static void gc_mark_all(void) {
int64_t i;
unsigned k;
pins_prune();
for (i = 0; i < pins_n; i++) mark_push(pins[i].o);
for (i = 0; i < roots_n; i++) {
const flan_desc *d = roots[i].desc;
if (d == NULL) mark_value(*(flan_dyn *)roots[i].base);
@ -3106,6 +3168,9 @@ static inline int is_map(flan_dyn v) {
* t str (read as a copy; never written from here)
* a<n>;T a fixed [n T]
* sT a slice [T]
* cT a [const T]: only in what a crossing *into* a
* written type wants ([flan_dyn_need_as]); no view
* is ever of one
* vT a (Vec T)
* {Name;f1;T1f2;T2} a struct, its fields in declaration order
*
@ -3136,7 +3201,7 @@ static const uint8_t *desc_name_end(const uint8_t *d) {
static const uint8_t *desc_skip(const uint8_t *d) {
switch (*d) {
case 'a': d++; desc_int(&d); return desc_skip(d);
case 's': case 'v': return desc_skip(d + 1);
case 's': case 'c': case 'v': return desc_skip(d + 1);
case '{':
d = desc_name_end(d + 1);
while (*d != '}') d = desc_skip(desc_name_end(d));
@ -3152,7 +3217,7 @@ static void desc_lay(const uint8_t *d, int64_t *size, int64_t *align) {
case 'b': case 'B': case '?': *size = 1; *align = 1; return;
case 'h': case 'H': *size = 2; *align = 2; return;
case 'i': case 'I': case 'f': *size = 4; *align = 4; return;
case 't': case 's': *size = 16; *align = 8; return;
case 't': case 's': case 'c': *size = 16; *align = 8; return;
case 'v': *size = 40; *align = 8; return;
case 'a': {
int64_t n, s, a;
@ -3271,6 +3336,10 @@ static void desc_spell(const uint8_t *d, char *buf, size_t cap) {
desc_spell(d + 1, inner, sizeof inner);
snprintf(buf, cap, "[%s]", inner);
return;
case 'c':
desc_spell(d + 1, inner, sizeof inner);
snprintf(buf, cap, "[const %s]", inner);
return;
case 'v':
desc_spell(d + 1, inner, sizeof inner);
snprintf(buf, cap, "(Vec %s)", inner);
@ -3916,6 +3985,379 @@ flan_dyn flan_dyn_view_flat(void *data, int64_t len, int32_t elem) {
return view_make(data, len, old_elem_desc(elem), VIEW_FLAT, 0);
}
/* ── Dyn into a written type ───────────────────────────────────────────
*
* The reverse of a view: a dyn value reaching typed code that wrote a str, a
* slice, a fixed array or a struct (lib/check.ml, [into_typed]). [want] is
* the written type's descriptor, the prefix code above, with [c] for a
* [const T]; [out] is where the typed value goes — a str's or slice's two
* words, or the array's or struct's bytes. Four answers, in order:
*
* a text its own bytes, for a str or a [const u8], pinned
* ([pin_text]) and never copied;
* a view of typed storage whose element type is the one wanted: that
* storage, checked for staleness in a dev build, never copied;
* a vec, map copied and unboxed element by element, each checked, for a
* [const T], a fixed array or a struct — a [const T]'s block
* in the temp arena ([flan_temp_block]);
* anything else traps, naming the element and what it is.
*
* A [T] that can be written through is never a copy. A write through a copy
* would not reach the dyn vec, and the same program would then answer
* differently with the vec typed or dyn; so a plain dyn vec, or a view of
* other elements, into a [T] traps and names [const T]. A fixed array and a
* struct are values, copied on the typed side as well, so a copy there
* changes nothing. */
void *flan_temp_block(int64_t bytes, int64_t align, int64_t elem,
const char *type, int64_t typelen);
typedef struct into_site {
const uint8_t *loc;
int64_t loclen;
char op[140]; /* "into [const i64]" */
char where[192]; /* "element 2", "field :x of element 2"; "" */
} into_site;
static _Noreturn void into_trap(into_site *s, const char *trap,
const char *fmt, ...) {
char msg[640];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof msg, fmt, ap);
va_end(ap);
flan_say(s->loc, s->loclen, "dyn %s: %s", s->op, msg);
dyn_trap((const uint8_t *)trap, (int64_t)strlen(trap));
}
static const char *into_who(into_site *s) {
return s->where[0] ? s->where : "this";
}
/* "element 2 is a text, "a", and an i64 is wanted there". A view is checked
* for staleness before it is rendered, since rendering reads it. */
static _Noreturn void into_wrong(into_site *s, flan_dyn x, const char *why) {
char sx[SAY_MAX];
const char *t = tag_of(x);
if (dyn_boxed(x) && dyn_box(x) == BOX_OBJ && dyn_obj(x) != NULL
&& dyn_obj(x)->kind == OBJ_VIEW)
view_guard_check(s->loc, s->loclen, s->op, dyn_obj(x));
if (flan_dyn_tag(x) == FLAN_DYN_TAG_NIL)
into_trap(s, "DynType", "%s is nil, and %s", into_who(s), why);
say(sx, SAY_MAX, x);
into_trap(s, "DynType", "%s is %s %s, %s, and %s", into_who(s), an(t), t,
sx, why);
}
/* "an i64 is wanted there". */
static void into_wanted(char *buf, size_t cap, const uint8_t *d) {
char ty[128];
desc_spell(d, ty, sizeof ty);
snprintf(buf, cap, "%s %s is wanted there", an(ty), ty);
}
/* Whether a view's descriptor [a] starts with the whole of the code [b].
* The code is prefix-free, so a view's (inside a longer one) matches exactly
* when these bytes do. */
static int desc_same(const uint8_t *a, const uint8_t *b) {
size_t n = (size_t)(desc_skip(b) - b);
return memcmp(a, b, n) == 0;
}
/* A vec's length and element [i], a plain one's word or a view's element
* boxed. [o] is a vec-tagged object whose guard has been checked. */
static int64_t into_len(into_site *s, flan_obj *o) {
return o->kind == OBJ_VIEW ? view_len(s->loc, s->loclen, s->op, o) : o->len;
}
static flan_dyn into_at(into_site *s, flan_obj *o, int64_t i) {
if (o->kind == OBJ_VIEW)
return view_read(s->loc, s->loclen, s->op, o, o->u.view.desc,
view_elem_at(o, i));
return o->u.v.items[i];
}
/* A view object, or NULL; checked for staleness when it is one. */
static flan_obj *into_view(into_site *s, flan_dyn x) {
flan_obj *o;
if (!dyn_boxed(x) || dyn_box(x) != BOX_OBJ) return NULL;
o = dyn_obj(x);
if (o == NULL || o->kind != OBJ_VIEW) return NULL;
view_guard_check(s->loc, s->loclen, s->op, o);
return o;
}
/* Steps [s->where] into a part of what it names; [into_leave] steps back. */
static void into_enter(into_site *s, const char *fmt, ...) {
char part[96], rest[192];
va_list ap;
va_start(ap, fmt);
vsnprintf(part, sizeof part, fmt, ap);
va_end(ap);
memcpy(rest, s->where, sizeof rest);
if (rest[0] == '\0') snprintf(s->where, sizeof s->where, "%s", part);
else snprintf(s->where, sizeof s->where, "%s of %s", part, rest);
}
static void into_leave(into_site *s, const char *saved) {
memcpy(s->where, saved, sizeof s->where);
}
static void into_put(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p);
static void into_slice(into_site *s, const uint8_t *d, flan_dyn x,
uint8_t *p);
/* A text's bytes as a str's two words, pinned. */
static void into_text(flan_dyn x, uint8_t *p) {
flan_obj *o = dyn_obj(x);
const uint8_t *b = obj_text_bytes(o);
pin_text(o);
memcpy(p, &b, 8);
memcpy(p + 8, &o->len, 8);
}
/* [n] elements of [e] from the vec-tagged [o] into [p]. */
static void into_elems(into_site *s, const uint8_t *e, flan_obj *o, int64_t n,
uint8_t *p) {
int64_t i, sz = desc_size(e);
char saved[192];
memcpy(saved, s->where, sizeof saved);
for (i = 0; i < n; i++) {
flan_dyn x;
into_enter(s, "element %lld", (long long)i);
x = into_at(s, o, i);
into_put(s, e, x, p + i * sz);
into_leave(s, saved);
}
}
static void into_put(into_site *s, const uint8_t *d, flan_dyn x, uint8_t *p) {
char why[256], ty[128];
int64_t lo, hi;
if (int_range(*d, &lo, &hi)) {
int64_t n;
if (flan_dyn_tag(x) != FLAN_DYN_TAG_INT) {
into_wanted(why, sizeof why, d);
into_wrong(s, x, why);
}
n = dyn_int_value(x);
if (n < lo || n > hi) {
desc_spell(d, ty, sizeof ty);
if (*d == 'L')
into_trap(s, "DynRange", "%s is %lld, and a u64 holds no negative "
"number", into_who(s), (long long)n);
into_trap(s, "DynRange", "%s is %lld, and %s %s holds %lld to %lld",
into_who(s), (long long)n, an(ty), ty, (long long)lo,
(long long)hi);
}
switch (*d) {
case 'b': case 'B': { uint8_t b = (uint8_t)n; memcpy(p, &b, 1); return; }
case 'h': case 'H': { uint16_t h = (uint16_t)n; memcpy(p, &h, 2); return; }
case 'i': case 'I': { uint32_t w = (uint32_t)n; memcpy(p, &w, 4); return; }
default: memcpy(p, &n, 8); return;
}
}
switch (*d) {
case 'f': case 'd': {
double f = 0;
/* An int goes into a float when the float holds it exactly: a view's
element write and a class's float slot take the same rule. */
if (flan_dyn_tag(x) == FLAN_DYN_TAG_INT) {
int64_t n = dyn_int_value(x);
f = *d == 'f' ? (double)(float)n : (double)n;
if (!(f >= -9223372036854775808.0 && f < 9223372036854775808.0)
|| (int64_t)f != n)
into_trap(s, "DynRange", "%s is %lld, which has no exact %s. Write "
"it as a float, as in %lld.0", into_who(s), (long long)n,
*d == 'f' ? "f32" : "f64", (long long)n);
} else if (flan_dyn_tag(x) == FLAN_DYN_TAG_FLOAT)
f = dyn_num_value(x);
else {
into_wanted(why, sizeof why, d);
into_wrong(s, x, why);
}
if (*d == 'f') { float g = (float)f; memcpy(p, &g, 4); }
else memcpy(p, &f, 8);
return;
}
case '?':
if (flan_dyn_tag(x) != FLAN_DYN_TAG_BOOL) {
into_wanted(why, sizeof why, d);
into_wrong(s, x, why);
}
*p = dyn_payload(x) ? 1 : 0;
return;
case 't':
if (!is_text(x))
into_wrong(s, x, "a str is wanted there, which only a text becomes");
into_text(x, p);
return;
case 'a': {
const uint8_t *e = d + 1;
int64_t n = desc_int(&e), len;
flan_obj *o;
if (!is_vec(x)) {
desc_spell(d, ty, sizeof ty);
snprintf(why, sizeof why, "%s %s is made from a vec", an(ty), ty);
into_wrong(s, x, why);
}
o = into_view(s, x);
if (o == NULL) o = dyn_obj(x);
len = into_len(s, o);
if (len != n) {
desc_spell(d, ty, sizeof ty);
into_trap(s, "DynRange", "%s has %lld element%s, and %s %s holds "
"exactly %lld", s->where[0] ? s->where : "this vec",
(long long)len, len == 1 ? "" : "s", an(ty), ty,
(long long)n);
}
/* A view of the same elements is the same bytes: an array is a value,
copied on the typed side too. */
if (o->kind == OBJ_VIEW && desc_same(o->u.view.desc, e)) {
if (n > 0) memcpy(p, view_base(o), (size_t)(n * desc_size(e)));
return;
}
into_elems(s, e, o, n, p);
return;
}
case '{': {
const uint8_t *at, *name, *fty;
int64_t off, namelen, foff;
flan_obj *o;
char saved[192];
if (!is_map(x)) {
desc_spell(d, ty, sizeof ty);
snprintf(why, sizeof why, "%s %s is made from a map", an(ty), ty);
into_wrong(s, x, why);
}
o = into_view(s, x);
if (o != NULL && desc_same(o->u.view.desc, d)) {
memcpy(p, o->u.view.base, (size_t)desc_size(d));
return;
}
memcpy(saved, s->where, sizeof saved);
at = desc_fields(d, &off);
while (desc_next(&at, &off, &name, &namelen, &foff, &fty)) {
flan_dyn k = flan_dyn_kw(name, namelen), v;
if (!flan_dyn_truthy(flan_dyn_map_contains_at(x, k, s->loc,
s->loclen))) {
desc_spell(d, ty, sizeof ty);
into_trap(s, "DynType", "%s has no :%.*s, and %s %s needs every "
"field", s->where[0] ? s->where : "this map",
(int)namelen, (const char *)name, an(ty), ty);
}
v = flan_dyn_get(x, k, s->loc, s->loclen);
into_enter(s, "field :%.*s", (int)namelen, (const char *)name);
into_put(s, fty, v, p + foff);
into_leave(s, saved);
}
return;
}
case 's': case 'c':
into_slice(s, d, x, p);
return;
default:
desc_spell(d, ty, sizeof ty);
snprintf(why, sizeof why, "%s %s is not made from a dyn value", an(ty), ty);
into_wrong(s, x, why);
}
}
/* The copy a [const T] reads, of a plain vec or a view of other elements. */
static void into_copy(into_site *s, const uint8_t *e, flan_obj *o,
uint8_t *out) {
static const char scalars[] = "bBhHiIlLfd?t";
static const char *const words[] = { "i8", "u8", "i16", "u16", "i32", "u32",
"i64", "u64", "f32", "f64", "bool",
"str" };
const char *w = *e != '\0' ? strchr(scalars, *e) : NULL;
/* The registry keeps the name by pointer, so it is static text. */
const char *type = w != NULL ? words[w - scalars] : "element";
int64_t n = into_len(s, o), size, align;
uint8_t *block = NULL;
desc_lay(e, &size, &align);
if (n > 0 && size > 0) {
block = (uint8_t *)flan_temp_block(n * size, align, size, type,
(int64_t)strlen(type));
if (block == NULL) trap_oom(s->loc, s->loclen, n * size);
memset(block, 0, (size_t)(n * size));
into_elems(s, e, o, n, block);
}
memcpy(out, &block, 8);
memcpy(out + 8, &n, 8);
}
/* A [T] or a [const T], at the top or inside an array or a struct. A text
* is a [const u8]'s bytes, a view of the very elements wanted is its own
* storage, and a [const T] of anything else vec-shaped is a copy; a [T]
* never is, since a write through it would not reach the vec. */
static void into_slice(into_site *s, const uint8_t *d, flan_dyn x,
uint8_t *p) {
const uint8_t *e = d + 1;
int mut = *d == 's';
const char *who = s->where[0] ? s->where : "this vec";
char ty[128], ety[128], why[256];
flan_obj *o;
desc_spell(d, ty, sizeof ty);
desc_spell(e, ety, sizeof ety);
if (is_text(x) && *e == 'B') {
if (mut)
into_wrong(s, x, "a text is read-only, so it becomes a str or a "
"[const u8] and never a [u8]");
into_text(x, p);
return;
}
if (!is_vec(x)) {
snprintf(why, sizeof why, "only a vec becomes %s %s", an(ty), ty);
into_wrong(s, x, why);
}
o = into_view(s, x);
if (o != NULL && desc_same(o->u.view.desc, e)) {
void *b = view_base(o);
int64_t n = view_len(s->loc, s->loclen, s->op, o);
memcpy(p, &b, 8);
memcpy(p + 8, &n, 8);
return;
}
if (mut) {
char have[128];
if (o != NULL) {
desc_spell(o->u.view.desc, have, sizeof have);
into_trap(s, "DynType", "%s is a view of %s elements, so %s %s of it "
"would be a copy, and a write through the copy would never "
"reach the vec. Take it as [const %s], which reads a copy",
who, have, an(ty), ty, ety);
}
into_trap(s, "DynType", "%s is a dyn vec, so %s %s of it would be a "
"copy, and a write through the copy would never reach the vec. "
"Take it as [const %s], which reads a copy", who, an(ty), ty,
ety);
}
into_copy(s, e, o != NULL ? o : dyn_obj(x), p);
}
void flan_dyn_need_as(flan_dyn v, const uint8_t *want, int64_t wantlen,
void *out, const uint8_t *loc, int64_t loclen) {
into_site s;
char ty[128];
uint8_t *p = (uint8_t *)out;
(void)wantlen;
s.loc = loc;
s.loclen = loclen;
s.where[0] = '\0';
desc_spell(want, ty, sizeof ty);
snprintf(s.op, sizeof s.op, "into %s", ty);
switch (*want) {
case 't':
if (!is_text(v)) into_wrong(&s, v, "only a text becomes a str");
into_text(v, p);
return;
default:
into_put(&s, want, v, p);
return;
}
}
static flan_dyn len_walk(flan_dyn v) {
if (is_text(v)) return flan_dyn_from_i64(dyn_obj(v)->len);
/* A map's length is its slot count, so a stale instance would answer the

View File

@ -348,6 +348,18 @@ flan_dyn flan_dyn_view_slice(void *data, int64_t len, const uint8_t *desc,
flan_dyn flan_dyn_view_at(void *addr, int64_t len, const uint8_t *desc,
int64_t desclen, int32_t shape, int32_t here);
/* The other direction: a dyn value where typed code wrote a str, a [T], a
* [const T], a fixed [n T] or a struct. [want] is that type's descriptor
* (the code above, and 'c' before a [const T]'s element); [out] receives the
* str's or slice's two words, or the array's or struct's bytes. A text
* becomes a str or a [const u8] as its own bytes, kept from the collector
* until the temp arena's next free-all; a view of the wanted elements
* becomes its own storage; a vec or a map becomes a checked copy — for a
* [const T] in the temp arena — and a [T] is never a copy. Anything else
* traps at [loc], naming the element and what it holds. */
void flan_dyn_need_as(flan_dyn v, const uint8_t *want, int64_t wantlen,
void *out, const uint8_t *loc, int64_t loclen);
/* print, =, length and has-key? with the site they were written at: a view
* that traps inside one names it. */
void flan_dyn_print_at(flan_dyn v, const uint8_t *loc, int64_t loclen);

View File

@ -2593,6 +2593,40 @@ int8_t flan_f64_temp(double x, flan_slice *out) {
return flan_temp_text(render_f64, &x, out);
}
/* For flan_dyn.c's crossing of a dyn value into a written type
* ([flan_dyn_need_as]). A dyn vec copied into a [const T] or a text's bytes
* lent to a str live exactly as long as i64->bytes's text does: until the
* temp arena's next free-all. [flan_temp_block] is the copy's block, noted
* in a dev build's registry like any temp slice so a read after free-temp
* traps or reads poison; NULL only when malloc itself failed. The stamp is
* the arena and the incarnation and epoch it is at now, and a stamp is live
* while that arena is still at both — a free-temp, the agent's wipe, the end
* of a scratch evaluation or a destroy ends it. An arena record is never
* freed (see [flan_retired]), so an old stamp is always safe to read. */
void *flan_temp_block(int64_t bytes, int64_t align, int64_t elem,
const char *type, int64_t typelen) {
flan_allocator *a = flan_context_temp();
void *q;
if (a == NULL || bytes <= 0) return NULL;
q = a->proc(a, FLAN_ALLOC_ALLOC, NULL, 0, bytes, align);
if (q != NULL) flan_dev_reg_note_sliced(q, bytes, elem, type, typelen, a);
return q;
}
const void *flan_temp_stamp(uint64_t *inc, uint64_t *epoch) {
flan_allocator *a = flan_context_temp();
*inc = a ? a->incarnation : 0;
*epoch = a ? a->epoch : 0;
return a;
}
int32_t flan_temp_stamp_live(const void *p, uint64_t inc, uint64_t epoch) {
const flan_allocator *a = (const flan_allocator *)p;
/* No temp arena could be made: the pin is kept for good. */
if (a == NULL) return 1;
return a->incarnation == inc && a->epoch == epoch;
}
/* The allocator's identity, for the condition's :allocator field. The pointer
* is the identity — the same thing the epoch hangs off. */
int64_t flan_alloc_id(flan_allocator *a) { return (int64_t)(intptr_t)a; }

View File

@ -1386,6 +1386,31 @@ static void walk_hook(const uint8_t *name, int64_t namelen) {
longjmp(walk_out, 1);
}
/* flan_dyn_need_as: a text is a str's own bytes, a view of the same
* elements its own storage, and a plain vec a checked copy. */
static void into(void) {
static int64_t a[3] = { 1, 2, 3 };
struct { const uint8_t *p; int64_t n; } s;
int64_t arr[2];
flan_dyn t, v, w;
flan_gc_init();
t = flan_dyn_from_bytes((const uint8_t *)"abc", 3);
flan_dyn_root_push(&t);
flan_dyn_need_as(t, (const uint8_t *)"t", 1, &s, NULL, 0);
check(s.n == 3 && memcmp(s.p, "abc", 3) == 0, "into str");
v = flan_dyn_view_slice(a, 3, (const uint8_t *)"l", 1, 0);
flan_dyn_root_push(&v);
flan_dyn_need_as(v, (const uint8_t *)"sl", 2, &s, NULL, 0);
check((const void *)s.p == (const void *)a && s.n == 3, "into [i64] view");
w = flan_dyn_vec_new();
flan_dyn_root_push(&w);
flan_dyn_push(w, flan_dyn_from_i64(7), NULL, 0);
flan_dyn_push(w, flan_dyn_from_i64(8), NULL, 0);
flan_dyn_need_as(w, (const uint8_t *)"a2;l", 4, arr, NULL, 0);
check(arr[0] == 7 && arr[1] == 8, "into [2 i64] copy");
flan_dyn_root_pop(3);
}
static void walkreset(void) {
static uint64_t big[1] = { UINT64_MAX };
flan_dyn v, w, m;
@ -1416,6 +1441,7 @@ int main(int argc, char **argv) {
}
if (strcmp(argv[1], "ops") == 0) {
ops();
into();
printf(failures == 0 ? "ops ok\n" : "ops failed\n");
return failures == 0 ? 0 : 1;
}

View File

@ -0,0 +1,115 @@
;;;; A dyn value where typed code wrote a str, a slice, a fixed array or a
;;;; struct. Mode 0 is the survey; the others are one trap each, since a trap
;;;; ends the process. test_acceptance.ml runs it on both backends and under
;;;; --dev, where mode 8 (a view of a returned call's local) traps as well.
(declare gc-collect [] () "flan_gc_collect")
(declare gc-count [] i64 "flan_gc_count")
(defstruct Point [x f64 y i32])
(defstruct Named [name str id u32])
(defclass pt [x y])
;; Unannotated parameters and returns are dyn.
(defn keep [d] dyn d)
(defn text-of [d] dyn (slice d 0 5))
(defn shout [s str] () (println s))
(defn keep-str [s str] str s)
;; The text crosses in this frame, whose dyn slots are gone once it returns.
(defn fetch [] str (keep-str (text-of (keep "pinned text"))))
(defn sum [xs [const i64]] i64
(let [t (i64 0)]
(dotimes [i (length xs)] (set t (+ t (at xs i))))
t))
(defn bump [xs [i64]] ()
(dotimes [i (length xs)] (set (at xs i) (+ (at xs i) 100))))
(defn sumf [xs [const f64]] f64
(let [t 0.0]
(dotimes [i (length xs)] (set t (+ t (at xs i))))
t))
(defn bytes-sum [xs [const u8]] i64
(let [t (i64 0)]
(dotimes [i (length xs)] (set t (+ t (i64 (at xs i)))))
t))
(defn names [xs [const str]] ()
(dotimes [i (length xs)] (println (at xs i))))
(defn words [xs [const [const u8]]] i64 (length (at xs 1)))
(defn triple [a [3 i64]] i64 (+ (at a 0) (at a 2)))
(defn grid [g [2 [2 i32]]] i32 (at g 1 0))
(defn px [p Point] f64 (+ (.x p) (f64 (.y p))))
(defn named [n Named] () (println (.name n) (.id n)))
(defn f64s [xs [f64]] () (println (length xs)))
(defn raw [xs [u8]] () (println (length xs)))
;; A view of a local, kept past the call that owns the local.
(defonce held dyn nil)
(defn leak-local [] ()
(let [a [(i64 1) 2 3]]
(set held (keep a))))
(defn main [args [str]] i32
(let [n (i32 (bytes->i64 (bytes-view (at args 1))))]
(cond
(= n 0)
(do
;; A text becomes a str.
(shout (keep "hello"))
(println (length (keep-str (keep "four"))))
;; A str kept past its crossing, the text reachable only through
;; the pin, while collections run and the heap is refilled.
(let [s (fetch)]
(gc-collect)
(dotimes [i 20000] (text-of (keep "XXXXXXXXXX")))
(gc-collect)
(dotimes [i 20000] (text-of (keep "XXXXXXXXXX")))
(println s))
;; A pin ends at free-temp: a thousand crossed texts are collected.
(free-temp)
(gc-collect)
(let [before (gc-count)]
(dotimes [i 1000] (keep-str (text-of (keep "abcdefgh"))))
(free-temp)
(gc-collect)
(println (< (- (gc-count) before) 100)))
;; A view of typed storage comes back as that storage.
(let [a [(i64 1) 2 3]]
(bump (keep a))
(println a)
(println (sum (keep a))))
(let [v (vec-new i64)]
(push v 5) (push v 6)
(bump (keep v))
(println (at v 0) (at v 1))
(free v))
;; A plain dyn vec is a checked copy for a [const T].
(println (sum (the dyn [1 2 3 4])))
(println (sumf (the dyn [1 2.5])))
(println (bytes-sum (the dyn [1 2 255])))
(println (bytes-sum (keep "AB")))
(names (the dyn ["ada" "bo"]))
(println (words (the dyn ["x" "yyy"])))
;; A view of other elements too.
(let [b [(i32 7) 8]]
(println (sum (keep b))))
;; A fixed array and a struct are values: a copy either way.
(println (triple (the dyn [10 20 30])))
(let [a [(i64 4) 5 6]] (println (triple (keep a))))
(println (grid (the dyn [[1 2] [3 4]])))
(println (px (the dyn {:x 1.5 :y 2})))
(println (px (pt 2.5 3)))
(let [p (Point {.x 0.25 .y 4})] (println (px (keep p))))
(named (the dyn {:name "cy" :id 9}))
0)
(= n 1) (do (println (sum (the dyn [1 "a" 3]))) 0)
(= n 2) (do (bump (the dyn [1 2])) 0)
(= n 3) (do (println (sum (keep "abc"))) 0)
(= n 4) (do (println (bytes-sum (the dyn [1 300]))) 0)
(= n 5) (do (println (triple (the dyn [1 2]))) 0)
(= n 6) (do (println (px (the dyn {:x 1.5}))) 0)
(= n 7) (do (shout (keep 5)) 0)
(= n 8) (do (leak-local) (bump held) 0)
(= n 9) (do (raw (keep "abc")) 0)
(= n 10) (let [a [(f32 1) 2]] (f64s (keep a)) 0)
(= n 11) (do (println (px (the dyn {:x 1.5 :y "no"}))) 0)
:else 1)))

View File

@ -6023,6 +6023,67 @@ level "1"
dyn_view_any ~dev:true ();
dyn_view_any ~dev:true ~x86:true ();
(* ── A dyn value into a written type ───────────────────────────────
programs/dyn-into-typed.flan: a text into a str, kept past the frame
it crossed in while collections run; a view back into its own
storage, written through; a dyn vec, map or instance copied into a
[const T], a fixed array or a struct (mode 0); then one trap per mode,
each naming the element and what it holds. Mode 8, a view of a
returned call's local, traps under --dev only. *)
let into_out =
"hello\n4\npinne\ntrue\n[101 102 103]\n306\n105 106\n10\n3.5\n258\n\
131\nada\nbo\n3\n15\n40\n10\n3\n3.5\n5.5\n4.25\ncy 9\n"
and into_traps =
[ ("1", "dyn-into-typed.flan:104:33: dyn into [const i64]: element 1 \
is a text, \"a\", and an i64 is wanted there");
("2", "dyn into [i64]: this vec is a dyn vec, so a [i64] of it would \
be a copy, and a write through the copy would never reach the \
vec. Take it as [const i64], which reads a copy");
("3", "this is a text, \"abc\", and only a vec becomes a [const i64]");
("4", "element 1 is 300, and a u8 holds 0 to 255");
("5", "this vec has 2 elements, and a [3 i64] holds exactly 3");
("6", "dyn into Point: this map has no :y, and a Point needs every \
field");
("7", "dyn into str: this is an int, 5, and only a text becomes a str");
("9", "a text is read-only, so it becomes a str or a [const u8] and \
never a [u8]");
("10", "this vec is a view of f32 elements");
("11", "field :y is a text, \"no\", and an i32 is wanted there") ]
and into_stale =
[ ("8", "dyn into [i64]: this view points into a local of leak-local, \
and that call has returned") ]
in
let dyn_into ?opt ?(x86 = false) ?(dev = false) () =
let exe = compile ?opt ~x86 ~dev "programs/dyn-into-typed.flan" in
let name what =
"dyn: into a written type" ^ what
^ (match opt with Some o -> ", " ^ o | None -> "")
^ (if x86 then ", --x86" else "") ^ (if dev then ", --dev" else "")
in
let code, text = run exe (Some "0") in
if code <> 0 || text <> into_out then begin
incr failures;
Printf.printf "FAIL %s\n got: %S (exit %d)\n wanted: %S\n"
(name "") text code into_out
end;
List.iter
(fun (mode, needle) ->
let code, text = run exe (Some mode) in
if code <> 134 || not (contains text needle) then begin
incr failures;
Printf.printf
"FAIL %s\n got: %S (exit %d)\n wanted a trap \
saying %S\n" (name (", mode " ^ mode)) text code needle
end)
(into_traps @ if dev then into_stale else []);
(try Sys.remove exe with Sys_error _ -> ())
in
dyn_into ();
dyn_into ~opt:"-O0" ();
dyn_into ~x86:true ();
dyn_into ~dev:true ();
dyn_into ~dev:true ~x86:true ();
(* 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
about. flan_dyn.c's trigger has a one-megabyte floor, and not one

View File

@ -7713,9 +7713,18 @@ let () =
rejects_check "the refuses a dyn and names the cast"
"(defn f [x dyn] i32 (the i32 x))" ~needle:"write (i32 x) to convert it";
accepts "the cast that refusal names compiles" "(defn f [x dyn] i32 (i32 x))";
rejects_check "the refuses a dyn at a type a dyn does not become"
rejects_check "the refuses a dyn at str, which a dyn becomes where passed"
"(defn f [x dyn] str (the str x))"
~needle:"dyn — str does not cross into a written type yet";
~needle:"a dyn becomes a str where a str is passed";
accepts "a dyn becomes a slice, an array and a struct where passed"
"(defstruct P [x i32]) (defn f [a [const i64] b [2 f32] c P s str] () ()) \
(defn g [d dyn] () (f d d d d))";
rejects_check "a dyn does not become an array of Vecs"
"(defn f [d dyn] [2 (Vec i64)] d)"
~needle:"it holds a Vec, which owns its storage";
rejects_check "a dyn does not become a slice of dyn"
"(defn f [d dyn] [const dyn] d)"
~needle:"[const dyn] does not cross into a written type yet";
rejects_check "the refuses a dyn at bool, which a dyn becomes where passed"
"(defn f [x dyn] bool (the bool x))"
~needle:"a dyn becomes a bool where a bool is passed";