diff --git a/test/dev_limits.c b/test/dev_limits.c new file mode 100644 index 0000000..d4e444d --- /dev/null +++ b/test/dev_limits.c @@ -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 +#include +#include +#include + +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; +} diff --git a/test/dune b/test/dune index ac37170..ba76fc1 100644 --- a/test/dune +++ b/test/dune @@ -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. diff --git a/test/test_reload.ml b/test/test_reload.ml index d4d66e5..e686a18 100644 --- a/test/test_reload.ml +++ b/test/test_reload.ml @@ -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;