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.
This commit is contained in:
parent
afec482722
commit
421e09e0d6
@ -5,7 +5,11 @@
|
||||
;;;; screen itself and this example wants to compare it against the target it
|
||||
;;;; set, so it needs the number back.
|
||||
;;;;
|
||||
;;;; Three TextFormats in the C, all drawn here by examples/digits.flan.
|
||||
;;;; Three TextFormats in the C. Two of the three are a fixed number of
|
||||
;;;; decimal places, which `f64->bytes` cannot do — it is "%g" — so they
|
||||
;;;; still go through examples/digits.flan. The integer one does too, only
|
||||
;;;; because its width is needed to put the next piece after it; the draw
|
||||
;;;; itself is now (string (i64->bytes n)) and one draw-text.
|
||||
;;;;
|
||||
;;;; One faithfulness note that is a bug in the C and is kept anyway: it draws
|
||||
;;;; `TextFormat("Frame time: %02.02f ms", GetFrameTime())` — GetFrameTime is
|
||||
|
||||
@ -25,12 +25,14 @@
|
||||
;;;; transcendental. The protractor needs both, so they are two more `declare`
|
||||
;;;; lines here, in the same shape. They link: libm is already on the line.
|
||||
;;;;
|
||||
;;;; **No string formatting**, as everywhere. The C prints the angle with
|
||||
;;;; `TextFormat("%f", ...)`, finds the decimal point with `TextFindIndex` and
|
||||
;;;; cuts two digits past it with `TextSubtext`. `draw-f32` in
|
||||
;;;; examples/digits.flan does the whole thing in one call, and rounds rather
|
||||
;;;; than truncating — which is the one place this screen differs from the C's
|
||||
;;;; by a digit.
|
||||
;;;; **No string formatting.** A number can be made into a string now —
|
||||
;;;; (string (i64->bytes n)) — but a *format* still cannot: f64->bytes is
|
||||
;;;; "%g", with no way to ask for two decimal places. The C prints the angle
|
||||
;;;; with `TextFormat("%f", ...)`, finds the decimal point with
|
||||
;;;; `TextFindIndex` and cuts two digits past it with `TextSubtext`;
|
||||
;;;; `draw-f32` in examples/digits.flan does the whole thing in one call, and
|
||||
;;;; rounds rather than truncating — which is the one place this screen
|
||||
;;;; differs from the C's by a digit.
|
||||
;;;;
|
||||
;;;; **No local fixed arrays.** The C declares `char gestureLog[20][12]` and
|
||||
;;;; `Vector2 touchPosition[32]` inside main. A `let` binding takes no type
|
||||
|
||||
@ -3,17 +3,16 @@
|
||||
;;;; examples/core/core_input_mouse_wheel.c. Needed get-mouse-wheel-move, now
|
||||
;;;; bound.
|
||||
;;;;
|
||||
;;;; This is the first example with a `TextFormat` in it, and therefore the
|
||||
;;;; first that runs into the gap: the C draws
|
||||
;;;; This is the first example with a `TextFormat` in it. The C draws
|
||||
;;;;
|
||||
;;;; DrawText(TextFormat("Box position Y: %03i", boxPositionY), ...)
|
||||
;;;;
|
||||
;;;; and Flan has no way to make a string out of a number. The label is drawn
|
||||
;;;; with draw-text and the number after it with draw-int-padded from
|
||||
;;;; examples/digits.flan, which is one glyph per digit out of a [10 string]
|
||||
;;;; table. The "%03i" is the `3` argument — the leading zeroes are there, and
|
||||
;;;; a number wider than three digits is drawn in full, exactly as printf's
|
||||
;;;; minimum-width means.
|
||||
;;;; and the number half of that is now sayable: (string (i64->bytes n)).
|
||||
;;;; What is not is the "%03" — i64->bytes has no field width — so the label
|
||||
;;;; is drawn with draw-text and the number with draw-int-padded from
|
||||
;;;; examples/digits.flan, which draws the leading zeroes itself and then the
|
||||
;;;; number in one call. A number wider than three digits is drawn in full,
|
||||
;;;; exactly as printf's minimum-width means.
|
||||
;;;;
|
||||
;;;; The position is signed and goes negative as soon as the box scrolls past
|
||||
;;;; the top, which is why draw-int-padded handles a sign at all.
|
||||
|
||||
@ -23,7 +23,6 @@
|
||||
;;;; without hardware.
|
||||
|
||||
(import rl "vendor:raylib")
|
||||
(import d "digits.flan")
|
||||
|
||||
(defconst screen-width 800)
|
||||
(defconst screen-height 450)
|
||||
@ -57,11 +56,13 @@
|
||||
;; something the port introduced.
|
||||
(when (and (> (.x p) 0.0) (> (.y p) 0.0))
|
||||
(rl/draw-circle-v p 34.0 rl/orange)
|
||||
;; The C's TextFormat("%d", i) — one digit, drawn by the shared
|
||||
;; helper rather than by a codepoint call, so every number on
|
||||
;; screen in these ten files goes through the same path.
|
||||
(d/draw-int i (- (i32 (.x p)) 10) (- (i32 (.y p)) 70) 40
|
||||
rl/black))))
|
||||
;; The C's TextFormat("%d", i). This used to need examples/digits.flan
|
||||
;; and a table of one-character strings; (string b) makes the
|
||||
;; number a string with no instructions, so it is one draw-text and
|
||||
;; this file imports nothing but raylib.
|
||||
(rl/draw-text (string (i64->bytes (i64 i)))
|
||||
(- (i32 (.x p)) 10) (- (i32 (.y p)) 70) 40
|
||||
rl/black))))
|
||||
|
||||
(rl/draw-text "touch the screen at multiple locations to get multiple balls"
|
||||
10 10 20 rl/darkgray)
|
||||
|
||||
@ -36,7 +36,6 @@
|
||||
;;;; top-level `defvar`, since a `let` binding takes no type annotation.
|
||||
|
||||
(import rl "vendor:raylib")
|
||||
(import d "digits.flan")
|
||||
|
||||
(defconst screen-width 800)
|
||||
(defconst screen-height 450)
|
||||
@ -189,7 +188,8 @@
|
||||
;; Not in the C: which button the search picked, as a number, so the
|
||||
;; headless case and the window agree about the same thing.
|
||||
(rl/draw-text "button: " 10 34 20 rl/lightgray)
|
||||
(d/draw-int pressed (+ 10 (rl/measure-text "button: " 20)) 34 20
|
||||
rl/lightgray)
|
||||
(rl/draw-text (string (i64->bytes (i64 pressed)))
|
||||
(+ 10 (rl/measure-text "button: " 20)) 34 20
|
||||
rl/lightgray)
|
||||
|
||||
(rl/end-drawing))))
|
||||
|
||||
@ -1,51 +1,59 @@
|
||||
;;;; Drawing a number, because the language cannot make one into a string.
|
||||
;;;; Drawing a number, now that the language can make one into a string.
|
||||
;;;;
|
||||
;;;; Five of the ten ported examples call raylib's `TextFormat` to put a number
|
||||
;;;; on the screen. Flan has no string formatting and no way to reach it:
|
||||
;;;; 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.
|
||||
;;;;
|
||||
;;;; - `i64->bytes` is a builtin and answers a `[u8]`;
|
||||
;;;; - `draw-text` takes a `string`;
|
||||
;;;; - nothing converts a `[u8]` into a `string`. A string is a compile-time
|
||||
;;;; literal or a parameter, and there is no allocator to build one in.
|
||||
;;;; `(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.
|
||||
;;;;
|
||||
;;;; `TextFormat` itself is not bindable either, and not because of the FFI
|
||||
;;;; rules: it is variadic, so its signature is not a signature — declaring it
|
||||
;;;; with fixed arguments would be a claim about the ABI that is false on every
|
||||
;;;; target at once, and it returns a `char *` into a rotating static buffer,
|
||||
;;;; which `declare-c` refuses by name anyway ("a string only crosses as a
|
||||
;;;; parameter — a C function that *returns* one returns something Flan has no
|
||||
;;;; owner for").
|
||||
;;;; 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.
|
||||
;;;;
|
||||
;;;; So a number reaches the screen one digit at a time, each digit drawn as a
|
||||
;;;; one-character `string` out of the table below. sand.flan already does this
|
||||
;;;; for a single digit with `draw-text-codepoint`; this is the same trick
|
||||
;;;; generalised, in one place, so the gap shows up in the report as one gap
|
||||
;;;; rather than as five separate improvisations.
|
||||
;;;; `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 digit on top of the first.
|
||||
;;;; stack every piece on top of the first.
|
||||
|
||||
(import rl "vendor:raylib")
|
||||
|
||||
;; A `[10 string]` — a fixed array whose element type is `string`. That works,
|
||||
;; which is worth recording: a string is ptr+len and the array is ten of those
|
||||
;; laid out flat, with the bytes themselves in the module's constant data.
|
||||
(defconst digit-glyphs [10 string]
|
||||
["0" "1" "2" "3" "4" "5" "6" "7" "8" "9"])
|
||||
;; 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")
|
||||
|
||||
(defconst minus-glyph "-")
|
||||
(defconst dot-glyph ".")
|
||||
|
||||
;; One glyph, and how far the pen moved. raylib's default font is not
|
||||
;; 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, which is also what keeps the spacing identical to what
|
||||
;; draw-text would have produced for the whole string at once.
|
||||
(defn draw-glyph [g string x i32 y i32 size i32 color rl/Color] i32
|
||||
(rl/draw-text g x y size color)
|
||||
(rl/measure-text g size))
|
||||
;; 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.
|
||||
;; 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)]
|
||||
@ -60,42 +68,36 @@
|
||||
(set p (* p 10)))
|
||||
p))
|
||||
|
||||
;; The whole point of the file. 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 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.
|
||||
;;
|
||||
;; Negative numbers get the sign and then the magnitude. i32's most negative
|
||||
;; value is NOT handled: negating it wraps to itself, so it would print its own
|
||||
;; bit pattern with a minus in front. Nothing here ever reaches it — these are
|
||||
;; screen coordinates, frame counts and axis readings — and guarding it would
|
||||
;; be a branch that no call site can take.
|
||||
;; 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
|
||||
(let [cx x
|
||||
v n]
|
||||
(when (< v 0)
|
||||
(set cx (+ cx (draw-glyph minus-glyph cx y size color)))
|
||||
(set v (- 0 v)))
|
||||
(let [count (digit-count v)]
|
||||
(dotimes [i count]
|
||||
(let [d (% (/ v (pow10 (- (- count 1) i))) 10)]
|
||||
(set cx (+ cx (draw-glyph (at digit-glyphs d) cx y size color))))))
|
||||
(- cx x)))
|
||||
(draw-piece (string (i64->bytes (i64 n))) x y size color))
|
||||
|
||||
;; The same with a fixed number of leading zeroes — the C's "%03i". A number
|
||||
;; 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-glyph minus-glyph cx y size color)))
|
||||
(set cx (+ cx (draw-piece "-" cx y size color)))
|
||||
(set v (- 0 v)))
|
||||
(let [count (max width (digit-count v))]
|
||||
(dotimes [i count]
|
||||
(let [d (% (/ v (pow10 (- (- count 1) i))) 10)]
|
||||
(set cx (+ cx (draw-glyph (at digit-glyphs d) cx y size color))))))
|
||||
(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
|
||||
@ -106,11 +108,16 @@
|
||||
;; 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-glyph minus-glyph cx y size color)))
|
||||
(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
|
||||
@ -122,6 +129,6 @@
|
||||
frac (% total scale)]
|
||||
(set cx (+ cx (draw-int whole cx y size color)))
|
||||
(when (> places 0)
|
||||
(set cx (+ cx (draw-glyph dot-glyph cx y size color)))
|
||||
(set cx (+ cx (draw-piece "." cx y size color)))
|
||||
(set cx (+ cx (draw-int-padded frac places cx y size color)))))
|
||||
(- cx x)))
|
||||
|
||||
42
lib/check.ml
42
lib/check.ml
@ -1197,6 +1197,48 @@ and named_call ctx ~want loc name args =
|
||||
arity loc name 1 args;
|
||||
prim Tast.Bytes (Types.Slice (Types.Int Types.U8))
|
||||
[ check ctx ~want:Types.String (List.hd args) ]
|
||||
|
||||
(* (string b): a [u8] seen as a string. The mirror of (bytes s), spelled the
|
||||
same way — a type name in head position, like (bytes s) and unlike the
|
||||
numeric casts, which go through [is_cast] and really do convert.
|
||||
|
||||
It costs nothing. emit.ml lowers Types.String and Types.Slice _ to the
|
||||
same %slice, 16 bytes at align 8, so a string and a [u8] are already the
|
||||
identical value at run time; both this and [Bytes] emit as the argument
|
||||
itself. What changes is only what the checker will let the value be
|
||||
passed to — which is the whole gap: i64->bytes answers a [u8] and every
|
||||
declare-c text parameter wants a string, and nothing joined them.
|
||||
|
||||
Two decisions are baked in here.
|
||||
|
||||
1. It does NOT check UTF-8, because `string` does not claim UTF-8. The
|
||||
prelude settles this: valid-utf8? is an ordinary function you call when
|
||||
you care, decode-rune/rune-at/rune-count all take [u8] rather than
|
||||
string, and decode-rune answers {:ok false :width 1} on a malformed
|
||||
byte rather than assuming its input is well-formed. 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. So there is no code
|
||||
that would be wrong about a string of arbitrary bytes, and a check here
|
||||
would be the only enforcement point in the language — a claim the rest
|
||||
of it does not make.
|
||||
|
||||
2. It does not widen the literal-write hole (NEXT.md, "Writing through a
|
||||
string literal"). That hole is the other direction: (bytes "Hi") hands
|
||||
you a writable-looking slice over constant data. This direction only
|
||||
loses the ability to write — a string is read-only everywhere — so the
|
||||
result of (string b) can reach strictly fewer stores than b could.
|
||||
Provenance is still what the other direction needs; nothing here
|
||||
depends on having it.
|
||||
|
||||
The one sharp edge is not new but is easier to trip over now: the slice
|
||||
that i64->bytes / f64->bytes / u64->bytes answer is a view into one shared
|
||||
static buffer in the runtime, overwritten by the next such call. Calling
|
||||
it a string does not copy it. Use it before formatting the next number;
|
||||
you cannot hold two at once. *)
|
||||
| "string" ->
|
||||
arity loc name 1 args;
|
||||
prim Tast.StrOfBytes Types.String [ byte_slice ctx (List.hd args) ]
|
||||
| "bytes->f64" ->
|
||||
arity loc name 1 args;
|
||||
prim Tast.BytesToF64 (Types.Float Types.F64) [ byte_slice ctx (List.hd args) ]
|
||||
|
||||
@ -1208,6 +1208,9 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
|
||||
(* string and [u8] have the same layout, so bytes is the identity — a view,
|
||||
no copy (plan.org, Milestone-2 primitives). *)
|
||||
| Tast.Bytes, [ x ] -> value f x
|
||||
(* (string b), and the same non-instruction for the same reason: String and
|
||||
Slice _ are both %slice. See check.ml's "string" case. *)
|
||||
| Tast.StrOfBytes, [ x ] -> value f x
|
||||
| Tast.BytesToF64, [ x ] -> shim_in f "@flan_bytes_to_f64" "double" x
|
||||
| Tast.BytesToI64, [ x ] -> shim_in f "@flan_bytes_to_i64" "i64" x
|
||||
| Tast.F64ToBytes, [ x ] -> shim_out f "@flan_f64_to_bytes" x
|
||||
|
||||
@ -28,6 +28,9 @@ type prim =
|
||||
*text*: bytes->f64 parses "12.5", f64->bytes renders it — that is what
|
||||
calc-me's tokenizer and the prelude's printers each need. *)
|
||||
| Bytes | BytesToF64 | BytesToI64 | F64ToBytes | I64ToBytes
|
||||
(* (string b): the other direction of [Bytes], and the same non-instruction.
|
||||
See check.ml's "string" case for why it is unchecked. *)
|
||||
| StrOfBytes
|
||||
(* No surface name: the structural printer is the only thing that builds
|
||||
these. U64ToBytes because u64 is not i64 with a flag, EscapeBytes for a
|
||||
string nested inside a printed structure. *)
|
||||
|
||||
63
test/programs/string-of-bytes.flan
Normal file
63
test/programs/string-of-bytes.flan
Normal file
@ -0,0 +1,63 @@
|
||||
;;;; (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)
|
||||
@ -225,6 +225,24 @@ let () =
|
||||
outputs "substring, trim and parse-f64" "programs/bytes2.flan" bytes2_out;
|
||||
outputs ~opt:"-O0" "substring, trim and parse-f64, -O0" "programs/bytes2.flan"
|
||||
bytes2_out;
|
||||
(* (string b). The conversion emits nothing — String and Slice _ are the
|
||||
same %slice — so the rows are about length and ownership rather than
|
||||
arithmetic: a number round-tripped, an empty slice, sub-views whose
|
||||
length is not the underlying storage's, and the result crossing a
|
||||
declare-c boundary where the shim NUL-terminates a copy. That last one
|
||||
is the load-bearing case: "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. Both levels, because a reinterpretation that had accidentally
|
||||
been undefined would fail one way at -O0 and the other at -O2. *)
|
||||
let string_of_bytes_out =
|
||||
"[42] 2\n[-7] 2\n[0] 1\n[] 0\n\
|
||||
[hello] 5\n[world] 5\n[] 0\n\
|
||||
7\n\
|
||||
hello\nok\n12345\nok\n\nok\n"
|
||||
in
|
||||
outputs "string of bytes" "programs/string-of-bytes.flan" string_of_bytes_out;
|
||||
outputs ~opt:"-O0" "string of bytes, -O0" "programs/string-of-bytes.flan"
|
||||
string_of_bytes_out;
|
||||
(* handler-bind and signal, spec-conditions.md §1 and §2: signal returns
|
||||
Unit and carries on, an unhandled one is a no-op, a nested frame does
|
||||
not displace the one outside it, and the stack is restored after. *)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user