The 4K result cap and the registry's 4096 names, run for the first time

Neither limit had any coverage: a renderer that emits more than 4K and
a program that introduces more than 4096 run-time names are both past
anything the corpus does, so the truncation and the abort were code
that had never executed. dev_limits.c drives them directly — they are C
entry points with no Flan spelling, and flan_dev.c is compiled into
every build — one process per mode, because the name table never
shrinks and the overflow case aborts.

The cap case pins the length, the ellipsis, a byte from before the cut,
the generation moving exactly once, and the flag being cleared so a
short value after a truncated one does not inherit its ellipsis. The
registry case pins that 4096 fit and the next one stops the process
with its reason. Dropping result_full and moving the slot check by one
were both planted and watched fail.
This commit is contained in:
Joseph Ferano 2026-09-12 10:54:11 +07:00
parent aa0b799bb6
commit 9b80fc2084
3 changed files with 157 additions and 1 deletions

107
test/dev_limits.c Normal file
View File

@ -0,0 +1,107 @@
/* dev_limits.c — the two fixed-size limits in runtime/flan_dev.c, driven
* directly.
*
* Neither is reachable from a Flan program that behaves. The result buffer
* only truncates when a renderer emits more than 4K, which no fixture does,
* and the name table only fills after 4096 distinct run-time-introduced names,
* which is more forms than the whole corpus has. So both were code nothing had
* ever run: the first thing either would do in anger is either lose the end of
* a value or scribble past the end of a static array, and the ellipsis and the
* abort are the two behaviours that say which.
*
* A C main, for the same reason reload_host.c is one: these are C entry points
* with no Flan spelling, and flan_dev.c is compiled into every build (see
* build.ml), so linking any Flan program brings them along. The .flan this is
* linked against therefore has no [main] of its own.
*
* One mode per run, chosen by argv, because both of them are one-way: the
* name table never shrinks and the overflow case aborts the process.
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void **flan_dev_cell(const char *name);
void flan_dev_result_begin(void);
void flan_dev_emit(const uint8_t *bytes, int64_t len);
void flan_dev_result_end(void);
const char *flan_dev_result_get(uint64_t *gen, uint64_t *len);
void flan_rt_init(int32_t argc, char **argv);
#define CAP 4096
/* Emit more than fits, twice the buffer's worth, and report what came back.
* What is printed is everything a wrong cap gets wrong: the length, the three
* characters that say it was truncated, a byte from the middle to show the
* content up to the cut is the content that was emitted, and the generation
* counter, which is what a reader waits on and must move exactly once. */
static int cap(void) {
static uint8_t a[3000], b[3000];
uint64_t gen0, gen1, len;
const char *r;
memset(a, 'a', sizeof a);
memset(b, 'b', sizeof b);
r = flan_dev_result_get(&gen0, &len);
flan_dev_result_begin();
flan_dev_emit(a, (int64_t)sizeof a);
/* A negative length is not a huge one: the cast to size_t would make it
* about 2^64 and the clamp would then be the only thing between it and a
* memcpy of everything. */
flan_dev_emit(b, -1);
flan_dev_emit(b, (int64_t)sizeof b);
flan_dev_result_end();
r = flan_dev_result_get(&gen1, &len);
printf("len %llu\n", (unsigned long long)len);
printf("tail %.3s\n", len >= 3 ? r + len - 3 : "");
printf("mid %c\n", len > 3500 ? r[3500] : '?');
printf("head %c\n", len > 0 ? r[0] : '?');
printf("gen %llu\n", (unsigned long long)(gen1 - gen0));
/* And a short value after a truncated one: the flag has to be cleared by
* [begin] or every later render ends in an ellipsis it did not earn. */
flan_dev_result_begin();
flan_dev_emit((const uint8_t *)"12", 2);
flan_dev_result_end();
r = flan_dev_result_get(&gen1, &len);
printf("again %.*s\n", (int)len, r);
return 0;
}
/* Fill the name table and then ask for one more. The table is fixed and never
* moves a module holds the address of a cell for as long as it is loaded
* so the only honest answer past the end is to stop. */
static int names(void) {
char buf[32];
for (int i = 0; i < CAP; i++) {
snprintf(buf, sizeof buf, "n%d", i);
if (flan_dev_cell(buf) == NULL) {
printf("null cell at %d\n", i);
return 1;
}
}
/* Distinct names, all of them: a table that deduplicated wrongly would not
* be full here and the line below would not abort. */
printf("interned %d\n", CAP);
fflush(stdout);
(void)flan_dev_cell("one-too-many");
printf("survived\n");
return 0;
}
int main(int argc, char **argv) {
flan_rt_init(argc, argv);
if (argc < 2) {
fprintf(stderr, "usage: %s cap|names\n", argv[0]);
return 2;
}
if (strcmp(argv[1], "cap") == 0) return cap();
if (strcmp(argv[1], "names") == 0) return names();
fprintf(stderr, "unknown mode %s\n", argv[1]);
return 2;
}

