The runtime's twin emitters share their bodies, and six dead symbols go

This commit is contained in:
Joseph Ferano 2026-09-20 12:06:48 +07:00
commit 26439dc22e
9 changed files with 173 additions and 195 deletions

View File

@ -653,8 +653,6 @@ let stamp_of prog =
| st -> Printf.sprintf "%s:%d:%f" path st.Unix.st_size st.Unix.st_mtime | st -> Printf.sprintf "%s:%d:%f" path st.Unix.st_size st.Unix.st_mtime
| exception Unix.Unix_error _ -> path | 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 (* 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 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 to carry [opt] and [target]: the acceptance table builds the same programs

View File

@ -3395,17 +3395,17 @@ let ignore_sigpipe () =
and this is a *daemon* whose client died. and this is a *daemon* whose client died.
What makes this answerable without a new protocol is that the connection is 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 per session and not per request. [emacs/flan.el] opens one
[make-network-process] in [flan-dev--open], keeps it in [make-network-process] in [flan--open], keeps it in [flan--connection], and
[flan-dev--connection], and reopens only when the process is dead; every reopens only when the process is dead; every site that tears it down
site that tears it down deliberately sends [close] first. [serve] mirrors deliberately sends [close] first. [serve] mirrors
that shape it loops on the one fd until EOF so while an editor is 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 attached this loop is not even cycling. An editor left open and idle
overnight is therefore *attached* overnight, and the grace below can never overnight is therefore *attached* overnight, and the grace below can never
accumulate under it. That is structural, not a number chosen to outlast 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 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 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. never is indistinguishable from any other attached client.
So the signal is client *absence*, and the only absence this is allowed to 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 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 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 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. *) nothing this loop can see. *)
let accept_loop ?grace t ls = let accept_loop ?grace t ls =
let grace = match grace with Some g -> g | None -> client_grace () in let grace = match grace with Some g -> g | None -> client_grace () in

View File

