Decoding is the only part of a string library that needs no allocator
Odin's core/strings and all of core/fmt take an allocator; core/unicode/utf8 does not, because decoding is classification and every answer is a number. That line is where the port stops, and the refusals at the foot of the file say so by name rather than leaving a caller to find out. The accept_sizes table becomes a cond over the lead byte. Its four awkward rows are the ones a hand-written decoder gets wrong one at a time, so they are written out: 0xc0/0xc1 lead nothing, 0xe0 and 0xf0 have a raised second-byte floor against overlongs, 0xed has a lowered ceiling against the surrogates. Two divergences from Odin, both the parse-i64 argument again. A malformed sequence carries ok:false instead of decoding to U+FFFD, which is a real code point a caller cannot tell from a failure; and encode-rune! answers None rather than silently substituting U+FFFD for a rune it was not given. Width stays 1 on a bad byte, which is Odin's rule and load-bearing: every loop here advances by it, and a 0 would hang rather than answer wrong. split cannot return a sequence it would have to own, so the cursor is what survives. It follows the allocating strings.split rather than Odin's own iterator, which drops a trailing empty field and disagrees with it. Case conversion is byte-wise and not in place: a literal is emitted into read-only memory, so lowering (bytes "Hi") would type check and segfault.
This commit is contained in:
parent
a5980734dc
commit
2acb70b8db
281
lib/prelude.ml
281
lib/prelude.ml
@ -407,6 +407,287 @@ let source = {flan|
|
||||
;; Trailing junk is the case strtod is silent about, so the position has
|
||||
;; to land exactly on the end.
|
||||
(if (= i (len s)) (Some (bytes->f64 s)) None)))
|
||||
|
||||
;; ── UTF-8 ─────────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Ported from Odin's core/unicode/utf8/utf8.odin, which is the one corner of
|
||||
;; a string library that is allocation-free by construction: decoding is
|
||||
;; classification, and every answer it gives is a number. Everything else in
|
||||
;; Odin's core/strings and all of core/fmt takes `allocator :=
|
||||
;; context.allocator`, and is therefore refused below rather than ported.
|
||||
;;
|
||||
;; Odin's 256-entry accept_sizes table becomes a cond over the lead byte here.
|
||||
;; The table is the cache-friendly form and the cond is the one you can check
|
||||
;; by reading, and nothing in a game decodes UTF-8 in a hot loop — DrawText
|
||||
;; hands the bytes straight to raylib.
|
||||
;;
|
||||
;; The four rules that table encodes, and which a hand-written decoder gets
|
||||
;; wrong one at a time:
|
||||
;;
|
||||
;; 0x80..0xc1 never a lead byte. 0x80..0xbf are continuation bytes, and
|
||||
;; 0xc0 and 0xc1 could only ever begin an *overlong* two-byte
|
||||
;; spelling of an ASCII character — the encoding that lets
|
||||
;; "\xc0\xaf" smuggle a "/" past a check for one.
|
||||
;; 0xe0 second byte 0xa0..0xbf and not 0x80..0xbf; the low half is
|
||||
;; the overlong three-byte range.
|
||||
;; 0xed second byte 0x80..0x9f. The high half is U+D800..U+DFFF,
|
||||
;; the UTF-16 surrogates, which are not scalar values.
|
||||
;; 0xf0, 0xf4 second byte 0x90..0xbf and 0x80..0x8f: overlong below,
|
||||
;; and past U+10FFFF above. 0xf5..0xff lead nothing at all.
|
||||
;;
|
||||
;; A rune is an i32 and not a type of its own. That is Odin's answer too —
|
||||
;; its `rune` is a four-byte integer distinguished only by a flag on the
|
||||
;; basic-type row (src/types.cpp, the Basic_rune entry) — so nothing in the
|
||||
;; checker has to learn a new type for any of this.
|
||||
|
||||
;; One deliberate divergence from Odin, and it is the parse-i64 argument over
|
||||
;; again. Odin's decode_rune answers RUNE_ERROR — U+FFFD — for malformed
|
||||
;; bytes, and U+FFFD is a perfectly real code point that a well-formed string
|
||||
;; may contain, so a caller cannot tell a decoded replacement character from a
|
||||
;; failure to decode. This carries `ok` instead, and leaves `code` 0 when it
|
||||
;; is false.
|
||||
;;
|
||||
;; `width` is 1 on a malformed byte and 0 only for an empty input. That is
|
||||
;; Odin's rule and it is load-bearing rather than cosmetic: every loop below
|
||||
;; advances by `width`, so a 0 there on a bad byte is an infinite loop, not a
|
||||
;; wrong number.
|
||||
(defstruct Rune [code i32 width i32 ok bool])
|
||||
|
||||
(defn rune-start? [b u8] bool
|
||||
(!= (bit-and b 0xc0) 0x80))
|
||||
|
||||
(defn decode-rune [s [u8]] Rune
|
||||
(when (= (len s) 0)
|
||||
(return (Rune {:code 0 :width 0 :ok false})))
|
||||
(let [b0 (at s 0)]
|
||||
(when (< b0 0x80)
|
||||
(return (Rune {:code (i32 b0) :width 1 :ok true})))
|
||||
;; size 0 means "this byte cannot lead"; lo/hi are the *second* byte's
|
||||
;; accepted range, which is the only place the overlong and surrogate
|
||||
;; rules live. Bytes three and four are always 0x80..0xbf.
|
||||
(let [size 0
|
||||
lo (u8 0x80)
|
||||
hi (u8 0xbf)]
|
||||
(cond
|
||||
(< b0 0xc2) (set size 0)
|
||||
(<= b0 0xdf) (set size 2)
|
||||
(= b0 0xe0) (do (set size 3) (set lo (u8 0xa0)))
|
||||
(<= b0 0xec) (set size 3)
|
||||
(= b0 0xed) (do (set size 3) (set hi (u8 0x9f)))
|
||||
(<= b0 0xef) (set size 3)
|
||||
(= b0 0xf0) (do (set size 4) (set lo (u8 0x90)))
|
||||
(<= b0 0xf3) (set size 4)
|
||||
(= b0 0xf4) (do (set size 4) (set hi (u8 0x8f)))
|
||||
:else (set size 0))
|
||||
(when (= size 0)
|
||||
(return (Rune {:code 0 :width 1 :ok false})))
|
||||
;; A sequence cut off by the end of the slice. Width 1, so a caller
|
||||
;; scanning a buffer boundary makes progress instead of stalling.
|
||||
(when (> size (len s))
|
||||
(return (Rune {:code 0 :width 1 :ok false})))
|
||||
(let [b1 (at s 1)]
|
||||
(when (or (< b1 lo) (> b1 hi))
|
||||
(return (Rune {:code 0 :width 1 :ok false})))
|
||||
(when (= size 2)
|
||||
(return (Rune {:code (bit-or (<< (i32 (bit-and b0 0x1f)) 6)
|
||||
(i32 (bit-and b1 0x3f)))
|
||||
:width 2 :ok true})))
|
||||
(let [b2 (at s 2)]
|
||||
(when (or (< b2 0x80) (> b2 0xbf))
|
||||
(return (Rune {:code 0 :width 1 :ok false})))
|
||||
(when (= size 3)
|
||||
(return (Rune {:code (bit-or (bit-or (<< (i32 (bit-and b0 0x0f)) 12)
|
||||
(<< (i32 (bit-and b1 0x3f)) 6))
|
||||
(i32 (bit-and b2 0x3f)))
|
||||
:width 3 :ok true})))
|
||||
(let [b3 (at s 3)]
|
||||
(when (or (< b3 0x80) (> b3 0xbf))
|
||||
(return (Rune {:code 0 :width 1 :ok false})))
|
||||
(Rune {:code (bit-or (bit-or (<< (i32 (bit-and b0 0x07)) 18)
|
||||
(bit-or (<< (i32 (bit-and b1 0x3f)) 12)
|
||||
(<< (i32 (bit-and b2 0x3f)) 6)))
|
||||
(i32 (bit-and b3 0x3f)))
|
||||
:width 4 :ok true})))))))
|
||||
|
||||
;; Decode at a byte offset. None when the offset is not on a rune boundary or
|
||||
;; the bytes there are malformed, which is stricter than Odin's rune_at — that
|
||||
;; one hands back RUNE_ERROR and the caller carries on with a wrong character.
|
||||
(defn rune-at [s [u8] i i32] (Option i32)
|
||||
(if (or (< i 0) (>= i (len s)))
|
||||
None
|
||||
(let [r (decode-rune (slice s i (len s)))]
|
||||
(if (.ok r) (Some (.code r)) None))))
|
||||
|
||||
;; Counted through decode-rune rather than through a second walk of its own.
|
||||
;; Odin keeps a separate rune_count_in_bytes that re-implements the size
|
||||
;; table; two copies of that classification is two places for the surrogate
|
||||
;; rule to be right in only one of them.
|
||||
;;
|
||||
;; A malformed byte counts as one, which is what a replacement-character
|
||||
;; renderer would draw, so this agrees with what the screen shows.
|
||||
(defn rune-count [s [u8]] i32
|
||||
(let [i 0
|
||||
n 0]
|
||||
(while (< i (len s))
|
||||
(let [r (decode-rune (slice s i (len s)))]
|
||||
(set i (+ i (.width r)))
|
||||
(set n (+ n 1))))
|
||||
n))
|
||||
|
||||
(defn valid-utf8? [s [u8]] bool
|
||||
(let [i 0]
|
||||
(while (< i (len s))
|
||||
(let [r (decode-rune (slice s i (len s)))]
|
||||
(when (not (.ok r))
|
||||
(return false))
|
||||
(set i (+ i (.width r)))))
|
||||
true))
|
||||
|
||||
;; How many bytes this code point encodes to, or None if it is not a scalar
|
||||
;; value. Odin's rune_size answers -1 for the refusals; a sentinel index is
|
||||
;; exactly what index-of-i32 avoids above, so this is an Option like the rest
|
||||
;; of the file.
|
||||
(defn rune-size [code i32] (Option i32)
|
||||
(cond
|
||||
(< code 0) None
|
||||
(<= code 0x7f) (Some 1)
|
||||
(<= code 0x7ff) (Some 2)
|
||||
(and (>= code 0xd800) (<= code 0xdfff)) None
|
||||
(<= code 0xffff) (Some 3)
|
||||
(<= code 0x10ffff) (Some 4)
|
||||
:else None))
|
||||
|
||||
;; Encoding is the one operation here whose result is not a slice of its
|
||||
;; input, because the bytes it makes existed nowhere before. With no allocator
|
||||
;; the only shape left is Odin's own allocation-free one — strings.Builder
|
||||
;; built by builder_from_bytes over a caller's backing array (builder.odin,
|
||||
;; builder_from_bytes: "Uses Nil Allocator - Does NOT allocate") — reduced to
|
||||
;; its essential case: write into a buffer the caller owns, and say how much
|
||||
;; was written.
|
||||
;;
|
||||
;; None rather than a partial write when the buffer is short, and None rather
|
||||
;; than Odin's silent substitution of U+FFFD for an invalid rune. Odin's
|
||||
;; encode_rune rewrites a surrogate or an out-of-range value to the
|
||||
;; replacement character and reports success; the caller then finds three
|
||||
;; bytes of U+FFFD in its buffer and no indication that it asked for something
|
||||
;; else. Nothing is written at all when this answers None.
|
||||
(defn encode-rune! [dst [u8] code i32] (Option i32)
|
||||
(match (rune-size code)
|
||||
None None
|
||||
(Some w)
|
||||
(if (> w (len dst))
|
||||
None
|
||||
(do
|
||||
(cond
|
||||
(= w 1)
|
||||
(set (at dst 0) (u8 code))
|
||||
(= w 2)
|
||||
(do (set (at dst 0) (u8 (bit-or 0xc0 (>> code 6))))
|
||||
(set (at dst 1) (u8 (bit-or 0x80 (bit-and code 0x3f)))))
|
||||
(= w 3)
|
||||
(do (set (at dst 0) (u8 (bit-or 0xe0 (>> code 12))))
|
||||
(set (at dst 1) (u8 (bit-or 0x80 (bit-and (>> code 6) 0x3f))))
|
||||
(set (at dst 2) (u8 (bit-or 0x80 (bit-and code 0x3f)))))
|
||||
:else
|
||||
(do (set (at dst 0) (u8 (bit-or 0xf0 (>> code 18))))
|
||||
(set (at dst 1) (u8 (bit-or 0x80 (bit-and (>> code 12) 0x3f))))
|
||||
(set (at dst 2) (u8 (bit-or 0x80 (bit-and (>> code 6) 0x3f))))
|
||||
(set (at dst 3) (u8 (bit-or 0x80 (bit-and code 0x3f))))))
|
||||
(Some w)))))
|
||||
|
||||
;; ── Splitting ─────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; `split` returning a sequence of fields must allocate the sequence, and
|
||||
;; there is no allocator — so it is refused by name at the bottom of this
|
||||
;; file, and this is the shape that survives. It is Odin's
|
||||
;; split_by_byte_iterator (strings.odin): a cursor holding the rest of the
|
||||
;; input, handing back one field at a time. Every field is a slice *of the
|
||||
;; caller's bytes*; nothing is copied and nothing is owned.
|
||||
;;
|
||||
;; One divergence, and it is a wart of Odin's rather than a decision. Odin's
|
||||
;; iterator stops on an empty final field, so "a,b," iterates a and b and the
|
||||
;; trailing empty field is lost — while Odin's own allocating strings.split
|
||||
;; returns ["a", "b", ""] for the same input. The two disagree. This follows
|
||||
;; split: n separators always yield n+1 fields, an empty input yields one
|
||||
;; empty field, and `rest` is exhausted only after the last one is taken. That
|
||||
;; is the rule you can state without exceptions, and the one a caller counting
|
||||
;; comma-separated columns needs.
|
||||
(defstruct Split [rest [u8] sep u8 more bool])
|
||||
|
||||
(defn split-on-byte [s [u8] sep u8] Split
|
||||
(Split {:rest s :sep sep :more true}))
|
||||
|
||||
(defn split-next! [it (Ptr Split)] (Option [u8])
|
||||
(when (not (.more it))
|
||||
(return None))
|
||||
(match (index-of-byte (.rest it) (.sep it))
|
||||
(Some i)
|
||||
(let [field (slice (.rest it) 0 i)]
|
||||
(set (.rest it) (slice (.rest it) (+ i 1) (len (.rest it))))
|
||||
(Some field))
|
||||
None
|
||||
(let [field (.rest it)]
|
||||
(set (.more it) false)
|
||||
(set (.rest it) (slice (.rest it) (len (.rest it)) (len (.rest it))))
|
||||
(Some field))))
|
||||
|
||||
;; ── ASCII case ────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Byte in, byte out, and *not* a function over a slice. Odin's to_lower and
|
||||
;; to_upper both allocate a new string (core/strings/conversion.odin), which
|
||||
;; is not available here; the obvious substitute — lowering a [u8] in place —
|
||||
;; is a trap, and it is worth saying why rather than shipping it. A string
|
||||
;; literal is emitted `private unnamed_addr constant` (emit.ml), so it lives
|
||||
;; in read-only memory, and (bytes "Hello") is a [u8] pointing straight at it.
|
||||
;; An in-place lower-ascii! would type check against that slice and segfault
|
||||
;; on the store. Given a byte function, a caller that really does own its
|
||||
;; buffer writes the two-line loop itself and can see what it is writing to.
|
||||
;;
|
||||
;; ASCII only, and only the 26 letters: case outside ASCII is not a byte
|
||||
;; operation at all — it is per-code-point, it is not length-preserving (ß
|
||||
;; upcases to SS), and it is locale-dependent (Turkish dotless ı). A byte
|
||||
;; table that pretended otherwise would be wrong in the quiet way.
|
||||
(defn lower-ascii [b u8] u8
|
||||
(if (and (>= b \A) (<= b \Z)) (+ b 32) b))
|
||||
|
||||
(defn upper-ascii [b u8] u8
|
||||
(if (and (>= b \a) (<= b \z)) (- b 32) b))
|
||||
|
||||
;; Case-insensitive comparison as a fold over both inputs, which is the useful
|
||||
;; half of to_lower and needs no storage at all: comparing two lowered copies
|
||||
;; is what a caller wanted, and this is that answer without either copy.
|
||||
(defn bytes-ci=? [a [u8] b [u8]] bool
|
||||
(if (!= (len a) (len b))
|
||||
false
|
||||
(do
|
||||
(dotimes [i (len a)]
|
||||
(when (!= (lower-ascii (at a i)) (lower-ascii (at b i)))
|
||||
(return false)))
|
||||
true)))
|
||||
|
||||
;; ── Refused, by name ──────────────────────────────────────────────────
|
||||
;;
|
||||
;; Every one of these needs to produce bytes that did not exist in its input,
|
||||
;; and there is no allocator, so each is absent rather than approximated.
|
||||
;; None of them is hard to write once `(Vec u8)` and an allocator exist; all
|
||||
;; of them are impossible to write honestly today.
|
||||
;;
|
||||
;; join, concat build one buffer out of several inputs.
|
||||
;; to-lower, to-upper a new string, per Odin's conversion.odin. The
|
||||
;; byte-wise and folding-comparison forms above are
|
||||
;; what is available without one.
|
||||
;; split the *sequence* of fields is itself an allocation.
|
||||
;; split-on-byte / split-next! above is the same
|
||||
;; information with no sequence to own.
|
||||
;; replace, repeat, pad same reason as join.
|
||||
;; string-from-bytes a [u8] cannot become a `string` here even though
|
||||
;; the layouts are identical; see the report.
|
||||
;; format, sprintf Odin's fmt.aprintf family, all allocating.
|
||||
;; Builder strings.Builder is (defstruct Builder [buf
|
||||
;; (Vec u8)]), which spec-memory.md already makes
|
||||
;; move-only by the rule that a struct containing a
|
||||
;; Vec is move-only. It needs the Vec, not a spec
|
||||
;; change.
|
||||
|flan}
|
||||
|
||||
let file = "<prelude>"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user