Pin the escape buffer's bound, and say what it reserves

The guard reserves 9 bytes but the comment explained 5, which is the
longest escape alone -- it did not account for the three writes after the
loop (the ellipsis and the closing quote), so the next person to touch
the escape table would have preserved the wrong invariant.

Swept every length to 1300 against \x01, a quote, a backslash and 'a'
under ASan with a red zone past the buffer: no write past 1024, worst
output 1021. Correct, but by three bytes, which is exactly why the
reserve is now written down as the four things it is spent on.

Nothing exercised truncation -- the longest nested string in the fixture
was 18 bytes -- so println.flan now prints a struct with an 1100-byte
string field, and the expected output spells the surviving count out as
a number so a change to the buffer shows up as one.
This commit is contained in:
Joseph Ferano 2026-09-12 04:58:16 +07:00
parent 93231e8c9e
commit e4db079c57
3 changed files with 22 additions and 3 deletions

View File

@ -222,8 +222,12 @@ void flan_escape_bytes(const uint8_t *p, int64_t n, flan_slice *out) {
size_t len = n < 0 ? 0 : (size_t)n;
size_t w = 0;
int cut = 0;
/* 5 is the longest single escape (\xNN is 4, plus room for the close
* quote); leaving it spare means the loop never writes a partial escape. */
/* The guard reserves 9 bytes, and all 9 are spoken for: 4 for the longest
* single escape (\xNN), 3 for the ellipsis, 1 for the closing quote, 1
* spare. So the loop never writes a partial escape and the three writes
* after it never need a bound of their own. Swept over every length to 1300
* against \x01, '"', '\\' and 'a': the worst output is 1021 of 1024. If the
* escape table ever grows a longer form, this 9 grows with it. */
escaped[w++] = '"';
for (size_t i = 0; i < len; i++) {
if (w + 5 + 4 >= ESCAPE_MAX) { cut = 1; break; }

View File

@ -31,6 +31,8 @@
(defvar wide [10 i32])
(defvar deep D1)
(defvar n i32)
(defstruct Long [s string])
(defvar long-one Long)
(defn nothing [] )
@ -108,6 +110,13 @@
(println (slice arr 0 2)))
(println (slice arr 2 4))
;; A nested string longer than the escape buffer. Escaping is the one
;; conversion whose output is not a bounded handful of characters, so it
;; truncates -- with an ellipsis inside the quotes, because a value that
;; prints as a shorter value is the failure nobody notices.
(set (.s long-one) "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
(println long-one)
;; print is println without the newline.
(print "a")
(print "b")

View File

@ -149,7 +149,13 @@ let () =
(Blob {:id 7 :name \"sandy \\\"quoted\\\"\" :pos (V {:x 1.5 :y -2}) :tags [ 0 42 0]})\n\
[ 0 0 9 0]\n[ 0 0 0 0 0 0 0 0 ...]\n[ 0 9 0]\n\
(D1 {:d (D2 {:d (D3 {:d (D4 {:d (D5 {:n ...})})})})})\n[ 0 0]\n\
[ 0 0]\n[ 9 0]\nabc\n"
[ 0 0]\n[ 9 0]\n"
(* The escape buffer is 1024 and the input is 1100 x's, so this is the
truncation: the ellipsis goes *inside* the quotes, and the count is
spelled out rather than pasted so that a change to the buffer or to
the reserve shows up here as a number and not as a wall of x. *)
^ "(Long {:s \"" ^ String.make 1014 'x' ^ "...\"})\n"
^ "abc\n"
in
outputs "println, every arm" "programs/println.flan" println_out;
outputs ~opt:"-O0" "println, every arm, -O0" "programs/println.flan"