View File

@ -27,6 +27,9 @@
(glob_files programs/*.flan)
; The reload primitive's host: a C main that dlopens what Build.shared made.
(file reload_host.c)
; The other C main: flan_dev.c's two fixed limits, which no Flan program
; reaches, driven directly.
(file dev_limits.c)
; test_dev runs the compiler itself: flan dev launches and owns a program.
(file %{workspace_root}/bin/main.exe)
; The Emacs client, which test_emacs drives against a real daemon.

View File

@ -226,6 +226,52 @@ let () =
if not (has said "size changed") then
fail "a retyped global did not stop on the size guard: %S" said;
(* The two fixed-size limits in flan_dev.c, which nothing had ever
reached: the 4K result buffer a renderer emits into, and the 4096-name
registry. Both are driven from dev_limits.c rather than from Flan,
because neither has a Flan spelling and a program that reached either
one by accident would be a program nobody wants in the corpus.
One process per mode. The name table never shrinks, so the two cases
would contaminate each other, and the overflow case ends in abort. *)
let limits = tmp "limits" in
ignore
(Build.executable ~opts:dev ~csrcs:[ "dev_limits.c" ] p1 ~out:limits);
let mode m =
let o = tmp ("limits-" ^ m ^ ".out") and e = tmp ("limits-" ^ m ^ ".err") in
let code =
Sys.command
(Printf.sprintf "%s %s > %s 2> %s" (Filename.quote limits) m
(Filename.quote o) (Filename.quote e))
in
let out = In_channel.with_open_bin o In_channel.input_all in
let err = In_channel.with_open_bin e In_channel.input_all in
List.iter (fun p -> try Sys.remove p with Sys_error _ -> ()) [ o; e ];
(code, out, err)
in
(* 6000 bytes emitted into 4096. The length is the cap itself, the three
dots are what says the value was cut rather than being that short, the
middle byte says the content before the cut is the content that was
emitted, and the generation moved exactly once a reader waits on that
counter and a value published twice would be read half-formed. The last
line is the flag being cleared: a short value after a truncated one must
not inherit its ellipsis. *)
let code, out, _ = mode "cap" in
let want_cap = "len 4096\ntail ...\nmid b\nhead a\ngen 1\nagain 12\n" in
if code <> 0 || out <> want_cap then
fail "the 4K result cap\n got: %S (exit %d)\n wanted: %S"
out code want_cap;
(* 4096 distinct names fit; the next one stops the process. The table is
fixed and never moves, because a loaded module holds the address of a
cell in it, so growing is not available and overrunning is the only
other thing it could do. *)
let code, out, err = mode "names" in
if code = 0 then fail "the registry accepted a 4097th name (exit 0)";
if out <> "interned 4096\n" then
fail "the registry did not take 4096 names first: %S" out;
if not (has err "out of dev name slots") then
fail "the registry overflowed without saying so: %S" err;
Printf.printf
"reload: emit %.1fms llc %.1fms ld %.1fms (v2: emit %.1fms llc %.1fms ld %.1fms) host run %.1fms\n"
emit_ms t1.Build.llc_ms t1.Build.link_ms emit2_ms t2.Build.llc_ms
@ -233,7 +279,7 @@ let () =
print_string timings;
List.iter (fun p -> try Sys.remove p with Sys_error _ -> ())
[ host; so1; so2; so3; so4; so5; out; out5; err5; tmp "err" ];
[ host; limits; so1; so2; so3; so4; so5; out; out5; err5; tmp "err" ];
if !failures = 0 then print_endline "reload: all tests passed"
else begin
Printf.printf "\n%d failure(s)\n" !failures;