(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.
135 lines
6.1 KiB
Plaintext
135 lines
6.1 KiB
Plaintext
;;;; Drawing a number, now that the language can make one into a string.
|
|
;;;;
|
|
;;;; This file used to be the workaround for a gap: five of the ten ported
|
|
;;;; examples call raylib's `TextFormat` to put a number on the screen, and
|
|
;;;; nothing in Flan could reach `draw-text` with one — `i64->bytes` answered a
|
|
;;;; `[u8]`, `draw-text` wanted a `string`, and there was no bridge and no
|
|
;;;; allocator to build one in. So a number was drawn one glyph at a time out
|
|
;;;; of a `[10 string]` table.
|
|
;;;;
|
|
;;;; `(string b)` closed that. It is the mirror of `(bytes s)` and costs no
|
|
;;;; instructions — a `string` and a `[u8]` are the same 16-byte %slice — so
|
|
;;;; `(string (i64->bytes n))` draws in one call and the table, the per-glyph
|
|
;;;; pen and the digit arithmetic behind them are gone.
|
|
;;;;
|
|
;;;; What is left is the part `(string ...)` does not answer, which is
|
|
;;;; *formatting*: `i64->bytes` has no field width, so "%03i" still has to be
|
|
;;;; assembled, and `f64->bytes` is `%g` and not "%.02f", so a fixed number of
|
|
;;;; decimal places still has to be split and drawn in two pieces. Those two
|
|
;;;; are why the file survives at all, and the three signatures are unchanged
|
|
;;;; so the five callers did not have to move.
|
|
;;;;
|
|
;;;; `TextFormat` itself is still not bindable, and not because of the FFI
|
|
;;;; rules: it is variadic, so its signature is not a signature, and it returns
|
|
;;;; a `char *` into a rotating static buffer, which `declare-c` refuses by
|
|
;;;; name anyway.
|
|
;;;;
|
|
;;;; ONE TRAP, and it is the reason every function below is written as a strict
|
|
;;;; sequence of format-draw-measure rather than as a let of several pieces:
|
|
;;;; `i64->bytes` and `f64->bytes` both write into a single shared static
|
|
;;;; buffer in the runtime, overwritten by the next such call. `(string ...)`
|
|
;;;; does not copy it. So a number must be *drawn before the next one is
|
|
;;;; formatted* — holding two at once is wrong pixels with no crash and no
|
|
;;;; diagnostic.
|
|
;;;;
|
|
;;;; Everything here needs a window: `measure-text` answers 0 for every string
|
|
;;;; until init-window has loaded the default font, and a zero advance would
|
|
;;;; stack every piece on top of the first.
|
|
|
|
(import rl "vendor:raylib")
|
|
|
|
;; Padding is drawn from a literal, one zero at a time. This is the last of the
|
|
;; old glyph table and it is here only because there is no field width.
|
|
(defconst zero-glyph "0")
|
|
|
|
;; One piece of text, and how far the pen moved. raylib's default font is not
|
|
;; monospaced — "1" is narrower than "8" — so the advance is measured rather
|
|
;; than assumed.
|
|
;;
|
|
;; Nothing may be formatted between the draw and the measure: `s` may be a view
|
|
;; of the shared buffer, and both calls have to see the same bytes.
|
|
(defn draw-piece [s string x i32 y i32 size i32 color rl/Color] i32
|
|
(rl/draw-text s x y size color)
|
|
(rl/measure-text s size))
|
|
|
|
;; How many decimal digits `n` has, for n >= 0. 0 has one. Only the padded
|
|
;; forms need it now — it is how many zeroes go in front.
|
|
(defn digit-count [n i32] i32
|
|
(let [d 1
|
|
r (/ n 10)]
|
|
(while (> r 0)
|
|
(set d (+ d 1))
|
|
(set r (/ r 10)))
|
|
d))
|
|
|
|
(defn pow10 [e i32] i32
|
|
(let [p 1]
|
|
(dotimes [i e]
|
|
(set p (* p 10)))
|
|
p))
|
|
|
|
;; The whole number, in one draw-text. Answers the width drawn, so a caller can
|
|
;; put something after it — which is how the `TextFormat("%s: %i", ...)` shapes
|
|
;; in the C are reassembled here: draw the literal part with draw-text, then
|
|
;; this at x plus its width.
|
|
;;
|
|
;; The sign comes free now: i64->bytes renders "-7" itself, which also retires
|
|
;; the old note about i32's most negative value — it is widened to i64 before
|
|
;; formatting, so there is no negation to wrap.
|
|
(defn draw-int [n i32 x i32 y i32 size i32 color rl/Color] i32
|
|
(draw-piece (string (i64->bytes (i64 n))) x y size color))
|
|
|
|
;; The same with a fixed minimum number of digits — the C's "%03i". A number
|
|
;; wider than `width` is drawn in full rather than truncated, which is what
|
|
;; printf does too.
|
|
;;
|
|
;; The zeroes are drawn first and the number after, so the one formatted value
|
|
;; is still live when it is drawn. A sign goes in front of the padding, as
|
|
;; printf's "%03i" does for -7 → "-07"; hence the magnitude is what gets
|
|
;; counted and the minus is drawn separately.
|
|
(defn draw-int-padded [n i32 width i32 x i32 y i32 size i32 color rl/Color]
|
|
i32
|
|
(let [cx x
|
|
v n]
|
|
(when (< v 0)
|
|
(set cx (+ cx (draw-piece "-" cx y size color)))
|
|
(set v (- 0 v)))
|
|
(let [pad (- width (digit-count v))]
|
|
(dotimes [i pad]
|
|
(set cx (+ cx (draw-piece zero-glyph cx y size color)))))
|
|
(set cx (+ cx (draw-int v cx y size color)))
|
|
(- cx x)))
|
|
|
|
;; "%.02f" and friends. `places` digits after the point, rounded by adding a
|
|
;; half at that scale before the split — so 0.999 at two places is "1.00" and
|
|
;; not "0.99", which is what the C's printf would have done and what a reader
|
|
;; comparing the two screens would expect.
|
|
;;
|
|
;; f32 and not f64 deliberately: every number this draws comes out of raylib,
|
|
;; and raylib's are floats. Widening them here would suggest a precision the
|
|
;; value does not have.
|
|
;;
|
|
;; `f64->bytes` is not used at all — it is "%g", which would print 0.5 for a
|
|
;; value asked for at three places and 1e+06 for a large one. The split into
|
|
;; two integers is what buys the fixed width, and it also keeps every formatted
|
|
;; value drawn before the next one is made.
|
|
(defn draw-f32 [v f32 places i32 x i32 y i32 size i32 color rl/Color] i32
|
|
(let [cx x
|
|
av v]
|
|
(when (< av 0.0)
|
|
(set cx (+ cx (draw-piece "-" cx y size color)))
|
|
(set av (- 0.0 av)))
|
|
(let [scale (pow10 places)
|
|
;; The rounding and the split happen in one integer so the two halves
|
|
;; cannot disagree: rounding them separately is how "0.999" becomes
|
|
;; "0.100" — the fraction carries and the whole part does not hear
|
|
;; about it.
|
|
total (i32 (+ (* av (f32 scale)) 0.5))
|
|
whole (/ total scale)
|
|
frac (% total scale)]
|
|
(set cx (+ cx (draw-int whole cx y size color)))
|
|
(when (> places 0)
|
|
(set cx (+ cx (draw-piece "." cx y size color)))
|
|
(set cx (+ cx (draw-int-padded frac places cx y size color)))))
|
|
(- cx x)))
|