From 67c9268907f93d9fe2024845e21e739d965abe14 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 11:20:56 +0700 Subject: [PATCH] Reach the two paths a new type can die on, and stop println consuming a Vec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/check.ml | 8 ++++++-- lib/emit.ml | 2 +- runtime/flan_rt.c | 16 +++++++++++++--- test/programs/vec.flan | 7 +++++++ test/test_acceptance.ml | 19 ++++++++++++++++++- 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/lib/check.ml b/lib/check.ml index d956f2e..bc2e694 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -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 diff --git a/lib/emit.ml b/lib/emit.ml index 8dcc84d..f78eb6d 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -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) diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index 7adc26d..e794efb 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -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; diff --git a/test/programs/vec.flan b/test/programs/vec.flan index e352866..4a8811e 100644 --- a/test/programs/vec.flan +++ b/test/programs/vec.flan @@ -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) ; + (println context/allocator) ; + (free v)) ;; A second element type over the same runtime, and a struct element, so diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 2587326..427f5f0 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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\ + \n\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