diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index 1489c781..30267398 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -106,19 +106,36 @@ typedef struct { * was cut rather than passing for the whole of it. */ #define FLAN_MESSAGE_MAX 512 +/* The most the break loop's sentence takes, and the unhandled message: the + * full text, not a message's cut copy. */ +#define FLAN_SENTENCE_MAX 2048 + +/* How many of [p]'s [len] bytes fit in [max] without splitting a UTF-8 + * character: a cut that lands on a continuation byte backs up to the start + * of that character, so a shortened text is still text. */ +static int64_t utf8_fit(const uint8_t *p, int64_t len, int64_t max) { + int64_t m; + if (len <= max) return len; + m = max < 0 ? 0 : max; + while (m > 0 && (p[m] & 0xC0) == 0x80) m--; + return m; +} + +static const char ellipsis[] = "\xe2\x80\xa6"; + /* Where [render] prints: the buffer of the signal asking, set around the one * synchronous call. A printer only formats, so nothing nests inside it. */ static char *msg_out; +static int64_t msg_cap; static int64_t msg_len; static int msg_cut; static void msg_append(const uint8_t *p, int64_t n) { - static const char ell[] = "\xe2\x80\xa6"; if (msg_out == NULL || msg_cut || n <= 0) return; - if (msg_len + n > FLAN_MESSAGE_MAX - 4) { - int64_t room = FLAN_MESSAGE_MAX - 4 - msg_len; + if (msg_len + n > msg_cap - 4) { + int64_t room = utf8_fit(p, n, msg_cap - 4 - msg_len); if (room > 0) { memcpy(msg_out + msg_len, p, (size_t)room); msg_len += room; } - memcpy(msg_out + msg_len, ell, 3); + memcpy(msg_out + msg_len, ellipsis, 3); msg_len += 3; msg_cut = 1; return; @@ -132,7 +149,7 @@ void flan_msg_emit(const uint8_t *p, int64_t n) { msg_append(p, n); } /* The view of [condition] for a parent's handler, its message in [buf], * which the caller owns and which lives as long as the handler runs. */ static void rt_view(const flan_condesc *d, void *condition, char *buf, - flan_view *v) { + int64_t cap, flan_view *v) { if (d->flags & FLAN_CONDESC_SELF) { *v = *(const flan_view *)condition; return; @@ -141,6 +158,7 @@ static void rt_view(const flan_condesc *d, void *condition, char *buf, v->namelen = d->namelen; v->message = (const uint8_t *)buf; msg_out = buf; + msg_cap = cap; msg_len = 0; msg_cut = 0; if (d->messagelen > 0) @@ -193,7 +211,10 @@ void flan_signal(const flan_condesc *d, void *condition, void *xfer) { for (flan_handler *h = saved; h != NULL; h = h->prev) { int how = flan_handles(h->type_id, d); if (how) { - if (how == 2 && !viewed) { rt_view(d, condition, buf, &v); viewed = 1; } + if (how == 2 && !viewed) { + rt_view(d, condition, buf, FLAN_MESSAGE_MAX, &v); + viewed = 1; + } handlers = h->prev; h->fn(how == 1 ? condition : (void *)&v, xfer, h->env); handlers = saved; @@ -884,14 +905,20 @@ _Noreturn void flan_trap(const uint8_t *name, int64_t namelen) { * nothing to point at. */ const uint8_t *flan_break_site; int64_t flan_break_site_len; -char flan_break_sentence[512]; +char flan_break_sentence[FLAN_SENTENCE_MAX]; int64_t flan_break_sentence_len; static void rt_sentencev(const char *fmt, va_list ap) { int n = vsnprintf(flan_break_sentence, sizeof flan_break_sentence, fmt, ap); if (n < 0) n = 0; - if (n >= (int)sizeof flan_break_sentence) - n = (int)sizeof flan_break_sentence - 1; + if (n >= (int)sizeof flan_break_sentence) { + /* Cut at a character and said to be cut. */ + int64_t m = utf8_fit((const uint8_t *)flan_break_sentence, + (int64_t)sizeof flan_break_sentence - 1, + (int64_t)sizeof flan_break_sentence - 4); + memcpy(flan_break_sentence + m, ellipsis, 3); + n = (int)m + 3; + } flan_break_sentence_len = n; } @@ -905,8 +932,13 @@ static void rt_sentence(const char *fmt, ...) { /* The sentence just formatted, NUL-terminated into a caller's buffer. */ static void rt_sentence_copy(char *out) { int64_t n = flan_break_sentence_len; - if (n > FLAN_MESSAGE_MAX - 1) n = FLAN_MESSAGE_MAX - 1; - memcpy(out, flan_break_sentence, (size_t)n); + if (n > FLAN_MESSAGE_MAX - 1) { + n = utf8_fit((const uint8_t *)flan_break_sentence, n, FLAN_MESSAGE_MAX - 4); + memcpy(out, flan_break_sentence, (size_t)n); + memcpy(out + n, ellipsis, 3); + n += 3; + } else + memcpy(out, flan_break_sentence, (size_t)n); out[n] = 0; } @@ -1032,9 +1064,9 @@ void flan_error(const flan_condesc *d, void *condition, void *xfer) { /* What the condition is, as a parent's handler would read it: the * sentence, the printed condition, or an Error-shaped condition's own * message. A condition with no parent and no sentence has none. */ - char buf[FLAN_MESSAGE_MAX]; + char buf[FLAN_SENTENCE_MAX]; flan_view v; - rt_view(d, condition, buf, &v); + rt_view(d, condition, buf, FLAN_SENTENCE_MAX, &v); if (flan_break_hook != NULL) { flan_break_site = d->loclen > 0 ? d->loc : NULL; flan_break_site_len = d->loclen; @@ -1197,7 +1229,6 @@ static void rt_condesc(flan_condesc *d, uint32_t chain[2], const uint8_t *name, * which case the caller returns and its caller's guard carries the transfer * out. */ static int rt_error_break(const flan_condesc *d, void *condition, void *xfer) { - rt_sentence("%.*s", (int)d->messagelen, (const char *)d->message); if (flan_break_hook != NULL) { flan_break_site = d->loc; flan_break_site_len = d->loclen; @@ -1229,6 +1260,11 @@ static int flan_bounds_signal(const uint8_t *loc, int64_t loclen, void *xfer, loclen); flan_signal(&d, &c, xfer); if (*(void **)xfer != NULL) return 1; + /* Formatted again, in full: [said] may be cut, and a handler the walk ran + * may have written a sentence of its own over the shared one. */ + if (kind == BOUNDS_AT) bounds_sentence(low, len); + else if (kind == BOUNDS_SLICE) slice_sentence(low, high, len); + else promise_sentence(high); return rt_error_break(&d, &c, xfer); } @@ -1399,6 +1435,7 @@ void flan_arith_error(const uint8_t *loc, int64_t loclen, int32_t op, loclen); flan_signal(&d, &c, xfer); if (*(void **)xfer != NULL) return; + arith_sentence(op, lhs, rhs); /* in full; see flan_bounds_signal */ if (rt_error_break(&d, &c, xfer)) return; rt_print_sentence(loc, loclen); rt_die(); @@ -1449,6 +1486,16 @@ static flan_slice flan_stale_copy(const char *s) { return r; } +static void stale_sentence(const char *callee, const char *want, + const char *now) { + rt_sentence("this call to %s was compiled for %s, and %s is defined as %s. " + "Evaluating the function this call is in again fixes its next " + "call. A function that is still running, such as main's loop, " + "is never called again: define %s with %s again, or run the " + "program again.", + callee, want, callee, now, callee, want); +} + void flan_stale_call(const char *site, const char *callee, const char *want, void *const *cell, void *xfer) { /* A registry cell nothing has published into yet has no text. */ @@ -1461,17 +1508,13 @@ void flan_stale_call(const char *site, const char *callee, const char *want, flan_condesc d; uint32_t chain[2]; char said[FLAN_MESSAGE_MAX]; - rt_sentence("this call to %s was compiled for %s, and %s is defined as %s. " - "Evaluating the function this call is in again fixes its next " - "call. A function that is still running, such as main's loop, " - "is never called again: define %s with %s again, or run the " - "program again.", - callee, want, callee, now, callee, want); + stale_sentence(callee, want, now); rt_sentence_copy(said); rt_condesc(&d, chain, flan_stale_name, FLAN_STALE_NAMELEN, said, where.ptr, where.len); flan_signal(&d, &c, xfer); if (*(void **)xfer != NULL) return; + stale_sentence(callee, want, now); /* in full; see flan_bounds_signal */ if (rt_error_break(&d, &c, xfer)) return; rt_print_sentence(where.ptr, where.len); rt_die(); diff --git a/test/programs/condition-longmessage.flan b/test/programs/condition-longmessage.flan new file mode 100644 index 00000000..160bf339 --- /dev/null +++ b/test/programs/condition-longmessage.flan @@ -0,0 +1,14 @@ +;;;; A message longer than the buffer that holds it is cut at a character, +;;;; never inside one, and says it was cut. The printed condition below starts +;;;; with an odd number of bytes before 400 two-byte characters, so a cut at a +;;;; fixed byte count lands inside one. The unhandled message at the end is +;;;; printed in full: it is not made from the handler's shortened copy. + +(defstruct Wide :parent Error [code i32 why string]) +(defstruct Long :parent Error) + +(defn main [] i32 + (handler-case (do (error (Wide {.code 123 .why "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé"})) 0) + [(Error [e] (println (.message e)) 0)]) + (error (Long {.name "long" .message "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé"})) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index a748004a..27207c6e 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -2929,6 +2929,24 @@ let () = "programs/condition-messages.flan" messages_out; outputs ~x86:true "a parent's message outlives the unwind, --x86" "programs/condition-messages.flan" messages_out; + (* A message longer than its buffer is cut at a character boundary and + ends in an ellipsis; the unhandled message is the full text. *) + let e n = String.concat "" (List.init n (fun _ -> "\xc3\xa9")) in + let cut = "(Wide {.code 123 .why \"" ^ e 242 ^ "\xe2\x80\xa6\n" in + let full = "unhandled Long: " ^ e 400 ^ "\n" in + List.iter + (fun x86 -> + let exe = compile ~x86 "programs/condition-longmessage.flan" in + let code, text = run exe None in + if code <> 134 || not (contains text cut) || not (contains text full) + then begin + incr failures; + Printf.printf + "FAIL a long message is cut at a character%s\n got: %S (exit %d)\n" + (if x86 then ", --x86" else "") text code + end; + (try Sys.remove exe with Sys_error _ -> ())) + [ false; true ]; (* And the half that finishes that thought. bounds-condition.flan's last line is `10 99 12 13` — an abandoned frame's leftovers — and a restart diff --git a/vendor/agent/flan_agent.c b/vendor/agent/flan_agent.c index e53bba80..fa8f2f86 100644 --- a/vendor/agent/flan_agent.c +++ b/vendor/agent/flan_agent.c @@ -570,7 +570,7 @@ typedef struct { /* The runtime's sentence about the stop, copied and consumed with the * site. Empty for a stop whose condition says what it is in its fields. */ int32_t sentencelen; - char sentence[512]; + char sentence[2048]; /* FLAN_SENTENCE_MAX */ } snapshot; /* One per nested break loop, because an inner break must not answer with the @@ -718,7 +718,11 @@ static int snap_push(int resumable, void *cond) { s->sentencelen = 0; if (flan_break_sentence_len > 0) { int64_t k = flan_break_sentence_len; - if (k > (int64_t)sizeof s->sentence) k = (int64_t)sizeof s->sentence; + if (k > (int64_t)sizeof s->sentence) { + /* Never splitting a UTF-8 character. */ + k = (int64_t)sizeof s->sentence; + while (k > 0 && ((uint8_t)flan_break_sentence[k] & 0xC0) == 0x80) k--; + } memcpy(s->sentence, flan_break_sentence, (size_t)k); /* One line on the wire: a newline in it would end the reply early. */ for (int64_t i = 0; i < k; i++)