The review batch: honest headers, named temps, the seqlock's odd window
# Conflicts: # FIX.org
This commit is contained in:
commit
0e9707c9ef
58
FIX.org
58
FIX.org
@ -959,3 +959,61 @@ reserve, with a typed vec-new and map-new silent between them); and dyn
|
|||||||
arithmetic answering nothing at all. test/test_dev.ml drives [(:op "memory")]
|
arithmetic answering nothing at all. test/test_dev.ml drives [(:op "memory")]
|
||||||
over the socket against programs/dev-dyn-global.flan, whose one line is two
|
over the socket against programs/dev-dyn-global.flan, whose one line is two
|
||||||
gc crossings at two columns and no native allocation anywhere.
|
gc crossings at two columns and no native allocation anywhere.
|
||||||
|
|
||||||
|
* Review-batch findings, 2026-09-20
|
||||||
|
Two things found in review that this lane could not fix in the files it owned.
|
||||||
|
Written down here so they are not lost with the branch.
|
||||||
|
|
||||||
|
** The daemon leaves its temp directory behind, forever
|
||||||
|
Every session makes =/tmp/flan-dev-<pid>/= — lib/dev.ml:3516 for the
|
||||||
|
two-process daemon and lib/dev.ml:4415 for the merged one — and nothing ever
|
||||||
|
removes it. It holds the built =program=, the host's =host.ll= or =host.s=,
|
||||||
|
the reload modules and =agent.sock=: about 7MB a session. The review counted
|
||||||
|
873 of them, 5GB, on the morning of 2026-09-19; this lane counted 68 and 457MB
|
||||||
|
on 2026-09-20. Whatever removed the difference, nothing in the tree did, and
|
||||||
|
the count climbs again with every =M-x flan=.
|
||||||
|
|
||||||
|
The fix is small and the merged daemon already has the one place for it. Its
|
||||||
|
session ends at lib/dev.ml:4395: [accept_loop] returns, the listening socket
|
||||||
|
closes, the editor socket is unlinked, and [Unix._exit 0] follows. A recursive
|
||||||
|
remove of [dir] belongs between the unlink and the flush — that one site
|
||||||
|
covers all three ways a session ends cleanly, because all three come back
|
||||||
|
through [accept_loop]:
|
||||||
|
- =close= from the editor (lib/dev.ml:3359, which returns [true] and ends the
|
||||||
|
loop);
|
||||||
|
- no editor connected for the grace period (lib/dev.ml:3472);
|
||||||
|
- the program finished and the parked process is let go.
|
||||||
|
The two-process daemon needs the same thing at its own session end.
|
||||||
|
|
||||||
|
Two deliberate non-goals, and they are the reason this is worth spelling out
|
||||||
|
rather than just doing:
|
||||||
|
- *Not on a crash.* The sibling branch at lib/dev.ml:4393 is [accept_loop]
|
||||||
|
raising, and the directory is the post-mortem — the binary and the exact IR
|
||||||
|
it was built from. Only the clean return cleans up.
|
||||||
|
- *Not other sessions' directories.* A sweep of =/tmp/flan-dev-*= would delete
|
||||||
|
the working directory of a daemon that is still running, and a stale pid is
|
||||||
|
not proof of anything. Each session removes its own and no more.
|
||||||
|
|
||||||
|
Not done here because lib/dev.ml belongs to another lane that has not merged.
|
||||||
|
|
||||||
|
** and's last operand gets a misdirected caret in a want-free position
|
||||||
|
[shortcircuit] in lib/parse.ml documents this at the site; the summary is that
|
||||||
|
=(println (and true true (vec-new i32)))= reports "expected (Vec i32), found
|
||||||
|
bool" with the caret on the second =true=. The last operand of an =and= is the
|
||||||
|
then arm, check_if types the then arm first, and the mismatch is therefore
|
||||||
|
reported against the else arm, which carries the *previous* operand's loc.
|
||||||
|
Every other operand position is right, because an operand anywhere but last is
|
||||||
|
a condition and check_truthy blames it at its own loc; =or= is right
|
||||||
|
everywhere, because there the chain and not the sentinel sits in the else arm.
|
||||||
|
Compiled on the tree at every operand position of both forms, want-free and
|
||||||
|
want-ful; want-ful is right everywhere too, because the want reaches each arm
|
||||||
|
instead of the arms being unified against each other.
|
||||||
|
|
||||||
|
Not fixed. Three candidate fixes were considered and rejected: giving the else
|
||||||
|
arm the last operand's loc makes the sentence read backwards ("expected (Vec
|
||||||
|
i32)" under a caret on the thing that is the (Vec i32)); answering a bool
|
||||||
|
sentinel again reverts the fix that made =(or nil "x")= answer ="x"=; and
|
||||||
|
inverting the condition to move the last operand into the else arm costs a
|
||||||
|
[not] per operand and worse locs than it buys. What would fix it is check_if
|
||||||
|
preferring the arm that is not a compiler temp when it decides which one to
|
||||||
|
blame — a change in check.ml, which this lane did not own.
|
||||||
|
|||||||
7
NEXT.md
7
NEXT.md
@ -2395,8 +2395,11 @@ plan.org's single line on it (831) names a `for` the language does not have and
|
|||||||
What is missing is the half only an editor can do — the leverage SBCL lacks. `eval` already compiles and runs an
|
What is missing is the half only an editor can do — the leverage SBCL lacks. `eval` already compiles and runs an
|
||||||
expression inside the live program and the daemon already holds the struct layouts, so "ask the human, type-check
|
expression inside the live program and the daemon already holds the struct layouts, so "ask the human, type-check
|
||||||
the answer, hand it over" is a short hop, and it is the one path the runtime today *refuses*: a restart with
|
the answer, hand it over" is a short hop, and it is the one path the runtime today *refuses*: a restart with
|
||||||
parameters taken from the break loop traps, because `flan_break_resume` and `flan_restart_take` aim the channel at
|
parameters taken from the break loop traps, because the pair a break loop resumes through — `flan_restart_frame`,
|
||||||
a frame and have nothing to fill its buffer with. What it needs, end to end:
|
which hands back the frame the human picked, and `flan_restart_take`, which aims the transfer channel at it —
|
||||||
|
reaches the frame and has nothing to fill its buffer with. (`flan_break_resume` is what this paragraph named
|
||||||
|
before; there is no such function, and `flan_break_hook` is the unrelated thing that gets the break loop *into*
|
||||||
|
the agent in the first place.) What it needs, end to end:
|
||||||
- the frame already carries the arity and the signature as a string — `flan_restart_arity` and `flan_restart_sig`
|
- the frame already carries the arity and the signature as a string — `flan_restart_arity` and `flan_restart_sig`
|
||||||
beside `flan_restart_name`, the same walk, so `restarts` can say what each one takes;
|
beside `flan_restart_name`, the same walk, so `restarts` can say what each one takes;
|
||||||
- `:restarts` on the wire carries the signature per entry, so the minibuffer can show `use-value (i32)` rather
|
- `:restarts` on the wire carries the signature per entry, so the minibuffer can show `use-value (i32)` rather
|
||||||
|
|||||||
@ -425,23 +425,28 @@ and reload semantics are working." Under this reading the gating item changed. C
|
|||||||
|
|
||||||
## 9. Print, hash, sort — one family already duplicated, two not yet
|
## 9. Print, hash, sort — one family already duplicated, two not yet
|
||||||
|
|
||||||
**Printing is duplicated three ways on one side, and it is not the dyn/typed split.**
|
**Printing was duplicated three ways on one side, and it is not the dyn/typed split. Two of the three are
|
||||||
`runtime/flan_dyn.c:308-313` says so itself:
|
now one.** This section found three escape tables; the runtime dedupe lane took the two that were on the
|
||||||
|
same side and made them one function with two framings, which is what this section asked for. What is left
|
||||||
|
is the split it argued was correct.
|
||||||
|
|
||||||
> A text inside a structure, quoted and escaped. The same table as `flan_rt.c`'s `flan_escape_bytes` and
|
The table lives once, as `flan_escape_char` (`runtime/flan_rt.c:388`), and both typed-side printers call
|
||||||
> `flan_dev.c`'s `flan_dev_emit_str`...
|
it: `flan_escape_bytes` (`:422`), which builds a capped slice for `println`, and `put_str`
|
||||||
|
(`runtime/flan_dev.c:302`), which streams into the inspector's fixed buffer so the REPL can parse the
|
||||||
|
printed form back. The framing is each caller's and is the part that is genuinely theirs; the switch is
|
||||||
|
shared, because two printers disagreeing about what a string looks like is two wire formats.
|
||||||
|
|
||||||
Three escape tables: `runtime/flan_rt.c:383` (`flan_escape_bytes`, with `:366-372` noting it is already "the
|
The dyn copy stays, as `emit_escaped` (`runtime/flan_dyn.c:472`), and says why at `:465-470`:
|
||||||
same escape table as `flan_dev_emit_str`"), `runtime/flan_dev.c:237` and again at `:551`, and
|
|
||||||
`runtime/flan_dyn.c:314` (`emit_escaped`). `SPIKE-DYNAMIC.md:329-332` calls its own "the third copy" and
|
|
||||||
leaves the instruction "If that table changes, change all three."
|
|
||||||
|
|
||||||
Two of those three are on the same side. `flan_rt.c`'s is `println`'s; `flan_dev.c`'s is the inspector's, so
|
> This copy is deliberate, and the argument for it is docs/SPIKE-DUPLICITY.md §9's: the dyn printer lives
|
||||||
the REPL can parse the printed form back. That pair predates dyn and is same-side duplication that dyn has
|
> inside the runtime that owns the storage it walks, which is why it prints a dyn vec structurally where
|
||||||
now made three-way. The *dyn* copy is the one with a defence, and `SPIKE-DYNAMIC.md:334-338` gives it: a
|
> the typed printer answers `<vec>`. The whole printer is this side's; the table it shares with the other
|
||||||
typed `Vec` prints as `<vec>` because the typed printer will not walk storage it does not own, while a dyn
|
> side is the part that must not drift. If that table changes, change this one.
|
||||||
vec's storage belongs to the collector and the printer is inside the runtime that owns it. That is correct
|
|
||||||
duplicity. Three tables is not.
|
That is the defence `SPIKE-DYNAMIC.md` gives and this section accepted: a typed `Vec` prints as `<vec>`
|
||||||
|
because the typed printer will not walk storage it does not own, while a dyn vec's storage belongs to the
|
||||||
|
collector and the printer is inside the runtime that owns it. One copy per *side*, which is the doctrine.
|
||||||
|
Two tables is the answer; three was not.
|
||||||
|
|
||||||
**Hashing is typed-only.** `flan_hash_fn`, `flan_hash_flat`, `flan_hash_str`, `flan_hash_combine`
|
**Hashing is typed-only.** `flan_hash_fn`, `flan_hash_flat`, `flan_hash_str`, `flan_hash_combine`
|
||||||
(`runtime/flan_rt.c:1807, 1911, 1939, 1954`), reached through `Types.keyable` and the checker's `key_pair`
|
(`runtime/flan_rt.c:1807, 1911, 1939, 1954`), reached through `Types.keyable` and the checker's `key_pair`
|
||||||
|
|||||||
@ -326,10 +326,12 @@ running program**, not read off `lib/render.ml` — that file is the REPL's insp
|
|||||||
| nil | — | `nil` |
|
| nil | — | `nil` |
|
||||||
|
|
||||||
The leading space before every element is what `lib/render.ml`'s slice loop emits and what a Flan program
|
The leading space before every element is what `lib/render.ml`'s slice loop emits and what a Flan program
|
||||||
prints today; a tidier answer would diverge from an acceptance test. The escape table is the third copy
|
prints today; a tidier answer would diverge from an acceptance test. The escape table is the dyn side's
|
||||||
of the one in `flan_rt.c`'s `flan_escape_bytes` and `flan_dev.c`'s `flan_dev_emit_str` — those two are
|
own copy of the typed side's. The typed side used to have two and now has one — `flan_escape_char`
|
||||||
already kept identical because the REPL parses the printed form back, and this one joins them. If that
|
(`runtime/flan_rt.c:388`), which `flan_escape_bytes` calls for `println` and `flan_dev.c`'s `put_str`
|
||||||
table changes, change all three.
|
calls for the inspector, each framing the result its own way — so there are two tables in the tree, not
|
||||||
|
three, and the split that is left is the dyn/typed one argued just below. If that table changes, change
|
||||||
|
this one too.
|
||||||
|
|
||||||
**A typed `Vec` prints as `<vec>` and a dyn vec does not.** That looks like a mismatch and is not. The
|
**A typed `Vec` prints as `<vec>` and a dyn vec does not.** That looks like a mismatch and is not. The
|
||||||
typed printer's refusal is about walking storage it does not own — `render.ml` says so, and directs you to
|
typed printer's refusal is about walking storage it does not own — `render.ml` says so, and directs you to
|
||||||
|
|||||||
43
lib/parse.ml
43
lib/parse.ml
@ -14,15 +14,21 @@ let sym (f : Form.t) =
|
|||||||
| Sym s -> s
|
| Sym s -> s
|
||||||
| _ -> fail f "expected a name, found %s" (Form.to_string f)
|
| _ -> fail f "expected a name, found %s" (Form.to_string f)
|
||||||
|
|
||||||
(* Names for the temporaries a destructuring binding needs — the value is bound
|
(* Names for the temporaries this file mints — the value is bound once and
|
||||||
once and every name in the pattern reads *that*, so a pattern over a call
|
everything that needs it reads *that*, so a destructuring pattern over a
|
||||||
calls it once. [~] is a delimiter in the reader, so no symbol anyone can
|
call calls it once and a short-circuit operand is evaluated once. [~] is a
|
||||||
write contains one: these cannot collide with a source name and a source
|
delimiter in the reader, so no symbol anyone can write contains one: these
|
||||||
name cannot shadow one. Reset per program so the names, and therefore the
|
cannot collide with a source name and a source name cannot shadow one.
|
||||||
slot numbering downstream, are the same every run. *)
|
Reset per program so the names, and therefore the slot numbering
|
||||||
|
downstream, are the same every run.
|
||||||
|
|
||||||
|
The purpose is part of the name because these names are shown: the
|
||||||
|
inspector lists a frame's locals by name, and a short-circuit temp called
|
||||||
|
[destructure~3] is a plain lie about where it came from. One counter across
|
||||||
|
all purposes, so a name is still unique whatever minted it. *)
|
||||||
let temps = ref 0
|
let temps = ref 0
|
||||||
|
|
||||||
let fresh_temp () = incr temps; Printf.sprintf "destructure~%d" !temps
|
let fresh_temp what = incr temps; Printf.sprintf "%s~%d" what !temps
|
||||||
|
|
||||||
(* Destructuring binds in [let] and nowhere else. Every other binding position —
|
(* Destructuring binds in [let] and nowhere else. Every other binding position —
|
||||||
a [defn] parameter, a [defstruct] field, an [fn] parameter, a [dotimes]
|
a [defn] parameter, a [defstruct] field, an [fn] parameter, a [dotimes]
|
||||||
@ -673,7 +679,7 @@ and bindings f (items : Form.t list) : Ast.binding list =
|
|||||||
the reference as well as the binding is what makes the two impossible to
|
the reference as well as the binding is what makes the two impossible to
|
||||||
separate by accident. *)
|
separate by accident. *)
|
||||||
and temp (p : Form.t) (v : Ast.expr) : Ast.expr * Ast.binding =
|
and temp (p : Form.t) (v : Ast.expr) : Ast.expr * Ast.binding =
|
||||||
let t = fresh_temp () in
|
let t = fresh_temp "destructure" in
|
||||||
({ Ast.e = Ast.Var t; loc = p.loc },
|
({ Ast.e = Ast.Var t; loc = p.loc },
|
||||||
{ Ast.bname = t; bty = None; bval = v; bloc = p.loc })
|
{ Ast.bname = t; bty = None; bval = v; bloc = p.loc })
|
||||||
|
|
||||||
@ -915,7 +921,24 @@ and cond f (args : Form.t list) : Ast.expr =
|
|||||||
because the sentinel it answered was a bool literal that boxed to fit
|
because the sentinel it answered was a bool literal that boxed to fit
|
||||||
whatever the real branch was; neither is now, and they are at least
|
whatever the real branch was; neither is now, and they are at least
|
||||||
symmetric about it. Making bool and dyn arms join as dyn is a check_if
|
symmetric about it. Making bool and dyn arms join as dyn is a check_if
|
||||||
question, noted in FIX.org under item 7 and not decided here. *)
|
question, noted in FIX.org under item 7 and not decided here.
|
||||||
|
|
||||||
|
One known wart, measured rather than guessed, and left alone deliberately.
|
||||||
|
In a want-free position — [(println (and true true (vec-new i32)))] — the
|
||||||
|
caret lands on the *second* [true] and not on the vec: the last operand is
|
||||||
|
the then arm, check_if types the then arm first, and the mismatch is
|
||||||
|
therefore reported against the else arm, which is the previous operand's
|
||||||
|
temp. An operand anywhere but last is a condition instead, so check_truthy
|
||||||
|
blames it at its own loc and the caret is right; [or] is right everywhere,
|
||||||
|
because there the chain and not the sentinel sits in the else arm. Giving
|
||||||
|
the else arm's [Var] node the *last* operand's loc moves the caret onto the
|
||||||
|
vec and makes the sentence read backwards — "expected (Vec i32), found
|
||||||
|
bool" under a caret on the thing that is the (Vec i32) — so it is not an
|
||||||
|
improvement; answering a bool literal again would revert the paragraph
|
||||||
|
above; and inverting the condition to move the last operand into the else
|
||||||
|
arm buys a [not] per operand and worse locs than it fixes. What would
|
||||||
|
actually fix it is check_if preferring the arm that is not a compiler temp
|
||||||
|
when it reports, which is check.ml's call. Written up in FIX.org. *)
|
||||||
and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
|
and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
|
||||||
let mk e = { Ast.e; loc = f.loc } in
|
let mk e = { Ast.e; loc = f.loc } in
|
||||||
let rec go = function
|
let rec go = function
|
||||||
@ -923,7 +946,7 @@ and shortcircuit f (args : Form.t list) ~is_and : Ast.expr =
|
|||||||
| [ last ] -> expr last
|
| [ last ] -> expr last
|
||||||
| x :: rest ->
|
| x :: rest ->
|
||||||
let ex = expr x in
|
let ex = expr x in
|
||||||
let t = fresh_temp () in
|
let t = fresh_temp (if is_and then "and" else "or") in
|
||||||
let tvar = { Ast.e = Ast.Var t; loc = ex.Ast.loc } in
|
let tvar = { Ast.e = Ast.Var t; loc = ex.Ast.loc } in
|
||||||
let bind = { Ast.bname = t; bty = None; bval = ex; bloc = ex.Ast.loc } in
|
let bind = { Ast.bname = t; bty = None; bval = ex; bloc = ex.Ast.loc } in
|
||||||
let rest = go rest in
|
let rest = go rest in
|
||||||
|
|||||||
@ -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 ───────────────────── */
|
||||||
|
|||||||
@ -3,12 +3,21 @@
|
|||||||
* This header is not compiled into a program. The build embeds the runtime's
|
* This header is not compiled into a program. The build embeds the runtime's
|
||||||
* .c files as strings and hands each one to clang on its own, with no include
|
* .c files as strings and hands each one to clang on its own, with no include
|
||||||
* path (see [Build.compile_c]), so runtime/flan_dyn.c declares everything it
|
* path (see [Build.compile_c]), so runtime/flan_dyn.c declares everything it
|
||||||
* defines and this file declares it a second time. That second copy is not
|
* defines and this file declares it a second time. What keeps that second
|
||||||
* held honest by hand the way a repeated escape table would be: it is made
|
* copy honest is partial, and the exact shape of it is worth writing down
|
||||||
* mechanical, because test/dyn_ops.c includes this header and names every
|
* rather than overclaiming: test/dyn_ops.c includes this header and calls
|
||||||
* function below, so a signature that drifts from the implementation is a
|
* most of what it declares, so a name that is spelled one way here and
|
||||||
* link error in `dune test` rather than a surprise at someone else's call
|
* another way in flan_dyn.c fails to link in `dune test`.
|
||||||
* site.
|
*
|
||||||
|
* Two holes in that. A signature that drifts while the name stays is not
|
||||||
|
* caught at all — C links on names and not on types, so a changed parameter
|
||||||
|
* or return type compiles on both sides and goes wrong at the call site
|
||||||
|
* instead of failing the build. And five of the names below are never
|
||||||
|
* referenced by dyn_ops.c — [flan_dyn_map_get], [flan_dyn_map_set],
|
||||||
|
* [flan_dyn_map_contains], [flan_dyn_is_nil] and [flan_dyn_need_not_nil] —
|
||||||
|
* so not even the rename check reaches them. Calling those five from
|
||||||
|
* dyn_ops.c would close the second hole; nothing available from here closes
|
||||||
|
* the first.
|
||||||
*
|
*
|
||||||
* Who reads it: the compiler lane, which emits calls to these names, and the
|
* Who reads it: the compiler lane, which emits calls to these names, and the
|
||||||
* C tests. The whole of the boundary is here. What is behind it — the value
|
* C tests. The whole of the boundary is here. What is behind it — the value
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user