Reach the two paths a new type can die on, and stop println consuming a Vec

The debug-info arm and the structural printer are each a separate path from
everything the suite was exercising: `outputs ~dev:true` goes through the cells,
not through DWARF, and no program printed a Vec or an allocator. That is
NEXT.md's landed item 2 exactly — field_addr took only Types.Named, so the
printer's Option arm had never run and would have died on the first (Option T)
pointed at it. Both arms work; both are now reached, and the DWARF row asserts
the composite's size as well as its name, because an element count that
disagreed with `lay` would print plausible values for the wrong fields.

Printing a Vec did not work: `println` checked its argument as an ordinary read,
so it moved, and every printing of a Vec would have been its last. Printing is a
borrow — the walk goes over the value and keeps nothing.

And `vec-new` with an explicitly named null allocator no longer substitutes the
heap for it. Adopting the context for a *zeroed* Vec is the documented rule;
quietly substituting for an allocator the program named is the same "released
the region / never made one" collapse free-all already traps for, except silent
and found later as a leak. The no-allocator-named case never arrives as null —
the checker passes flan_context_allocator(), which always answers one.
This commit is contained in:
Joseph Ferano 2026-09-12 11:20:56 +07:00
parent c6f276cbff
commit 67c9268907
5 changed files with 45 additions and 7 deletions

View File

@ -1693,7 +1693,7 @@ and named_call ctx ~want loc name args =
let attempt =
rt loc (Types.Int Types.I8) "flan_vec_init"
[ mk loc (Types.Vec elem) (Tast.Local v); a; i64_at loc 0L;
size_of loc elem; align_of loc elem ]
size_of loc elem; align_of loc elem; here loc ]
in
expect loc ~want
(mk loc (Types.Vec elem)
@ -1980,7 +1980,11 @@ and named_call ctx ~want loc name args =
nested, which is why it lives here and not in the walk. *)
| "print" | "println" ->
arity loc name 1 args;
let a = check ctx (List.hd args) in
(* Printing is a read, not a move: the walk goes over the value and keeps
nothing. Without this, (println v) would consume a Vec and every
printing of one would be its last. *)
let target = List.hd args in
let a = borrowed ctx target (fun () -> check ctx target) in
let bslice = Types.Slice (Types.Int Types.U8) in
let write x = mk loc Types.Unit (Tast.Prim (Tast.WriteStdout, [ x ])) in
let conv pr x = mk loc bslice (Tast.Prim (pr, [ x ])) in

View File

@ -1686,7 +1686,7 @@ declare i64 @flan_alloc_fail_align()
declare i64 @flan_alloc_fail_id()
declare i64 @flan_alloc_budget(ptr)
declare void @flan_alloc_set_budget(ptr, i64)
declare i8 @flan_vec_init(ptr, ptr, i64, i64, i64)
declare i8 @flan_vec_init(ptr, ptr, i64, i64, i64, ptr, i64)
declare i8 @flan_vec_reserve(ptr, i64, i64, i64, ptr, i64)
declare i8 @flan_vec_push(ptr, ptr, i64, i64, ptr, i64)
declare i8 @flan_vec_clone(ptr, ptr, ptr, i64, i64, ptr, i64)

View File

@ -867,12 +867,22 @@ static int8_t flan_vec_grow(flan_vec *v, int64_t want, int64_t size,
}
int8_t flan_vec_init(flan_vec *v, flan_allocator *a, int64_t cap, int64_t size,
int64_t align) {
int64_t align, const uint8_t *loc, int64_t loclen) {
/* [a] is NULL only when no allocator was named at the site and the context
* is being used. An allocator *named* at the site and null is a zeroed
* Allocator nobody assigned, and substituting the heap for it would be the
* same "released the region / never made one" collapse flan_alloc_free_all
* traps for except silent, and discovered as a leak. The checker cannot
* see it, because a null is a run-time value.
*
* The no-allocator-named case never arrives here as NULL: the checker passes
* flan_context_allocator(), which always answers one. */
if (!a) flan_null_alloc_fail(loc, loclen);
v->ptr = NULL;
v->len = 0;
v->cap = 0;
v->gen = 0;
v->alloc = a ? a : flan_context_allocator();
v->alloc = a;
v->epoch = (int64_t)v->alloc->epoch;
if (cap <= 0) return 1;
return flan_vec_grow(v, cap, size, align);
@ -946,7 +956,7 @@ int8_t flan_vec_clone(flan_vec *dst, flan_vec *src, flan_allocator *a,
int64_t size, int64_t align, const uint8_t *loc,
int64_t loclen) {
flan_vec_check(src, loc, loclen);
if (!flan_vec_init(dst, a, src->len, size, align)) return 0;
if (!flan_vec_init(dst, a, src->len, size, align, loc, loclen)) return 0;
if (src->len > 0) memcpy(dst->ptr, src->ptr, (size_t)(src->len * size));
dst->len = src->len;
return 1;

View File

@ -64,6 +64,13 @@
(push v 40)
(println (len v)) ; 4
;; The structural printer reaches both new types. Neither is followed: a
;; Vec's elements are printed through (as-slice v), which says at the call
;; site that it borrowed, and an allocator's contents are the runtime's and
;; its address is not stable across runs.
(println v) ; <vec>
(println context/allocator) ; <allocator>
(free v))
;; A second element type over the same runtime, and a struct element, so

View File

@ -299,11 +299,28 @@ let () =
moves are here too into a call and out of one because a Vec that
cannot be handed to a function is not a container anyone can use. *)
let vec_out =
"0\n3\n10\n30\n99\n139\n2\n99\n-1\n10\n3\n4\n2\n4\n7\nfalse\n11\n5\n"
"0\n3\n10\n30\n99\n139\n2\n99\n-1\n10\n3\n4\n\
<vec>\n<allocator>\n2\n4\n7\nfalse\n11\n5\n"
in
outputs "vec" "programs/vec.flan" vec_out;
outputs ~opt:"-O0" "vec, -O0" "programs/vec.flan" vec_out;
outputs ~dev:true "vec, dev" "programs/vec.flan" vec_out;
(* A debug build, because [dty] is a separate path from everything above:
[outputs ~dev:true] goes through the cells, not through DWARF, and a
type with no arm there dies at emit rather than being merely undebugged.
That is NEXT.md's landed item 2 exactly [field_addr] took only
[Types.Named], so the printer's Option arm had never run. Asserted on
the metadata as well as on the program still working: a composite whose
element count disagreed with [lay] would print plausible values for the
wrong fields, which is the failure debug info has. *)
let dbg = Emit.program ~debug:true (Check.program
(Parse.program (Reader.read_file "programs/vec.flan"))) in
if not (contains dbg "name: \"Allocator\"")
|| not (contains dbg "name: \"(Vec i32)\", size: 384")
then begin
incr failures;
print_endline "FAIL debug info for Allocator and (Vec T)"
end;
(* StorageExhausted and retry. The allocator is genuinely exhausted — a
ceiling on live bytes, hit repeatedly and the handler raises it and