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:
parent
aa0b799bb6
commit
9b80fc2084
107
test/dev_limits.c
Normal file
107
test/dev_limits.c
Normal 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;
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user