The length write-back moves back inside the seqlock odd window
end_value did the release store and left the caller to narrow its length afterwards, which is a length a reader is entitled to have missed. Split into truncate_value and close_value so flan_dev_watch_end can store its 32-bit length between them. Benign today -- only a full slot is truncated and a full slot length is already WATCH_VAL -- and not a rule anyone would keep. The two externs flan_dev.c borrows from flan_rt.c now get a value probe, once per process on the first render, the way flan_vec_layout ties the three statements of the vec header together: nothing else compares those prototypes and the link matches names, not types.
This commit is contained in:
parent
2daf7e9164
commit
6461322aa9
@ -246,12 +246,6 @@ static void put_i64(sink out, int64_t x) {
|
|||||||
* level rather than on the number. */
|
* level rather than on the number. */
|
||||||
extern int flan_f64_format(double x, char *buf, size_t cap);
|
extern int flan_f64_format(double x, char *buf, size_t cap);
|
||||||
|
|
||||||
static void put_f64(sink out, double x) {
|
|
||||||
char buf[64];
|
|
||||||
flan_f64_format(x, buf, sizeof buf);
|
|
||||||
put(out, buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quoted and escaped, in C, because doing it in the generated IR would be a
|
/* Quoted and escaped, in C, because doing it in the generated IR would be a
|
||||||
* loop per string and the language has no allocator to build the result in.
|
* loop per string and the language has no allocator to build the result in.
|
||||||
* A string whose content is not escaped does not round-trip and reads as a
|
* A string whose content is not escaped does not round-trip and reads as a
|
||||||
@ -259,11 +253,55 @@ static void put_f64(sink out, double x) {
|
|||||||
* string would become a second row of the table on the wire.
|
* string would become a second row of the table on the wire.
|
||||||
*
|
*
|
||||||
* The table is flan_rt.c's [flan_escape_char], which is also what println's
|
* The table is flan_rt.c's [flan_escape_char], which is also what println's
|
||||||
* [flan_escape_bytes] escapes through. One table, three framings of it. */
|
* [flan_escape_bytes] escapes through: one table, and the two callers differ
|
||||||
|
* only in how they frame what comes out of it. flan_dyn.c has a copy of the
|
||||||
|
* table rather than a call to it, deliberately and for a reason argued there
|
||||||
|
* and in docs/SPIKE-DUPLICITY.md §9. */
|
||||||
extern int flan_escape_char(unsigned char c, char *out);
|
extern int flan_escape_char(unsigned char c, char *out);
|
||||||
|
|
||||||
|
/* The two [extern]s above are this file's hand copies of prototypes flan_rt.c
|
||||||
|
* owns, and nothing in the build compares the two: each translation unit is
|
||||||
|
* compiled on its own with no include path (see [Build.compile_c]), and the
|
||||||
|
* link that joins them matches names and not types. A parameter added on one
|
||||||
|
* side, a [size_t] cap that becomes an [int], a return that stops being a
|
||||||
|
* length — all of those build clean and then go wrong here, inside a render,
|
||||||
|
* with nothing pointing at the cause.
|
||||||
|
*
|
||||||
|
* So they are checked the way the tree checks its other cross-file agreement
|
||||||
|
* it cannot #include its way out of — by value, at run time, loudly. That is
|
||||||
|
* [flan_vec_layout] and test/dyn_ops.c's "layout" mode for the vec header;
|
||||||
|
* this is the same idea one function wide. Two calls with known answers, once
|
||||||
|
* per process, on the first value this file renders: if the callee is not the
|
||||||
|
* function these declarations describe, the answers do not come back right
|
||||||
|
* and the process stops here rather than emitting a wrong wire format.
|
||||||
|
*
|
||||||
|
* Not a constructor, because flan_dev.c is linked into every build and not
|
||||||
|
* only a dev one, and nothing should run in a release image that its program
|
||||||
|
* did not ask for. On the first render instead: the branch is one predictable
|
||||||
|
* test per value, and it runs in the dev paths only, which are the only paths
|
||||||
|
* that reach these two functions from here. */
|
||||||
|
static void check_shared(void) {
|
||||||
|
static int checked;
|
||||||
|
char buf[64];
|
||||||
|
char e[4];
|
||||||
|
if (checked) return;
|
||||||
|
checked = 1;
|
||||||
|
if (flan_f64_format(1.5, buf, sizeof buf) != 3 || strcmp(buf, "1.5") != 0)
|
||||||
|
die("flan_f64_format is not the function this file declares", "1.5");
|
||||||
|
if (flan_escape_char('\n', e) != 2 || e[0] != '\\' || e[1] != 'n')
|
||||||
|
die("flan_escape_char is not the function this file declares", "newline");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void put_f64(sink out, double x) {
|
||||||
|
char buf[64];
|
||||||
|
check_shared();
|
||||||
|
flan_f64_format(x, buf, sizeof buf);
|
||||||
|
put(out, buf);
|
||||||
|
}
|
||||||
|
|
||||||
static void put_str(sink out, const uint8_t *bytes, int64_t len) {
|
static void put_str(sink out, const uint8_t *bytes, int64_t len) {
|
||||||
size_t n = len < 0 ? 0 : (size_t)len;
|
size_t n = len < 0 ? 0 : (size_t)len;
|
||||||
|
check_shared();
|
||||||
put(out, "\"");
|
put(out, "\"");
|
||||||
for (size_t i = 0; i < n; i++) {
|
for (size_t i = 0; i < n; i++) {
|
||||||
char e[4];
|
char e[4];
|
||||||
@ -273,19 +311,20 @@ static void put_str(sink out, const uint8_t *bytes, int64_t len) {
|
|||||||
put(out, "\"");
|
put(out, "\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Closing a value: the ellipsis a full buffer earns, and the counter back to
|
/* Closing a value, in the two halves the seqlock needs it in. Shared by
|
||||||
* even. Shared by [flan_dev_result_end] and [flan_dev_watch_end], which is
|
* [flan_dev_result_end] and [flan_dev_watch_end], which is the same seqlock
|
||||||
* the same seqlock twice over two different buffers.
|
* twice over two different buffers.
|
||||||
*
|
*
|
||||||
* Room for the ellipsis is made rather than assumed: the buffer is full by
|
* The first half is the ellipsis a full buffer earns. Room for it is made
|
||||||
* definition when [full] is set.
|
* rather than assumed: the buffer is full by definition when [full] is set.
|
||||||
*
|
*
|
||||||
* The counter goes last, and back to even, so a reader that sees the new
|
* They are two functions and not one because everything a reader will look at
|
||||||
* generation sees the whole value. [| 1] first for the same reason [begin]
|
* has to be stored before the generation is, and a caller may have a length
|
||||||
* sets rather than increments: this must land on an even count whatever state
|
* of its own to write back — the watch slot keeps its length in 32 bits and
|
||||||
* an abandoned write left behind. */
|
* so cannot pass its own field here. Splitting lets that write-back land
|
||||||
static void end_value(char *buf, size_t *len, size_t cap, int full,
|
* inside the odd window where it belongs, rather than after the release
|
||||||
uint64_t *gen) {
|
* store, where it would be a tear the day a length actually changed. */
|
||||||
|
static void truncate_value(char *buf, size_t *len, size_t cap, int full) {
|
||||||
if (full) {
|
if (full) {
|
||||||
const char *ell = "...";
|
const char *ell = "...";
|
||||||
size_t k = strlen(ell);
|
size_t k = strlen(ell);
|
||||||
@ -293,6 +332,14 @@ static void end_value(char *buf, size_t *len, size_t cap, int full,
|
|||||||
memcpy(buf + *len, ell, k);
|
memcpy(buf + *len, ell, k);
|
||||||
*len += k;
|
*len += k;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The second half: the counter last, and back to even, so a reader that sees
|
||||||
|
* the new generation sees the whole value. [| 1] first for the same reason
|
||||||
|
* [begin] sets rather than increments: this must land on an even count
|
||||||
|
* whatever state an abandoned write left behind. Nothing a reader reads may
|
||||||
|
* be written after this returns. */
|
||||||
|
static void close_value(uint64_t *gen) {
|
||||||
__atomic_store_n(gen, (*gen | 1) + 1, __ATOMIC_RELEASE);
|
__atomic_store_n(gen, (*gen | 1) + 1, __ATOMIC_RELEASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,7 +352,8 @@ void flan_dev_emit_str(const uint8_t *bytes, int64_t len) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void flan_dev_result_end(void) {
|
void flan_dev_result_end(void) {
|
||||||
end_value(result, &result_len, RESULT_MAX, result_full, &generation);
|
truncate_value(result, &result_len, RESULT_MAX, result_full);
|
||||||
|
close_value(&generation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Copy the current value out, with the counter that says which one it is.
|
/* Copy the current value out, with the counter that says which one it is.
|
||||||
@ -563,12 +611,21 @@ void flan_dev_watch_end(void) {
|
|||||||
watch_slot *s = watch_cur;
|
watch_slot *s = watch_cur;
|
||||||
watch_cur = NULL;
|
watch_cur = NULL;
|
||||||
if (s == NULL) return;
|
if (s == NULL) return;
|
||||||
/* [len] widened and narrowed around the shared close, which counts in
|
/* [len] widened and narrowed around the shared truncate, which counts in
|
||||||
* size_t because the result buffer does; a slot's own length is 32 bits
|
* size_t because the result buffer does; a slot's own length is 32 bits
|
||||||
* and [WATCH_VAL] is 192, so neither conversion can lose anything. */
|
* and [WATCH_VAL] is 192, so neither conversion can lose anything.
|
||||||
|
*
|
||||||
|
* The narrowing write-back goes before [close_value] and not after it: a
|
||||||
|
* reader takes [len] and [val] together under the generation, so a length
|
||||||
|
* stored after the release store is a length the reader is entitled to
|
||||||
|
* have missed. It happens to write back the same bit pattern today — only
|
||||||
|
* a [full] slot is truncated, and a full slot's length is already
|
||||||
|
* [WATCH_VAL] — but that is a fact about the current cap arithmetic and
|
||||||
|
* not a rule anyone reading this would keep. */
|
||||||
size_t len = s->len;
|
size_t len = s->len;
|
||||||
end_value(s->val, &len, WATCH_VAL, s->full, &s->gen);
|
truncate_value(s->val, &len, WATCH_VAL, s->full);
|
||||||
s->len = (uint32_t)len;
|
s->len = (uint32_t)len;
|
||||||
|
close_value(&s->gen);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Watching one scalar, with no compiler change ───────────────────── */
|
/* ── Watching one scalar, with no compiler change ───────────────────── */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user