A clock, the rest of libm, getenv, and the small file verbs

This commit is contained in:
Joseph Ferano 2026-09-17 22:21:24 +07:00
commit aaca776afd
12 changed files with 1006 additions and 20 deletions

View File

@ -4777,6 +4777,70 @@ and named_call ctx ~want loc name args =
[ file_guard ctx loc ~path_slot:ps ~op:1 steps ])))
| _ -> assert false)
(* ── the three that change the filesystem ──────────────────────────
[delete-file], [rename-file] and [make-directory] are [barf]'s shape with
a different runtime call, and they are here rather than as prelude
[declare]s for the one thing a declare cannot do: signal [FileError] with
the two restarts the compiler emits. A declare could only answer a bool,
and "the delete failed, here is a boolean" is the shape decision 5 exists
to keep out of this language a handler that made the parent directory
and wants [retry], or that has another path and wants [use-value], has
nothing to hold onto.
Each answers [()] and not a bool for the same reason [barf] does: the
failure is the condition, so a return value would only ever be true. The
questions that are *not* failures does this exist, how big is it
answer a value instead, and those two are prelude functions over one
[declare] because nothing about them needs a restart.
[op] continues the FileError numbering the prelude names: 0 read, 1 write,
and 2, 3, 4 here. A handler matching on it is matching on the prelude's
[file-op-delete] and friends, not on a literal. *)
| "delete-file" | "make-directory" ->
arity loc name 1 args;
let sym, op =
if String.equal name "delete-file" then "flan_file_delete", 2
else "flan_file_mkdir", 4
in
let path = check ctx ~want:Types.String (List.hd args) in
let ps = fresh_slot ctx Types.String in
let steps try_ =
[ try_ (rt loc (Types.Int Types.I8) sym
[ mk loc Types.String (Tast.Local ps) ]) ]
in
expect loc ~want
(mk loc Types.Unit
(Tast.Let ([ (ps, path) ],
[ file_guard ctx loc ~path_slot:ps ~op steps ])))
(* Two paths and one restart slot, so the guard holds the *source*: a
[use-value] renames a different file to the same destination. That is the
direction a handler can act on the destination it asked for is the one
thing it already knows and it is written down here because the other
reading is equally plausible until somebody says which it is.
The destination is bound before the loop, exactly as [barf] binds its
data, so a retry re-attempts the rename and not the expression that
computed where to. *)
| "rename-file" ->
arity loc name 2 args;
(match args with
| [ from_; to_ ] ->
let from_ = check ctx ~want:Types.String from_ in
let to_ = check ctx ~want:Types.String to_ in
let ps = fresh_slot ctx Types.String in
let ds = fresh_slot ctx Types.String in
let steps try_ =
[ try_ (rt loc (Types.Int Types.I8) "flan_file_rename"
[ mk loc Types.String (Tast.Local ps);
mk loc Types.String (Tast.Local ds) ]) ]
in
expect loc ~want
(mk loc Types.Unit
(Tast.Let ([ (ps, from_); (ds, to_) ],
[ file_guard ctx loc ~path_slot:ps ~op:3 steps ])))
| _ -> assert false)
(* ── containers ────────────────────────────────────────────────── *)
(* [at] and [len] were already the names for a fixed array and a slice, so a
Vec extends them rather than adding a parallel pair which is the

View File

@ -2809,6 +2809,14 @@ declare i64 @flan_hash_combine(i64, i64)
; here. `embed` needs none of these: it is a compile-time constant.
declare i8 @flan_file_size(ptr, i64, ptr)
declare i8 @flan_file_write(ptr, i64, ptr, i64)
; The three that change the filesystem. flan_file_stat is not here for the
; reason flan_file_read is not: nothing emitted calls it. It is reached from
; the prelude through a `declare`, because file-exists? and file-size answer a
; value rather than signalling and so need none of the guard machinery these
; three do.
declare i8 @flan_file_delete(ptr, i64)
declare i8 @flan_file_rename(ptr, i64, ptr, i64)
declare i8 @flan_file_mkdir(ptr, i64)
declare i64 @flan_file_fail_reason()
declare i8 @flan_slurp_into(ptr, ptr, i64)
|}

View File

