The [At] arm of [permanent_root] recursed through any indexed target, so an element of a global SLICE answered permanent the way an element of a global ARRAY does. An array's elements are inside the global's storage; a slice's are ptr+len pointing wherever, which can be a frame already returned — the program that stashes (slice local 0 2) in a global slice and views an element compiled and segfaulted with no diagnostic. The arm now recurses only when the target's own type is an Array. With it, the refusal/acceptance pair in test_flan.ml (one word apart) and a view over an element of a global array in dyn-view.flan's mode 0. The element check now runs before the lifetime check in all three container arms: a local (Vec string) was told to make it a global, and a global (Vec string) is refused anyway, so the advice was a dead end. And the four strings that claimed more than the code does. flan_dyn.h already had the honest version — a view is exactly as stale-safe as the thing it is a view of — so the refusal message, box's comment and FIX.org now say that instead of promising a dyn value can never dangle; a global [i64] cut from a dead frame still passes and still reads it (ASan: stack-use-after-scope in view_box). The element message no longer tells a (Vec string) that string is not the case the restriction exists for. dyn_ops.c's hand_vec comment no longer says flan_rt.c is unlinked when it calls two of its functions; flan_rt.c said the same thing and is fixed too. FIX.org's arena paragraph now separates the header's lifetime (compile time, already covered) from releasing the arena under a live view: free-all traps cleanly on the epoch, arena-destroy is a heap-use-after-free in view_vec_check, the same gap flan_vec_check has on the typed side.
953 lines
41 KiB
C
953 lines
41 KiB
C
/* dyn_ops.c — runtime/flan_dyn.c, driven directly.
|
|
*
|
|
* A C main, for the reason dev_limits.c and reload_host.c are C mains: the
|
|
* dynamic-value runtime's surface is a C ABI and has no Flan spelling yet, so
|
|
* there is no program that could reach it. The .flan this links against
|
|
* therefore has no [main] of its own; see programs/dyn-host.flan.
|
|
*
|
|
* This file includes runtime/flan_dyn.h and calls every function the header
|
|
* declares. That is not tidiness — the build compiles flan_dyn.c on its own
|
|
* with no include path, so the implementation declares its own prototypes and
|
|
* the header is a second copy of them. Including it *here* is what makes a
|
|
* divergence between the two a compile or link error in `dune test` rather
|
|
* than a surprise in the compiler lane's emitted code.
|
|
*
|
|
* One mode per run, chosen by argv, because most of the modes end in a trap
|
|
* and a trap ends the process. The happy paths share one run; each refusal
|
|
* gets its own.
|
|
*/
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
/* Resolved because [Build] drops runtime/flan_dyn.h into the directory it
|
|
* compiles each translation unit in, beside the .c it writes there. That is
|
|
* the only include path a package's C — or this — ever gets, and it is there
|
|
* so that C which computes with dyn values has one declaration to agree with
|
|
* rather than a hand-copied list. */
|
|
#include "flan_dyn.h"
|
|
|
|
void flan_rt_init(int32_t argc, char **argv);
|
|
void flan_vec_free(void *v, int64_t size, int64_t align, const uint8_t *loc,
|
|
int64_t loclen);
|
|
void flan_vec_layout(int64_t out[6]);
|
|
|
|
static int failures;
|
|
|
|
static void fail(const char *what) {
|
|
printf("FAIL %s\n", what);
|
|
failures++;
|
|
}
|
|
|
|
static void check(int ok, const char *what) {
|
|
if (!ok) fail(what);
|
|
}
|
|
|
|
/* A value's printed form, captured, so the print assertions can be exact
|
|
* strings rather than eyeballed. stdout is redirected into a pipe for the
|
|
* length of one call — cheaper and more honest than a second renderer that
|
|
* would have to be kept in step with the one under test. */
|
|
static char shown[4096];
|
|
|
|
static void show(flan_dyn v) {
|
|
int saved = dup(1);
|
|
int fds[2];
|
|
ssize_t n;
|
|
shown[0] = '\0';
|
|
if (pipe(fds) != 0) { fail("pipe"); return; }
|
|
fflush(stdout);
|
|
dup2(fds[1], 1);
|
|
close(fds[1]);
|
|
flan_dyn_print(v);
|
|
fflush(stdout);
|
|
dup2(saved, 1);
|
|
close(saved);
|
|
n = read(fds[0], shown, sizeof shown - 1);
|
|
close(fds[0]);
|
|
shown[n > 0 ? (size_t)n : 0] = '\0';
|
|
}
|
|
|
|
static void prints(flan_dyn v, const char *want) {
|
|
show(v);
|
|
if (strcmp(shown, want) != 0) {
|
|
printf("FAIL print: got %s, wanted %s\n", shown, want);
|
|
failures++;
|
|
}
|
|
}
|
|
|
|
static int truth(flan_dyn v) { return flan_dyn_need_bool(v) != 0; }
|
|
static int64_t num(flan_dyn v) { return flan_dyn_need_i64(v); }
|
|
|
|
static flan_dyn text(const char *s) {
|
|
return flan_dyn_from_bytes((const uint8_t *)s, (int64_t)strlen(s));
|
|
}
|
|
|
|
/* ── The happy paths ───────────────────────────────────────────────────*/
|
|
|
|
static void ops(void) {
|
|
/* Six slots and one push of six, which is the shape the compiler lane
|
|
emits: a frame's dyn locals are rooted as a block on entry and popped as a
|
|
block on the way out. Every one is nil before it is pushed, which is the
|
|
contract the header states — the collector reads these addresses on every
|
|
mark, and an unwritten slot is a word of stack garbage. */
|
|
flan_dyn a = flan_dyn_nil(), b = flan_dyn_nil(), c = flan_dyn_nil();
|
|
flan_dyn v = flan_dyn_nil(), w = flan_dyn_nil(), s = flan_dyn_nil();
|
|
flan_dyn_root_push(&a);
|
|
flan_dyn_root_push(&b);
|
|
flan_dyn_root_push(&c);
|
|
flan_dyn_root_push(&v);
|
|
flan_dyn_root_push(&w);
|
|
flan_dyn_root_push(&s);
|
|
|
|
/* Tags, and the words they are called by. The words are what a trap message
|
|
says, so they are asserted here rather than only read. */
|
|
check(flan_dyn_tag(flan_dyn_nil()) == FLAN_DYN_TAG_NIL, "tag nil");
|
|
check(flan_dyn_tag(flan_dyn_from_bool(1)) == FLAN_DYN_TAG_BOOL, "tag bool");
|
|
check(flan_dyn_tag(flan_dyn_from_i64(7)) == FLAN_DYN_TAG_INT, "tag int");
|
|
check(flan_dyn_tag(flan_dyn_from_f64(1.5)) == FLAN_DYN_TAG_FLOAT, "tag float");
|
|
check(flan_dyn_tag(text("x")) == FLAN_DYN_TAG_TEXT, "tag text");
|
|
check(flan_dyn_tag(flan_dyn_vec_new()) == FLAN_DYN_TAG_VEC, "tag vec");
|
|
check(strcmp(flan_dyn_tag_name(FLAN_DYN_TAG_NIL), "nil") == 0, "word nil");
|
|
check(strcmp(flan_dyn_tag_name(FLAN_DYN_TAG_BOOL), "bool") == 0, "word bool");
|
|
check(strcmp(flan_dyn_tag_name(FLAN_DYN_TAG_INT), "int") == 0, "word int");
|
|
check(strcmp(flan_dyn_tag_name(FLAN_DYN_TAG_FLOAT), "float") == 0, "word float");
|
|
check(strcmp(flan_dyn_tag_name(FLAN_DYN_TAG_TEXT), "text") == 0, "word text");
|
|
check(strcmp(flan_dyn_tag_name(FLAN_DYN_TAG_VEC), "vec") == 0, "word vec");
|
|
|
|
/* Every double is itself, at both ends of the range and at the values a
|
|
NaN-box could get wrong. -0.0 is the one that would be lost by a scheme
|
|
that normalised more than NaN. */
|
|
check(flan_dyn_need_f64(flan_dyn_from_f64(0.0)) == 0.0, "f64 zero");
|
|
check(flan_dyn_need_f64(flan_dyn_from_f64(-1.25)) == -1.25, "f64 neg");
|
|
check(flan_dyn_need_f64(flan_dyn_from_f64(1e308)) == 1e308, "f64 huge");
|
|
{
|
|
double z = flan_dyn_need_f64(flan_dyn_from_f64(-0.0));
|
|
check(z == 0.0 && 1.0 / z < 0, "f64 negative zero keeps its sign");
|
|
}
|
|
{
|
|
/* A NaN survives as a NaN, which is all a NaN promises. Its *sign* does
|
|
not, on purpose: flan_rt.c already refuses to print it and the box
|
|
needs the bit. */
|
|
double n = flan_dyn_need_f64(flan_dyn_from_f64(0.0 / 0.0));
|
|
check(n != n, "f64 nan is still nan");
|
|
}
|
|
|
|
/* Integers, including the two that do not fit the payload and go to the
|
|
heap. The round trip is what says a boxed int is the same int. */
|
|
check(num(flan_dyn_from_i64(0)) == 0, "i64 zero");
|
|
check(num(flan_dyn_from_i64(-1)) == -1, "i64 minus one");
|
|
check(num(flan_dyn_from_i64(140737488355327LL)) == 140737488355327LL,
|
|
"i64 largest inline");
|
|
check(num(flan_dyn_from_i64(-140737488355328LL)) == -140737488355328LL,
|
|
"i64 smallest inline");
|
|
check(num(flan_dyn_from_i64(140737488355328LL)) == 140737488355328LL,
|
|
"i64 first boxed");
|
|
check(num(flan_dyn_from_i64(INT64_MAX)) == INT64_MAX, "i64 max");
|
|
check(num(flan_dyn_from_i64(INT64_MIN)) == INT64_MIN, "i64 min");
|
|
check(flan_dyn_tag(flan_dyn_from_i64(INT64_MAX)) == FLAN_DYN_TAG_INT,
|
|
"a boxed int is still an int");
|
|
|
|
/* Arithmetic. Two ints answer an int; a float anywhere answers a float. */
|
|
check(num(flan_dyn_add(flan_dyn_from_i64(2), flan_dyn_from_i64(3))) == 5, "+");
|
|
check(num(flan_dyn_sub(flan_dyn_from_i64(2), flan_dyn_from_i64(3))) == -1, "-");
|
|
check(num(flan_dyn_mul(flan_dyn_from_i64(2), flan_dyn_from_i64(3))) == 6, "*");
|
|
check(num(flan_dyn_div(flan_dyn_from_i64(7), flan_dyn_from_i64(2))) == 3, "/");
|
|
check(num(flan_dyn_rem(flan_dyn_from_i64(7), flan_dyn_from_i64(2))) == 1, "%");
|
|
check(num(flan_dyn_rem(flan_dyn_from_i64(-7), flan_dyn_from_i64(2))) == -1,
|
|
"% keeps the sign of the dividend");
|
|
check(flan_dyn_need_f64(
|
|
flan_dyn_add(flan_dyn_from_i64(1), flan_dyn_from_f64(0.5))) == 1.5,
|
|
"int and float promote");
|
|
check(flan_dyn_need_f64(
|
|
flan_dyn_div(flan_dyn_from_f64(1.0), flan_dyn_from_f64(4.0))) == 0.25,
|
|
"float /");
|
|
check(flan_dyn_need_f64(
|
|
flan_dyn_rem(flan_dyn_from_f64(7.5), flan_dyn_from_f64(2.0))) == 1.5,
|
|
"float %");
|
|
/* The boxed end of the range arithmetically, not only as a round trip. */
|
|
check(num(flan_dyn_add(flan_dyn_from_i64(140737488355327LL),
|
|
flan_dyn_from_i64(1))) == 140737488355328LL,
|
|
"+ crosses into the box");
|
|
|
|
/* Ordering. Numbers against numbers across the two tags, text bytewise, and
|
|
a NaN that is none of less, equal or greater. */
|
|
check(truth(flan_dyn_lt(flan_dyn_from_i64(1), flan_dyn_from_i64(2))), "<");
|
|
check(!truth(flan_dyn_lt(flan_dyn_from_i64(2), flan_dyn_from_i64(2))), "< eq");
|
|
check(truth(flan_dyn_le(flan_dyn_from_i64(2), flan_dyn_from_i64(2))), "<=");
|
|
check(truth(flan_dyn_gt(flan_dyn_from_f64(2.5), flan_dyn_from_i64(2))), ">");
|
|
check(truth(flan_dyn_ge(flan_dyn_from_i64(2), flan_dyn_from_f64(2.0))), ">=");
|
|
check(truth(flan_dyn_lt(text("abc"), text("abd"))), "< text");
|
|
check(truth(flan_dyn_lt(text("ab"), text("abc"))), "< text prefix");
|
|
check(!truth(flan_dyn_lt(text("abc"), text("abc"))), "< text equal");
|
|
{
|
|
flan_dyn n = flan_dyn_from_f64(0.0 / 0.0), one = flan_dyn_from_i64(1);
|
|
check(!truth(flan_dyn_lt(n, one)) && !truth(flan_dyn_gt(n, one))
|
|
&& !truth(flan_dyn_le(n, one)) && !truth(flan_dyn_ge(n, one)),
|
|
"nan is unordered in all four directions");
|
|
}
|
|
|
|
/* Equality. Structural, never a trap, and numeric across the tags. */
|
|
check(truth(flan_dyn_eq(flan_dyn_nil(), flan_dyn_nil())), "= nil");
|
|
check(truth(flan_dyn_eq(flan_dyn_from_bool(1), flan_dyn_from_bool(1))), "= bool");
|
|
check(!truth(flan_dyn_eq(flan_dyn_from_bool(1), flan_dyn_from_bool(0))), "= bool no");
|
|
check(truth(flan_dyn_eq(flan_dyn_from_i64(1), flan_dyn_from_f64(1.0))),
|
|
"= across int and float");
|
|
check(truth(flan_dyn_eq(flan_dyn_from_i64(INT64_MAX),
|
|
flan_dyn_from_i64(INT64_MAX))),
|
|
"= two boxed ints");
|
|
check(!truth(flan_dyn_eq(flan_dyn_from_i64(1), text("1"))),
|
|
"= on unrelated tags answers false rather than trapping");
|
|
check(!truth(flan_dyn_eq(flan_dyn_nil(), flan_dyn_from_bool(0))),
|
|
"nil is not false");
|
|
{
|
|
flan_dyn n = flan_dyn_from_f64(0.0 / 0.0);
|
|
check(!truth(flan_dyn_eq(n, n)), "nan is not equal to itself");
|
|
}
|
|
|
|
/* Text: identity is not equality, and equality is the bytes.
|
|
[a] and [b] are separately built from separate storage and must be equal;
|
|
they must also be *different objects*, because from_bytes copies and does
|
|
not intern, and a test that did not say so would pass against an
|
|
implementation that silently shared. */
|
|
a = text("hello");
|
|
b = text("hello");
|
|
c = text("hellp");
|
|
check(a != b, "two texts with the same bytes are two objects");
|
|
check(truth(flan_dyn_eq(a, b)), "= text is bytewise");
|
|
check(!truth(flan_dyn_eq(a, c)), "= text sees the last byte");
|
|
check(truth(flan_dyn_eq(a, a)), "= text against itself");
|
|
check(num(flan_dyn_len(a)) == 5, "len text");
|
|
check(num(flan_dyn_at(a, flan_dyn_from_i64(0))) == 'h', "at text");
|
|
check(num(flan_dyn_at(a, flan_dyn_from_i64(4))) == 'o', "at text last");
|
|
{
|
|
/* Embedded NUL, because a length-prefixed text is the claim and strlen is
|
|
how that claim gets quietly broken. */
|
|
flan_dyn z = flan_dyn_from_bytes((const uint8_t *)"a\0b", 3);
|
|
check(num(flan_dyn_len(z)) == 3, "len counts past a NUL");
|
|
check(num(flan_dyn_at(z, flan_dyn_from_i64(2))) == 'b', "at past a NUL");
|
|
check(!truth(flan_dyn_eq(z, text("a"))), "= does not stop at a NUL");
|
|
}
|
|
{
|
|
flan_dyn e = flan_dyn_from_bytes((const uint8_t *)"", 0);
|
|
check(num(flan_dyn_len(e)) == 0, "len of the empty text");
|
|
check(truth(flan_dyn_eq(e, flan_dyn_from_bytes(NULL, 0))),
|
|
"= two empty texts");
|
|
}
|
|
|
|
/* Vecs. */
|
|
v = flan_dyn_vec_new();
|
|
check(num(flan_dyn_len(v)) == 0, "len of a new vec");
|
|
flan_dyn_push(v, flan_dyn_from_i64(1));
|
|
flan_dyn_push(v, flan_dyn_from_i64(2));
|
|
flan_dyn_push(v, flan_dyn_from_i64(3));
|
|
check(num(flan_dyn_len(v)) == 3, "len after three pushes");
|
|
check(num(flan_dyn_at(v, flan_dyn_from_i64(1))) == 2, "at vec");
|
|
flan_dyn_set_at(v, flan_dyn_from_i64(1), text("two"));
|
|
check(truth(flan_dyn_eq(flan_dyn_at(v, flan_dyn_from_i64(1)), text("two"))),
|
|
"set-at vec");
|
|
{
|
|
/* Past the initial capacity, so the growth path runs and the elements
|
|
survive the realloc. */
|
|
int i;
|
|
flan_dyn big = flan_dyn_vec_new();
|
|
flan_dyn_root_push(&big);
|
|
for (i = 0; i < 100; i++) flan_dyn_push(big, flan_dyn_from_i64(i));
|
|
check(num(flan_dyn_len(big)) == 100, "len after a hundred pushes");
|
|
check(num(flan_dyn_at(big, flan_dyn_from_i64(0))) == 0, "first survived");
|
|
check(num(flan_dyn_at(big, flan_dyn_from_i64(99))) == 99, "last survived");
|
|
flan_dyn_root_pop(1);
|
|
}
|
|
|
|
/* Vecs compare structurally, element by element and one level down. */
|
|
{
|
|
flan_dyn p = flan_dyn_vec_new(), q = flan_dyn_vec_new();
|
|
flan_dyn_root_push(&p);
|
|
flan_dyn_root_push(&q);
|
|
flan_dyn_push(p, flan_dyn_from_i64(1));
|
|
flan_dyn_push(p, text("x"));
|
|
flan_dyn_push(q, flan_dyn_from_i64(1));
|
|
flan_dyn_push(q, text("x"));
|
|
check(p != q, "two vecs are two objects");
|
|
check(truth(flan_dyn_eq(p, q)), "= vec is element by element");
|
|
flan_dyn_push(q, flan_dyn_nil());
|
|
check(!truth(flan_dyn_eq(p, q)), "= vec sees the length");
|
|
flan_dyn_root_pop(2);
|
|
}
|
|
|
|
/* A vec that contains itself. [=] must answer rather than recurse for
|
|
ever — the identity shortcut is what makes it answer — and [print] must
|
|
stop at its depth cap. */
|
|
{
|
|
flan_dyn cyc = flan_dyn_vec_new();
|
|
flan_dyn_root_push(&cyc);
|
|
flan_dyn_push(cyc, flan_dyn_from_i64(1));
|
|
flan_dyn_push(cyc, cyc);
|
|
check(truth(flan_dyn_eq(cyc, cyc)), "= on a cycle answers");
|
|
show(cyc);
|
|
check(strlen(shown) > 0 && strstr(shown, "...") != NULL,
|
|
"print stops at its depth cap on a cycle");
|
|
flan_dyn_root_pop(1);
|
|
}
|
|
|
|
/* Printing, per tag, against what a Flan program prints for the
|
|
corresponding type. Captured from a run of `flan run`, not read off
|
|
lib/render.ml — the leading space before each element is what the slice
|
|
printer emits and what an acceptance test would compare against. */
|
|
prints(flan_dyn_nil(), "nil");
|
|
prints(flan_dyn_from_bool(1), "true");
|
|
prints(flan_dyn_from_bool(0), "false");
|
|
prints(flan_dyn_from_i64(42), "42");
|
|
prints(flan_dyn_from_i64(INT64_MIN), "-9223372036854775808");
|
|
prints(flan_dyn_from_f64(3.5), "3.5");
|
|
prints(flan_dyn_from_f64(1.0), "1");
|
|
prints(flan_dyn_from_f64(0.0 / 0.0), "nan");
|
|
prints(flan_dyn_from_f64(-(0.0 / 0.0)), "nan");
|
|
prints(text("hi"), "hi");
|
|
{
|
|
flan_dyn nums = flan_dyn_vec_new();
|
|
flan_dyn strs = flan_dyn_vec_new();
|
|
flan_dyn_root_push(&nums);
|
|
flan_dyn_root_push(&strs);
|
|
flan_dyn_push(nums, flan_dyn_from_i64(1));
|
|
flan_dyn_push(nums, flan_dyn_from_i64(2));
|
|
flan_dyn_push(nums, flan_dyn_from_i64(3));
|
|
prints(nums, "[ 1 2 3]");
|
|
/* A text inside a structure is quoted and escaped, and bare at the top
|
|
level. That is flan_rt.c's rule and the two have to agree, because the
|
|
REPL parses the printed form back. */
|
|
flan_dyn_push(strs, text("x"));
|
|
flan_dyn_push(strs, text("a b"));
|
|
flan_dyn_push(strs, text("q\"\n"));
|
|
prints(strs, "[ \"x\" \"a b\" \"q\\\"\\n\"]");
|
|
/* And a vec of vecs, nested twice. */
|
|
{
|
|
flan_dyn outer = flan_dyn_vec_new();
|
|
flan_dyn_root_push(&outer);
|
|
flan_dyn_push(outer, nums);
|
|
flan_dyn_push(outer, strs);
|
|
prints(outer, "[ [ 1 2 3] [ \"x\" \"a b\" \"q\\\"\\n\"]]");
|
|
flan_dyn_root_pop(1);
|
|
}
|
|
prints(flan_dyn_vec_new(), "[]");
|
|
flan_dyn_root_pop(2);
|
|
}
|
|
|
|
/* The typed boundary, on the tags it accepts. What it refuses is three
|
|
separate modes below — each one ends the process. */
|
|
check(flan_dyn_need_i64(flan_dyn_from_i64(-5)) == -5, "need-i64");
|
|
check(flan_dyn_need_f64(flan_dyn_from_f64(2.5)) == 2.5, "need-f64");
|
|
check(flan_dyn_need_bool(flan_dyn_from_bool(1)) == 1, "need-bool true");
|
|
check(flan_dyn_need_bool(flan_dyn_from_bool(0)) == 0, "need-bool false");
|
|
|
|
s = text("kept");
|
|
w = v;
|
|
flan_dyn_root_pop(6);
|
|
(void)w;
|
|
(void)s;
|
|
}
|
|
|
|
/* A typed container's own header, restated a third time — flan_rt.c's
|
|
* [flan_vec], flan_dyn.c's [flan_dyn_vec_hdr], and this. The three must
|
|
* agree on layout, and none of them can [#include] another's to say so at
|
|
* compile time (see [Build.compile_c]) — so the header below is built by
|
|
* hand, the same five words [flan_vec_grow] would leave behind after a few
|
|
* pushes. flan_rt.c IS linked into this binary (the [flan_vec_free] and
|
|
* [flan_vec_layout] calls below are its), but without its header there is
|
|
* no [flan_vec] type to declare an initialiser over; the restatement is
|
|
* what the [#include] restriction costs, not a missing link.
|
|
*
|
|
* What actually ties the three together is [layout], further down: it reads
|
|
* flan_rt.c's [flan_vec_layout] and flan_dyn.c's [flan_dyn_vec_hdr_layout]
|
|
* and compares both against [offsetof] on this very struct, so a field
|
|
* reordered in any one of the three is a FAIL line here rather than a
|
|
* silent corruption at whatever call site next dereferences the wrong
|
|
* offset. A declared-as-[void*] prototype on its own proves nothing about
|
|
* layout — it was named as if it did in an earlier version of this
|
|
* comment, which was wrong, and [layout] is what makes the claim true. */
|
|
typedef struct {
|
|
void *ptr;
|
|
int64_t len;
|
|
int64_t cap;
|
|
void *alloc;
|
|
int64_t epoch;
|
|
} hand_vec;
|
|
|
|
/* The runtime's half of M2 item 3: a typed container crossing into dyn as a
|
|
* view, driven directly with no compiler in the loop — [flan_dyn_view_vec]
|
|
* and [flan_dyn_view_flat] built by hand over a [hand_vec] and a plain
|
|
* array, exactly as the checker's [box] will build them over a real [(Vec
|
|
* i64)] and a real [[4]i64]. */
|
|
static void view(void) {
|
|
int64_t buf[4] = { 10, 20, 30, 40 };
|
|
flan_dyn flat = flan_dyn_nil(), vv = flan_dyn_nil();
|
|
flan_dyn_root_push(&flat);
|
|
flan_dyn_root_push(&vv);
|
|
|
|
/* A flat view over a fixed array: reads box, writes tag-check, and the
|
|
storage really is the array's own — a write through the view is read
|
|
back through the C array with no call into this file at all. */
|
|
flat = flan_dyn_view_flat(buf, 4, FLAN_VIEW_I64);
|
|
check(flan_dyn_tag(flat) == FLAN_DYN_TAG_VEC, "a view tags as a vec");
|
|
check(num(flan_dyn_len(flat)) == 4, "flat view len");
|
|
check(num(flan_dyn_at(flat, flan_dyn_from_i64(2))) == 30, "flat view at");
|
|
flan_dyn_set_at(flat, flan_dyn_from_i64(2), flan_dyn_from_i64(99));
|
|
check(buf[2] == 99, "flat view write reaches the array");
|
|
buf[3] = 7;
|
|
check(num(flan_dyn_at(flat, flan_dyn_from_i64(3))) == 7,
|
|
"the array's own write reaches the view — it is not a copy");
|
|
prints(flat, "[ 10 20 99 7]");
|
|
|
|
/* Structural equality, view-aware — review's third finding. [dyn_equal]'s
|
|
VEC arm used to read [x->len]/[x->u.v.items] regardless of kind, which
|
|
for a view answers 0 and garbage: two views with different contents
|
|
compared equal, a view and an equal heap vec compared unequal, and a
|
|
map keyed by any view collided with every other view. [buf] now reads
|
|
[ 10 20 99 7]; [same] is a second, independent view over the identical
|
|
bytes, and [other] a view over one differing element. */
|
|
{
|
|
int64_t same_buf[4] = { 10, 20, 99, 7 };
|
|
int64_t diff_buf[4] = { 10, 20, 99, 8 };
|
|
flan_dyn same = flan_dyn_view_flat(same_buf, 4, FLAN_VIEW_I64);
|
|
flan_dyn other = flan_dyn_view_flat(diff_buf, 4, FLAN_VIEW_I64);
|
|
flan_dyn heap = flan_dyn_vec_new();
|
|
flan_dyn_root_push(&same);
|
|
flan_dyn_root_push(&other);
|
|
flan_dyn_root_push(&heap);
|
|
check(truth(flan_dyn_eq(flat, same)),
|
|
"two views over equal bytes are equal");
|
|
check(!truth(flan_dyn_eq(flat, other)),
|
|
"two views over different bytes are not equal");
|
|
flan_dyn_push(heap, flan_dyn_from_i64(10));
|
|
flan_dyn_push(heap, flan_dyn_from_i64(20));
|
|
flan_dyn_push(heap, flan_dyn_from_i64(99));
|
|
flan_dyn_push(heap, flan_dyn_from_i64(7));
|
|
check(truth(flan_dyn_eq(flat, heap)),
|
|
"a view and an equal heap vec are equal");
|
|
flan_dyn_set_at(heap, flan_dyn_from_i64(3), flan_dyn_from_i64(0));
|
|
check(!truth(flan_dyn_eq(flat, heap)),
|
|
"a view and a differing heap vec are not equal");
|
|
flan_dyn_root_pop(3);
|
|
}
|
|
|
|
/* A vec view: points at the header's own address, so a push that grows
|
|
and moves it is seen on the very next read — there is no snapshot to
|
|
go stale. */
|
|
{
|
|
hand_vec hv;
|
|
hv.ptr = NULL; hv.len = 0; hv.cap = 0; hv.alloc = NULL; hv.epoch = 0;
|
|
vv = flan_dyn_view_vec(&hv, FLAN_VIEW_I64);
|
|
check(num(flan_dyn_len(vv)) == 0, "vec view starts empty");
|
|
{
|
|
int i;
|
|
for (i = 0; i < 20; i++) flan_dyn_push(vv, flan_dyn_from_i64(i));
|
|
}
|
|
check(num(flan_dyn_len(vv)) == 20, "vec view len after growth");
|
|
check(num(flan_dyn_at(vv, flan_dyn_from_i64(0))) == 0,
|
|
"first element survived the growth and the move");
|
|
check(num(flan_dyn_at(vv, flan_dyn_from_i64(19))) == 19,
|
|
"pushed element reachable after the header's ptr moved");
|
|
/* [hv]'s own fields moved under the view's feet, by construction — the
|
|
view never captured [hv.ptr]; it captured [&hv]. */
|
|
check(hv.len == 20 && hv.cap >= 20, "the hand-built header itself grew");
|
|
flan_dyn_set_at(vv, flan_dyn_from_i64(0), flan_dyn_from_i64(-1));
|
|
check(((int64_t *)hv.ptr)[0] == -1, "write through the view reaches hv");
|
|
flan_vec_free(&hv, 8, 8, (const uint8_t *)"view", 4);
|
|
}
|
|
|
|
/* A bool view and a float view, so the element-tag dispatch is exercised
|
|
on all three kinds and not only i64. */
|
|
{
|
|
uint8_t bools[2] = { 1, 0 };
|
|
double floats[2] = { 1.5, -2.0 };
|
|
flan_dyn bv = flan_dyn_view_flat(bools, 2, FLAN_VIEW_BOOL);
|
|
flan_dyn fv = flan_dyn_view_flat(floats, 2, FLAN_VIEW_F64);
|
|
check(truth(flan_dyn_at(bv, flan_dyn_from_i64(0))), "bool view at true");
|
|
check(!truth(flan_dyn_at(bv, flan_dyn_from_i64(1))), "bool view at false");
|
|
flan_dyn_set_at(bv, flan_dyn_from_i64(1), flan_dyn_from_bool(1));
|
|
check(bools[1] == 1, "bool view write");
|
|
check(flan_dyn_need_f64(flan_dyn_at(fv, flan_dyn_from_i64(0))) == 1.5,
|
|
"float view at");
|
|
flan_dyn_set_at(fv, flan_dyn_from_i64(0), flan_dyn_from_f64(3.25));
|
|
check(floats[0] == 3.25, "float view write");
|
|
}
|
|
|
|
flan_dyn_root_pop(2);
|
|
printf(failures == 0 ? "view ok\n" : "view failed\n");
|
|
}
|
|
|
|
/* Every wrong way to use a view: out of range, a mismatched write on each of
|
|
* the three element kinds, and a push against a fixed-size (flat) view. Each
|
|
* is its own mode because each ends the process. */
|
|
static void refuse_view(const char *what) {
|
|
static int64_t buf[2] = { 1, 2 };
|
|
flan_dyn v;
|
|
if (strcmp(what, "range") == 0) {
|
|
v = flan_dyn_view_flat(buf, 2, FLAN_VIEW_I64);
|
|
(void)flan_dyn_at(v, flan_dyn_from_i64(2));
|
|
} else if (strcmp(what, "wrongwrite") == 0) {
|
|
v = flan_dyn_view_flat(buf, 2, FLAN_VIEW_I64);
|
|
flan_dyn_set_at(v, flan_dyn_from_i64(0), text("nope"));
|
|
} else if (strcmp(what, "wrongbool") == 0) {
|
|
static uint8_t bb[1];
|
|
v = flan_dyn_view_flat(bb, 1, FLAN_VIEW_BOOL);
|
|
flan_dyn_set_at(v, flan_dyn_from_i64(0), flan_dyn_from_i64(1));
|
|
} else if (strcmp(what, "wrongfloat") == 0) {
|
|
static double ff[1];
|
|
v = flan_dyn_view_flat(ff, 1, FLAN_VIEW_F64);
|
|
flan_dyn_set_at(v, flan_dyn_from_i64(0), flan_dyn_from_i64(1));
|
|
} else if (strcmp(what, "flatpush") == 0) {
|
|
v = flan_dyn_view_flat(buf, 2, FLAN_VIEW_I64);
|
|
flan_dyn_push(v, flan_dyn_from_i64(9));
|
|
} else {
|
|
printf("no such refusal: %s\n", what);
|
|
exit(2);
|
|
}
|
|
printf("did not trap\n");
|
|
exit(3);
|
|
}
|
|
|
|
/* The three restatements of flan_vec's layout, compared — see [hand_vec]'s
|
|
* comment for why nothing at compile time otherwise ties them together.
|
|
* [offsetof] on [hand_vec] itself is this file's half; [flan_vec_layout]
|
|
* and [flan_dyn_vec_hdr_layout] are the other two's. */
|
|
static void layout(void) {
|
|
int64_t rt[6], dyn[6];
|
|
int64_t here[6] = {
|
|
(int64_t)sizeof(hand_vec),
|
|
(int64_t)offsetof(hand_vec, ptr),
|
|
(int64_t)offsetof(hand_vec, len),
|
|
(int64_t)offsetof(hand_vec, cap),
|
|
(int64_t)offsetof(hand_vec, alloc),
|
|
(int64_t)offsetof(hand_vec, epoch)
|
|
};
|
|
static const char *const names[6] =
|
|
{ "sizeof", "offset of ptr", "offset of len", "offset of cap",
|
|
"offset of alloc", "offset of epoch" };
|
|
int i;
|
|
char msg[128];
|
|
flan_vec_layout(rt);
|
|
flan_dyn_vec_hdr_layout(dyn);
|
|
for (i = 0; i < 6; i++) {
|
|
if (rt[i] != here[i]) {
|
|
snprintf(msg, sizeof msg, "flan_vec vs. hand_vec's %s: %lld vs. %lld",
|
|
names[i], (long long)rt[i], (long long)here[i]);
|
|
fail(msg);
|
|
}
|
|
if (dyn[i] != here[i]) {
|
|
snprintf(msg, sizeof msg,
|
|
"flan_dyn_vec_hdr vs. hand_vec's %s: %lld vs. %lld",
|
|
names[i], (long long)dyn[i], (long long)here[i]);
|
|
fail(msg);
|
|
}
|
|
}
|
|
printf(failures == 0 ? "layout ok\n" : "layout failed\n");
|
|
}
|
|
|
|
/* ── The collector ─────────────────────────────────────────────────────*/
|
|
|
|
/* Allocate a great many, hold a few, and assert the heap does not grow. The
|
|
* live set is a hundred texts in a rooted vec, rewritten round and round; the
|
|
* million texts that fall out of it have nothing pointing at them from the
|
|
* moment the next iteration overwrites their slot.
|
|
*
|
|
* The assertion is on the *bound* and not on any particular number: a
|
|
* collector's exact high-water mark is a fact about its trigger, and pinning
|
|
* it would make a tuning change a test failure. What must hold is that the
|
|
* figure stops climbing — that is the whole claim — so the test takes the
|
|
* heap's high-water mark over the run and requires it to be within a small
|
|
* multiple of what the live set actually needs.
|
|
*
|
|
* The floor is dropped to 64K first. A megabyte of floor would make this a
|
|
* megabyte of arithmetic before the first collection and prove nothing faster.
|
|
*/
|
|
static void gc(void) {
|
|
enum { LIVE = 100, ROUNDS = 1000000 };
|
|
flan_dyn keep = flan_dyn_nil();
|
|
int64_t peak = 0, settled, i;
|
|
int collections_happened;
|
|
|
|
flan_gc_init();
|
|
flan_gc_set_floor(64 * 1024);
|
|
flan_dyn_root_push(&keep);
|
|
keep = flan_dyn_vec_new();
|
|
for (i = 0; i < LIVE; i++) flan_dyn_push(keep, flan_dyn_nil());
|
|
|
|
for (i = 0; i < ROUNDS; i++) {
|
|
char buf[32];
|
|
int n = snprintf(buf, sizeof buf, "item-%lld", (long long)i);
|
|
flan_dyn_set_at(keep, flan_dyn_from_i64(i % LIVE),
|
|
flan_dyn_from_bytes((const uint8_t *)buf, n));
|
|
if (flan_gc_live_bytes() > peak) peak = flan_gc_live_bytes();
|
|
}
|
|
|
|
flan_gc_collect();
|
|
settled = flan_gc_live_bytes();
|
|
collections_happened = flan_gc_count() < LIVE + 64 + 8;
|
|
|
|
/* A million texts of forty-odd bytes is some forty megabytes allocated. A
|
|
heap that never collected would hold all of it, so any bound under a
|
|
megabyte is a bound only a working collector can meet, and 512K is
|
|
comfortably above twice the live set plus the floor plus the ring. */
|
|
printf("peak under 512K: %s\n", peak < 512 * 1024 ? "yes" : "no");
|
|
printf("settled under 32K: %s\n", settled < 32 * 1024 ? "yes" : "no");
|
|
printf("live objects bounded: %s\n", collections_happened ? "yes" : "no");
|
|
|
|
/* And the live set is intact: collecting a million times must not have lost
|
|
the hundred things that were rooted throughout. */
|
|
{
|
|
int intact = 1;
|
|
for (i = 0; i < LIVE; i++) {
|
|
char buf[32];
|
|
int64_t k = ROUNDS - LIVE + i;
|
|
int n = snprintf(buf, sizeof buf, "item-%lld", (long long)k);
|
|
flan_dyn got = flan_dyn_at(keep, flan_dyn_from_i64(k % LIVE));
|
|
if (!flan_dyn_need_bool(
|
|
flan_dyn_eq(got, flan_dyn_from_bytes((const uint8_t *)buf, n))))
|
|
intact = 0;
|
|
}
|
|
printf("live set intact: %s\n", intact ? "yes" : "no");
|
|
}
|
|
flan_dyn_root_pop(1);
|
|
}
|
|
|
|
/* Nested vecs, traced. A chain sixty-four deep reached through one root: every
|
|
* link has to survive a collection, which is what says the marker follows a
|
|
* vec's elements and not only its header. Sixty-four is also past the point
|
|
* where a recursive marker on a modest stack would be fine and a deeper one
|
|
* would not — the marker here is iterative, and this is the case that would
|
|
* notice if it stopped being. */
|
|
static void nested(void) {
|
|
enum { DEEP = 64 };
|
|
flan_dyn root = flan_dyn_nil(), cur;
|
|
int64_t i;
|
|
int ok = 1;
|
|
|
|
flan_gc_init();
|
|
flan_gc_set_floor(16 * 1024);
|
|
flan_dyn_root_push(&root);
|
|
root = flan_dyn_vec_new();
|
|
cur = root;
|
|
for (i = 0; i < DEEP; i++) {
|
|
flan_dyn inner = flan_dyn_vec_new();
|
|
flan_dyn_push(cur, flan_dyn_from_i64(i));
|
|
flan_dyn_push(cur, inner);
|
|
cur = inner;
|
|
}
|
|
flan_dyn_push(cur, text("bottom"));
|
|
|
|
/* Churn, so that collections certainly happen with the chain live, and then
|
|
one more by hand. */
|
|
for (i = 0; i < 20000; i++) (void)text("noise");
|
|
flan_gc_collect();
|
|
|
|
cur = root;
|
|
for (i = 0; i < DEEP; i++) {
|
|
if (flan_dyn_need_i64(flan_dyn_at(cur, flan_dyn_from_i64(0))) != i) ok = 0;
|
|
cur = flan_dyn_at(cur, flan_dyn_from_i64(1));
|
|
}
|
|
if (!flan_dyn_need_bool(
|
|
flan_dyn_eq(flan_dyn_at(cur, flan_dyn_from_i64(0)), text("bottom"))))
|
|
ok = 0;
|
|
printf("chain of %d intact: %s\n", DEEP, ok ? "yes" : "no");
|
|
flan_dyn_root_pop(1);
|
|
}
|
|
|
|
/* Interior sharing: one vec held twice, and a text held from two places.
|
|
* Three things have to be true and none of them follows from the others —
|
|
* the shared object is swept once and not twice (a double free would show as
|
|
* a crash or as a live-bytes figure that went negative), a write through one
|
|
* path is visible through the other (it is one object, not a copy), and
|
|
* dropping one of the two references does not collect it. */
|
|
static void sharing(void) {
|
|
flan_dyn holder = flan_dyn_nil(), shared = flan_dyn_nil();
|
|
flan_dyn was;
|
|
int i;
|
|
|
|
flan_gc_init();
|
|
flan_gc_set_floor(16 * 1024);
|
|
flan_dyn_root_push(&holder);
|
|
flan_dyn_root_push(&shared);
|
|
|
|
holder = flan_dyn_vec_new();
|
|
shared = flan_dyn_vec_new();
|
|
flan_dyn_push(shared, text("a"));
|
|
flan_dyn_push(holder, shared);
|
|
flan_dyn_push(holder, shared);
|
|
flan_dyn_push(holder, shared);
|
|
|
|
/* Three slots, one object. Identity and not equality: two vecs holding the
|
|
same text are equal and are still two vecs, so a structural test would
|
|
pass against an implementation that had quietly copied. The dyn word of a
|
|
vec *is* its address, so comparing the words is comparing the objects. */
|
|
printf("three slots hold one object: %s\n",
|
|
flan_dyn_at(holder, flan_dyn_from_i64(0))
|
|
== flan_dyn_at(holder, flan_dyn_from_i64(2))
|
|
? "yes" : "no");
|
|
|
|
/* And writing through one path is read through another. */
|
|
flan_dyn_set_at(flan_dyn_at(holder, flan_dyn_from_i64(0)),
|
|
flan_dyn_from_i64(0), text("b"));
|
|
printf("write through one path is seen through another: %s\n",
|
|
flan_dyn_need_bool(
|
|
flan_dyn_eq(flan_dyn_at(flan_dyn_at(holder, flan_dyn_from_i64(2)),
|
|
flan_dyn_from_i64(0)),
|
|
text("b")))
|
|
? "yes" : "no");
|
|
|
|
/* Dropping the direct root leaves it reachable through the holder, three
|
|
times over. It must still be there, at the same address — a collector
|
|
that swept it and handed the space to something else would answer this
|
|
with a different word, and one that swept it twice would be caught by the
|
|
sanitizer sweep rather than by an assertion. The churn in between is what
|
|
makes the collection real rather than a formality. */
|
|
was = shared;
|
|
shared = flan_dyn_nil();
|
|
for (i = 0; i < 5000; i++) (void)text("noise");
|
|
flan_gc_collect();
|
|
printf("shared object survives on the holder alone: %s\n",
|
|
flan_dyn_at(holder, flan_dyn_from_i64(1)) == was ? "yes" : "no");
|
|
printf("still reachable: %s\n",
|
|
flan_dyn_need_bool(
|
|
flan_dyn_eq(flan_dyn_at(flan_dyn_at(holder, flan_dyn_from_i64(1)),
|
|
flan_dyn_from_i64(0)),
|
|
text("b")))
|
|
? "yes" : "no");
|
|
|
|
/* And dropping the holder collects the lot, once. A double free of the
|
|
thrice-held vec is what this is really asking about: it would crash here,
|
|
or under @sanitize, or leave the byte count below zero. */
|
|
holder = flan_dyn_nil();
|
|
was = flan_dyn_nil();
|
|
for (i = 0; i < 5000; i++) (void)text("noise");
|
|
flan_gc_collect();
|
|
printf("live bytes after dropping everything: %s\n",
|
|
flan_gc_live_bytes() >= 0 && flan_gc_count() <= 64 ? "ok" : "wrong");
|
|
flan_dyn_root_pop(2);
|
|
}
|
|
|
|
/* An unrooted object is collected. The positive control for every assertion
|
|
* above: without this, a collector that never freed anything would pass the
|
|
* lot. The text is allocated, its object count noted, and then enough
|
|
* allocation happens to push it out of the temporaries ring — after which a
|
|
* collection must reclaim it. */
|
|
static void unrooted(void) {
|
|
int64_t before, after;
|
|
int i;
|
|
flan_gc_init();
|
|
flan_gc_set_floor(1 << 20); /* high, so only the explicit collect sweeps */
|
|
flan_gc_collect();
|
|
before = flan_gc_count();
|
|
for (i = 0; i < 500; i++) (void)text("garbage");
|
|
printf("allocated: %s\n", flan_gc_count() >= before + 500 ? "yes" : "no");
|
|
flan_gc_collect();
|
|
after = flan_gc_count();
|
|
/* The ring holds the last 64, by design, so the survivors are bounded by it
|
|
and not by zero. */
|
|
printf("reclaimed all but the ring: %s\n",
|
|
after <= before + 64 ? "yes" : "no");
|
|
}
|
|
|
|
/* An aggregate root: a struct with dyn fields somewhere inside it, rooted by
|
|
* its address and a descriptor rather than word by word. This is the runtime's
|
|
* half of the per-type descriptors, exercised with the descriptor written out
|
|
* here by hand — the compiler emits the same three words as static data, and a
|
|
* runtime that read them wrongly would be wrong in both lanes at once.
|
|
*
|
|
* The shape deliberately has a gap and a nesting in it: a header word that is
|
|
* not a dyn, an inner struct that carries one, and a trailing one.
|
|
*
|
|
* Two assertions, and the second is the one that tells an offset-driven marker
|
|
* from a word-driven one. The first — that the named fields survive — passes
|
|
* under both, because a marker that walked every word of the struct would keep
|
|
* them too. So [hidden] sits at an offset the descriptor does not name and
|
|
* holds five hundred objects: an offset-driven marker lets the lot go, and a
|
|
* word-driven one keeps them. The count is what says which happened. */
|
|
typedef struct {
|
|
int64_t n;
|
|
flan_dyn label;
|
|
struct { int32_t k; flan_dyn note; } inner;
|
|
flan_dyn tail;
|
|
flan_dyn hidden; /* deliberately absent from [offs] below */
|
|
} desc_row;
|
|
|
|
static void desc(void) {
|
|
static const int64_t offs[] = {
|
|
(int64_t)offsetof(desc_row, label),
|
|
(int64_t)offsetof(desc_row, inner.note),
|
|
(int64_t)offsetof(desc_row, tail)
|
|
};
|
|
static const flan_desc row_desc = {
|
|
(int64_t)sizeof(desc_row), 3, offs
|
|
};
|
|
desc_row row;
|
|
flan_dyn was_label, was_note;
|
|
int64_t before;
|
|
int i, ok = 1;
|
|
|
|
flan_gc_init();
|
|
flan_gc_set_floor(16 * 1024);
|
|
|
|
/* The contract the header states: the dyn words must hold valid values
|
|
before the push. Zero satisfies it; the header word need not. */
|
|
row.label = flan_dyn_nil();
|
|
row.inner.note = flan_dyn_nil();
|
|
row.tail = flan_dyn_nil();
|
|
row.hidden = flan_dyn_nil();
|
|
row.n = 0;
|
|
row.inner.k = 7;
|
|
flan_dyn_root_push_desc(&row, &row_desc);
|
|
|
|
row.label = flan_dyn_vec_new();
|
|
flan_dyn_push(row.label, text("held"));
|
|
row.inner.note = text("nested");
|
|
row.tail = flan_dyn_vec_new();
|
|
flan_dyn_push(row.tail, flan_dyn_from_i64(99));
|
|
was_label = row.label;
|
|
was_note = row.inner.note;
|
|
|
|
for (i = 0; i < 20000; i++) (void)text("noise");
|
|
flan_gc_collect();
|
|
|
|
if (row.label != was_label || row.inner.note != was_note) ok = 0;
|
|
if (!flan_dyn_need_bool(
|
|
flan_dyn_eq(flan_dyn_at(row.label, flan_dyn_from_i64(0)),
|
|
text("held")))) ok = 0;
|
|
if (!flan_dyn_need_bool(flan_dyn_eq(row.inner.note, text("nested")))) ok = 0;
|
|
if (flan_dyn_need_i64(flan_dyn_at(row.tail, flan_dyn_from_i64(0))) != 99)
|
|
ok = 0;
|
|
printf("aggregate root survives collection: %s\n", ok ? "yes" : "no");
|
|
|
|
/* And the half that discriminates. [hidden] is a dyn word inside the very
|
|
object the collector was handed, at an offset the descriptor leaves out,
|
|
and it holds five hundred and one objects. A marker that walked the struct
|
|
rather than the offsets would keep every one of them; one that reads the
|
|
offsets it was given lets them go. The ring holds the last sixty-four
|
|
allocations unconditionally, so the noise below is what puts them out of
|
|
its reach, and sixty-four is the slack the test allows. */
|
|
flan_gc_collect();
|
|
before = flan_gc_count();
|
|
/* Built under a dyn root of its own, because the descriptor does not name
|
|
this word and a collection in the middle of five hundred pushes would
|
|
free what the next push writes into. The pop is what leaves it reachable
|
|
from the unnamed offset and from nowhere else, which is the state the
|
|
question is about. */
|
|
flan_dyn_root_push(&row.hidden);
|
|
row.hidden = flan_dyn_vec_new();
|
|
for (i = 0; i < 500; i++) flan_dyn_push(row.hidden, text("hidden"));
|
|
flan_dyn_root_pop(1);
|
|
for (i = 0; i < 200; i++) (void)text("noise");
|
|
flan_gc_collect();
|
|
printf("a dyn at an offset the descriptor omits is not marked: %s\n",
|
|
flan_gc_count() <= before + 64 ? "yes" : "no");
|
|
|
|
/* And the positive control, which is the same one [unrooted] makes: drop the
|
|
fields and the objects go. One pop takes the aggregate entry off exactly
|
|
as it takes a dyn one off, which is what keeps a function's pop a count. */
|
|
row.label = flan_dyn_nil();
|
|
row.inner.note = flan_dyn_nil();
|
|
row.tail = flan_dyn_nil();
|
|
row.hidden = flan_dyn_nil();
|
|
flan_dyn_root_pop(1);
|
|
for (i = 0; i < 5000; i++) (void)text("noise");
|
|
flan_gc_collect();
|
|
printf("and is reclaimed once dropped: %s\n",
|
|
flan_gc_count() <= 128 ? "yes" : "no");
|
|
}
|
|
|
|
/* ── The refusals ──────────────────────────────────────────────────────
|
|
*
|
|
* One per mode, because each ends the process. The driver asserts on the
|
|
* sentence as well as on the exit status: a process that died some other way
|
|
* is not this guard firing, and the status alone cannot tell them apart. */
|
|
|
|
static void refuse(const char *what) {
|
|
flan_dyn v = flan_dyn_vec_new();
|
|
flan_dyn t = text("hi");
|
|
if (strcmp(what, "add") == 0) (void)flan_dyn_add(flan_dyn_from_i64(3), t);
|
|
else if (strcmp(what, "sub") == 0)
|
|
(void)flan_dyn_sub(flan_dyn_nil(), flan_dyn_from_i64(1));
|
|
else if (strcmp(what, "mul") == 0)
|
|
(void)flan_dyn_mul(flan_dyn_from_bool(1), flan_dyn_from_i64(2));
|
|
else if (strcmp(what, "div") == 0)
|
|
(void)flan_dyn_div(v, flan_dyn_from_i64(2));
|
|
else if (strcmp(what, "rem") == 0)
|
|
(void)flan_dyn_rem(flan_dyn_from_i64(2), flan_dyn_nil());
|
|
else if (strcmp(what, "divzero") == 0)
|
|
(void)flan_dyn_div(flan_dyn_from_i64(1), flan_dyn_from_i64(0));
|
|
else if (strcmp(what, "remzero") == 0)
|
|
(void)flan_dyn_rem(flan_dyn_from_i64(1), flan_dyn_from_i64(0));
|
|
else if (strcmp(what, "divover") == 0)
|
|
(void)flan_dyn_div(flan_dyn_from_i64(INT64_MIN), flan_dyn_from_i64(-1));
|
|
else if (strcmp(what, "lt") == 0)
|
|
(void)flan_dyn_lt(flan_dyn_from_i64(1), t);
|
|
else if (strcmp(what, "le") == 0) (void)flan_dyn_le(t, flan_dyn_nil());
|
|
else if (strcmp(what, "gt") == 0) (void)flan_dyn_gt(v, v);
|
|
else if (strcmp(what, "ge") == 0)
|
|
(void)flan_dyn_ge(flan_dyn_from_bool(0), flan_dyn_from_bool(1));
|
|
else if (strcmp(what, "len") == 0) (void)flan_dyn_len(flan_dyn_from_i64(1));
|
|
else if (strcmp(what, "at") == 0)
|
|
(void)flan_dyn_at(flan_dyn_from_i64(3), flan_dyn_from_i64(0));
|
|
else if (strcmp(what, "atindex") == 0) (void)flan_dyn_at(t, t);
|
|
else if (strcmp(what, "atrange") == 0)
|
|
(void)flan_dyn_at(t, flan_dyn_from_i64(9));
|
|
else if (strcmp(what, "atnegative") == 0)
|
|
(void)flan_dyn_at(t, flan_dyn_from_i64(-1));
|
|
else if (strcmp(what, "setattext") == 0)
|
|
flan_dyn_set_at(t, flan_dyn_from_i64(0), flan_dyn_from_i64(65));
|
|
else if (strcmp(what, "setatnotvec") == 0)
|
|
flan_dyn_set_at(flan_dyn_from_i64(1), flan_dyn_from_i64(0), t);
|
|
else if (strcmp(what, "setatrange") == 0)
|
|
flan_dyn_set_at(v, flan_dyn_from_i64(0), t);
|
|
else if (strcmp(what, "push") == 0) flan_dyn_push(t, flan_dyn_from_i64(1));
|
|
else if (strcmp(what, "needi64") == 0) (void)flan_dyn_need_i64(t);
|
|
else if (strcmp(what, "needf64") == 0)
|
|
(void)flan_dyn_need_f64(flan_dyn_from_i64(1));
|
|
else if (strcmp(what, "needbool") == 0)
|
|
(void)flan_dyn_need_bool(flan_dyn_nil());
|
|
else {
|
|
printf("no such refusal: %s\n", what);
|
|
exit(2);
|
|
}
|
|
/* Reached only if the operation returned, which is the failure this mode is
|
|
testing for. */
|
|
printf("did not trap\n");
|
|
exit(3);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
flan_rt_init(argc, argv);
|
|
if (argc < 2) {
|
|
printf("usage: dyn_ops <mode>\n");
|
|
return 2;
|
|
}
|
|
if (strcmp(argv[1], "ops") == 0) {
|
|
ops();
|
|
printf(failures == 0 ? "ops ok\n" : "ops failed\n");
|
|
return failures == 0 ? 0 : 1;
|
|
}
|
|
if (strcmp(argv[1], "gc") == 0) { gc(); return 0; }
|
|
if (strcmp(argv[1], "nested") == 0) { nested(); return 0; }
|
|
if (strcmp(argv[1], "sharing") == 0) { sharing(); return 0; }
|
|
if (strcmp(argv[1], "unrooted") == 0) { unrooted(); return 0; }
|
|
if (strcmp(argv[1], "desc") == 0) { desc(); return 0; }
|
|
if (strcmp(argv[1], "view") == 0) {
|
|
view();
|
|
return failures == 0 ? 0 : 1;
|
|
}
|
|
if (strcmp(argv[1], "layout") == 0) {
|
|
layout();
|
|
return failures == 0 ? 0 : 1;
|
|
}
|
|
if (strncmp(argv[1], "refuse:", 7) == 0) { refuse(argv[1] + 7); return 0; }
|
|
if (strncmp(argv[1], "refuseview:", 11) == 0) {
|
|
refuse_view(argv[1] + 11);
|
|
return 0;
|
|
}
|
|
printf("no such mode: %s\n", argv[1]);
|
|
return 2;
|
|
}
|