diff --git a/lib/build.ml b/lib/build.ml index 11b9593..f76bce3 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -653,8 +653,6 @@ let stamp_of prog = | st -> Printf.sprintf "%s:%d:%f" path st.Unix.st_size st.Unix.st_mtime | exception Unix.Unix_error _ -> path -let clang_stamp = lazy (stamp_of clang) - (* Compile one C translation unit to an object file, reusing a cached one when the source text, the compiler and the flags are all unchanged. The key has to carry [opt] and [target]: the acceptance table builds the same programs diff --git a/lib/dev.ml b/lib/dev.ml index 5df19ab..06e9f73 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -3395,17 +3395,17 @@ let ignore_sigpipe () = and this is a *daemon* whose client died. What makes this answerable without a new protocol is that the connection is - per session and not per request. [emacs/flan-dev.el] opens one - [make-network-process] in [flan-dev--open], keeps it in - [flan-dev--connection], and reopens only when the process is dead; every - site that tears it down deliberately sends [close] first. [serve] mirrors + per session and not per request. [emacs/flan.el] opens one + [make-network-process] in [flan--open], keeps it in [flan--connection], and + reopens only when the process is dead; every site that tears it down + deliberately sends [close] first. [serve] mirrors that shape — it loops on the one fd until EOF — so while an editor is attached this loop is not even cycling. An editor left open and idle overnight is therefore *attached* overnight, and the grace below can never accumulate under it. That is structural, not a number chosen to outlast human patience, and it is why no PID handshake and no heartbeat op is needed: holding the socket open is the heartbeat, and a client that only - ever polls — or one that has [flan-dev-poll-interval] set to nil and polls + ever polls — or one that has [flan-poll-interval] set to nil and polls never — is indistinguishable from any other attached client. So the signal is client *absence*, and the only absence this is allowed to @@ -3456,7 +3456,7 @@ let client_grace () = window would kill the session, which is the bug this whole change exists to remove, reintroduced one line further out. The grace does not reopen that hole: it reads the client, not the program, and all of an Emacs's windows - share the one [flan-dev--connection], so closing one of them changes + share the one [flan--connection], so closing one of them changes nothing this loop can see. *) let accept_loop ?grace t ls = let grace = match grace with Some g -> g | None -> client_grace () in diff --git a/lib/expand.ml b/lib/expand.ml index a179009..e771d4b 100644 --- a/lib/expand.ml +++ b/lib/expand.ml @@ -57,15 +57,11 @@ let tag_of_int = function dynload_stubs.c, one field at a time. Everything allocated here is owned by [Dynload] and released together after the call. *) -let rec marshal (f : Form.t) : Dynload.addr = - let p = Dynload.take form_size in - write p f; - p - (* Into an existing 24 bytes, which is what an argument array needs: the macro takes a [Form] slice, and a slice is contiguous elements and not an array of - pointers. *) -and write p (f : Form.t) = + pointers — so this writes *into* memory the caller took, and every caller + here takes it as part of an array. *) +let rec write p (f : Form.t) = let tag t = Dynload.poke_i32 p 0 (tag_int t) in let str t s = tag t; diff --git a/lib/js.ml b/lib/js.ml index 018977b..80184cc 100644 --- a/lib/js.ml +++ b/lib/js.ml @@ -656,13 +656,6 @@ let elem_ty loc (t : Types.t) = | Types.String -> Types.Int Types.U8 | t -> at loc "indexing %s is not in the JS dialect" (Types.to_string t) -let is_bytes (t : Types.t) = - match t with - | Types.String -> true - | Types.Slice (Types.Int Types.U8) | Types.Array (_, Types.Int Types.U8) -> - true - | _ -> false - let rec value f (e : Tast.expr) : string = refuse_ty e.Tast.loc e.Tast.ty; match e.Tast.e with diff --git a/runtime/flan_dev.c b/runtime/flan_dev.c index f21b519..00b2cb8 100644 --- a/runtime/flan_dev.c +++ b/runtime/flan_dev.c @@ -203,80 +203,109 @@ void flan_dev_emit(const uint8_t *bytes, int64_t len) { result_len += n; } -static void emit_cstr(const char *s) { - flan_dev_emit((const uint8_t *)s, (int64_t)strlen(s)); +/* ── Rendering a scalar, into whichever buffer is being written ──────── */ + +/* This file has two buffers a rendered value can go into: the result buffer + * above, which an evaluated expression's rendering streams into for the REPL + * to read back, and the current watch slot further down. What they hold is + * the same four renderings — an i64, a u64, an f64, a quoted string — and the + * only thing that differed was which of [flan_dev_emit] and + * [flan_dev_watch_emit] the bytes went to. So that is the parameter, and the + * eight exported entry points are eight one-line calls into these four. + * + * The exports stay eight. The compiler emits the result four by name — see + * [Session.externs] — and the watch four are exported C, which a program can + * reach through declare-c and which the [(watch ...)] arm described further + * down would be pointed at. They are ABI, and ABI does not collapse just + * because the bodies did. */ +typedef void (*sink)(const uint8_t *, int64_t); + +static void put(sink out, const char *s) { + out((const uint8_t *)s, (int64_t)strlen(s)); } /* Rendered in C so that u64 is not a lie: the language's own i64->bytes is * signed, and anything past 2^63 would come back negative. */ -void flan_dev_emit_u64(uint64_t x) { +static void put_u64(sink out, uint64_t x) { char buf[32]; snprintf(buf, sizeof buf, "%llu", (unsigned long long)x); - emit_cstr(buf); + put(out, buf); } -void flan_dev_emit_i64(int64_t x) { +static void put_i64(sink out, int64_t x) { char buf[32]; snprintf(buf, sizeof buf, "%lld", (long long)x); - emit_cstr(buf); + put(out, buf); } -/* Unsigned NaN, for flan_f64_to_bytes's reason and one of its own: the REPL - * and println must not disagree about what a value looks like, which is the - * rule the escape table below is already held to. A NaN's sign bit is decided - * by whether the value was folded or computed, so showing it makes the printed - * form depend on the backend and the optimisation level rather than on the - * number. */ -void flan_dev_emit_f64(double x) { +/* Unsigned NaN, and the rule is flan_rt.c's rather than a second statement of + * it: [flan_f64_format] is what println renders through, and the REPL, a watch + * row and println must not disagree about what one value looks like. A NaN's + * sign bit is decided by whether the value was folded or computed, so showing + * it would make the printed form depend on the backend and the optimisation + * 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]; - if (x != x) snprintf(buf, sizeof buf, "nan"); - else snprintf(buf, sizeof buf, "%g", x); - emit_cstr(buf); + 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 - * framing bug rather than as the value it is. */ -void flan_dev_emit_str(const uint8_t *bytes, int64_t len) { + * framing bug rather than as the value it is — and a newline in a watched + * 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. */ +extern int flan_escape_char(unsigned char c, char *out); + +static void put_str(sink out, const uint8_t *bytes, int64_t len) { size_t n = len < 0 ? 0 : (size_t)len; - emit_cstr("\""); + put(out, "\""); for (size_t i = 0; i < n; i++) { - unsigned char c = bytes[i]; - switch (c) { - case '"': emit_cstr("\\\""); break; - case '\\': emit_cstr("\\\\"); break; - case '\n': emit_cstr("\\n"); break; - case '\t': emit_cstr("\\t"); break; - case '\r': emit_cstr("\\r"); break; - default: - if (c < 0x20) { - char buf[8]; - snprintf(buf, sizeof buf, "\\x%02x", c); - emit_cstr(buf); - } else { - flan_dev_emit(&c, 1); - } - } + char e[4]; + int k = flan_escape_char(bytes[i], e); + out((const uint8_t *)e, (int64_t)k); } - emit_cstr("\""); + 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. + * + * Room for the ellipsis 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) { + if (full) { + const char *ell = "..."; + size_t k = strlen(ell); + if (*len > cap - k) *len = cap - k; + memcpy(buf + *len, ell, k); + *len += k; + } + __atomic_store_n(gen, (*gen | 1) + 1, __ATOMIC_RELEASE); +} + +void flan_dev_emit_u64(uint64_t x) { put_u64(flan_dev_emit, x); } +void flan_dev_emit_i64(int64_t x) { put_i64(flan_dev_emit, x); } +void flan_dev_emit_f64(double x) { put_f64(flan_dev_emit, x); } + +void flan_dev_emit_str(const uint8_t *bytes, int64_t len) { + put_str(flan_dev_emit, bytes, len); } void flan_dev_result_end(void) { - if (result_full) { - /* Room is made for it rather than assumed: the buffer is full by - * definition when this fires. */ - const char *ell = "..."; - size_t k = strlen(ell); - if (result_len > RESULT_MAX - k) result_len = RESULT_MAX - k; - memcpy(result + result_len, ell, k); - result_len += k; - } - /* 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. */ - __atomic_store_n(&generation, (generation | 1) + 1, __ATOMIC_RELEASE); + end_value(result, &result_len, RESULT_MAX, result_full, &generation); } /* Copy the current value out, with the counter that says which one it is. @@ -444,10 +473,6 @@ void flan_dev_watch_enable(int on) { __atomic_store_n(&watch_on, on ? 1 : 0, __ATOMIC_RELAXED); } -int flan_dev_watch_enabled(void) { - return __atomic_load_n(&watch_on, __ATOMIC_RELAXED); -} - /* The slot a name owns, or NULL if the table is full. * * Linear, because the table is 64 long and a hash would need a policy for @@ -523,76 +548,27 @@ void flan_dev_watch_emit(const uint8_t *bytes, int64_t len) { s->len += (uint32_t)n; } -static void watch_cstr(const char *str) { - flan_dev_watch_emit((const uint8_t *)str, (int64_t)strlen(str)); -} +/* The four renderings, aimed at the watch slot instead of the result buffer. + * The bodies are [put_i64] and friends above — see the note there for why the + * entry points stay four while the rendering is one. */ +void flan_dev_watch_emit_i64(int64_t x) { put_i64(flan_dev_watch_emit, x); } +void flan_dev_watch_emit_u64(uint64_t x) { put_u64(flan_dev_watch_emit, x); } +void flan_dev_watch_emit_f64(double x) { put_f64(flan_dev_watch_emit, x); } -void flan_dev_watch_emit_i64(int64_t x) { - char buf[32]; - snprintf(buf, sizeof buf, "%lld", (long long)x); - watch_cstr(buf); -} - -void flan_dev_watch_emit_u64(uint64_t x) { - char buf[32]; - snprintf(buf, sizeof buf, "%llu", (unsigned long long)x); - watch_cstr(buf); -} - -/* Unsigned NaN, the same rule [flan_dev_emit_f64] states: a watch row and a - * REPL answer for one value must read the same. */ -void flan_dev_watch_emit_f64(double x) { - char buf[64]; - if (x != x) snprintf(buf, sizeof buf, "nan"); - else snprintf(buf, sizeof buf, "%g", x); - watch_cstr(buf); -} - -/* Quoted and escaped, for [flan_dev_emit_str]'s reason and one more: a string - * whose content is not escaped does not round-trip, and a newline in one would - * become a second row of the table on the wire rather than part of a value. */ void flan_dev_watch_emit_str(const uint8_t *bytes, int64_t len) { - size_t n = len < 0 ? 0 : (size_t)len; - watch_cstr("\""); - for (size_t i = 0; i < n; i++) { - unsigned char c = bytes[i]; - switch (c) { - case '"': watch_cstr("\\\""); break; - case '\\': watch_cstr("\\\\"); break; - case '\n': watch_cstr("\\n"); break; - case '\t': watch_cstr("\\t"); break; - case '\r': watch_cstr("\\r"); break; - default: - if (c < 0x20) { - char buf[8]; - snprintf(buf, sizeof buf, "\\x%02x", c); - watch_cstr(buf); - } else { - flan_dev_watch_emit(&c, 1); - } - } - } - watch_cstr("\""); + put_str(flan_dev_watch_emit, bytes, len); } void flan_dev_watch_end(void) { watch_slot *s = watch_cur; watch_cur = NULL; if (s == NULL) return; - if (s->full) { - /* Room is made for it rather than assumed: the value is full by - * definition when this fires. */ - const char *ell = "..."; - size_t k = strlen(ell); - if (s->len > WATCH_VAL - k) s->len = (uint32_t)(WATCH_VAL - k); - memcpy(s->val + s->len, ell, k); - s->len += (uint32_t)k; - } - /* 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 an abandoned write - * left behind. */ - __atomic_store_n(&s->gen, (s->gen | 1) + 1, __ATOMIC_RELEASE); + /* [len] widened and narrowed around the shared close, 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. */ + size_t len = s->len; + end_value(s->val, &len, WATCH_VAL, s->full, &s->gen); + s->len = (uint32_t)len; } /* ── Watching one scalar, with no compiler change ───────────────────── */ @@ -630,13 +606,6 @@ int32_t flan_dev_watch_i64(const char *name, int64_t x) { return 1; } -int32_t flan_dev_watch_u64(const char *name, uint64_t x) { - if (!flan_dev_watch_begin(name)) return 0; - flan_dev_watch_emit_u64(x); - flan_dev_watch_end(); - return 1; -} - int32_t flan_dev_watch_f64(const char *name, double x) { if (!flan_dev_watch_begin(name)) return 0; flan_dev_watch_emit_f64(x); diff --git a/runtime/flan_dyn.c b/runtime/flan_dyn.c index f45f95d..9cfd605 100644 --- a/runtime/flan_dyn.c +++ b/runtime/flan_dyn.c @@ -457,11 +457,18 @@ static void emit(const char *s) { static void emit_n(const uint8_t *p, int64_t n) { flan_write_stdout(p, n); } /* A text inside a structure, quoted and escaped. The same table as - * flan_rt.c's [flan_escape_bytes] and flan_dev.c's [flan_dev_emit_str], and - * for the same reason those two are the same as each other: three printers - * that disagree about what a string looks like is three wire formats. If that - * table changes, change this one. Streamed rather than built, so there is no - * buffer to overrun and no length to cap. */ + * flan_rt.c's [flan_escape_char], which is where the typed side's printers — + * [flan_escape_bytes] and flan_dev.c's emitters — now share their one copy of + * it: printers that disagree about what a string looks like are that many + * wire formats. + * + * This copy is deliberate, and the argument for it is docs/SPIKE-DUPLICITY.md + * §9's: the dyn printer lives inside the runtime that owns the storage it + * walks, which is why it prints a dyn vec structurally where the typed + * printer answers . The whole printer is this side's; the table it + * shares with the other side is the part that must not drift. If that table + * changes, change this one. Streamed rather than built, so there is no buffer + * to overrun and no length to cap. */ static void emit_escaped(const uint8_t *p, int64_t n) { int64_t i; emit("\""); diff --git a/runtime/flan_dyn.h b/runtime/flan_dyn.h index 48f431a..3d58cb4 100644 --- a/runtime/flan_dyn.h +++ b/runtime/flan_dyn.h @@ -3,12 +3,12 @@ * 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 * path (see [Build.compile_c]), so runtime/flan_dyn.c declares everything it - * defines and this file declares it a second time. That is the same standing - * arrangement flan_escape_bytes and flan_dev_emit_str already live under — - * "if either table changes, change both" — and it is made mechanical rather - * than hopeful: test/dyn_ops.c includes this header and names every function - * below, so a signature that drifts from the implementation is a link error in - * `dune test` rather than a surprise at someone else's call site. + * defines and this file declares it a second time. That second copy is not + * held honest by hand the way a repeated escape table would be: it is made + * mechanical, because test/dyn_ops.c includes this header and names every + * function below, so a signature that drifts from the implementation is a + * link error in `dune test` rather than a surprise at someone else's call + * site. * * 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 diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index 0883940..e57ed4d 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -134,10 +134,9 @@ void *flan_restart_frame(int32_t i) { return NULL; } -/* Aim the transfer channel at a frame obtained earlier. The same store - * [flan_break_resume] makes and the same one an invoke-restart makes — this - * only spells it without a lookup, for a caller that did its looking up when - * the stack was worth reading. */ +/* Aim the transfer channel at a frame obtained earlier. The same store an + * invoke-restart makes — this only spells it without a lookup, for a caller + * that did its looking up when the stack was worth reading. */ void flan_restart_take(void *frame, void *xfer) { *(void **)xfer = frame; } /* [T] and string are both ptr+len — see Emit.ll. */ @@ -340,10 +339,19 @@ int64_t flan_bytes_to_i64(const uint8_t *p, int64_t n) { * * The test is x != x rather than isnan, which keeps math.h out of this file * and is the same comparison the prelude uses. An infinity still prints signed: - * there the sign is the value. */ + * there the sign is the value. + * + * Spelled as a format into a caller's buffer rather than inline here, because + * the rule has a second reader: flan_dev.c's REPL emitter and its watch table + * render a f64 for an editor to show, and a build where the inspector said + * "-nan" and println said "nan" would be the same disagreement one layer out. + * That file calls this; there is one copy of the rule. */ +int flan_f64_format(double x, char *buf, size_t cap) { + return (x != x) ? snprintf(buf, cap, "nan") : snprintf(buf, cap, "%g", x); +} + void flan_f64_to_bytes(double x, uint8_t *buf, flan_slice *out) { - int n = (x != x) ? snprintf((char *)buf, FLAN_NUM_BYTES, "nan") - : snprintf((char *)buf, FLAN_NUM_BYTES, "%g", x); + int n = flan_f64_format(x, (char *)buf, FLAN_NUM_BYTES); out->ptr = buf; out->len = fit(n); } @@ -364,15 +372,45 @@ void flan_u64_to_bytes(uint64_t x, uint8_t *buf, flan_slice *out) { out->len = fit(n); } +/* The escape table itself: what one byte reads as inside a quoted string, + * written into [out] and returning how many bytes that took. Never more than + * four, which is what every caller's headroom is sized from. + * + * A table rather than a printer, because the callers frame the same mapping + * differently and that framing is the part that is genuinely theirs: the one + * below builds a capped slice to hand back, and flan_dev.c's streams into a + * fixed buffer it does not own the end of. What they must not differ about is + * this switch — the REPL and println disagreeing about what a struct looks + * like is two wire formats — so this switch exists once and they share it. + * + * flan_dyn.c keeps its own copy of the table on purpose; see the comment + * there and docs/SPIKE-DUPLICITY.md §9. */ +int flan_escape_char(unsigned char c, char *out) { + switch (c) { + case '"': out[0] = '\\'; out[1] = '"'; return 2; + case '\\': out[0] = '\\'; out[1] = '\\'; return 2; + case '\n': out[0] = '\\'; out[1] = 'n'; return 2; + case '\t': out[0] = '\\'; out[1] = 't'; return 2; + case '\r': out[0] = '\\'; out[1] = 'r'; return 2; + default: + if (c < 0x20) { + out[0] = '\\'; + out[1] = 'x'; + out[2] = "0123456789abcdef"[c >> 4]; + out[3] = "0123456789abcdef"[c & 0xf]; + return 4; + } + out[0] = (char)c; + return 1; + } +} + /* A string *inside* a printed structure, quoted and escaped, so that the run * of bytes can be told from the punctuation around it — (S {:name "a b"}) has * two fields if the quotes are missing and one if they are there. * - * This is the same escape table as flan_dev_emit_str in flan_dev.c, and - * deliberately so: the REPL and println must not disagree about what a struct - * looks like. It cannot be the *same function* because the dev one streams - * into the result buffer and this one has to hand back a slice; if either - * table changes, change both. + * The table is [flan_escape_char] above, which is also what flan_dev.c's + * emitters use; this function is the framing and nothing else. * * Its own buffer, not `scratch`: escaping is the one conversion whose output * is not a bounded handful of characters. Over-long input is truncated with an @@ -394,20 +432,10 @@ void flan_escape_bytes(const uint8_t *p, int64_t n, flan_slice *out) { escaped[w++] = '"'; for (size_t i = 0; i < len; i++) { if (w + 5 + 4 >= ESCAPE_MAX) { cut = 1; break; } - unsigned char c = p[i]; - switch (c) { - case '"': escaped[w++] = '\\'; escaped[w++] = '"'; break; - case '\\': escaped[w++] = '\\'; escaped[w++] = '\\'; break; - case '\n': escaped[w++] = '\\'; escaped[w++] = 'n'; break; - case '\t': escaped[w++] = '\\'; escaped[w++] = 't'; break; - case '\r': escaped[w++] = '\\'; escaped[w++] = 'r'; break; - default: - if (c < 0x20) { - w += (size_t)snprintf(escaped + w, 5, "\\x%02x", c); - } else { - escaped[w++] = (char)c; - } - } + char e[4]; + int k = flan_escape_char(p[i], e); + memcpy(escaped + w, e, (size_t)k); + w += (size_t)k; } if (cut) { escaped[w++] = '.'; escaped[w++] = '.'; escaped[w++] = '.'; } escaped[w++] = '"'; @@ -603,16 +631,6 @@ static uint32_t flan_name_id(const uint8_t *s, int64_t n) { return h; } -/* What the break loop calls to resume: look a restart up by the name someone - * typed and aim the channel at it. 0 if no frame offers it, and then the loop - * says so rather than resuming into nothing. */ -int32_t flan_break_resume(const uint8_t *name, int64_t namelen, void *xfer) { - void *r = flan_find_restart(flan_name_id(name, namelen)); - if (r == NULL) return 0; - *(void **)xfer = r; - return 1; -} - void flan_error(uint32_t type_id, void *condition, void *xfer, const uint8_t *name, int64_t namelen) { flan_signal(type_id, condition, xfer); diff --git a/vendor/agent/flan_agent.c b/vendor/agent/flan_agent.c index e376ad1..e1386b7 100644 --- a/vendor/agent/flan_agent.c +++ b/vendor/agent/flan_agent.c @@ -85,7 +85,6 @@ uint64_t flan_dev_result_cap(void); * [enable] — the table is written only while somebody is reading it, so * opening and closing a watch buffer is a message that arrives here. */ void flan_dev_watch_enable(int on); -int flan_dev_watch_enabled(void); /* And [reset], which opens a new accumulation window for the numeric slots. * It moves one counter and touches no slot, so the game thread stays the only * writer of the table. */ @@ -310,8 +309,6 @@ extern void (*flan_break_hook)(const uint8_t *name, int64_t namelen, * flan_rt.c, which carries the argument for why they are two hooks and not * one. This end of it is [trap_stop] below. */ extern void (*flan_trap_hook)(const uint8_t *name, int64_t namelen); -extern int32_t flan_break_resume(const uint8_t *name, int64_t namelen, - void *xfer); extern int32_t flan_restart_count(void); /* The shadow stack (runtime/flan_dev.c). The compiler pushes a frame per Flan * call in a dev build; these read one, and only ever on the thread that owns