flan/test/programs/string-of-bytes.flan
Joseph Ferano 421e09e0d6 A number can reach draw-text now
(string b) is the mirror of (bytes s) and costs nothing: emit.ml already
lowers Types.String and Types.Slice _ to the same %slice, 16 bytes at
align 8, so a string and a [u8] are the identical value at run time and
both directions emit as the argument itself. What changes is only what
the checker will let the value be passed to — which was the whole gap.

Two decisions, both written into check.ml's comment.

It does not check UTF-8, because `string` does not claim UTF-8. The
prelude settles it: valid-utf8? is an ordinary function you call when you
care, decode-rune / rune-at / rune-count all take [u8] and not string,
and decode-rune answers {:ok false :width 1} on a malformed byte rather
than assuming well-formed input. The one place the runtime treats a
string differently from a byte slice is flan_escape_bytes, for a string
nested in a printed structure, and that is a byte-wise escape table with
no decoding in it. A check here would be the only enforcement point in
the language, which is a claim the rest of it does not make.

It does not widen the literal-write hole. That hole is the other
direction — (bytes "Hi") hands back a writable-looking slice over
constant data — and this direction only loses the ability to write, so
the result reaches strictly fewer stores than its argument could.
Provenance is still what the other direction needs; nothing here waits
on it.

The one sharp edge is not new but is easier to trip over now, and is
recorded in both the checker and digits.flan: i64->bytes, f64->bytes and
u64->bytes all view the same static buffer in the runtime, overwritten
by the next call, and calling it a string does not copy it. Format, draw,
then format the next one.

examples/digits.flan keeps its three signatures and loses its middle: the
[10 string] table, the per-glyph pen and the digit arithmetic are gone,
and draw-int is one draw-text. What survives is the part (string ...)
does not answer — i64->bytes has no field width, so "%03i" is still
assembled, and f64->bytes is "%g", so fixed decimal places are still a
split into two integers. core-input-multitouch and
core-input-virtual-controls ignored the width they were given, so both
inline the draw and stop importing digits.flan entirely.

test/programs/string-of-bytes.flan at -O2 and -O0: a number round-tripped,
an empty slice, sub-views whose length is not the underlying storage's,
and the result across a declare-c boundary. The last is the one that
could have been wrong — "hello world" cut to five bytes has a space where
C wants a NUL, so a shim that trusted the bytes would print all eleven.
2026-09-12 05:19:23 +07:00

64 lines
2.7 KiB
Plaintext

;;;; (string b) — a [u8] seen as a string.
;;;;
;;;; The conversion emits no instructions: String and Slice _ are both %slice,
;;;; 16 bytes at align 8. So what is worth testing is not arithmetic, it is the
;;;; four places a zero-instruction reinterpretation could still be wrong about
;;;; the *length* or about who owns the bytes.
;;;;
;;;; Run at -O2 and at -O0. The pair matters here for the same reason it
;;;; matters for the literal-write sharp edge: a conversion that accidentally
;;;; produced undefined behaviour would be a SIGSEGV at one level and a silent
;;;; deletion at the other, and agreeing at one level alone proves nothing.
;; puts and not a Flan printer: the point of this declaration is the *shim*,
;; which takes ptr+len and NUL-terminates a copy. A shim that instead trusted
;; the bytes to already be terminated would print the rest of the buffer for
;; every sub-view below, and nothing inside Flan would notice.
;;
;; Its return is C's "some nonnegative value", not a number worth printing, so
;; it is only shown as a sign — and it is printed through Flan's own writer,
;; which shares stdout's buffer with puts, so the interleaving is stable.
(declare-c c-puts [s string] i32 "puts")
(defn shows [s string]
(print-str "[")
(print-str s)
(print-str "] ")
(print-i64 (i64 (len (bytes s))))
(newline))
(defn main [] i32
;; A number. The gap this closes: i64->bytes answers a [u8], every text
;; parameter wants a string, and until now nothing joined them.
(shows (string (i64->bytes 42)))
(shows (string (i64->bytes -7)))
(shows (string (i64->bytes 0)))
;; An empty slice. Length 0, and no read of the pointer.
(shows (string (slice (bytes "abc") 1 1)))
;; A sub-view, whose length is not the underlying storage's. The bytes after
;; index 5 are still there and must not appear.
(let [s (bytes "hello world")]
(shows (string (slice s 0 5)))
(shows (string (slice s 6 11)))
(shows (string (slice s 11 11))))
;; Round trip: (bytes (string b)) is b, and both directions are the identity.
(let [b (i64->bytes 1234567)]
(print-i64 (i64 (len (bytes (string b)))))
(newline))
;; Across the declare-c boundary. The first is a sub-view — five bytes out of
;; eleven, the sixth of which is a space and not a NUL — so a shim that did
;; not copy would print "hello world" here.
(let [s (bytes "hello world")]
(print-str (if (>= (c-puts (string (slice s 0 5))) 0) "ok" "no"))
(newline))
(print-str (if (>= (c-puts (string (i64->bytes 12345))) 0) "ok" "no"))
(newline)
;; And an empty one: the shim's copy of a zero-length slice is "".
(print-str (if (>= (c-puts (string (slice (bytes "abc") 1 1))) 0) "ok" "no"))
(newline)
0)