The prelude's second tier, which returns things instead of filling buffers
This commit is contained in:
commit
a26469894e
126
BUILT.md
126
BUILT.md
@ -2606,6 +2606,132 @@ refused by the type reader's own message rather than as an unknown name. The che
|
||||
*parser* — a bracket in argument position could be a type there — but to a person it still looks like a two-element
|
||||
vector, which is the exact confusion being fixed. `zeroed` keeps its existing job: the empty value of whatever type the
|
||||
destination wants, inferred and never written. `array` is the one that is told.
|
||||
## The prelude's second tier: the functions that return new storage
|
||||
|
||||
Everything in the prelude before this was slice-based and allocation-free, and NEXT.md's diagnosis of why was exact:
|
||||
there was nothing to allocate from when it was written. `Vec`, `Map`, an arena and `StorageExhausted` changed that,
|
||||
and this is the tier that follows — 24 additions, of which the twelve that matter most **return new things**
|
||||
instead of writing into a buffer the caller supplies. The rest fill in the slice family at the element types that
|
||||
were missing, and one of them is a macro.
|
||||
|
||||
Three rules hold across all of it, and they are stated once at the head of the section rather than repeated:
|
||||
|
||||
1. **The result is owned and the caller frees it.** Nothing is released at scope exit — not at the end of a `let`,
|
||||
not at the end of a function (`spec-memory.md`). A caller writes `(free v)`, or lets a `(free-all a)` take the
|
||||
whole region.
|
||||
2. **The allocator is the context's, and `with-allocator` is the override.** This is the one design decision the
|
||||
spec did not settle by itself. `(vec-new)` and `(map-new)` take an optional trailing allocator because the
|
||||
*checker* builds them and can vary their arity; a Flan `defn` cannot, so the choice was an allocator parameter on
|
||||
every signature or none. None — `(with-allocator a (join parts sep))` is the override, the `Vec` records the
|
||||
arena, and `free` and `clone` never need it named again.
|
||||
3. **No `Result` anywhere.** Allocation failure signals `StorageExhausted` under `retry`, and no allocating
|
||||
operation returns an error, so every signature says what it produces and nothing about how it might fail.
|
||||
|
||||
### The builder is not a type
|
||||
|
||||
Odin's `strings.Builder` wraps a `[dynamic]u8`. Here the `(Vec u8)` already **is** that and already has `push`, so
|
||||
the struct would be a move-only wrapper whose only method is the one it wraps. What was actually missing is appending
|
||||
a *run* of bytes, and `append!` is that.
|
||||
|
||||
It takes a `(Ptr (Vec u8))` and not a `(Vec u8)`, and that is not style: a `Vec` parameter **moves**, so a by-value
|
||||
builder would be consumed by its first append and refused on the second.
|
||||
|
||||
`append-i64!` and `append-f64!` are the argument for the whole shape. NEXT.md's "Sharp edges" records that
|
||||
`flan_i64_to_bytes` and its neighbours render into one `static char scratch[64]`, so two formatted numbers cannot be
|
||||
held at once; these copy out of that buffer before returning, so the hazard ends at the call and a builder holds as
|
||||
many numbers as it likes. `strings.flan` puts two integers and a float on one line, which is the case that could not
|
||||
be written before.
|
||||
|
||||
### `split` answers a `(Vec [u8])`, and the owning shape is unrepresentable
|
||||
|
||||
The fields are slices *of the input*. That is not a performance choice — `(Vec (Vec u8))` is **refused outright**
|
||||
(`programs/vec-of-vec.flan`, "copies and releases elements bytewise"), so there is no owning shape to have chosen
|
||||
instead. It follows that the result dies with whatever the input pointed at, which is the same contract `trim` and
|
||||
`split-next!` already have.
|
||||
|
||||
The rule is `split-on-byte`'s, unchanged: n separators always yield n+1 fields, so an empty input yields one empty
|
||||
field and a trailing separator yields a trailing empty one. That is Odin's allocating `strings.split` and not Odin's
|
||||
`split_by_byte_iterator`, which disagree with each other on exactly that input.
|
||||
|
||||
Constructing it needed a one-line `(defn slices-new [] (Vec [u8]) (vec-new))`, because `check.ml`'s `vec_new_elem`
|
||||
takes the element type as a single bare symbol and `[u8]` is not one — so a `(Vec [u8])` can only be made where the
|
||||
*context* names the type, and a return type is a context while a `let` is not. Written down in NEXT.md as a compiler
|
||||
gap rather than worked around silently.
|
||||
|
||||
### `format-f64`, and the rounding rule it does not share with printf
|
||||
|
||||
`f64->bytes` is `snprintf "%g"`: six significant digits, exponent notation of its own accord, no precision to pass
|
||||
it. A frame time of 1/60 comes back `0.0166667` and a score past a million `1.23457e+06`.
|
||||
|
||||
`format-f64` returns a `Vec`, so it inherits neither that nor the shared scratch buffer, and it renders the integer
|
||||
part and the fraction through that buffer in strict sequence — the discipline `append-i64!` exists to make automatic.
|
||||
|
||||
It rounds **half away from zero at the last digit kept**, which is `round-f32`'s rule and the rest of the prelude's.
|
||||
printf rounds the *binary* value to nearest-even at the decimal digit, so `0.125` at two places is `0.13` here and
|
||||
`0.12` there. Matching printf would mean pinning a particular libc's answer, and that answer is not the same on every
|
||||
target anyway.
|
||||
|
||||
Three lines in it are the ones a plausible version ships without, and each is a separate test case:
|
||||
|
||||
- **The carry.** `0.999995` at five places scales to exactly `100000`, which is not a fraction — it is the next
|
||||
integer. Without the carry it prints `0.100000`.
|
||||
- **The zero padding.** The fraction of `1.005` at three places is `5`, and `5` is not `005`; without the pad it
|
||||
prints `1.5`.
|
||||
- **The sign.** It belongs to the number, not to its integer part: `-0.5` has an integer part of `0`, and
|
||||
`i64->bytes` of `0` carries no sign.
|
||||
|
||||
`-0.0` prints as `0.00`, because the sign test is `(< x 0.0)`, which `-0.0` fails. Past `9e18` the integer part does
|
||||
not fit in an `i64` and there are no fractional bits left anyway, so it falls back to `%g` rather than approximating.
|
||||
|
||||
### `clamp` is a macro, and `atan2`/`pow` are declares
|
||||
|
||||
`clamp` is the second prelude `defmacro` after `unless`, and the reason is the prelude's own objection to wrapping
|
||||
`(min hi (max lo x))` turned around rather than dropped. `min` and `max` are builtins at *every* numeric type and
|
||||
there are no generics, so a clamp **function** is one copy per type — `clamp-i32`, `clamp-f32`, `clamp-i64`. A macro
|
||||
is type-agnostic for free and emits nothing at all. `math2.flan` makes the same three-word call at `i32`, `i64`, `u8`
|
||||
and `f32` to show it, and counts evaluations to show each argument appears once.
|
||||
|
||||
`atan2-f32` and `pow-f32` inherit `sin-f32`/`cos-f32`'s caveat in full and not `sqrt-f32`'s: IEEE-754 requires
|
||||
nothing of `atan2f` or `powf` either, so they are the third and fourth places in the prelude where native and wasm32
|
||||
may differ in the last bit. Every case in `math2.flan` is therefore a value exact in binary — a quadrant boundary, a
|
||||
power of two, a perfect square — and the `-O0` run is the one that proves the symbols resolve, since at `-O2` LLVM
|
||||
constant-folds a `powf` of two literals and leaves nothing to link.
|
||||
|
||||
### What could not be built, and why it is not "no generics"
|
||||
|
||||
Four things on NEXT.md's list did not land, and the interesting part is that the reason differs in each case.
|
||||
|
||||
- **`Map` keys and values** need a **map iterator**, and there is none. `flan_map_len`, `_get`, `_put`, `_has`,
|
||||
`_clone`, `_reserve`, `_free` is the runtime's entire map surface; nothing walks the open-addressed block. One
|
||||
runtime function taking a cursor and one builtin in `check.ml` to emit the key and value sizes is the whole job,
|
||||
and none of it is a generics question.
|
||||
- **`map`, `filter`, `reduce` and a comparator sort** are blocked on **function values**, which is sharper than "no
|
||||
generics" and matters because generics alone would not fix it. `Types.Fn` exists; `check.ml` refuses it with "a
|
||||
function type is not implemented yet — milestone 5"; there is nothing in the language to pass. The concrete answer
|
||||
is the one that shipped: `sort-f32!` and `sort-bytes!` are the second and third sorts in the language, and
|
||||
`sum-i32`/`sum-f32` already are `reduce` with the `+` written in.
|
||||
- **The prelude is never macro-expanded**, so a prelude function may not call a prelude macro. `Macro.program` runs
|
||||
over the file being compiled; the prelude reaches the checker through `Check.program`'s prepend. The call resolves
|
||||
to the macro's underlying `defn` and reports an arity error, which is why `format-f64` writes
|
||||
`(min 9 (max 0 prec))`.
|
||||
- **A returned `Vec` is a move and the dead set spans the function**, so an early `(return v)` on one branch kills
|
||||
the binding at the foot of another. `replace-bytes` guards its empty needle with an `if` rather than a
|
||||
`when`/`return` for that reason.
|
||||
|
||||
### The refusal block is down from eight reasons to four
|
||||
|
||||
The list at the foot of `prelude.ml` used to be one sentence — every entry needed to produce bytes that did not exist
|
||||
in its input, and there was no allocator. `join`, `concat`, `split`, `to-lower`, `to-upper`, `repeat` and `replace`
|
||||
have moved up into the code; `string-from-bytes` turned out to be the `string` builtin all along, and
|
||||
`(string (as-slice v))` is the round trip, free precisely because the layouts are identical.
|
||||
|
||||
What remains is refused for four different reasons, and is now written that way: `pad`/`center` for *nothing at all*
|
||||
except that no caller has asked; `format`/`sprintf` for variadics of mixed type; `map`/`filter`/`reduce`/`sort-by`
|
||||
for function values; `map-keys`/`map-values` for the missing iterator.
|
||||
|
||||
Tests: `programs/strings.flan`, `programs/format.flan`, `programs/algorithms.flan`, `programs/math2.flan`, each at
|
||||
`-O2` and `-O0`, and `strings.flan` also in a dev build — the one that checks a container's recorded allocator epoch,
|
||||
so it is what would catch one of these `Vec`s being used after the arena under it was released.
|
||||
|
||||
## `defer` may be written in a `let`
|
||||
|
||||
|
||||
81
NEXT.md
81
NEXT.md
@ -533,26 +533,60 @@ hand-written backend is needed at all.
|
||||
|
||||
It is research first, not building. The batch below stays valid and none of it is blocked by the question.
|
||||
|
||||
## Queued: a second tier of the standard library, after macros
|
||||
## ~~Queued: a second tier of the standard library, after macros~~ — **landed**
|
||||
|
||||
Blocked only on `lib/prelude.ml`, which the macro lane holds. Start it when that merges.
|
||||
See [`BUILT.md`](BUILT.md), "The prelude's second tier". The diagnosis here was right and the prelude had 44
|
||||
allocation-free functions because there was nothing to allocate from; there are 24 more now, and the "Refused, by
|
||||
name" block at the foot of `prelude.ml` is down from eight entries to four, each with a *different* reason rather
|
||||
than the one shared sentence.
|
||||
|
||||
**The gap, stated plainly: the whole prelude predates the allocator.** All 44 functions are slice-based and
|
||||
allocation-free, because when they were written there was nothing to allocate from. `Vec` and `Map` now exist, so a
|
||||
second tier is possible — functions that *return new things* rather than writing into a buffer the caller supplies.
|
||||
What landed: `append!`/`append-i64!`/`append-f64!` (the builder), `concat`, `join`, `split` returning a
|
||||
`(Vec [u8])`, `repeat-bytes`, `replace-bytes`, `to-lower`, `to-upper`, `slices-new`; `format-f64` with a precision;
|
||||
`atan2-f32` and `pow-f32`; `clamp` as a `defmacro`; and the slice family at two more element types —
|
||||
`sort-f32!`, `reverse-f32!`, `swap-f32!`, `min-f32`, `max-f32`, `sum-f32`, `bytes<?`, `swap-bytes!`, `sort-bytes!`.
|
||||
Tests: `programs/strings.flan`, `programs/format.flan`, `programs/algorithms.flan`, `programs/math2.flan`.
|
||||
|
||||
Wanted, in rough order of how often it will be missed:
|
||||
`string-from-bytes`, refused in that block, turned out to already exist: `string` is a builtin and
|
||||
`(string (as-slice v))` is the round trip.
|
||||
|
||||
- **String building.** A `Vec u8` builder, `join`, and a `split` that returns a `Vec` instead of the
|
||||
`split-next!`/`split-on-byte` iterator dance the current one requires.
|
||||
- **`Vec` algorithms** — `map`, `filter`, `reduce`, and a `sort` that is not integers-only. `sort-i32!` is the only
|
||||
sort there is.
|
||||
- **`Map` helpers** — keys, values.
|
||||
- **Maths gaps**: `atan2`, `pow`, `clamp`. `sin-f32`/`cos-f32` exist with the caveat that IEEE-754 does not make them
|
||||
correctly rounded, so native and wasm32 may differ bit for bit; anything added here inherits that and should say so.
|
||||
- **Number formatting with a precision.** Note the sharp edge that constrains this: `flan_i64_to_bytes` and friends
|
||||
share one `static char scratch[64]`, so two formatted numbers cannot be held at once. A `Vec`-returning formatter
|
||||
would not have that problem, which is an argument for building it.
|
||||
**What could not be built, and why each one could not.** All four want a compiler or runtime change, and none of
|
||||
them wants a language decision.
|
||||
|
||||
- **`Map` keys and values.** The only item on the list above that could not be built at all. `len` reaches a Map and
|
||||
`get`/`put`/`has-key?` address one entry, but nothing walks the block: `flan_map_len`, `_get`, `_put`, `_has`,
|
||||
`_clone`, `_reserve` and `_free` is the runtime's whole map surface, with no iterator among them. It wants one
|
||||
runtime function — `flan_map_next` over the open-addressed block, taking a cursor — and one builtin in `check.ml`
|
||||
to emit the key and value sizes at the call site. It is *not* a generics problem, and it is a small job.
|
||||
|
||||
- **`map`, `filter`, `reduce`, and a sort taking a comparator.** Blocked on **function values**, not on generics,
|
||||
which is the sharper statement than the one this list made. `Types.Fn` exists; `check.ml` refuses it with "a
|
||||
function type is not implemented yet — milestone 5"; and there is nothing else in the language to pass. Generics on
|
||||
top of that is what would make them one copy rather than one per element type, but without function values there is
|
||||
nothing to be generic *over*. `sort-f32!` and `sort-bytes!` are the concrete answer in the meantime, and `sum-i32`
|
||||
and `sum-f32` already are `reduce` with the `+` written in.
|
||||
|
||||
- **`(vec-new [u8])` is refused**, so a `(Vec [u8])` can only be made where the *context* names the type.
|
||||
`check.ml`'s `vec_new_elem` accepts a single bare symbol naming a type and nothing else, and a `let` has no type
|
||||
annotation to say it the other way round — so `split` needs a one-line `(defn slices-new [] (Vec [u8]) (vec-new))`
|
||||
standing in as the place where the type is said. The fix is to let `vec_new_elem` take a type *expression* rather
|
||||
than a name, which is the same parser that already reads `[u8]` in a parameter list.
|
||||
|
||||
- **An array literal cannot say it is `[f32]`.** A float literal defaults to `f64`, an array literal has no context,
|
||||
and a `let` has no annotation, so `[3.5 -1.0]` is an `[f64]` and every element in `programs/algorithms.flan` is
|
||||
written `(f32 3.5)`. Same shape of gap as the one above and probably the same fix.
|
||||
|
||||
Two smaller findings, both written down beside the code that ran into them:
|
||||
|
||||
- **The prelude is never macro-expanded.** `macro.ml`'s pass runs over the file being compiled; the prelude reaches
|
||||
the checker through `Check.program`'s own prepend and never goes through the expander. So a prelude *function*
|
||||
calling a prelude *macro* resolves the macro's underlying `defn` — the one taking a `[Form]` — and reports an arity
|
||||
error. `format-f64` writes `(min 9 (max 0 prec))` where it wanted `clamp`. This is next to, and not the same as,
|
||||
"a prelude macro may not call a macro" below.
|
||||
|
||||
- **A returned `Vec` is a move, and the dead set spans the function**, so an early `(return v)` on one branch kills
|
||||
the binding for the `v` at the foot of another. `replace-bytes` guards its empty-needle case with an `if` rather
|
||||
than a `when`/`return` for that reason. Probably correct as it stands — the analysis is not path-sensitive and
|
||||
making it so is a real piece of work — but it is a shape that reads as though it should compile.
|
||||
|
||||
Already present and easy to miss: an **EDN parser**, at `vendor/edn/edn.flan`.
|
||||
|
||||
@ -1356,6 +1390,12 @@ memcheck sweep (`@valgrind`), whose alarm is looser at 5400s because memcheck is
|
||||
sequences itself strictly for this reason. `rl/draw-text` is safe because the shim's `flan_shim_cstr` copies out of
|
||||
ptr+len before the call.
|
||||
|
||||
**The prelude now has the shape that does not have this problem**, and it is the reason that shape exists.
|
||||
`append-i64!` and `append-f64!` copy out of the scratch buffer into a `(Vec u8)` before returning, so a builder
|
||||
holds as many rendered numbers as it likes, and `format-f64` answers a `Vec` rather than a view. The hazard is
|
||||
unchanged for anyone calling `i64->bytes` directly — nothing was taken away — but a caller assembling a line of
|
||||
text has a way not to meet it.
|
||||
|
||||
- **Writing through a string literal is undefined, and the two build modes
|
||||
disagree about how.** `(let [s (bytes "Hi")] (set (at s 0) \h))` stores into
|
||||
a `private unnamed_addr constant`. At `-O0` that is a store to read-only
|
||||
@ -1446,6 +1486,15 @@ What follows is only the part that is still missing.
|
||||
round it could be compiled in after something else. It would fail with an unknown name rather than with a reason,
|
||||
which is worth fixing the day the prelude wants one.
|
||||
|
||||
- **A prelude *function* may not call a prelude macro either**, which is the neighbouring gap and was found by
|
||||
walking into it. `Macro.program` runs over the file being compiled; the prelude arrives at the checker through
|
||||
`Check.program`'s own prepend and is never handed to the expander at all. A `defmacro` is an ordinary `defn` taking
|
||||
one `[Form]` by the time the checker sees it, so the call resolves to that and the report is "clamp takes 1
|
||||
argument, given 3" — pointing at the prelude, about a call the author wrote as a macro use. `format-f64` writes
|
||||
`(min 9 (max 0 prec))` in place of `(clamp prec 0 9)` because of it. The fix is not obviously cheap: expanding the
|
||||
prelude means building a macro module to compile the prelude that the macro module is built from, which is the same
|
||||
bootstrap the `when`/`dotimes` item above describes.
|
||||
|
||||
- **A quasiquote inside a quasiquote is refused.** Nothing counts nesting levels — not the reader, deliberately, and
|
||||
not the desugaring. Only a macro that writes a macro wants one.
|
||||
|
||||
|
||||
538
lib/prelude.ml
538
lib/prelude.ml
@ -90,15 +90,27 @@ let source = {flan|
|
||||
|
||||
;; ── Slice algorithms, all in place ────────────────────────────────────
|
||||
;;
|
||||
;; Over [i32] and nothing else. There are no generics, so one of these per
|
||||
;; element type is one *copy* per element type, emitted into every program;
|
||||
;; i32 is the type indices, ids and tile values already have, and f32 copies
|
||||
;; wait until a program actually wants them.
|
||||
;; One family per element type, because there are no generics: each of these
|
||||
;; is a *copy* per element type, and the set below is i32 (what indices, ids
|
||||
;; and tile values are), f32 (what positions, velocities and weights are) and
|
||||
;; [u8] (what a field coming out of `split` is).
|
||||
;;
|
||||
;; A slice is ptr+len and non-owning, so these mutate the storage they were
|
||||
;; handed: sorting (slice grid 4 9) sorts those five elements of grid and
|
||||
;; leaves the rest alone. That is the whole reason the shape is in-place —
|
||||
;; there is no allocator to return a new sequence from.
|
||||
;; leaves the rest alone. That was originally forced — there was no allocator
|
||||
;; to return a new sequence from — and it stays the right shape now that there
|
||||
;; is one, because sorting a thing you already own should not allocate. The
|
||||
;; allocating tier is further down, and a caller sorts a Vec by sorting
|
||||
;; (as-slice v).
|
||||
;;
|
||||
;; **map, filter, reduce and a sort taking a comparator are not here, and they
|
||||
;; are not blocked on generics.** They are blocked on *function values*: each
|
||||
;; of them takes a callable as an argument, Types.Fn exists but check.ml
|
||||
;; refuses it with "a function type is not implemented yet — milestone 5", and
|
||||
;; there is nothing else in the language to pass. Generics on top of that is
|
||||
;; what would make them one copy instead of one per element type; without
|
||||
;; either, the honest form is the concrete fold, which is what sum-i32 and
|
||||
;; sum-f32 below already are — (reduce + 0) with the + written in.
|
||||
|
||||
(defn swap-i32! [s [i32] i i32 j i32]
|
||||
(let [t (at s i)]
|
||||
@ -164,6 +176,74 @@ let source = {flan|
|
||||
(set t (+ t (i64 (at s i)))))
|
||||
t))
|
||||
|
||||
;; ── The same family over f32 ──────────────────────────────────────────
|
||||
;;
|
||||
;; sort-i32! was the only sort in the language, which is what NEXT.md's second
|
||||
;; tier means by "a sort that is not integers-only". This is the second, and it
|
||||
;; is a copy and not an abstraction — see the note above on why.
|
||||
;;
|
||||
;; One caveat that has no counterpart in the i32 family, because it cannot
|
||||
;; arise there: **a NaN in the input makes the order undefined.** Every
|
||||
;; comparison against a NaN is false, so the insertion loop never moves one and
|
||||
;; never moves anything past one; what comes out is sorted within each run
|
||||
;; between NaNs and not sorted across them. That is what C's qsort with a naive
|
||||
;; comparator does too. The fix is not to have NaNs in the array — which is
|
||||
;; also the only fix, since there is no ordering of the reals that a NaN sits
|
||||
;; anywhere in.
|
||||
|
||||
(defn swap-f32! [s [f32] i i32 j i32]
|
||||
(let [t (at s i)]
|
||||
(set (at s i) (at s j))
|
||||
(set (at s j) t)))
|
||||
|
||||
(defn reverse-f32! [s [f32]]
|
||||
(let [i 0
|
||||
j (- (len s) 1)]
|
||||
(while (< i j)
|
||||
(swap-f32! s i j)
|
||||
(set i (+ i 1))
|
||||
(set j (- j 1)))))
|
||||
|
||||
(defn sort-f32! [s [f32]]
|
||||
(let [i 1]
|
||||
(while (< i (len s))
|
||||
(let [j i]
|
||||
(while (and (> j 0) (> (at s (- j 1)) (at s j)))
|
||||
(swap-f32! s (- j 1) j)
|
||||
(set j (- j 1))))
|
||||
(set i (+ i 1)))))
|
||||
|
||||
;; None for an empty slice, exactly as min-i32 does. A NaN in the input is not
|
||||
;; special-cased and propagates the same way it does through the builtins: the
|
||||
;; comparison fails, so the running value simply does not change.
|
||||
(defn min-f32 [s [f32]] (Option f32)
|
||||
(if (= (len s) 0)
|
||||
None
|
||||
(let [m (at s 0)]
|
||||
(dotimes [i (len s)]
|
||||
(set m (min m (at s i))))
|
||||
(Some m))))
|
||||
|
||||
(defn max-f32 [s [f32]] (Option f32)
|
||||
(if (= (len s) 0)
|
||||
None
|
||||
(let [m (at s 0)]
|
||||
(dotimes [i (len s)]
|
||||
(set m (max m (at s i))))
|
||||
(Some m))))
|
||||
|
||||
;; Accumulates in f64 and widens each element explicitly, which is sum-i32's
|
||||
;; argument in its floating form and a stronger one: summing a screenful of f32
|
||||
;; in f32 does not wrap, it *absorbs* — once the running total is large enough,
|
||||
;; adding a small element rounds to no change at all, and the answer is silently
|
||||
;; short rather than obviously wrong. An f64 accumulator has 29 more bits of
|
||||
;; mantissa and pushes that failure out of reach of any array a game holds.
|
||||
(defn sum-f32 [s [f32]] f64
|
||||
(let [t 0.0]
|
||||
(dotimes [i (len s)]
|
||||
(set t (+ t (f64 (at s i)))))
|
||||
t))
|
||||
|
||||
;; ── Bytes ─────────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Over [u8] and not over string, so (bytes s) is what a caller writes and one
|
||||
@ -225,11 +305,10 @@ let source = {flan|
|
||||
|
||||
;; ── Numbers ───────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Only the ones that encode a decision. clamp is (min hi (max lo x)) over two
|
||||
;; builtins and abs is (max x (- 0 x)); a wrapper over those is a function
|
||||
;; emitted into every program to save a caller nothing. The one honest caveat
|
||||
;; on that abs: at the least representable integer it answers itself, because
|
||||
;; the negation wraps. That is what every two's-complement abs does, a
|
||||
;; Only the ones that encode a decision. abs is (max x (- 0 x)); a wrapper over
|
||||
;; that is a function emitted into every program to save a caller nothing. The
|
||||
;; one honest caveat on that abs: at the least representable integer it
|
||||
;; answers itself, because the negation wraps. That is what every two's-complement abs does, a
|
||||
;; function here would do it too, and the only fix is not to hand it that
|
||||
;; value — so it is written down rather than wrapped.
|
||||
;;
|
||||
@ -242,6 +321,28 @@ let source = {flan|
|
||||
;; NaN (every comparison fails, so the same max picks the subtracted side,
|
||||
;; which is still a NaN). There is nothing left for a function to fix.
|
||||
|
||||
;; clamp is a macro and not a function, and the reason is the objection above
|
||||
;; turned around rather than dropped. min and max are builtins that work at
|
||||
;; every numeric type; a clamp *function* cannot, because there are no
|
||||
;; generics, so it would be one copy per type — a clamp-i32, a clamp-f32, a
|
||||
;; clamp-i64 — each emitted into every program to save a caller eleven
|
||||
;; characters. A macro is type-agnostic for free and emits nothing at all: what
|
||||
;; the program contains after expansion is the (min hi (max lo x)) the caller
|
||||
;; would have written.
|
||||
;;
|
||||
;; Each of x, lo and hi appears exactly once in the expansion, so nothing here
|
||||
;; is evaluated twice and an argument with a side effect behaves as it reads.
|
||||
;;
|
||||
;; lo above hi is not checked, and the answer there is hi — the outer min wins.
|
||||
;; That is the same rule Odin's clamp follows and there is nowhere better to
|
||||
;; put a complaint: a macro has no error facility (see `unless` at the foot of
|
||||
;; this file), so a diagnostic would have to be a run-time one, in the one
|
||||
;; construct whose whole point is that it costs nothing at run time.
|
||||
(defmacro clamp [args]
|
||||
(if (!= (len args) 3)
|
||||
`(clamp-takes-a-value-a-low-and-a-high)
|
||||
`(min ~(at args 2) (max ~(at args 1) ~(at args 0)))))
|
||||
|
||||
;; Zero for zero, and zero for NaN — neither is positive nor negative, so
|
||||
;; neither comparison fires. A caller that needs to know which it got should
|
||||
;; be testing for NaN, not reading a sign.
|
||||
@ -375,6 +476,31 @@ let source = {flan|
|
||||
(declare sin-f32 [x f32] f32 "sinf")
|
||||
(declare cos-f32 [x f32] f32 "cosf")
|
||||
|
||||
;; atan2 and pow inherit the paragraph above in full, and not the sqrt one.
|
||||
;; IEEE-754 requires nothing of atan2f or powf either, so these are the third
|
||||
;; and fourth places in this file where native and wasm32 may disagree in the
|
||||
;; last bit, and the sand-grid rule stands unchanged: a hash compared across
|
||||
;; targets must not be routed through any of the four.
|
||||
;;
|
||||
;; They are here for the reason the trig pair is. Without them a program that
|
||||
;; wants a heading or a falloff curve writes the identical two declare lines at
|
||||
;; the top of its own file, which is the same libm call with the same caveat
|
||||
;; and nobody's name on it.
|
||||
;;
|
||||
;; atan2's y comes first, as it does in C, and the order is the answer rather
|
||||
;; than a convention: knowing the quadrant of (y, x) is the whole of what it
|
||||
;; has over (atan (/ y x)), and it is recovered from the two signs. It is
|
||||
;; defined at x = 0, where the division is not.
|
||||
;;
|
||||
;; One caveat of pow-f32's own, because it is the one that gets reported as a
|
||||
;; bug: it is not exact at integer exponents. powf goes through a logarithm,
|
||||
;; so (pow-f32 10.0 2.0) is 100.0 or the float next to it depending on the
|
||||
;; libm, and an index computed by casting that to i32 is off by one on the
|
||||
;; wrong side. A small integer power is a multiplication, and should be
|
||||
;; written as one.
|
||||
(declare atan2-f32 [y f32 x f32] f32 "atan2f")
|
||||
(declare pow-f32 [x f32 y f32] f32 "powf")
|
||||
|
||||
;; ── Byte classes ──────────────────────────────────────────────────────
|
||||
;;
|
||||
;; ASCII only, and deliberately: a byte is a byte here, there is no code point
|
||||
@ -481,8 +607,11 @@ let source = {flan|
|
||||
;; 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 core/strings takes `allocator := context.allocator`, which is why
|
||||
;; this corner came first and the rest waited; most of that rest is ported now
|
||||
;; and lives in the building section below. core/fmt is still absent, and the
|
||||
;; reason it stays absent is not allocation — see the refusal list at the foot
|
||||
;; of this file.
|
||||
;;
|
||||
;; 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
|
||||
@ -665,12 +794,13 @@ let source = {flan|
|
||||
|
||||
;; ── 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.
|
||||
;; The iterator, which owns nothing. `split` returning a sequence of fields has
|
||||
;; to allocate that sequence, and it does — it is in the building section below
|
||||
;; — but this stays the right call whenever you do not want to own the result:
|
||||
;; it is Odin's split_by_byte_iterator (strings.odin), a cursor holding the
|
||||
;; rest of the input and handing back one field at a time. Every field is a
|
||||
;; slice *of the caller's bytes*; nothing is copied, nothing is owned, and
|
||||
;; there is no free to remember. `split` is built on exactly this.
|
||||
;;
|
||||
;; 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
|
||||
@ -702,9 +832,12 @@ let source = {flan|
|
||||
;; ── 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
|
||||
;; to_upper both allocate a new string (core/strings/conversion.odin) and so do
|
||||
;; the ones in the building section below; these are the forms that allocate
|
||||
;; nothing, and they stay the right call when a copy is not wanted — folding a
|
||||
;; comparison over two inputs beats lowering both and comparing. What is *not*
|
||||
;; on offer is the third shape, lowering a [u8] in place, and it is worth
|
||||
;; saying why rather than shipping it. A string
|
||||
;; literal is emitted `private unnamed_addr constant` (emit.ml), so (bytes
|
||||
;; "Hello") is a [u8] pointing straight into read-only memory. An in-place
|
||||
;; lower-ascii! type checks against that slice, and what happens next depends
|
||||
@ -744,29 +877,348 @@ let source = {flan|
|
||||
(return false)))
|
||||
true)))
|
||||
|
||||
;; ── Refused, by name ──────────────────────────────────────────────────
|
||||
;; ── Ordering byte slices, and sorting them ────────────────────────────
|
||||
;;
|
||||
;; 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.
|
||||
;; The third element type the slice family covers, and the one a caller of
|
||||
;; `split` actually has: a [[u8]] of fields, wanting to come out in order.
|
||||
;;
|
||||
;; 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.
|
||||
;; The order is bytewise-lexicographic — memcmp's, and the one every sane
|
||||
;; sorted format uses. It is explicitly *not* alphabetical and not a collation:
|
||||
;; "Zebra" sorts before "apple" because 'Z' is 90 and 'a' is 97, and a
|
||||
;; non-ASCII byte sorts by its UTF-8 encoding, which for code points happens to
|
||||
;; agree with code-point order and for anything a human would call alphabetical
|
||||
;; does not. A locale-aware comparison is not a byte operation at all, for the
|
||||
;; same reasons the ASCII-case note above gives.
|
||||
;;
|
||||
;; The comparison is over u8 and therefore unsigned, which is the bug a version
|
||||
;; written over a signed byte type has: 0x80 would compare *below* 0x00 and
|
||||
;; every multi-byte character would sort before every ASCII one.
|
||||
;;
|
||||
;; A prefix sorts before what extends it — "ab" before "abc" — which falls out
|
||||
;; of running to the shorter length and then comparing lengths, and is the case
|
||||
;; a loop written to (len a) alone reads off the end for.
|
||||
(defn bytes<? [a [u8] b [u8]] bool
|
||||
(let [n (min (len a) (len b))]
|
||||
(dotimes [i n]
|
||||
(when (!= (at a i) (at b i))
|
||||
(return (< (at a i) (at b i)))))
|
||||
(< (len a) (len b))))
|
||||
|
||||
(defn swap-bytes! [s [[u8]] i i32 j i32]
|
||||
(let [t (at s i)]
|
||||
(set (at s i) (at s j))
|
||||
(set (at s j) t)))
|
||||
|
||||
;; The same insertion sort as sort-i32!, over the same in-place contract: the
|
||||
;; *slices* move, never the bytes they point at, so this sorts a [[u8]] of
|
||||
;; fields borrowed from one buffer without touching the buffer. Stable, and
|
||||
;; here that is observable — two equal fields are two distinct slices of
|
||||
;; different parts of the input, and a caller can see which one came first.
|
||||
(defn sort-bytes! [s [[u8]]]
|
||||
(let [i 1]
|
||||
(while (< i (len s))
|
||||
(let [j i]
|
||||
(while (and (> j 0) (bytes<? (at s j) (at s (- j 1))))
|
||||
(swap-bytes! s (- j 1) j)
|
||||
(set j (- j 1))))
|
||||
(set i (+ i 1)))))
|
||||
|
||||
;; ── Building bytes, which is the tier that needed an allocator ────────
|
||||
;;
|
||||
;; Everything above this line is slice-based and allocation-free, because when
|
||||
;; it was written there was nothing to allocate from. Everything below it
|
||||
;; *returns new storage*, which is the whole difference, and there are three
|
||||
;; rules that hold for all of it.
|
||||
;;
|
||||
;; **The result is owned and the caller frees it.** Each of these hands back a
|
||||
;; (Vec u8) or a (Vec [u8]), which is move-only: it goes with the call that
|
||||
;; takes it, and nothing is released at scope exit — not at the end of a let,
|
||||
;; not at the end of a function (spec-memory.md, "When storage is released").
|
||||
;; A caller writes (free v) or lets a (free-all a) take the whole region.
|
||||
;;
|
||||
;; **The allocator is the context's, and `with-allocator` is the override.**
|
||||
;; spec-memory.md makes allocation use the current implicit allocator and
|
||||
;; forbids falling back to a hidden global one. (vec-new) and (map-new) take an
|
||||
;; optional trailing allocator because the checker builds them; a Flan defn has
|
||||
;; fixed arity and cannot, so the choice here was an allocator parameter on
|
||||
;; every one of these signatures or none. None: a caller wanting a frame arena
|
||||
;; writes (with-allocator a (join parts sep)) and the Vec records the arena, so
|
||||
;; the free and the clone never need it named again.
|
||||
;;
|
||||
;; **No Result, anywhere.** Running out of storage signals StorageExhausted
|
||||
;; under a `retry` restart and no allocating operation returns an error
|
||||
;; (spec-memory.md, "Allocation failure"), so these signatures say what they
|
||||
;; produce and nothing about how they might fail.
|
||||
|
||||
;; The builder. It is not a type: strings.Builder in Odin is a struct wrapping
|
||||
;; a [dynamic]u8, and here the (Vec u8) *is* that, with push already on it — a
|
||||
;; wrapper would be a move-only struct owning a Vec whose only method is the
|
||||
;; one the Vec already has. What was actually missing is appending a run of
|
||||
;; bytes rather than one, and that is this.
|
||||
;;
|
||||
;; It takes a (Ptr (Vec u8)) and not a (Vec u8), and the difference is not
|
||||
;; style: a Vec parameter *moves*, so (append! b s) taking one by value would
|
||||
;; consume the caller's builder on the first call and refuse the second.
|
||||
(defn append! [b (Ptr (Vec u8)) s [u8]]
|
||||
(dotimes [i (len s)]
|
||||
(push (deref b) (at s i))))
|
||||
|
||||
;; The two number appends, and they are the reason this shape is worth having
|
||||
;; rather than a formatter that answers a slice. i64->bytes and f64->bytes
|
||||
;; render into one shared static buffer in the runtime, so two of their results
|
||||
;; cannot be held at once and (concat [(i64->bytes a) (i64->bytes b)]) is two
|
||||
;; views of the same bytes — the second call overwrote the first. These copy
|
||||
;; out of that buffer before returning, so the hazard ends at the call: a
|
||||
;; builder can hold as many numbers as it likes.
|
||||
(defn append-i64! [b (Ptr (Vec u8)) n i64]
|
||||
(append! b (i64->bytes n)))
|
||||
|
||||
(defn append-f64! [b (Ptr (Vec u8)) x f64]
|
||||
(append! b (f64->bytes x)))
|
||||
|
||||
;; concat and join. Both take a slice of slices, which is the shape a caller
|
||||
;; already has: an array literal of them, [(bytes "a") (bytes b)], slices to a
|
||||
;; [[u8]] and copies nothing.
|
||||
;;
|
||||
;; join with an empty separator is concat, and concat is here anyway because
|
||||
;; the empty (bytes "") a caller would have to write is the kind of argument
|
||||
;; that reads like a mistake at the call site.
|
||||
(defn concat [parts [[u8]]] (Vec u8)
|
||||
(let [b (vec-new u8)]
|
||||
(dotimes [i (len parts)]
|
||||
(append! (addr b) (at parts i)))
|
||||
b))
|
||||
|
||||
;; n parts yield n-1 separators, and the empty slice of parts yields the empty
|
||||
;; result rather than a leading separator — which is the off-by-one a join
|
||||
;; written as "append part then separator, then chop the tail" gets wrong on
|
||||
;; exactly that input, because there is no tail to chop.
|
||||
(defn join [parts [[u8]] sep [u8]] (Vec u8)
|
||||
(let [b (vec-new u8)]
|
||||
(dotimes [i (len parts)]
|
||||
(when (> i 0)
|
||||
(append! (addr b) sep))
|
||||
(append! (addr b) (at parts i)))
|
||||
b))
|
||||
|
||||
(defn repeat-bytes [s [u8] n i32] (Vec u8)
|
||||
(let [b (vec-new u8)]
|
||||
(dotimes [i n]
|
||||
(append! (addr b) s))
|
||||
b))
|
||||
|
||||
;; The allocating halves of the ASCII case pair. The note above lower-ascii
|
||||
;; explains why lowering a [u8] *in place* is a trap — a string literal is
|
||||
;; emitted into .rodata, so the store either segfaults at -O0 or is deleted at
|
||||
;; -O2 — and this is the shape that has no such hole: the bytes it writes are
|
||||
;; its own.
|
||||
(defn to-lower [s [u8]] (Vec u8)
|
||||
(let [b (vec-new u8)]
|
||||
(dotimes [i (len s)]
|
||||
(push b (lower-ascii (at s i))))
|
||||
b))
|
||||
|
||||
(defn to-upper [s [u8]] (Vec u8)
|
||||
(let [b (vec-new u8)]
|
||||
(dotimes [i (len s)]
|
||||
(push b (upper-ascii (at s i))))
|
||||
b))
|
||||
|
||||
;; Every non-overlapping occurrence, left to right, which is the rule that
|
||||
;; makes (replace-bytes (bytes "aaa") (bytes "aa") (bytes "b")) answer "ba" and
|
||||
;; not "bb" or "b".
|
||||
;;
|
||||
;; An empty `from` matches nothing and the result is a copy of the input. The
|
||||
;; alternative reading — that it matches at every position — is what turns this
|
||||
;; into an infinite loop, and Odin's replace guards the same case for the same
|
||||
;; reason.
|
||||
;;
|
||||
;; The guard is an `if` and not an early `(return b)`, which is not a style
|
||||
;; choice: returning a Vec *moves* it, and the move analysis is a dead set over
|
||||
;; the whole function, so a `return b` on one branch kills the binding for the
|
||||
;; `b` at the foot of the other. One exit, one move.
|
||||
(defn replace-bytes [s [u8] from [u8] to [u8]] (Vec u8)
|
||||
(let [b (vec-new u8)
|
||||
i 0]
|
||||
(if (= (len from) 0)
|
||||
(append! (addr b) s)
|
||||
(while (< i (len s))
|
||||
(match (index-of-bytes (slice s i (len s)) from)
|
||||
(Some k)
|
||||
(do
|
||||
(append! (addr b) (slice s i (+ i k)))
|
||||
(append! (addr b) to)
|
||||
(set i (+ i k (len from))))
|
||||
None
|
||||
(do
|
||||
(append! (addr b) (slice s i (len s)))
|
||||
(set i (len s))))))
|
||||
b))
|
||||
|
||||
;; A (Vec [u8]) cannot be written at a let, and this one-line function is where
|
||||
;; the type is said instead. (vec-new) takes its element type as a *bare
|
||||
;; symbol* — check.ml's vec_new_elem resolves one name and nothing else — so
|
||||
;; (vec-new [u8]) is not accepted, and a let has no type annotation to say it
|
||||
;; the other way. A return type does say it. That is a compiler gap rather than
|
||||
;; a language decision, and it is written down in NEXT.md.
|
||||
(defn slices-new [] (Vec [u8]) (vec-new))
|
||||
|
||||
;; split, which the file used to refuse by name. The fields are slices *of the
|
||||
;; input* and not copies, so nothing here owns bytes and the result dies with
|
||||
;; whatever `s` pointed at — a (Vec (Vec u8)) is the shape that would own them
|
||||
;; and it is refused outright, because a Vec's elements are copied and released
|
||||
;; bytewise and an owner cannot survive that.
|
||||
;;
|
||||
;; The rule is split-on-byte's, unchanged and worth restating: n separators
|
||||
;; always yield n+1 fields, so the empty input yields one empty field and a
|
||||
;; trailing separator yields a trailing empty one. That is Odin's allocating
|
||||
;; strings.split and not Odin's iterator, which disagree with each other.
|
||||
(defn split [s [u8] sep u8] (Vec [u8])
|
||||
(let [v (slices-new)
|
||||
it (split-on-byte s sep)
|
||||
going true]
|
||||
(while going
|
||||
(match (split-next! (addr it))
|
||||
(Some f) (push v f)
|
||||
None (set going false)))
|
||||
v))
|
||||
|
||||
;; ── A number with a precision ─────────────────────────────────────────
|
||||
;;
|
||||
;; The one formatting job the runtime cannot do. f64->bytes is snprintf "%g",
|
||||
;; which is six significant digits and switches to exponent notation on its
|
||||
;; own: a frame time of 0.0166667 is what a caller wanted two decimals of, and
|
||||
;; 1.23457e+06 is what a score looks like once it passes a million. There is no
|
||||
;; precision to pass it, and there cannot be — it renders into one shared
|
||||
;; static buffer in the runtime, which is the same reason two of its results
|
||||
;; cannot be held at once.
|
||||
;;
|
||||
;; This returns a Vec, so neither problem is inherited. It uses i64->bytes
|
||||
;; twice and the two calls are strictly sequential — the integer part is copied
|
||||
;; into the Vec before the fraction is rendered — which is the discipline the
|
||||
;; shared buffer requires and the one append-i64! exists to make automatic.
|
||||
;;
|
||||
;; Half away from zero, the same rule round-f32 follows, applied at the last
|
||||
;; digit kept. That is not bit-for-bit printf: printf rounds the *binary* value
|
||||
;; to nearest-even at the decimal digit, and this rounds the decimal expansion
|
||||
;; half-up, so a value sitting exactly on a half — 0.999995 at five places —
|
||||
;; comes out 1.00000 here and may come out 0.99999 there. Choosing the rule the
|
||||
;; rest of this file already uses beats matching a libc whose answer is not the
|
||||
;; same on every target anyway.
|
||||
;;
|
||||
;; Precision is clamped to 0..9 rather than refused. 10^9 is the largest power
|
||||
;; of ten that leaves room in the f64 product below, and a precision argument
|
||||
;; is almost always a literal, so a refusal would be a run-time condition for a
|
||||
;; mistake visible in the source.
|
||||
;;
|
||||
;; The clamp is written out as (min 9 (max 0 prec)) and not as the `clamp`
|
||||
;; macro two hundred lines up, and that is a limit rather than a preference:
|
||||
;; **the prelude is not macro-expanded**. macro.ml's pass runs over the file
|
||||
;; being compiled, and the prelude reaches the checker through Check.program's
|
||||
;; own prepend, having never been through the expander — so a prelude function
|
||||
;; calling a prelude macro resolves the macro's underlying defn, which takes
|
||||
;; one [Form] argument, and the report is an arity error at the call. It is
|
||||
;; written down in NEXT.md beside the other macro gaps.
|
||||
;;
|
||||
;; Three inputs do not have decimal expansions and are named before the cast
|
||||
;; that would be undefined on them: NaN, which fails every comparison and is
|
||||
;; therefore tested with (not (= x x)) and nothing else, and the two
|
||||
;; infinities, which are the values satisfying (= x (* x 2.0)) away from zero.
|
||||
;; A magnitude past 9e18 has no fractional bits left at all and would not fit
|
||||
;; in the i64 the integer part is carried in, so it falls back to f64->bytes —
|
||||
;; which is the honest answer there rather than an approximation of one.
|
||||
;;
|
||||
;; -0.0 prints as "0.00": the sign test is (< x 0.0), which -0.0 fails. A
|
||||
;; caller that needs the sign of a zero should not be reading it out of text.
|
||||
(defn format-f64 [x f64 prec i32] (Vec u8)
|
||||
(let [b (vec-new u8)
|
||||
p (min 9 (max 0 prec))]
|
||||
(cond
|
||||
(not (= x x))
|
||||
(append! (addr b) (bytes "nan"))
|
||||
|
||||
(and (= x (* x 2.0)) (!= x 0.0))
|
||||
(append! (addr b) (bytes (if (< x 0.0) "-inf" "inf")))
|
||||
|
||||
:else
|
||||
(let [neg (< x 0.0)
|
||||
m (if neg (- 0.0 x) x)]
|
||||
(if (>= m 9.0e18)
|
||||
(append! (addr b) (f64->bytes x))
|
||||
(let [scale (i64 1)]
|
||||
(dotimes [i p]
|
||||
(set scale (* scale 10)))
|
||||
;; The split is exact: (i64 m) truncates toward zero and m is
|
||||
;; non-negative here, and the subtraction of an integer from the
|
||||
;; float it came from is exact at every magnitude an f64 can hold.
|
||||
;; Only the scaling below rounds, and it rounds a value already
|
||||
;; under 1.
|
||||
(let [ip (i64 m)
|
||||
fr (i64 (+ (* (- m (f64 ip)) (f64 scale)) 0.5))]
|
||||
;; The carry, which is the bug this shape is otherwise written
|
||||
;; with: 0.999995 at five places scales to exactly 100000, which
|
||||
;; is not a fraction at all — it is the next integer, and without
|
||||
;; this line it prints as "0.100000".
|
||||
(when (>= fr scale)
|
||||
(set fr 0)
|
||||
(set ip (+ ip 1)))
|
||||
;; The sign goes on separately, because the integer part is a
|
||||
;; magnitude: -0.5 at one place has an integer part of 0, and
|
||||
;; i64->bytes of 0 has no sign to carry.
|
||||
(when neg
|
||||
(push b \-))
|
||||
(append-i64! (addr b) ip)
|
||||
(when (> p 0)
|
||||
(push b \.)
|
||||
;; Left-padded with zeros to exactly p digits. fr is under
|
||||
;; scale by the carry above, so it never needs more, and
|
||||
;; without the padding 1.005 at three places prints "1.5".
|
||||
(let [d (i64->bytes fr)]
|
||||
(dotimes [i (- p (len d))]
|
||||
(push b \0))
|
||||
(append! (addr b) d))))))))
|
||||
b))
|
||||
|
||||
;; ── Still refused, and what the reason is now ─────────────────────────
|
||||
;;
|
||||
;; This list used to be one sentence long — every entry needed to produce bytes
|
||||
;; that did not exist in its input, and there was no allocator. That sentence
|
||||
;; stopped being true when `Vec` landed, and most of the list has moved up into
|
||||
;; the building section above: join, concat, split, to-lower, to-upper, repeat
|
||||
;; and replace are all written now, and `string-from-bytes` turned out to be
|
||||
;; the `string` builtin all along — (string (as-slice v)) is the round trip,
|
||||
;; and the layouts being identical is exactly why it is free.
|
||||
;;
|
||||
;; What is left is refused for four *different* reasons, which is why they are
|
||||
;; named separately rather than under one heading.
|
||||
;;
|
||||
;; pad, center Nothing. These are three lines each over repeat-bytes
|
||||
;; and concat, and they are absent only because no
|
||||
;; caller has asked. Write them when one does.
|
||||
;; format, sprintf A format *string* — Odin's fmt.aprintf family. It
|
||||
;; needs variadic arguments of mixed type, which is a
|
||||
;; function-value and generics question, not an
|
||||
;; allocation one. format-f64 above is the piece of it
|
||||
;; that was actually wanted, and `print`/`println` are
|
||||
;; already the structural walk over any one value.
|
||||
;; map, filter, reduce Function values. See the head of the slice-algorithm
|
||||
;; sort-by section: check.ml refuses a function type outright,
|
||||
;; and there is nothing in the language to pass.
|
||||
;; map-keys, map-values A Map iterator. `len` reaches a Map and `get`,
|
||||
;; `put` and `has-key?` address one entry, but there is
|
||||
;; no entry point in the runtime that walks the block —
|
||||
;; flan_map_len, _get, _put, _has, _clone, _reserve and
|
||||
;; _free is the whole surface. This is the one item on
|
||||
;; NEXT.md's second-tier list that could not be built
|
||||
;; here at all, and it wants one runtime function and
|
||||
;; one builtin rather than anything from the language.
|
||||
;;
|
||||
;; Builder Not refused — declined. strings.Builder in Odin
|
||||
;; wraps a [dynamic]u8; here the (Vec u8) *is* that and
|
||||
;; already has push, so the struct would be a move-only
|
||||
;; wrapper whose only method is the one it wraps. What
|
||||
;; was missing was appending a run of bytes, and
|
||||
;; `append!` above is that.
|
||||
;; ── Files: embedding, slurp and barf ──────────────────────────────────
|
||||
;;
|
||||
;; One entry per file in an (embed-dir "...") — Odin's Load_Directory_File
|
||||
|
||||
120
test/programs/algorithms.flan
Normal file
120
test/programs/algorithms.flan
Normal file
@ -0,0 +1,120 @@
|
||||
;;;; The slice family at its second and third element types.
|
||||
;;;;
|
||||
;;;; sort-i32! was the only sort in the language. These are the other two, and
|
||||
;;;; they are copies rather than an abstraction: map, filter, reduce and a sort
|
||||
;;;; taking a comparator all need a *function value*, which check.ml refuses
|
||||
;;;; with "a function type is not implemented yet -- milestone 5". So the
|
||||
;;;; honest form is the concrete one, and the claim this file makes is only
|
||||
;;;; that the concrete ones are right.
|
||||
|
||||
(defn show-f32 [s [f32]]
|
||||
(dotimes [i (len s)]
|
||||
(print (at s i))
|
||||
(print " "))
|
||||
(println ""))
|
||||
|
||||
(defn show-fields [s [[u8]]]
|
||||
(dotimes [i (len s)]
|
||||
(print (string (at s i)))
|
||||
(print " "))
|
||||
(println ""))
|
||||
|
||||
(defn main [] i32
|
||||
;; Every float literal is cast. A literal defaults to f64 and an array
|
||||
;; literal has no context to say otherwise -- a let has no type annotation --
|
||||
;; so [3.5 -1.0] is an [f64] and (sort-f32!) refuses it by type. The cast is
|
||||
;; the only spelling available today.
|
||||
;;
|
||||
;; sort-f32!: duplicates, negatives, a zero and an odd length, which is the
|
||||
;; input shape the i32 sort is tested on for the same reasons.
|
||||
(let [xs [(f32 3.5) (f32 -1.0) (f32 0.0) (f32 3.5) (f32 -2.25) (f32 10.0) (f32 0.5)]]
|
||||
(sort-f32! (slice xs 0 7))
|
||||
(show-f32 (slice xs 0 7))) ; -2.25 -1 0 0.5 3.5 3.5 10
|
||||
|
||||
;; In place and ptr+len: sorting a subslice leaves its neighbours alone. That
|
||||
;; is the whole content of the in-place claim, and a version that copied
|
||||
;; would pass every test above and fail this one.
|
||||
(let [xs [(f32 9.0) (f32 4.0) (f32 3.0) (f32 2.0) (f32 1.0) (f32 9.0)]]
|
||||
(sort-f32! (slice xs 1 5))
|
||||
(show-f32 (slice xs 0 6))) ; 9 1 2 3 4 9
|
||||
|
||||
;; Already sorted, reverse sorted, and a single element -- the three inputs
|
||||
;; where an insertion loop with the comparison the wrong way round still
|
||||
;; looks plausible.
|
||||
(let [xs [(f32 1.0) (f32 2.0) (f32 3.0)]]
|
||||
(sort-f32! (slice xs 0 3))
|
||||
(show-f32 (slice xs 0 3))) ; 1 2 3
|
||||
(let [xs [(f32 3.0) (f32 2.0) (f32 1.0)]]
|
||||
(sort-f32! (slice xs 0 3))
|
||||
(show-f32 (slice xs 0 3))) ; 1 2 3
|
||||
(let [xs [(f32 7.0)]]
|
||||
(sort-f32! (slice xs 0 1))
|
||||
(show-f32 (slice xs 0 1))) ; 7
|
||||
;; The empty slice must not read (at s -1).
|
||||
(let [xs [(f32 7.0)]]
|
||||
(sort-f32! (slice xs 0 0))
|
||||
(show-f32 (slice xs 0 0))) ;
|
||||
|
||||
(let [xs [(f32 1.0) (f32 2.0) (f32 3.0) (f32 4.0)]]
|
||||
(reverse-f32! (slice xs 0 4))
|
||||
(show-f32 (slice xs 0 4))) ; 4 3 2 1
|
||||
|
||||
;; min, max and sum. The empty slice is None for the first two -- there is no
|
||||
;; least f32 that is also an honest answer -- and the sum accumulates in f64,
|
||||
;; which is why 16777216 + 1 does not absorb here the way it would in f32.
|
||||
(let [xs [(f32 3.5) (f32 -1.0) (f32 10.0)]]
|
||||
(match (min-f32 (slice xs 0 3)) (Some m) (print m) None (print "none"))
|
||||
(print " ")
|
||||
(match (max-f32 (slice xs 0 3)) (Some m) (print m) None (print "none"))
|
||||
(print " ")
|
||||
(print (sum-f32 (slice xs 0 3)))
|
||||
(println "")) ; -1 10 12.5
|
||||
(let [xs [(f32 1.0)]]
|
||||
(match (min-f32 (slice xs 0 0)) (Some m) (print m) None (print "none"))
|
||||
(print " ")
|
||||
(print (sum-f32 (slice xs 0 0)))
|
||||
(println "")) ; none 0
|
||||
;; The accumulator's width, made visible. 2^24 is where an f32 stops having
|
||||
;; a bit for 1, so an f32 running total absorbs both of these and the
|
||||
;; difference below is 0. In f64 they both land, and it is 2.
|
||||
(let [xs [(f32 16777216.0) (f32 1.0) (f32 1.0)]]
|
||||
(print (- (sum-f32 (slice xs 0 3)) 16777216.0))
|
||||
(println "")) ; 2
|
||||
|
||||
;; bytes<? is bytewise and unsigned, and explicitly not alphabetical: "Zebra"
|
||||
;; comes before "apple" because 'Z' is 90. A prefix comes before what extends
|
||||
;; it, which is the case a loop running only to (len a) reads off the end
|
||||
;; for, and 0x80 above 0x00 is the case a signed byte gets backwards.
|
||||
(print (bytes<? (bytes "a") (bytes "b"))) (print " ") ; true
|
||||
(print (bytes<? (bytes "b") (bytes "a"))) (print " ") ; false
|
||||
(print (bytes<? (bytes "a") (bytes "a"))) (print " ") ; false
|
||||
(print (bytes<? (bytes "ab") (bytes "abc"))) (print " ") ; true
|
||||
(print (bytes<? (bytes "abc") (bytes "ab"))) (print " ") ; false
|
||||
(print (bytes<? (bytes "") (bytes "a"))) (print " ") ; true
|
||||
(print (bytes<? (bytes "Zebra") (bytes "apple"))) ; true
|
||||
(println "")
|
||||
;; 0x00 below 0x80, which is the pair a comparison over a *signed* byte gets
|
||||
;; backwards -- and there is no \x escape in the reader, so these are built
|
||||
;; as u8 arrays rather than written as string literals.
|
||||
(let [lo [(u8 0)]
|
||||
hi [(u8 128)]]
|
||||
(print (bytes<? (slice lo 0 1) (slice hi 0 1))) (print " ") ; true
|
||||
(print (bytes<? (slice hi 0 1) (slice lo 0 1))) ; false
|
||||
(println ""))
|
||||
|
||||
;; sort-bytes! over the fields split out of one buffer. The slices move and
|
||||
;; the bytes never do, so this sorts a borrowed view of a string literal --
|
||||
;; which an in-place byte sort could not, since a literal lives in .rodata.
|
||||
(let [f (split (bytes "pear,apple,Fig,apple,banana") \,)]
|
||||
(sort-bytes! (as-slice f))
|
||||
(show-fields (as-slice f)) ; Fig apple apple banana pear
|
||||
(free f))
|
||||
|
||||
;; And the round trip the whole second tier is for: split, sort, join.
|
||||
(let [f (split (bytes "delta,alpha,charlie,bravo") \,)]
|
||||
(sort-bytes! (as-slice f))
|
||||
(let [j (join (as-slice f) (bytes " < "))]
|
||||
(println (string (as-slice j))) ; alpha < bravo < charlie < delta
|
||||
(free j))
|
||||
(free f))
|
||||
0)
|
||||
86
test/programs/format.flan
Normal file
86
test/programs/format.flan
Normal file
@ -0,0 +1,86 @@
|
||||
;;;; format-f64: a number rendered to a fixed number of decimal places.
|
||||
;;;;
|
||||
;;;; The runtime's f64->bytes is snprintf "%g" and there is no precision to
|
||||
;;;; pass it, so this is the first number formatter in the language that a
|
||||
;;;; caller can steer. Every case below is one a plausible wrong version gets
|
||||
;;;; wrong, and three of them are the ones that actually ship broken: the
|
||||
;;;; carry, where the rounded fraction equals the scale and is not a fraction
|
||||
;;;; at all; the zero padding, without which 1.005 prints as "1.5"; and the
|
||||
;;;; sign, which belongs to the number and not to its integer part, because
|
||||
;;;; -0.5 has an integer part of 0 and 0 carries no sign.
|
||||
|
||||
(defn show [x f64 p i32]
|
||||
(let [v (format-f64 x p)]
|
||||
(println (string (as-slice v)))
|
||||
(free v)))
|
||||
|
||||
(defn main [] i32
|
||||
;; The ordinary cases, and the one %g cannot do at all: 1/60 wanted to two
|
||||
;; places is a frame time, and "%g" answers 0.0166667.
|
||||
(show 3.14159 2) ; 3.14
|
||||
(show 0.0166667 2) ; 0.02
|
||||
(show 1234.5 1) ; 1234.5
|
||||
(show 2.0 0) ; 2
|
||||
(show 2.0 3) ; 2.000
|
||||
|
||||
;; Rounding is half away from zero at the last digit kept, on both signs.
|
||||
(show 0.125 2) ; 0.13
|
||||
(show -0.125 2) ; -0.13
|
||||
(show 2.5 0) ; 3
|
||||
(show -2.5 0) ; -3
|
||||
|
||||
;; The carry. 0.999995 scaled by 10^5 rounds to exactly 100000, which is the
|
||||
;; next integer; without the carry this prints "0.100000".
|
||||
(show 0.999995 5) ; 1.00000
|
||||
(show 9.99 1) ; 10.0
|
||||
(show -9.99 1) ; -10.0
|
||||
(show 0.99 0) ; 1
|
||||
|
||||
;; Zero padding. The fraction of 1.005 at three places is 5, and five digits
|
||||
;; is not the same number as 005.
|
||||
(show 1.005 3) ; 1.005
|
||||
(show 1.0001 4) ; 1.0001
|
||||
(show 7.0 6) ; 7.000000
|
||||
|
||||
;; The sign lives on the number, not on the integer part: both of these have
|
||||
;; an integer part of 0, which i64->bytes renders without a sign.
|
||||
(show -0.5 2) ; -0.50
|
||||
(show -0.004 2) ; -0.00
|
||||
|
||||
;; -0.0 prints as a plain zero. The sign test is (< x 0.0), which -0.0 fails,
|
||||
;; and text is not where the sign of a zero should be read from.
|
||||
(show 0.0 2) ; 0.00
|
||||
(show -0.0 2) ; 0.00
|
||||
|
||||
;; Precision is clamped rather than refused, at both ends.
|
||||
(show 1.5 -3) ; 2
|
||||
(show 1.5 40) ; 1.500000000
|
||||
|
||||
;; The three inputs with no decimal expansion.
|
||||
(show (/ 0.0 0.0) 2) ; nan
|
||||
(show (/ 1.0 0.0) 2) ; inf
|
||||
(show (/ -1.0 0.0) 2) ; -inf
|
||||
|
||||
;; Past 9e18 an f64 has no fractional bits and the integer part does not fit
|
||||
;; in an i64, so this falls back to %g rather than approximating.
|
||||
(show 1e20 2) ; 1e+20
|
||||
|
||||
;; A large magnitude that does fit, where the fraction is genuinely gone: an
|
||||
;; f64 has no bits below 1 up there, so the padding produces the zeros.
|
||||
(show 1234567890123.0 2) ; 1234567890123.00
|
||||
|
||||
;; And the thing it is for: a formatted number inside a built string, which
|
||||
;; needs the integer part copied out before the fraction is rendered, because
|
||||
;; both come through the runtime's one shared scratch buffer.
|
||||
(let [b (vec-new u8)]
|
||||
(append! (addr b) (bytes "fps "))
|
||||
(let [f (format-f64 59.94 1)]
|
||||
(append! (addr b) (as-slice f))
|
||||
(free f))
|
||||
(append! (addr b) (bytes " / frame "))
|
||||
(let [f (format-f64 0.0166667 4)]
|
||||
(append! (addr b) (as-slice f))
|
||||
(free f))
|
||||
(println (string (as-slice b))) ; fps 59.9 / frame 0.0167
|
||||
(free b))
|
||||
0)
|
||||
78
test/programs/math2.flan
Normal file
78
test/programs/math2.flan
Normal file
@ -0,0 +1,78 @@
|
||||
;;;; atan2, pow, and clamp — the three gaps NEXT.md's second-tier list names
|
||||
;;;; under "maths gaps".
|
||||
;;;;
|
||||
;;;; Two of them are declares over libm and the third is a macro, and that
|
||||
;;;; split is the whole content of this file. atan2f and powf are not correctly
|
||||
;;;; rounded under IEEE-754, exactly as sinf and cosf are not, so every case
|
||||
;;;; below is a value whose answer is exact in binary — a quadrant boundary, a
|
||||
;;;; power of two, a perfect square — rather than one that would pin a
|
||||
;;;; particular libm's last bit and then fail on wasi-libc.
|
||||
;;;;
|
||||
;;;; clamp is a macro because a function could not be: min and max are builtins
|
||||
;;;; at every numeric type and there are no generics, so a clamp function is
|
||||
;;;; one copy per type. The proof that the macro is not one copy per type is
|
||||
;;;; that the same three-word call below is made at i32, i64, u8 and f32.
|
||||
|
||||
(defn show [x f32]
|
||||
(print x)
|
||||
(print " "))
|
||||
|
||||
;;; clamp must evaluate each of its three arguments exactly once. A macro that
|
||||
;;; repeated one — say (if (< x lo) lo (if (> x hi) hi x)), which names x twice
|
||||
;;; — would read identically and call this twice.
|
||||
(defvar calls i32 0)
|
||||
|
||||
(defn tick [x i32] i32
|
||||
(set calls (+ calls 1))
|
||||
x)
|
||||
|
||||
(defn main [] i32
|
||||
;; atan2 across all four quadrants and on both axes, which is the whole
|
||||
;; reason for it over a division: the quadrant is recovered from two signs,
|
||||
;; and (/ y x) has thrown it away before atan sees it. The two on the x = 0
|
||||
;; axis are where the division does not exist at all.
|
||||
(show (atan2-f32 1.0 1.0)) ; pi/4
|
||||
(show (atan2-f32 1.0 -1.0)) ; 3pi/4
|
||||
(show (atan2-f32 -1.0 -1.0)) ; -3pi/4
|
||||
(show (atan2-f32 -1.0 1.0)) ; -pi/4
|
||||
(show (atan2-f32 0.0 1.0)) ; 0
|
||||
(show (atan2-f32 1.0 0.0)) ; pi/2
|
||||
(show (atan2-f32 -1.0 0.0)) ; -pi/2
|
||||
(println "")
|
||||
|
||||
;; pow. A power of two, a half-power that is a perfect square, a negative
|
||||
;; exponent, and the two edge exponents every implementation special-cases.
|
||||
(show (pow-f32 2.0 10.0)) ; 1024
|
||||
(show (pow-f32 9.0 0.5)) ; 3
|
||||
(show (pow-f32 2.0 -2.0)) ; 0.25
|
||||
(show (pow-f32 5.0 0.0)) ; 1
|
||||
(show (pow-f32 5.0 1.0)) ; 5
|
||||
(println "")
|
||||
|
||||
;; clamp: below the range, above it, and inside it untouched.
|
||||
(print (clamp 0 1 3)) (print " ") ; 1
|
||||
(print (clamp 5 1 3)) (print " ") ; 3
|
||||
(print (clamp 2 1 3)) (print " ") ; 2
|
||||
;; Both ends are inclusive, which is the off-by-one a hand-written clamp
|
||||
;; gets wrong with a < where it wanted <=.
|
||||
(print (clamp 1 1 3)) (print " ") ; 1
|
||||
(print (clamp 3 1 3)) ; 3
|
||||
(println "")
|
||||
|
||||
;; The same three words at three more types, none of which a function could
|
||||
;; have served without a copy of its own.
|
||||
(print (clamp (i64 900) (i64 0) (i64 255))) (print " ") ; 255
|
||||
(print (clamp (u8 200) (u8 0) (u8 255))) (print " ") ; 200
|
||||
(show (clamp 2.5 0.0 1.0)) ; 1
|
||||
(println "")
|
||||
|
||||
;; lo above hi is not an error and answers hi: the outer min wins. Written
|
||||
;; down here rather than left for someone to discover.
|
||||
(print (clamp 7 5 1))
|
||||
(println "")
|
||||
|
||||
;; Three arguments, three evaluations.
|
||||
(print (clamp (tick 5) (tick 1) (tick 3))) (print " ")
|
||||
(print calls)
|
||||
(println "")
|
||||
0)
|
||||
154
test/programs/strings.flan
Normal file
154
test/programs/strings.flan
Normal file
@ -0,0 +1,154 @@
|
||||
;;;; The prelude's second tier: the functions that return new storage.
|
||||
;;;;
|
||||
;;;; Every one of these was refused by name in prelude.ml until there was an
|
||||
;;;; allocator to return a Vec from, and this file is the corpus that says the
|
||||
;;;; refusals are lifted. The cases are chosen the way the slice-algorithm
|
||||
;;;; tests were: each is an input a plausible wrong version gets wrong.
|
||||
;;;;
|
||||
;;;; Everything allocated here is freed, even though leaking is defined
|
||||
;;;; behaviour (spec-memory.md), because this file is the example people copy.
|
||||
|
||||
;;; A (Vec u8) printed as text, without the caller writing the two-step every
|
||||
;;; time. as-slice borrows -- it copies ptr+len and never the elements -- so v
|
||||
;;; is still the owner afterwards and is still free-able.
|
||||
(defn show [v (Ptr (Vec u8))]
|
||||
(println (string (as-slice (deref v)))))
|
||||
|
||||
(defn main [] i32
|
||||
;; The builder. Three appends and two numbers into one Vec, which is the
|
||||
;; case the shared static scratch buffer in the runtime makes impossible for
|
||||
;; i64->bytes on its own: two of its results cannot be held at once, and
|
||||
;; these two numbers are both in the answer.
|
||||
(let [b (vec-new u8)]
|
||||
(append! (addr b) (bytes "x="))
|
||||
(append-i64! (addr b) 42)
|
||||
(append! (addr b) (bytes " y="))
|
||||
(append-i64! (addr b) -7)
|
||||
(append! (addr b) (bytes " r="))
|
||||
(append-f64! (addr b) 1.5)
|
||||
(show (addr b)) ; x=42 y=-7 r=1.5
|
||||
(free b))
|
||||
|
||||
;; concat over three parts, and over none -- the empty result rather than a
|
||||
;; trap.
|
||||
(let [parts [(bytes "one") (bytes "") (bytes "two")]]
|
||||
(let [c (concat (slice parts 0 3))]
|
||||
(show (addr c)) ; onetwo
|
||||
(free c)))
|
||||
(let [parts [(bytes "unused")]]
|
||||
(let [c (concat (slice parts 0 0))]
|
||||
(println (len c)) ; 0
|
||||
(free c)))
|
||||
|
||||
;; join: n parts, n-1 separators. The one-part case is the one that must not
|
||||
;; emit a separator at all, and the zero-part case is the one a "append then
|
||||
;; chop the tail" join gets wrong because there is no tail.
|
||||
(let [parts [(bytes "a") (bytes "b") (bytes "c")]]
|
||||
(let [j (join (slice parts 0 3) (bytes ", "))]
|
||||
(show (addr j)) ; a, b, c
|
||||
(free j))
|
||||
(let [j (join (slice parts 0 1) (bytes ", "))]
|
||||
(show (addr j)) ; a
|
||||
(free j))
|
||||
(let [j (join (slice parts 0 0) (bytes ", "))]
|
||||
(println (len j)) ; 0
|
||||
(free j))
|
||||
;; An empty separator is concat.
|
||||
(let [j (join (slice parts 0 3) (bytes ""))]
|
||||
(show (addr j)) ; abc
|
||||
(free j)))
|
||||
|
||||
;; repeat, including zero times.
|
||||
(let [r (repeat-bytes (bytes "ab") 3)]
|
||||
(show (addr r)) ; ababab
|
||||
(free r))
|
||||
(let [r (repeat-bytes (bytes "ab") 0)]
|
||||
(println (len r)) ; 0
|
||||
(free r))
|
||||
|
||||
;; The allocating case pair. The input is a string literal, which lives in
|
||||
;; .rodata -- an in-place lower would either segfault at -O0 or be deleted at
|
||||
;; -O2, and that is exactly why these exist. Digits and punctuation pass
|
||||
;; through untouched, which is the range check a table-free version gets
|
||||
;; wrong by shifting every byte.
|
||||
(let [l (to-lower (bytes "Hello, World 42!"))]
|
||||
(show (addr l)) ; hello, world 42!
|
||||
(free l))
|
||||
(let [u (to-upper (bytes "Hello, World 42!"))]
|
||||
(show (addr u)) ; HELLO, WORLD 42!
|
||||
(free u))
|
||||
|
||||
;; replace. "aaa" with "aa" -> "b" is the non-overlapping rule: the answer is
|
||||
;; "ba", because the match consumes both a's and the scan resumes after them.
|
||||
(let [r (replace-bytes (bytes "aaa") (bytes "aa") (bytes "b"))]
|
||||
(show (addr r)) ; ba
|
||||
(free r))
|
||||
;; A replacement longer than what it replaces, and one that is empty.
|
||||
(let [r (replace-bytes (bytes "a,b,c") (bytes ",") (bytes " -- "))]
|
||||
(show (addr r)) ; a -- b -- c
|
||||
(free r))
|
||||
(let [r (replace-bytes (bytes "a,b,c") (bytes ",") (bytes ""))]
|
||||
(show (addr r)) ; abc
|
||||
(free r))
|
||||
;; No occurrence is a copy, and an empty `from` is a copy -- the reading
|
||||
;; where it matches everywhere is an infinite loop.
|
||||
(let [r (replace-bytes (bytes "abc") (bytes "z") (bytes "!"))]
|
||||
(show (addr r)) ; abc
|
||||
(free r))
|
||||
(let [r (replace-bytes (bytes "abc") (bytes "") (bytes "!"))]
|
||||
(show (addr r)) ; abc
|
||||
(free r))
|
||||
|
||||
;; split. n separators, n+1 fields, always -- so the trailing empty field is
|
||||
;; present, which is where Odin's own iterator and its allocating split
|
||||
;; disagree with each other.
|
||||
(let [f (split (bytes "a,b,c") \,)]
|
||||
(println (len f)) ; 3
|
||||
(println (string (at f 0))) ; a
|
||||
(println (string (at f 2))) ; c
|
||||
(free f))
|
||||
(let [f (split (bytes "a,b,") \,)]
|
||||
(println (len f)) ; 3
|
||||
(println (len (at f 2))) ; 0
|
||||
(free f))
|
||||
(let [f (split (bytes ",a") \,)]
|
||||
(println (len f)) ; 2
|
||||
(println (len (at f 0))) ; 0
|
||||
(free f))
|
||||
;; No separator at all is one field, and the empty input is one empty field.
|
||||
(let [f (split (bytes "abc") \,)]
|
||||
(println (len f)) ; 1
|
||||
(println (string (at f 0))) ; abc
|
||||
(free f))
|
||||
(let [f (split (bytes "") \,)]
|
||||
(println (len f)) ; 1
|
||||
(println (len (at f 0))) ; 0
|
||||
(free f))
|
||||
|
||||
;; The fields are slices of the input and nothing was copied: this one
|
||||
;; round-trips through join, and the separator it rebuilds with is a
|
||||
;; different one, so an implementation that handed back the original slice
|
||||
;; would print the original string.
|
||||
(let [f (split (bytes "a,b,c") \,)]
|
||||
(let [j (join (as-slice f) (bytes "/"))]
|
||||
(show (addr j)) ; a/b/c
|
||||
(free j))
|
||||
(free f))
|
||||
|
||||
;; The allocator is the context's, so with-allocator moves the whole tier
|
||||
;; into an arena -- which is the answer to the fixed arity of a defn, and the
|
||||
;; reason none of these takes an allocator argument. free-all is what
|
||||
;; releases the region, and arena-destroy hands it back.
|
||||
(let [a (arena-new 4096)]
|
||||
(with-allocator a
|
||||
(let [parts [(bytes "in") (bytes "arena")]]
|
||||
(let [j (join (slice parts 0 2) (bytes "-"))]
|
||||
(show (addr j)) ; in-arena
|
||||
;; The free is written because the binding is dead after it either
|
||||
;; way, and it keeps the block: an arena cannot release one, which
|
||||
;; is the difference the capability set exists to state. free-all
|
||||
;; below is what actually releases this.
|
||||
(free j))))
|
||||
(free-all a)
|
||||
(arena-destroy a))
|
||||
0)
|
||||
@ -214,6 +214,27 @@ let () =
|
||||
in
|
||||
outputs "rounding and sqrt" "programs/math.flan" math_out;
|
||||
outputs ~opt:"-O0" "rounding and sqrt, -O0" "programs/math.flan" math_out;
|
||||
(* atan2, pow and clamp. Every float here is exact in binary — quadrant
|
||||
boundaries, powers of two, a perfect square — because atan2f and powf
|
||||
are no more correctly rounded than sinf is, and a case pinning one
|
||||
libm's last bit would pass native and fail wasi. The -O0 run is the one
|
||||
that proves the two symbols resolve: at -O2 LLVM constant-folds a powf
|
||||
of two literals and nothing is left to link, which is the same trap the
|
||||
sqrt note describes. clamp is a macro, so the interesting lines are the
|
||||
four types it is called at (a function would be four copies) and the
|
||||
call counter, which is 3 and would be 4 or 5 for a macro that named an
|
||||
argument twice. *)
|
||||
let math2_out =
|
||||
"0.785398 2.35619 -2.35619 -0.785398 0 1.5708 -1.5708 \n\
|
||||
1024 3 0.25 1 5 \n\
|
||||
1 3 2 1 3\n\
|
||||
255 200 1 \n\
|
||||
1\n\
|
||||
3 3\n"
|
||||
in
|
||||
outputs "atan2, pow and clamp" "programs/math2.flan" math2_out;
|
||||
outputs ~opt:"-O0" "atan2, pow and clamp, -O0" "programs/math2.flan"
|
||||
math2_out;
|
||||
(* index-of-bytes, trim, the byte classes and parse-f64. The search cases
|
||||
are the ones that separate a correct loop from a lucky one: a match
|
||||
only at the end, "aab" in "aaab" (where the first byte matches twice
|
||||
@ -440,6 +461,86 @@ let () =
|
||||
outputs "vec" "programs/vec.flan" vec_out;
|
||||
outputs ~opt:"-O0" "vec, -O0" "programs/vec.flan" vec_out;
|
||||
outputs ~dev:true "vec, dev" "programs/vec.flan" vec_out;
|
||||
(* The prelude's second tier: the functions that return new storage, every
|
||||
one of which the prelude used to refuse by name for want of an
|
||||
allocator. The cases are the ones that separate a correct version from a
|
||||
lucky one — join over zero and one parts, where the separator count is
|
||||
-1 and 0; "aaa" with "aa" -> "b", which is "ba" only if the match is
|
||||
non-overlapping; an empty `from` to replace, which is an infinite loop
|
||||
under the other reading; and split's trailing and leading empty fields,
|
||||
which is where Odin's iterator and Odin's own allocating split disagree.
|
||||
The builder line holds two rendered integers and a float at once, which
|
||||
is precisely what the runtime's shared static scratch makes impossible
|
||||
for i64->bytes on its own.
|
||||
|
||||
The dev run earns its place here more than most: it is the one that
|
||||
checks a container's recorded allocator epoch, so it is what would catch
|
||||
one of these Vecs being used after the arena under it was released. *)
|
||||
let strings_out =
|
||||
"x=42 y=-7 r=1.5\nonetwo\n0\na, b, c\na\n0\nabc\nababab\n0\n\
|
||||
hello, world 42!\nHELLO, WORLD 42!\n\
|
||||
ba\na -- b -- c\nabc\nabc\nabc\n\
|
||||
3\na\nc\n3\n0\n2\n0\n1\nabc\n1\n0\na/b/c\nin-arena\n"
|
||||
in
|
||||
outputs "string building" "programs/strings.flan" strings_out;
|
||||
outputs ~opt:"-O0" "string building, -O0" "programs/strings.flan"
|
||||
strings_out;
|
||||
outputs ~dev:true "string building, dev" "programs/strings.flan"
|
||||
strings_out;
|
||||
(* format-f64, the first number formatter a caller can steer. The three
|
||||
lines that would ship wrong are pinned deliberately: 0.999995 at five
|
||||
places, where the rounded fraction equals the scale and is the next
|
||||
integer rather than a fraction; 1.005 at three, where dropping the zero
|
||||
padding prints "1.5"; and -0.5, where the sign belongs to the number and
|
||||
the integer part it would otherwise ride on is 0, which i64->bytes
|
||||
renders unsigned.
|
||||
|
||||
0.125 at two places answers 0.13 and printf's "%.2f" answers 0.12. That
|
||||
is not a defect: this rounds the decimal expansion half away from zero,
|
||||
which is round-f32's rule and the rest of this file's, where printf
|
||||
rounds the binary value to nearest-even. Pinning 0.12 here would be
|
||||
pinning a libc.
|
||||
|
||||
The last line is the one the whole shape is for — two numbers in one
|
||||
built string, which the runtime's single shared scratch buffer makes
|
||||
impossible for a formatter that answers a slice. *)
|
||||
let format_out =
|
||||
"3.14\n0.02\n1234.5\n2\n2.000\n\
|
||||
0.13\n-0.13\n3\n-3\n\
|
||||
1.00000\n10.0\n-10.0\n1\n\
|
||||
1.005\n1.0001\n7.000000\n\
|
||||
-0.50\n-0.00\n0.00\n0.00\n\
|
||||
2\n1.500000000\n\
|
||||
nan\ninf\n-inf\n1e+20\n1234567890123.00\n\
|
||||
fps 59.9 / frame 0.0167\n"
|
||||
in
|
||||
outputs "a number with a precision" "programs/format.flan" format_out;
|
||||
outputs ~opt:"-O0" "a number with a precision, -O0" "programs/format.flan"
|
||||
format_out;
|
||||
(* The slice family at its second and third element types. sort-i32! was
|
||||
the only sort in the language; these are copies rather than an
|
||||
abstraction, because map/filter/reduce and a comparator sort all need a
|
||||
function value and check.ml refuses one outright.
|
||||
|
||||
Two lines carry the claims that are not about sorting. The subslice sort
|
||||
leaves its neighbours alone, which is the in-place ptr+len contract and
|
||||
the one thing a version that copied would fail. And the 2 is the width of
|
||||
sum-f32's accumulator made visible: 2^24 is where an f32 stops having a
|
||||
bit for 1, so an f32 running total absorbs both addends and prints 0. *)
|
||||
let algorithms_out =
|
||||
"-2.25 -1 0 0.5 3.5 3.5 10 \n\
|
||||
9 1 2 3 4 9 \n\
|
||||
1 2 3 \n1 2 3 \n7 \n\n\
|
||||
4 3 2 1 \n\
|
||||
-1 10 12.5\nnone 0\n2\n\
|
||||
true false false true false true true\ntrue false\n\
|
||||
Fig apple apple banana pear \n\
|
||||
alpha < bravo < charlie < delta\n"
|
||||
in
|
||||
outputs "sorting f32 and byte slices" "programs/algorithms.flan"
|
||||
algorithms_out;
|
||||
outputs ~opt:"-O0" "sorting f32 and byte slices, -O0"
|
||||
"programs/algorithms.flan" algorithms_out;
|
||||
(* A debug build, because [dty] is a separate path from everything above:
|
||||
[outputs ~dev:true] goes through the cells, not through DWARF, and a
|
||||
type with no arm there dies at emit rather than being merely undebugged.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user