@ -676,6 +676,258 @@ let source = {flan|
(declare atan2-f32 [y f32 x f32] f32 "atan2f")
(declare pow-f32 [x f32 y f32] f32 "powf")
;; The rest of libm, and both widths
;;
;; The five above were the whole of it for a long time, and the reason they
;; were is the reason the rest are here now: every one of these is a line, a
;; symbol that is already on the link, and nothing in the compiler. A program
;; that wanted a logarithm wrote the `declare` at the top of its own file
;; which is the identical call with none of the caveats written down and
;; nobody's name on it.
;;
;; **The f64 half is not decoration.** f32 is what a position and a colour
;; are, and f64 is what a *measurement* is: the clock below is nanoseconds in
;; an i64 and seconds in an f64, parse-f64 and format-f64 are both f64, and a
;; sum over more than a few thousand f32 terms has already lost the low bits
;; the answer was about. Having only the f32 face forced a cast down and back
;; at every one of those boundaries, and a cast down is where the precision
;; went.
;;
;; The split below is the one the sqrt paragraph draws, applied to the whole
;; family, and it is the only thing here worth knowing before calling:
;;
;; **Exact on every target.** IEEE-754 specifies these as exact operations
;; or as correctly rounded, so the answer is the same bit pattern under
;; glibc, musl and wasi-libc, and a hash taken across targets may be routed
;; through them. sqrt, fabs, floor, ceil, round, fmod.
;;
;; **Not.** IEEE-754 requires nothing of these and the three libms do
;; differ in the last bit. The sand-grid rule from the sin/cos paragraph
;; above covers all of them without change: a value compared across targets
;; must not have been through one. Everything else here.
(declare sqrt-f64 [x f64] f64 "sqrt")
;; Magnitude, and the f32/f64 pair is libm's because fabs is a sign-bit clear
;; that the compiler folds into one instruction cheaper than the branch a
;; Flan body would be, and right for -0.0 and for NaN, which a (< x 0.0) test
;; is not: -0.0 is not less than zero, so the branch returns it unchanged and
;; hands back a negative zero from a function named abs.
(declare abs-f32 [x f32] f32 "fabsf")
(declare abs-f64 [x f64] f64 "fabs")
;; The f64 faces of the three rounding functions floor-f32, ceil-f32 and
;; round-f32 are libm's rather than Flan's, and that is not an inconsistency.
;; Those three are Flan because of a cast: (i32 x) is the whole of floor-f32's
;; body, and it works precisely because every f32 with a fractional part fits
;; in an i32. At f64 it does not the exact range runs to 2^53 and i64's cast
;; would have to carry its own guard so the trick that made them free is not
;; available and the libm call is both shorter and exact.
(declare floor-f64 [x f64] f64 "floor")
(declare ceil-f64 [x f64] f64 "ceil")
(declare round-f64 [x f64] f64 "round")
;; Remainder, and it is C's fmod and not a modulo: the sign follows the
;; *dividend*, so (fmod-f32 -1.0 3.0) is -1.0 and not 2.0. An angle wrapped
;; into [0, tau) therefore needs the add-and-fmod-again that every wrap
;; function has, and this is the line where that is written down rather than
;; discovered. It is exact the result is the true remainder, representable
;; by construction so it belongs to the first group above.
(declare fmod-f32 [x f32 y f32] f32 "fmodf")
(declare fmod-f64 [x f64 y f64] f64 "fmod")
;; The trigonometric family, in full and at both widths. tan is separate from
;; (/ (sin-f32 x) (cos-f32 x)) for the reason atan2 is separate from a
;; division: near pi/2 the quotient is a ratio of two small errors and tanf
;; is not.
(declare tan-f32 [x f32] f32 "tanf")
(declare sin-f64 [x f64] f64 "sin")
(declare cos-f64 [x f64] f64 "cos")
(declare tan-f64 [x f64] f64 "tan")
;; The inverses. asin and acos answer NaN outside [-1, 1] rather than
;; clamping, which is what catches a dot product that drifted to 1.0000001
;; through rounding clamp it at the call site, on purpose, and the drift is
;; visible instead of silently becoming an angle of zero.
(declare asin-f32 [x f32] f32 "asinf")
(declare acos-f32 [x f32] f32 "acosf")
(declare atan-f32 [x f32] f32 "atanf")
(declare asin-f64 [x f64] f64 "asin")
(declare acos-f64 [x f64] f64 "acos")
(declare atan-f64 [x f64] f64 "atan")
(declare atan2-f64 [y f64 x f64] f64 "atan2")
;; Logarithms and the exponential. log is the natural one, as in C and unlike
;; the spreadsheet convention log2 and log10 are the other two and are named
;; for their bases, so nothing here is ambiguous. log2 is not (/ (log x)
;; (log 2.0)): it is exact at every power of two, which is the whole reason a
;; bit-width or an octave is computed with it.
;;
;; All four answer -inf at zero and NaN below it rather than signalling. A
;; condition per logarithm would cost a handler search on a path whose callers
;; are loops over samples, and NaN is the value that propagates to wherever
;; the caller does check.
(declare log-f32 [x f32] f32 "logf")
(declare log2-f32 [x f32] f32 "log2f")
(declare log10-f32 [x f32] f32 "log10f")
(declare exp-f32 [x f32] f32 "expf")
(declare log-f64 [x f64] f64 "log")
(declare log2-f64 [x f64] f64 "log2")
(declare log10-f64 [x f64] f64 "log10")
(declare exp-f64 [x f64] f64 "exp")
(declare pow-f64 [x f64 y f64] f64 "pow")
;; hypot over (sqrt-f32 (+ (* x x) (* y y))) because the obvious form
;; overflows on inputs the answer does not: the square of an f32 above ~1.8e19
;; is infinity, so a distance between two far-apart points comes back inf when
;; the distance itself is perfectly representable. hypotf scales first. It
;; costs more than the naive form and is worth it exactly when the naive form
;; is wrong.
(declare hypot-f32 [x f32 y f32] f32 "hypotf")
(declare hypot-f64 [x f64 y f64] f64 "hypot")
;; Cube root, and it is here because (pow-f32 x 0.33333334) is not it: pow
;; goes through a logarithm, which is undefined for a negative base, so the
;; obvious spelling answers NaN for every negative number where cbrt answers
;; the negative root.
(declare cbrt-f32 [x f32] f32 "cbrtf")
(declare cbrt-f64 [x f64] f64 "cbrt")
;; Integer magnitude, one per width because there are no generics over the
;; numeric types and min and max are builtins rather than functions, so a
;; single abs is not expressible today.
;;
;; The most negative value of each width has no positive counterpart, and this
;; does not special-case it: the subtraction is the same subtraction written
;; anywhere else and meets whatever the build's overflow rule is. Saturating
;; to the maximum would be a wrong answer returned quietly, which is the one
;; thing this file does not do.
(defn abs-i32 [x i32] i32
(if (< x 0) (- 0 x) x))
(defn abs-i64 [x i64] i64
(if (< x 0) (- 0 x) x))
;; pi and tau at both widths, because a defconst has a type and a cast between
;; them is where digits go missing. tau is 2pi and is written out rather than
;; multiplied, so the f32 one is the nearest f32 to tau and not twice the
;; nearest f32 to pi which is the same number here and is not guaranteed to
;; be for the derived form in general.
;;
;; Both are given to more digits than either width holds. That is deliberate:
;; the literal is rounded once, by the compiler, to the nearest value of the
;; declared type, which is the best available answer and is the same answer on
;; both targets.
(defconst pi-f32 f32 3.14159265358979323846)
(defconst pi-f64 f64 3.14159265358979323846)
(defconst tau-f32 f32 6.28318530717958647692)
(defconst tau-f64 f64 6.28318530717958647692)
;; The clock
;;
;; Until this section nothing in the language could tell the time. A game got
;; one from raylib and a program that was not a game had none at all, which
;; made "how long did that take" unanswerable in a tool the half of daily
;; use that has no window.
;;
;; **Two clocks, and they are not interchangeable.** This is the whole of what
;; a caller has to know, and the names are chosen so that picking the wrong
;; one reads wrong:
;;
;; `monotonic-` measures. It never goes backwards, it is not moved by NTP
;; or by a user setting the clock, and its zero is arbitrary the first
;; time the program reads it. It is meaningless on its own and correct as a
;; difference.
;;
;; `unix-` dates. Seconds (or nanoseconds) since 1970-01-01 UTC, which is
;; what goes in a file, a log line or a save. It *can* jump, forwards or
;; backwards, so a duration computed from two readings of it can be
;; negative, and timing anything with it is the bug this pair exists to make
;; hard to write.
;;
;; Odin draws exactly this line and this is its shape: core/time/time.odin has
;; `Time` for the date and `Tick` for the measurement, both an i64 of
;; nanoseconds, and core/time/time_linux.odin implements them as REALTIME and
;; MONOTONIC. The nanosecond integer is the primitive there and the f64 of
;; seconds is derived, which is why it is derived here too three C functions,
;; six names.
;;
;; **Which face to use.** The i64 of nanoseconds is exact and is what a
;; difference should be taken in. The f64 of seconds is what a frame loop
;; wants, and it is the shape raylib's `get-time` already answers with
;; (vendor/raylib/raylib.flan, `(declare-c get-time [] f64 "GetTime")`), so the
;; two mix without a conversion at every site. The monotonic origin is latched
;; at the first read rather than being boot see runtime/flan_rt.c so that
;; the f64 stays integer-exact in nanoseconds for a hundred days of process
;; life, which a boot-relative clock on a long-lived machine does not.
(declare monotonic-ns [] i64 "flan_monotonic_ns")
(declare unix-ns [] i64 "flan_unix_ns")
;; Nanoseconds, so the caller writes the unit rather than counting zeroes, and
;; so that a duration in the language is one type rather than a per-unit
;; family. Odin spells the same idea as `Duration` constants in core/time.
(defconst ns-per-microsecond i64 1000)
(defconst ns-per-millisecond i64 1000000)
(defconst ns-per-second i64 1000000000)
(defn monotonic-seconds [] f64
(/ (f64 (monotonic-ns)) 1000000000.0))
(defn unix-seconds [] f64
(/ (f64 (unix-ns)) 1000000000.0))
;; **Not a frame limiter.** A sleep asks the operating system to stop this
;; thread for *at least* the time given and says nothing about the upper
;; bound: a default Linux kernel wakes a sleeper on the timer tick after the
;; deadline, so a request for one millisecond commonly returns after rather
;; more, and the error is on the late side every time. A frame loop that
;; sleeps a fixed slice per frame therefore runs slow and drifts; the shape
;; that works is to sleep until a deadline computed from `monotonic-ns` and to
;; recompute it from the same clock each turn, so that a long frame is
;; absorbed instead of accumulated.
;;
;; A zero or negative request returns immediately rather than being refused,
;; which is what a deadline that has already passed produces and is not an
;; error see flan_sleep_ns for why, and for the EINTR loop that keeps a
;; signal from cutting the wait short.
(declare sleep-ns [ns i64] () "flan_sleep_ns")
(defn sleep-seconds [s f64] ()
(sleep-ns (i64 (* s 1000000000.0))))
;; The environment
;;
;; One lookup, and `argv` and `exit` are the rest of the OS surface. Setting a
;; variable is not here and is not an omission: `setenv` mutates a table the
;; slice below views, and nothing in the language can spawn the process that
;; would be the only reason to set one.
;;
;; **The result borrows.** It is a view of the process environment, not a copy:
;; it needs no allocator and no free, and it stays valid because there is no
;; writer that is the same promise `slice-from-ptr` asks a caller to make,
;; kept here once so that no caller has to. A program that wants to hold the
;; value past a point where that reasoning stops being obvious should copy it
;; into a Vec, which `concat` of one part already does.
;;
;; `None` and an empty `Some` are different answers and both occur: an unset
;; variable is None, and `FOO=` set to nothing is `(Some [])`. A caller that
;; wants to treat them alike says so.
;;
;; The absent case rides in the length and not in the pointer, because there is
;; no null test to write here a (Ptr T) in this language always addresses
;; something. flan_getenv answers a length of -1 and a pointer at a valid empty
;; string, so the test below is arithmetic and the pointer is never dereferenced
;; on the absent path.
(declare getenv-raw [name string out-len (Ptr i64)] (Ptr u8) "flan_getenv")
(defn getenv [name string] (Option [u8])
(let [n (i64 0)
p (getenv-raw name (addr n))]
(if (< n 0)
None
(Some (slice-from-ptr p (i32 n))))))
;; Byte classes
;;
;; ASCII only, and deliberately: a byte is a byte here, there is no code point
@ -1439,6 +1691,9 @@ let source = {flan|
(defconst file-op-read i32 0)
(defconst file-op-write i32 1)
(defconst file-op-delete i32 2)
(defconst file-op-rename i32 3)
(defconst file-op-mkdir i32 4)
(defconst file-missing i32 1)
(defconst file-denied i32 2)
@ -1447,9 +1702,45 @@ let source = {flan|
;; desktop-only, and it signals rather than refusing at build time (Flan has no
;; conditional compilation, so isolating code to desktop is not expressible) or
;; silently doing nothing (which is how a save file disappears with nothing
;; said).
;; said). `delete-file`, `rename-file` and `make-directory` carry the same
;; decision: all three change the filesystem, so all three signal this on the
;; web rather than quietly succeeding into a filesystem the page throws away.
(defconst file-unsupported i32 4)
;; The two file questions that are not failures, and they are prelude
;; functions rather than builtins because of that: nothing here needs a
;; restart, so nothing here needs the compiler.
;;
;; That is the line the whole file surface is drawn on. `slurp`, `barf`,
;; `delete-file`, `rename-file` and `make-directory` can fail in ways a
;; handler can *answer* make the parent and retry, supply another path so
;; each signals FileError with those two restarts. "Is it there" and "how big
;; is it" have no such answer: absence is the reply, not a fault, and a
;; condition would make the ordinary case cost a handler search.
(declare file-stat-raw [path string out-size (Ptr i64)] i8 "flan_file_stat")
;; True for anything the path resolves to a file, a directory, a device
;; because that is what the question asks and a caller wanting "and it is a
;; regular file" is asking a second question this does not pretend to answer.
;;
;; **It is a reading and not a guarantee.** Between this answering true and the
;; next line opening the file, anything may have removed it; the race is
;; unavoidable and is the reason `slurp` signals rather than requiring this
;; first. Reach for it when the answer is the point choosing a config path,
;; deciding whether to write a default and not as a guard in front of an
;; operation that already reports its own failure properly.
(defn file-exists? [path string] bool
(let [n (i64 0)]
(= (file-stat-raw path (addr n)) 1)))
;; None for a path that does not resolve, which folds every reason into one
;; answer that is the trade a caller makes by asking a question with no
;; restart on it. A caller that needs to tell "missing" from "denied" wants
;; `slurp`, whose FileError carries the reason.
(defn file-size [path string] (Option i64)
(let [n (i64 0)]
(if (= (file-stat-raw path (addr n)) 1) (Some n) None)))
;; Form: what a macro takes and what it answers
;;
;; The reader's output, mirrored on the Flan side, because a macro is a

View File

@ -2749,3 +2749,245 @@ int8_t flan_slurp_into(flan_vec *v, const uint8_t *path, int64_t n) {
v->len = got;
return 1;
}
/* ── Time, and the environment ─────────────────────────────────────────
*
* Three clock primitives and one environment lookup, added as a block at the
* end so that the two halves of this file the milestone-2 ABI above and the
* host services below stay separable. <time.h> is included here rather than
* at the top for the reason <errno.h> is: an include beside the only section
* that needs it says which section that is.
*
* The rule the whole file is written to applies hardest here: a primitive is
* the only thing implemented twice, so the seconds-valued faces of all three
* (monotonic-seconds, unix-seconds, sleep-seconds) are Flan in the prelude,
* over these. Odin draws the same line core/time/time_linux.odin is exactly
* _now, _tick_now, _sleep and _yield over clock_gettime and nanosleep, and
* duration_seconds is derived arithmetic in core/time/time.odin. */
#include <time.h>
/* The two clocks are kept apart on purpose, because the mistake they invite is
* using one for the other's job.
*
* MONOTONIC never goes backwards and is not adjusted by NTP or by the user
* setting the clock, which is what makes it the one to *measure* with: a frame
* time taken across a daylight-saving change is still a frame time. It has no
* meaning as a date its zero is arbitrary so it can only ever be
* subtracted from another reading of itself.
*
* REALTIME is the date, and it is the one that jumps: it can move backwards,
* and a duration computed from two readings of it can be negative. It is here
* to answer "when", not "how long". */
/* The origin is the first read of this clock in the process, not boot, and
* that is a decision rather than an accident.
*
* The reason is the f64 face above it. CLOCK_MONOTONIC counts from boot, so on
* a machine up a hundred days the raw value is past 2^53 nanoseconds beyond
* where an f64 holds consecutive integers and (monotonic-seconds) would
* quietly lose sub-microsecond resolution depending on how long the *machine*
* had been running, which is the worst kind of bug to be handed. Latched to
* first read, the f64 stays integer-exact for a hundred days of *process*
* life, and nothing this language builds runs that long without a restart.
*
* It also matches what a game already expects: raylib's GetTime is seconds
* since InitWindow, not seconds since boot, and the two now mix without a
* caller having to notice one of them is a much larger number.
*
* A plain static and no atomics, because the language has no threads. If it
* ever gets them, the worst a race here can do is latch two origins a few
* nanoseconds apart, which costs a reading that is early by that much and
* cannot make the clock run backwards. */
static int64_t flan_mono_origin;
static int flan_mono_armed;
static int64_t flan_clock_ns(clockid_t which) {
struct timespec ts;
/* A failure here is not reachable with a constant clock id the platform
* has, and there is no channel to report it on that a caller could act on:
* the answer to "what time is it" cannot be a condition without every
* reading of it costing a handler search. A zeroed timespec is what a
* failure reads as, and for MONOTONIC that is the origin. */
if (clock_gettime(which, &ts) != 0) { ts.tv_sec = 0; ts.tv_nsec = 0; }
return (int64_t)ts.tv_sec * 1000000000 + (int64_t)ts.tv_nsec;
}
int64_t flan_monotonic_ns(void) {
int64_t now = flan_clock_ns(CLOCK_MONOTONIC);
if (!flan_mono_armed) { flan_mono_armed = 1; flan_mono_origin = now; }
return now - flan_mono_origin;
}
int64_t flan_unix_ns(void) { return flan_clock_ns(CLOCK_REALTIME); }
/* A negative or zero request returns at once rather than being refused: the
* caller that computed "sleep until the frame's deadline" and arrived late
* wants to carry on, not to be told it is late, and that is by far the most
* common way this is called.
*
* The EINTR loop is the reason this is C and not two Flan lines over a raw
* nanosleep: a signal a profiler's timer, the dev loop's own otherwise
* cuts the wait short and the caller's frame pacing wobbles for reasons
* nothing in the program explains. The remaining time comes back in the same
* timespec, so resuming is a second call with no arithmetic. Odin's _sleep in
* core/time/time_linux.odin loops on EINTR for the same reason.
*
* On emscripten this is still nanosleep, which there spins rather than yields:
* the sleep is the length asked for, and it burns a core and blocks the frame
* doing it. Correct, and not what a browser build should be reaching for a
* web frame loop waits by returning to the browser, not by sleeping. */
void flan_sleep_ns(int64_t ns) {
struct timespec ts;
if (ns <= 0) return;
ts.tv_sec = (time_t)(ns / 1000000000);
ts.tv_nsec = (long)(ns % 1000000000);
while (nanosleep(&ts, &ts) != 0 && errno == EINTR) { }
}
/* getenv, with the absent case carried in the length rather than in the
* pointer, so that the Flan side never has to compare a pointer against null
* a test the language does not offer, since a (Ptr T) only ever arrives from a
* declare and nothing in the type says it may be nothing. Absent is *len = -1
* and a pointer to a valid empty string; present is *len >= 0 and the
* environment's own bytes, which (slice-from-ptr) then views.
*
* The bytes are the process environment's and are not copied. They outlive the
* call nothing in this language can call setenv or spawn a process, so there
* is no writer and they are not the caller's to free. The prelude's `getenv`
* says so where a caller will read it.
*
* A name with an embedded NUL reads as absent rather than as the shorter name
* before it, which is flan_path_cstr's rule and for its reason: the name
* looked up must be the name written. */
const uint8_t *flan_getenv(const uint8_t *name, int64_t n, int64_t *len) {
static const char empty[1] = { 0 };
char buf[FLAN_PATH_MAX];
const char *v;
*len = -1;
if (!flan_path_cstr(name, n, buf)) return (const uint8_t *)empty;
v = getenv(buf);
if (!v) return (const uint8_t *)empty;
*len = (int64_t)strlen(v);
return (const uint8_t *)v;
}
/* ── The rest of the file surface ──────────────────────────────────────
*
* Four more POSIX-shaped calls under the same rules as flan_file_size,
* flan_file_read and flan_file_write above: a path as ptr+len, 1 or 0, and the
* reason in flan_file_fail where the compiler's file_guard reads it. Nothing
* here holds a descriptor between calls, so a second target implements four
* functions and inherits the Flan that sits on them.
*
* The errno mapping is flan_errno_reason's and is not extended. Its three
* buckets missing, denied, io are what a *handler* can act on: retry after
* making the directory, use-value with another path, or give up. EEXIST and
* ENOTEMPTY land in io along with everything else, and that is the honest
* place for them until conditions have a hierarchy to hang a fourth reason
* off (see the FileError note in the prelude). */
#include <sys/stat.h>
#include <unistd.h>
/* One call behind both file-exists? and file-size, because they are one
* question: stat answers whether the path resolves and how big it is in the
* same breath, and two entry points would be two chances for them to disagree.
*
* stat and not the fopen-plus-ftell that flan_file_size uses. That one is
* shaped by slurp's needs it is about to read the file, so opening it is the
* test that matters and it is wrong as a general size: fopen on a directory
* succeeds on Linux and ftell then answers a number that is not a file size.
* The two coexist deliberately and answer different questions. */
int8_t flan_file_stat(const uint8_t *path, int64_t n, int64_t *size) {
char buf[FLAN_PATH_MAX];
struct stat st;
*size = 0;
if (!flan_path_cstr(path, n, buf)) {
flan_file_fail = FLAN_FILE_MISSING;
return 0;
}
errno = 0;
if (stat(buf, &st) != 0) { flan_file_fail = flan_errno_reason(); return 0; }
*size = (int64_t)st.st_size;
flan_file_fail = FLAN_FILE_OK;
return 1;
}
/* The three that change the filesystem, and they carry flan_file_write's
* decision 2 unchanged: on the web they signal, every time, with the path in
* the condition. Not a build-time refusal, because Flan has no conditional
* compilation and "isolate this to desktop" is therefore not expressible in
* source; and not a silent no-op, because that is how a save directory fails
* to appear with nothing said. */
int8_t flan_file_delete(const uint8_t *path, int64_t n) {
#if defined(__EMSCRIPTEN__)
(void)path; (void)n;
flan_file_fail = FLAN_FILE_UNSUPPORTED;
return 0;
#else
char buf[FLAN_PATH_MAX];
if (!flan_path_cstr(path, n, buf)) {
flan_file_fail = FLAN_FILE_MISSING;
return 0;
}
errno = 0;
/* remove(), so that an empty directory is deletable by the same call a file
* is it is unlink or rmdir depending on what the path names, which is the
* distinction a caller of a language with one `delete-file` does not want to
* have to make. A non-empty directory fails, and that is deliberate:
* recursive deletion is a loop the caller writes and sees. */
if (remove(buf) != 0) { flan_file_fail = flan_errno_reason(); return 0; }
flan_file_fail = FLAN_FILE_OK;
return 1;
#endif
}
/* Two paths, so two conversions, and the failure of either is reported as a
* missing path the same answer flan_path_cstr's refusal gets everywhere
* else. rename() is atomic within one filesystem and fails with EXDEV across
* two rather than copying, which lands in the io bucket; a caller that wants
* a move across devices writes slurp and barf, and sees that it did. */
int8_t flan_file_rename(const uint8_t *from, int64_t fn, const uint8_t *to,
int64_t tn) {
#if defined(__EMSCRIPTEN__)
(void)from; (void)fn; (void)to; (void)tn;
flan_file_fail = FLAN_FILE_UNSUPPORTED;
return 0;
#else
char a[FLAN_PATH_MAX], b[FLAN_PATH_MAX];
if (!flan_path_cstr(from, fn, a) || !flan_path_cstr(to, tn, b)) {
flan_file_fail = FLAN_FILE_MISSING;
return 0;
}
errno = 0;
if (rename(a, b) != 0) { flan_file_fail = flan_errno_reason(); return 0; }
flan_file_fail = FLAN_FILE_OK;
return 1;
#endif
}
/* 0777 and not 0755, because the process umask is what decides: a program that
* hardcodes 0755 has overridden a user's umask for no reason it could know.
* One level only an intervening directory that does not exist is ENOENT,
* which reaches the caller as `missing` and is answerable by a handler that
* makes the parent and takes `retry`, which is the restart that path exists
* for. */
int8_t flan_file_mkdir(const uint8_t *path, int64_t n) {
#if defined(__EMSCRIPTEN__)
(void)path; (void)n;
flan_file_fail = FLAN_FILE_UNSUPPORTED;
return 0;
#else
char buf[FLAN_PATH_MAX];
if (!flan_path_cstr(path, n, buf)) {
flan_file_fail = FLAN_FILE_MISSING;
return 0;
}
errno = 0;
if (mkdir(buf, 0777) != 0) { flan_file_fail = flan_errno_reason(); return 0; }
flan_file_fail = FLAN_FILE_OK;
return 1;
#endif
}

107
test/programs/files.flan Normal file
View File

@ -0,0 +1,107 @@
;;;; The file surface beyond slurp and barf: file-exists?, file-size,
;;;; delete-file, rename-file and make-directory.
;;;;
;;;; The split down the middle of that list is the whole design and this file
;;;; is arranged to show it. The two that ask a *question* — is it there, how
;;;; big is it — answer a value, because absence is a reply and not a fault;
;;;; they are prelude functions over one declare and the compiler knows
;;;; nothing about them. The three that *change* the filesystem answer () and
;;;; signal FileError with the two restarts slurp and barf already establish,
;;;; because each of their failures is one a handler can act on: make the
;;;; parent directory and retry, or supply another path.
;;;;
;;;; Everything is made and removed inside this program, so it leaves the
;;;; directory as it found it — checked at the end rather than assumed.
;; Handlers cannot see the locals of the function that established them, so the
;; observations are globals, as in slurp.flan.
(defvar seen i64)
(defvar last-reason i32)
(defvar last-op i32)
(defn main [] i32
;; ── The questions ─────────────────────────────────────────────────
(println (file-exists? "programs/assets/a.txt")) ; true
(println (file-exists? "programs/assets/nope")) ; false
;; A directory resolves, which is what the name asks and not "is a regular
;; file" — a caller wanting the narrower question is asking a second one.
(println (file-exists? "programs/assets")) ; true
(match (file-size "programs/assets/a.txt")
(Some n) (println n) ; 13
None (println "missing"))
;; None folds every reason into one answer, which is the trade a question
;; with no restart on it makes.
(match (file-size "programs/assets/nope")
(Some n) (println n)
None (println "none"))
;; ── make-directory, rename-file, delete-file ──────────────────────
(make-directory "files-tmp")
(println (file-exists? "files-tmp")) ; true
(barf "files-tmp/one.txt" (bytes "0123456789"))
(match (file-size "files-tmp/one.txt")
(Some n) (println n) ; 10
None (println "missing"))
(rename-file "files-tmp/one.txt" "files-tmp/two.txt")
(println (file-exists? "files-tmp/one.txt")) ; false
(println (file-exists? "files-tmp/two.txt")) ; true
(delete-file "files-tmp/two.txt")
(println (file-exists? "files-tmp/two.txt")) ; false
;; ── retry, after the handler made the parent ──────────────────────
;; The restart this family exists for. Writing into a directory that is not
;; there is ENOENT, which arrives as `missing`; the handler makes the
;; directory and takes `retry`, and the second attempt succeeds. Nothing in
;; the failing code knows any of that happened.
(handler-bind
[(FileError [c]
(set seen (+ seen 1))
(set last-reason (.reason c))
(set last-op (.op c))
(make-directory "files-tmp/sub")
(invoke-restart 'retry))]
(barf "files-tmp/sub/deep.txt" (bytes "deep")))
(println seen) ; 1
(println (= last-reason file-missing)) ; true
(println (= last-op file-op-write)) ; true
(println (file-exists? "files-tmp/sub/deep.txt")) ; true
;; ── use-value, on a delete ────────────────────────────────────────
;; The same restart slurp's read offers, on an operation that writes: the
;; handler names a path that is there and the delete resumes against it.
(set seen 0)
(handler-bind
[(FileError [c]
(set seen (+ seen 1))
(set last-op (.op c))
(invoke-restart 'use-value "files-tmp/sub/deep.txt"))]
(delete-file "files-tmp/sub/not-there.txt"))
(println seen) ; 1
(println (= last-op file-op-delete)) ; true
(println (file-exists? "files-tmp/sub/deep.txt")) ; false
;; ── A non-empty directory does not delete ─────────────────────────
;; remove() is unlink or rmdir depending on what the path names, so an empty
;; directory goes by the same call a file does — and a full one does not,
;; which is deliberate: a recursive delete is a loop the caller writes and
;; sees. Here the handler declines to answer, which is what an unhandled
;; condition would do, so it counts and lets the program carry on by
;; supplying the child path instead.
(set seen 0)
(handler-bind
[(FileError [c]
(set seen (+ seen 1))
(set last-op (.op c))
(invoke-restart 'use-value "files-tmp/sub"))]
(delete-file "files-tmp"))
(println seen) ; 1
(println (= last-op file-op-delete)) ; true
;; And now it is empty, so it goes.
(delete-file "files-tmp")
(println (file-exists? "files-tmp")) ; false
0)

92
test/programs/math3.flan Normal file
View File

@ -0,0 +1,92 @@
;;;; The rest of libm, at both widths — what math.flan and math2.flan left out.
;;;;
;;;; The rule those two set holds here unchanged and is the only reason this
;;;; file looks the way it does: none of these is correctly rounded under
;;;; IEEE-754 except sqrt, fabs, the rounding three and fmod, so every value
;;;; below is one whose answer is exact in binary — zero, one, a power of two,
;;;; a perfect square, a perfect cube. A case that pinned glibc's last bit
;;;; would pass here and fail on wasi-libc.
;;;;
;;;; The acceptance table builds this at -O0 as well, and that run is the one
;;;; that matters: at -O2 LLVM folds a libm call over two literals and leaves
;;;; no symbol to resolve, which is how a missing -lm hid the first time.
(defn show [x f32] ()
(print x)
(print " "))
(defn show64 [x f64] ()
(print x)
(print " "))
(defn main [] i32
;; The f32 half. tan, the three inverses, the three logarithms and exp.
(show (tan-f32 0.0)) ; 0
(show (asin-f32 0.0)) ; 0
(show (acos-f32 1.0)) ; 0
(show (atan-f32 0.0)) ; 0
(show (log-f32 1.0)) ; 0
(show (log2-f32 8.0)) ; 3
(show (log10-f32 1000.0)) ; 3
(show (exp-f32 0.0)) ; 1
(println "")
(show (fmod-f32 7.0 4.0)) ; 3
;; The sign follows the dividend and not the divisor, which is the line a
;; caller reaching for a modulo gets wrong. Written down as a case.
(show (fmod-f32 -1.0 3.0)) ; -1
(show (hypot-f32 3.0 4.0)) ; 5
(show (cbrt-f32 27.0)) ; 3
;; The negative root, where (pow-f32 x 0.33333334) would be NaN: pow goes
;; through a logarithm and cbrt does not.
(show (cbrt-f32 -8.0)) ; -2
(show (abs-f32 -2.5)) ; 2.5
(println "")
;; The f64 half, at the same exact values. This block is the whole point of
;; the f64 face existing: before it, every one of these was a cast down to
;; f32 and back, and the cast down is where the precision went.
(show64 (sqrt-f64 16.0)) ; 4
(show64 (sin-f64 0.0)) ; 0
(show64 (cos-f64 0.0)) ; 1
(show64 (tan-f64 0.0)) ; 0
(show64 (asin-f64 0.0)) ; 0
(show64 (acos-f64 1.0)) ; 0
(show64 (atan-f64 0.0)) ; 0
(show64 (atan2-f64 0.0 1.0)) ; 0
(println "")
(show64 (log-f64 1.0)) ; 0
(show64 (log2-f64 1024.0)) ; 10
(show64 (log10-f64 100.0)) ; 2
(show64 (exp-f64 0.0)) ; 1
(show64 (pow-f64 2.0 10.0)) ; 1024
(show64 (fmod-f64 7.0 4.0)) ; 3
(show64 (hypot-f64 3.0 4.0)) ; 5
(show64 (cbrt-f64 8.0)) ; 2
(show64 (abs-f64 -1.5)) ; 1.5
(println "")
;; The f64 rounding family, which is libm's where the f32 one is Flan's —
;; and it agrees with the Flan one where they overlap: half away from zero,
;; so -2.5 goes to -3 and not to -2.
(show64 (floor-f64 -2.5)) ; -3
(show64 (ceil-f64 -2.5)) ; -2
(show64 (round-f64 -2.5)) ; -3
(show64 (round-f64 2.5)) ; 3
(println "")
;; Integer magnitude, one per width.
(print (abs-i32 -7)) (print " ") ; 7
(print (abs-i64 (i64 -7))) (print " ") ; 7
(print (abs-i32 7)) (print " ") ; 7
;; tau is 2pi at both widths. Pinning the relation rather than the digits is
;; what catches a constant written to too few of them.
(print (= tau-f32 (* 2.0 pi-f32))) (print " ")
(print (= tau-f64 (* 2.0 pi-f64)))
(println "")
;; pi is the one value here that can be pinned without pinning a libm: it is
;; a literal the compiler rounds, so it is the same on every target.
(println (and (> pi-f64 3.14159265) (< pi-f64 3.14159266)))
0)

71
test/programs/time.flan Normal file
View File

@ -0,0 +1,71 @@
;;;; The clock and the environment.
;;;;
;;;; Every line of output here is an invariant and not a reading, and that is
;;;; forced rather than chosen: this file is in the corpus @x86 builds twice
;;;; and diffs, and the acceptance table matches its stdout exactly, so a
;;;; timestamp or an elapsed count would fail a correct compiler on the second
;;;; run. What is left is what a clock actually has to promise — that it does
;;;; not go backwards, that a sleep does not return early, that the two faces
;;;; of one clock describe one instant — and those are the properties worth
;;;; pinning anyway. A test that asserted "this took under 3ms" would be a
;;;; test of the machine's load.
(defn main [] i32
;; Monotonic, twice. The whole contract in one line: it never goes
;; backwards. Equal is allowed and is not a bug — two reads inside one tick
;; of a coarse timer are the same nanosecond.
(let [t1 (monotonic-ns)
t2 (monotonic-ns)]
(println (>= t2 t1)))
;; And the origin is the first read rather than boot, so the first readings
;; a program takes are small. Bounded rather than pinned: the number is
;; whatever this process spent between the calls above and this one, which
;; is not a second on any machine that can run the suite at all.
(println (< (monotonic-ns) ns-per-second))
;; The f64 face is the i64 one divided, and what is checked is that the two
;; describe the same instant: a later reading in seconds is at or past an
;; earlier reading in nanoseconds converted the same way. A clock whose two
;; faces came from different sources fails this.
(let [a (/ (f64 (monotonic-ns)) 1000000000.0)
b (monotonic-seconds)]
(println (>= b a)))
;; The wall clock is a date, so the invariant is a date one: it is after
;; 2020 and before 2100. That pins the epoch and the unit at once — a clock
;; counting microseconds, or counting from boot, fails both halves.
(let [now (unix-seconds)]
(println (and (> now 1577836800.0) (< now 4102444800.0))))
;; Sleep is specified as *at least*, so at-least is what is asserted; the
;; upper bound belongs to the scheduler and not to this language. Two
;; milliseconds because the shortest sleep a default kernel actually
;; performs is a timer tick, and a shorter request would make this a test of
;; how that kernel was configured.
(let [before (monotonic-ns)]
(sleep-ns (* 2 ns-per-millisecond))
(println (>= (- (monotonic-ns) before) (* 2 ns-per-millisecond))))
;; Zero and negative return at once rather than being refused, which is what
;; a deadline already passed produces. That they return at all is the
;; assertion; nothing here is timed.
(sleep-ns 0)
(sleep-ns -1)
(sleep-seconds 0.0)
(println "slept")
;; ── The environment ──────────────────────────────────────────────
;; A variable nothing sets. None is the answer, and it is a different answer
;; from a variable set to nothing.
(match (getenv "FLAN_NO_SUCH_VARIABLE_AT_ALL")
(Some v) (println "unexpectedly set")
None (println "unset"))
;; PATH is set for every process that gets as far as running this, and the
;; only portable thing about its contents is that there are some.
(match (getenv "PATH")
(Some v) (println (> (len v) 0))
None (println "no PATH"))
0)

View File

@ -268,6 +268,32 @@ let () =
outputs "atan2, pow and clamp" "programs/math2.flan" math2_out;
outputs ~opt:"-O0" "atan2, pow and clamp, -O0" "programs/math2.flan"
math2_out;
(* The rest of libm, at both widths. Same rule as math2 above and for the
same reason every value is exact in binary and the -O0 pass is
doing the same job: at -O2 LLVM folds a libm call over two literals and
leaves no symbol to resolve, so that run is the one proving all thirty
new declares actually link. *)
let math3_out =
"0 0 0 0 0 3 3 1 \n\
3 -1 5 3 -2 2.5 \n\
4 0 1 0 0 0 0 0 \n\
0 10 2 1 1024 3 5 2 1.5 \n\
-3 -2 -3 3 \n\
7 7 7 true true\n\
true\n"
in
outputs "the rest of libm, both widths" "programs/math3.flan" math3_out;
outputs ~opt:"-O0" "the rest of libm, both widths, -O0"
"programs/math3.flan" math3_out;
(* The clock and the environment. Every line of that program's output is
an invariant a monotonicity, a date range, a sleep that did not
return early and not a reading, because the same file is in the
corpus @x86 builds twice and diffs, so a timestamp would fail a correct
compiler on its second run. *)
let time_out = "true\ntrue\ntrue\ntrue\ntrue\nslept\nunset\ntrue\n" in
outputs "the clock and getenv" "programs/time.flan" time_out;
outputs ~opt:"-O0" "the clock and getenv, -O0" "programs/time.flan"
time_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
@ -824,6 +850,37 @@ let () =
end;
(try Sys.remove exe with Sys_error _ -> ());
(* The rest of the file surface. What is being checked as much as the
calls is the line drawn through them: file-exists? and file-size answer
a value because absence is a reply and not a fault, and the three that
change the filesystem signal FileError with the same two restarts slurp
and barf establish. Both restarts are taken here on operations that
write - retry after the handler made the parent directory, and
use-value on a delete - which is what the pair is for and what a bool
return could not have offered.
The program makes and removes its own tree, so the cleanup below is for
a run that failed part way through and not for a passing one. *)
let clean_dir () =
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
[ "files-tmp/sub/deep.txt"; "files-tmp/one.txt"; "files-tmp/two.txt" ];
List.iter (fun d -> try Unix.rmdir d with Unix.Unix_error _ -> ())
[ "files-tmp/sub"; "files-tmp" ]
in
let files_out =
"true\nfalse\ntrue\n13\nnone\ntrue\n10\nfalse\ntrue\nfalse\n\
1\ntrue\ntrue\ntrue\n1\ntrue\nfalse\n1\ntrue\nfalse\n"
in
clean_dir ();
outputs "the rest of the file surface" "programs/files.flan" files_out;
clean_dir ();
outputs ~opt:"-O0" "the rest of the file surface, -O0" "programs/files.flan"
files_out;
clean_dir ();
outputs ~dev:true "the rest of the file surface, dev" "programs/files.flan"
files_out;
clean_dir ();
(* The epoch trap: a container whose allocator has been released. This is
spec-memory.md's shipping answer to "Open: catching a use-after-release
statically" — detection, loud and immediate, rather than a static rule

View File

@ -128,9 +128,14 @@ let corpus =
"programs/edn.flan", [];
"programs/enum-compare.flan", [];
"programs/error.flan", [];
(* Makes and removes its own tree, so the two runs of the sweep see the
same directory; the new C here is three more path buffers, which is
exactly what this tool is for. *)
"programs/files.flan", [];
"programs/handles.flan", [];
"programs/machine.flan", [];
"programs/math.flan", [];
"programs/math3.flan", [];
"programs/pkg-diamond.flan", [];
"programs/pkg-return.flan", [];
"programs/pkg-shared.flan", [];
@ -143,6 +148,10 @@ let corpus =
"programs/slices.flan", [];
"programs/string-of-bytes.flan", [];
"programs/text.flan", [];
(* The clock and getenv. getenv hands back a slice viewing the process
environment and never a copy, so a report here would be the one that
matters. *)
"programs/time.flan", [];
"programs/unit-main.flan", [];
"programs/utf8.flan", [];
"programs/values.flan", [];

View File

@ -215,6 +215,7 @@ let corpus =
"programs/error.flan", [];
"programs/exhausted.flan", [];
"programs/exhausted-unhandled.flan", [];
"programs/files.flan", [];
"programs/free-all-refused.flan", [];
"programs/handles.flan", [];
"programs/machine.flan", [];
@ -222,6 +223,7 @@ let corpus =
"programs/map-stale-region.flan", [];
"programs/maps.flan", [];
"programs/math.flan", [];
"programs/math3.flan", [];
"programs/pool-stale-region.flan", [];
"programs/pkg-macro.flan", [];
"programs/pkg-diamond.flan", [];
@ -241,6 +243,7 @@ let corpus =
"programs/stale-region.flan", [];
"programs/string-of-bytes.flan", [];
"programs/text.flan", [];
"programs/time.flan", [];
"programs/datas.flan", [];
"programs/unit-main.flan", [];
"programs/utf8.flan", [];

View File

@ -1,6 +1,11 @@
;; A plain `declare` names a C symbol in a signature Flan can already spell:
;; no aggregate crosses, so no wrapper is generated.
(declare cos-f64 [x f64] f64 "cos")
;;
;; cosh and not cos, because the prelude already declares cos-f64 and a second
;; declaration of a name is refused with both sites named. That refusal is the
;; useful half of this example: the prelude is where the common libm calls
;; live, and a `declare` is how you reach one it does not name.
(declare cosh-f64 [x f64] f64 "cosh")
(defn main [] ()
(print (cos-f64 0.0)) (println ""))
(print (cosh-f64 0.0)) (println ""))

View File

@ -1047,7 +1047,7 @@ takes the value as it is and prints the number it holds.</p>
<h2 id="prelude">The prelude</h2>
<p>The prelude is written in Flan, all but five lines of it, and prepended to every
<p>The prelude is written in Flan, all but its <code>declare</code> lines, and prepended to every
program, so nothing in it needs importing. It holds no printing of its own:
<code>print</code> and <code>println</code> are the compiler's, and
<code>write-stdout</code> — the one output primitive — is what they are written
@ -1063,7 +1063,10 @@ over.</p>
<tr><td>text</td><td><code>split-on-byte</code>, <code>split-next!</code>, <code>split</code>, <code>lower-ascii</code>, <code>upper-ascii</code>, <code>to-lower</code>, <code>to-upper</code></td></tr>
<tr><td>building bytes</td><td><code>append!</code>, <code>append-i64!</code>, <code>append-f64!</code>, <code>concat</code>, <code>join</code>, <code>repeat-bytes</code>, <code>replace-bytes</code>, <code>slices-new</code>, <code>format-f64</code></td></tr>
<tr><td>UTF-8</td><td><code>decode-rune</code>, <code>rune-at</code>, <code>rune-count</code>, <code>rune-size</code>, <code>rune-start?</code>, <code>valid-utf8?</code>, <code>encode-rune!</code></td></tr>
<tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>clamp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, and the five <code>declare</code>s: <code>sqrt-f32</code>, <code>sin-f32</code>, <code>cos-f32</code>, <code>atan2-f32</code>, <code>pow-f32</code></td></tr>
<tr><td>numbers</td><td><code>sign-f32</code>, <code>lerp</code>, <code>clamp</code>, <code>floor-f32</code>, <code>ceil-f32</code>, <code>round-f32</code>, <code>abs-i32</code>, <code>abs-i64</code>, the constants <code>pi-f32</code>, <code>pi-f64</code>, <code>tau-f32</code>, <code>tau-f64</code>, and libm through a <code>declare</code> at both widths: <code>sqrt</code>, <code>abs</code>, <code>floor</code>, <code>ceil</code>, <code>round</code>, <code>fmod</code>, <code>sin</code>, <code>cos</code>, <code>tan</code>, <code>asin</code>, <code>acos</code>, <code>atan</code>, <code>atan2</code>, <code>log</code>, <code>log2</code>, <code>log10</code>, <code>exp</code>, <code>pow</code>, <code>hypot</code>, <code>cbrt</code> — each spelled <code>-f32</code> or <code>-f64</code></td></tr>
<tr><td>time</td><td><code>monotonic-ns</code>, <code>monotonic-seconds</code>, <code>unix-ns</code>, <code>unix-seconds</code>, <code>sleep-ns</code>, <code>sleep-seconds</code>, and <code>ns-per-second</code> and its two smaller siblings</td></tr>
<tr><td>files</td><td><code>file-exists?</code> and <code>file-size</code>, which answer a value; <code>slurp</code>, <code>barf</code>, <code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code>, which signal <code>FileError</code> under <code>retry</code> and <code>use-value</code></td></tr>
<tr><td>the operating system</td><td><code>getenv</code>, which answers an <code>(Option [u8])</code> viewing the process environment</td></tr>
<tr><td>random</td><td><code>rand-seed</code>, <code>rand-u32</code>, <code>rand-f32</code>, <code>rand-i32-range</code>, <code>rand-f32-range</code></td></tr>
<tr><td>forms, for macros</td><td><code>form-nil</code>, <code>form-cons</code>, <code>form-append</code>, <code>form-rest</code>, <code>form-items</code>, <code>form-pair</code>, <code>form-sym?</code>, <code>form-is-sym?</code>, <code>gensym</code>, and <code>unless</code> and <code>into</code>, which are macros written here rather than special forms</td></tr>
<tr><td>the rest</td><td><code>pause</code>, which signals the <code>Pause</code> condition the break loop stops on, and <code>embed-find</code></td></tr>
@ -1084,18 +1087,47 @@ native and on wasm32. <strong>The parsers are ours too</strong>:
<code>"abc"</code> and 12 for <code>"12x"</code>, which are three wrong answers a caller
cannot tell from a real 12.</p>
<p>Five functions in the file are not Flan, and they are libm's:
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code> and the same line for
<code>sinf</code>, <code>cosf</code>, <code>atan2f</code> and <code>powf</code>. Every
other number here is reachable from the four operations and a cast; a square root is
not, and the usual trick of seeding Newton's method from the exponent bits needs a
bit-cast between <code>f32</code> and <code>u32</code> that the language does not have.
IEEE-754 makes <code>sqrt</code> correctly rounded, so libm gives the same bit pattern
on both targets anyway. <strong>The other four are not</strong>: IEEE-754 requires
nothing of <code>sinf</code>, <code>cosf</code>, <code>atan2f</code> or
<code>powf</code>, and glibc, musl and wasi-libc do differ in the last bit — so the
byte-identical-hash property the RNG exists for does not survive a hash routed through
any of them. Every link carries <code>-lm</code>.</p>
<p>The maths in the file is not Flan, and it is libm's:
<code>(declare sqrt-f32 [x f32] f32 "sqrtf")</code> and the same line for thirty-odd
more. Every other number here is reachable from the four operations and a cast; a square
root is not, and the usual trick of seeding Newton's method from the exponent bits needs
a bit-cast between <code>f32</code> and <code>u32</code> that the language does not have.
A <code>declare</code> is also the cheapest thing in the language to add — a line, a
symbol already on the link, and nothing in either backend — which is why the surface is
now the whole family at both widths rather than the five it started as.</p>
<p><strong>One split is worth knowing before calling any of them.</strong> IEEE-754
specifies <code>sqrt</code>, <code>fabs</code>, <code>floor</code>, <code>ceil</code>,
<code>round</code> and <code>fmod</code> as exact or correctly rounded, so those give the
same bit pattern under glibc, musl and wasi-libc. <strong>It requires nothing of the
rest</strong><code>sin</code>, <code>cos</code>, <code>tan</code>, the inverses, the
logarithms, <code>exp</code>, <code>pow</code>, <code>hypot</code>, <code>cbrt</code>
and the three libms do differ in the last bit, so the byte-identical-hash property the
RNG exists for does not survive a value routed through any of them. Every link carries
<code>-lm</code>.</p>
<p><strong>The clock is two clocks and they are not interchangeable.</strong>
<code>monotonic-ns</code> measures: it never goes backwards, nothing adjusts it, and its
zero is the first time the program reads it, so it is meaningless alone and correct as a
difference. <code>unix-ns</code> dates: nanoseconds since 1970, which is what goes in a
save file or a log line, and which can jump in either direction when the system clock is
set. Odin draws the same line — <code>Tick</code> against <code>Time</code> in
<code>core/time</code> — and the nanosecond integer is the primitive on both sides, with
the <code>-seconds</code> faces derived from it. The <code>f64</code> of seconds is the
shape raylib's <code>get-time</code> already answers with, so the two mix; it stays
integer-exact in nanoseconds for a hundred days of process life, which is why the
monotonic origin is the first read and not boot.</p>
<p><strong>The file surface is split by whether a handler could do anything.</strong>
<code>file-exists?</code> and <code>file-size</code> answer a <code>bool</code> and an
<code>(Option i64)</code>: absence is the reply, not a fault, and a condition would make
the ordinary case pay for a handler search. <code>slurp</code>, <code>barf</code>,
<code>delete-file</code>, <code>rename-file</code> and <code>make-directory</code> signal
<code>FileError</code> instead, under the two restarts Common Lisp establishes for a
file error — <code>retry</code>, because the handler may have just made the directory,
and <code>use-value</code> with another path. Nothing here returns an error code, which
is the same rule allocation follows. Streaming, stdin and directory listings are not
here; a whole file at a time is the surface.</p>
<p>The primitives underneath are few — a primitive is the only thing implemented
twice per backend: <code>argv</code>,
@ -1386,14 +1418,19 @@ structural rule could tell them apart.</p>
<p><code>declare</code> names a C symbol in a signature Flan can already spell. Nothing
is generated; a Flan string crosses as ptr+len, exactly as it is stored.</p>
<pre><code>(declare cos-f64 [x f64] f64 "cos")
<pre><code>(declare cosh-f64 [x f64] f64 "cosh")
(defn main [] ()
(print (cos-f64 0.0)) (println ""))</code></pre>
(print (cosh-f64 0.0)) (println ""))</code></pre>
<pre><code class="sh">1</code></pre>
<p>That is 1.0, printed by the same rule as before.</p>
<p>That is 1.0, printed by the same rule as before. It is <code>cosh</code> and not
<code>cos</code> because <a href="#prelude">the prelude</a> already declares
<code>cos-f64</code>, and a second declaration of a name is refused with both sites
named — which is the other half of what this example shows. The prelude is where the
common libm calls live; a <code>declare</code> is how you reach one it does not
name.</p>
<p><code>declare-c</code> names the C library's own function in the C library's own
signature, and the compiler writes the wrapper. This is what raylib's package is made