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. */
|
||||
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
|
||||
* 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
|
||||
@ -259,11 +253,55 @@ static void put_f64(sink out, double x) {
|
||||
* 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
|
||||
* [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);
|
||||
|
||||
/* 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) {
|
||||
size_t n = len < 0 ? 0 : (size_t)len;
|
||||
check_shared();
|
||||
put(out, "\"");
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
char e[4];
|
||||
@ -273,19 +311,20 @@ static void put_str(sink out, const uint8_t *bytes, int64_t len) {
|
||||
put(out, "\"");
|
||||
}
|
||||
|
||||
/* Closing a value: the ellipsis a full buffer earns, and the counter back to
|
||||
* even. Shared by [flan_dev_result_end] and [flan_dev_watch_end], which is
|
||||
* the same seqlock twice over two different buffers.
|
||||
/* Closing a value, in the two halves the seqlock needs it in. Shared by
|
||||
* [flan_dev_result_end] and [flan_dev_watch_end], which is the same seqlock
|
||||
* twice over two different buffers.
|
||||
*
|
||||
* Room for the ellipsis is made rather than assumed: the buffer is full by
|
||||
* definition when [full] is set.
|
||||
* The first half is the ellipsis a full buffer earns. Room for it is made
|
||||
* 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
|
||||
* 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. */
|
||||
static void end_value(char *buf, size_t *len, size_t cap, int full,
|
||||
uint64_t *gen) {
|
||||
* They are two functions and not one because everything a reader will look at
|
||||
* has to be stored before the generation is, and a caller may have a length
|
||||
* of its own to write back — the watch slot keeps its length in 32 bits and
|
||||
* so cannot pass its own field here. Splitting lets that write-back land
|
||||
* inside the odd window where it belongs, rather than after the release
|
||||
* 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) {
|
||||
const char *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);
|
||||
*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);
|
||||
}
|
||||
|
||||
@ -305,7 +352,8 @@ void flan_dev_emit_str(const uint8_t *bytes, int64_t len) {
|
||||
}
|
||||
|
||||
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.
|
||||
@ -563,12 +611,21 @@ void flan_dev_watch_end(void) {
|
||||
watch_slot *s = watch_cur;
|
||||
watch_cur = NULL;
|
||||
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
|
||||
* 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;
|
||||
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;
|
||||
close_value(&s->gen);
|
||||
}
|
||||
|
||||
/* ── Watching one scalar, with no compiler change ───────────────────── */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user