@ -57,15 +57,11 @@ let tag_of_int = function
dynload_stubs.c, one field at a time. Everything allocated here is owned by dynload_stubs.c, one field at a time. Everything allocated here is owned by
[Dynload] and released together after the call. *) [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 (* 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 takes a [Form] slice, and a slice is contiguous elements and not an array of
pointers. *) pointers so this writes *into* memory the caller took, and every caller
and write p (f : Form.t) = 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 tag t = Dynload.poke_i32 p 0 (tag_int t) in
let str t s = let str t s =
tag t; tag t;

View File

@ -656,13 +656,6 @@ let elem_ty loc (t : Types.t) =
| Types.String -> Types.Int Types.U8 | Types.String -> Types.Int Types.U8
| t -> at loc "indexing %s is not in the JS dialect" (Types.to_string t) | 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 = let rec value f (e : Tast.expr) : string =
refuse_ty e.Tast.loc e.Tast.ty; refuse_ty e.Tast.loc e.Tast.ty;
match e.Tast.e with match e.Tast.e with

View File

@ -203,80 +203,109 @@ void flan_dev_emit(const uint8_t *bytes, int64_t len) {
result_len += n; result_len += n;
} }
static void emit_cstr(const char *s) { /* ── Rendering a scalar, into whichever buffer is being written ──────── */
flan_dev_emit((const uint8_t *)s, (int64_t)strlen(s));
/* 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 /* 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. */ * 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]; char buf[32];
snprintf(buf, sizeof buf, "%llu", (unsigned long long)x); 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]; char buf[32];
snprintf(buf, sizeof buf, "%lld", (long long)x); 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 /* Unsigned NaN, and the rule is flan_rt.c's rather than a second statement of
* and println must not disagree about what a value looks like, which is the * it: [flan_f64_format] is what println renders through, and the REPL, a watch
* rule the escape table below is already held to. A NaN's sign bit is decided * row and println must not disagree about what one value looks like. A NaN's
* by whether the value was folded or computed, so showing it makes the printed * sign bit is decided by whether the value was folded or computed, so showing
* form depend on the backend and the optimisation level rather than on the * it would make the printed form depend on the backend and the optimisation
* number. */ * level rather than on the number. */
void flan_dev_emit_f64(double x) { extern int flan_f64_format(double x, char *buf, size_t cap);
static void put_f64(sink out, double x) {
char buf[64]; char buf[64];
if (x != x) snprintf(buf, sizeof buf, "nan"); flan_f64_format(x, buf, sizeof buf);
else snprintf(buf, sizeof buf, "%g", x); put(out, buf);
emit_cstr(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
* framing bug rather than as the value it is. */ * framing bug rather than as the value it is and a newline in a watched
void flan_dev_emit_str(const uint8_t *bytes, int64_t len) { * 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; size_t n = len < 0 ? 0 : (size_t)len;
emit_cstr("\""); put(out, "\"");
for (size_t i = 0; i < n; i++) { for (size_t i = 0; i < n; i++) {
unsigned char c = bytes[i]; char e[4];
switch (c) { int k = flan_escape_char(bytes[i], e);
case '"': emit_cstr("\\\""); break; out((const uint8_t *)e, (int64_t)k);
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);
}
}
} }
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) { void flan_dev_result_end(void) {
if (result_full) { end_value(result, &result_len, RESULT_MAX, result_full, &generation);
/* 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);
} }
/* 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.
@ -444,10 +473,6 @@ void flan_dev_watch_enable(int on) {
__atomic_store_n(&watch_on, on ? 1 : 0, __ATOMIC_RELAXED); __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. /* 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 * 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; s->len += (uint32_t)n;
} }
static void watch_cstr(const char *str) { /* The four renderings, aimed at the watch slot instead of the result buffer.
flan_dev_watch_emit((const uint8_t *)str, (int64_t)strlen(str)); * 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) { void flan_dev_watch_emit_str(const uint8_t *bytes, int64_t len) {
size_t n = len < 0 ? 0 : (size_t)len; put_str(flan_dev_watch_emit, bytes, 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("\"");
} }
void flan_dev_watch_end(void) { 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;
if (s->full) { /* [len] widened and narrowed around the shared close, which counts in
/* Room is made for it rather than assumed: the value is full by * size_t because the result buffer does; a slot's own length is 32 bits
* definition when this fires. */ * and [WATCH_VAL] is 192, so neither conversion can lose anything. */
const char *ell = "..."; size_t len = s->len;
size_t k = strlen(ell); end_value(s->val, &len, WATCH_VAL, s->full, &s->gen);
if (s->len > WATCH_VAL - k) s->len = (uint32_t)(WATCH_VAL - k); s->len = (uint32_t)len;
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);
} }
/* ── Watching one scalar, with no compiler change ───────────────────── */ /* ── 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; 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) { int32_t flan_dev_watch_f64(const char *name, double x) {
if (!flan_dev_watch_begin(name)) return 0; if (!flan_dev_watch_begin(name)) return 0;
flan_dev_watch_emit_f64(x); flan_dev_watch_emit_f64(x);

View File

@ -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); } 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 /* 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 * flan_rt.c's [flan_escape_char], which is where the typed side's printers
* for the same reason those two are the same as each other: three printers * [flan_escape_bytes] and flan_dev.c's emitters now share their one copy of
* that disagree about what a string looks like is three wire formats. If that * it: printers that disagree about what a string looks like are that many
* table changes, change this one. Streamed rather than built, so there is no * wire formats.
* buffer to overrun and no length to cap. */ *
* 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 <vec>. 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) { static void emit_escaped(const uint8_t *p, int64_t n) {
int64_t i; int64_t i;
emit("\""); emit("\"");

View File

@ -3,12 +3,12 @@
* 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 is the same standing * defines and this file declares it a second time. That second copy is not
* arrangement flan_escape_bytes and flan_dev_emit_str already live under * held honest by hand the way a repeated escape table would be: it is made
* "if either table changes, change both" and it is made mechanical rather * mechanical, because test/dyn_ops.c includes this header and names every
* than hopeful: test/dyn_ops.c includes this header and names every function * function below, so a signature that drifts from the implementation is a
* below, so a signature that drifts from the implementation is a link error in * link error in `dune test` rather than a surprise at someone else's call
* `dune test` rather than a surprise at someone else's call site. * site.
* *
* 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

View File

@ -134,10 +134,9 @@ void *flan_restart_frame(int32_t i) {
return NULL; return NULL;
} }
/* Aim the transfer channel at a frame obtained earlier. The same store /* Aim the transfer channel at a frame obtained earlier. The same store an
* [flan_break_resume] makes and the same one an invoke-restart makes this * invoke-restart makes this only spells it without a lookup, for a caller
* only spells it without a lookup, for a caller that did its looking up when * that did its looking up when the stack was worth reading. */
* the stack was worth reading. */
void flan_restart_take(void *frame, void *xfer) { *(void **)xfer = frame; } void flan_restart_take(void *frame, void *xfer) { *(void **)xfer = frame; }
/* [T] and string are both ptr+len — see Emit.ll. */ /* [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 * 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: * 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) { void flan_f64_to_bytes(double x, uint8_t *buf, flan_slice *out) {
int n = (x != x) ? snprintf((char *)buf, FLAN_NUM_BYTES, "nan") int n = flan_f64_format(x, (char *)buf, FLAN_NUM_BYTES);
: snprintf((char *)buf, FLAN_NUM_BYTES, "%g", x);
out->ptr = buf; out->ptr = buf;
out->len = fit(n); 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); 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 /* 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 * 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. * 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 * The table is [flan_escape_char] above, which is also what flan_dev.c's
* deliberately so: the REPL and println must not disagree about what a struct * emitters use; this function is the framing and nothing else.
* 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.
* *
* Its own buffer, not `scratch`: escaping is the one conversion whose output * 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 * 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++] = '"'; escaped[w++] = '"';
for (size_t i = 0; i < len; i++) { for (size_t i = 0; i < len; i++) {
if (w + 5 + 4 >= ESCAPE_MAX) { cut = 1; break; } if (w + 5 + 4 >= ESCAPE_MAX) { cut = 1; break; }
unsigned char c = p[i]; char e[4];
switch (c) { int k = flan_escape_char(p[i], e);
case '"': escaped[w++] = '\\'; escaped[w++] = '"'; break; memcpy(escaped + w, e, (size_t)k);
case '\\': escaped[w++] = '\\'; escaped[w++] = '\\'; break; w += (size_t)k;
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;
}
}
} }
if (cut) { escaped[w++] = '.'; escaped[w++] = '.'; escaped[w++] = '.'; } if (cut) { escaped[w++] = '.'; 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; 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, void flan_error(uint32_t type_id, void *condition, void *xfer,
const uint8_t *name, int64_t namelen) { const uint8_t *name, int64_t namelen) {
flan_signal(type_id, condition, xfer); flan_signal(type_id, condition, xfer);

View File

@ -85,7 +85,6 @@ uint64_t flan_dev_result_cap(void);
* [enable] the table is written only while somebody is reading it, so * [enable] the table is written only while somebody is reading it, so
* opening and closing a watch buffer is a message that arrives here. */ * opening and closing a watch buffer is a message that arrives here. */
void flan_dev_watch_enable(int on); 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. /* 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 * It moves one counter and touches no slot, so the game thread stays the only
* writer of the table. */ * 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 * flan_rt.c, which carries the argument for why they are two hooks and not
* one. This end of it is [trap_stop] below. */ * one. This end of it is [trap_stop] below. */
extern void (*flan_trap_hook)(const uint8_t *name, int64_t namelen); 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); extern int32_t flan_restart_count(void);
/* The shadow stack (runtime/flan_dev.c). The compiler pushes a frame per Flan /* 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 * call in a dev build; these read one, and only ever on the thread that owns