diff --git a/lib/check.ml b/lib/check.ml
index 13c9203..46c3f10 100644
--- a/lib/check.ml
+++ b/lib/check.ml
@@ -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
diff --git a/lib/emit.ml b/lib/emit.ml
index c9e771a..7f7eb1e 100644
--- a/lib/emit.ml
+++ b/lib/emit.ml
@@ -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)
|}
diff --git a/lib/prelude.ml b/lib/prelude.ml
index 3ed65e6..30d6435 100644
--- a/lib/prelude.ml
+++ b/lib/prelude.ml
@@ -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
diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c
index 2ca6ff4..db27a46 100644
--- a/runtime/flan_rt.c
+++ b/runtime/flan_rt.c
@@ -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. is included here rather than
+ * at the top for the reason 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
+
+/* 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
+#include
+
+/* 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
+}
diff --git a/test/programs/files.flan b/test/programs/files.flan
new file mode 100644
index 0000000..adf8178
--- /dev/null
+++ b/test/programs/files.flan
@@ -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)
diff --git a/test/programs/math3.flan b/test/programs/math3.flan
new file mode 100644
index 0000000..7f8a381
--- /dev/null
+++ b/test/programs/math3.flan
@@ -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)
diff --git a/test/programs/time.flan b/test/programs/time.flan
new file mode 100644
index 0000000..6836c24
--- /dev/null
+++ b/test/programs/time.flan
@@ -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)
diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml
index a99ab31..3b51865 100644
--- a/test/test_acceptance.ml
+++ b/test/test_acceptance.ml
@@ -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
diff --git a/test/test_sanitize.ml b/test/test_sanitize.ml
index 12090cd..971d768 100644
--- a/test/test_sanitize.ml
+++ b/test/test_sanitize.ml
@@ -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", [];
diff --git a/test/test_valgrind.ml b/test/test_valgrind.ml
index e58a06e..3577948 100644
--- a/test/test_valgrind.ml
+++ b/test/test_valgrind.ml
@@ -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", [];
diff --git a/web/examples/ffi.flan b/web/examples/ffi.flan
index d34ca00..6429f95 100644
--- a/web/examples/ffi.flan
+++ b/web/examples/ffi.flan
@@ -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 ""))
diff --git a/web/index.html b/web/index.html
index a4ab6ec..8860532 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1047,7 +1047,7 @@ takes the value as it is and prints the number it holds.
The prelude
-The prelude is written in Flan, all but five lines of it, and prepended to every
+
The prelude is written in Flan, all but its declare lines, and prepended to every
program, so nothing in it needs importing. It holds no printing of its own:
print and println are the compiler's, and
write-stdout — the one output primitive — is what they are written
@@ -1063,7 +1063,10 @@ over.
| text | split-on-byte, split-next!, split, lower-ascii, upper-ascii, to-lower, to-upper |
| building bytes | append!, append-i64!, append-f64!, concat, join, repeat-bytes, replace-bytes, slices-new, format-f64 |
| UTF-8 | decode-rune, rune-at, rune-count, rune-size, rune-start?, valid-utf8?, encode-rune! |
-| numbers | sign-f32, lerp, clamp, floor-f32, ceil-f32, round-f32, and the five declares: sqrt-f32, sin-f32, cos-f32, atan2-f32, pow-f32 |
+| numbers | sign-f32, lerp, clamp, floor-f32, ceil-f32, round-f32, abs-i32, abs-i64, the constants pi-f32, pi-f64, tau-f32, tau-f64, and libm through a declare at both widths: sqrt, abs, floor, ceil, round, fmod, sin, cos, tan, asin, acos, atan, atan2, log, log2, log10, exp, pow, hypot, cbrt — each spelled -f32 or -f64 |
+| time | monotonic-ns, monotonic-seconds, unix-ns, unix-seconds, sleep-ns, sleep-seconds, and ns-per-second and its two smaller siblings |
+| files | file-exists? and file-size, which answer a value; slurp, barf, delete-file, rename-file and make-directory, which signal FileError under retry and use-value |
+| the operating system | getenv, which answers an (Option [u8]) viewing the process environment |
| random | rand-seed, rand-u32, rand-f32, rand-i32-range, rand-f32-range |
| forms, for macros | form-nil, form-cons, form-append, form-rest, form-items, form-pair, form-sym?, form-is-sym?, gensym, and unless and into, which are macros written here rather than special forms |
| the rest | pause, which signals the Pause condition the break loop stops on, and embed-find |
@@ -1084,18 +1087,47 @@ native and on wasm32. The parsers are ours too:
"abc" and 12 for "12x", which are three wrong answers a caller
cannot tell from a real 12.
-Five functions in the file are not Flan, and they are libm's:
-(declare sqrt-f32 [x f32] f32 "sqrtf") and the same line for
-sinf, cosf, atan2f and powf. 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 f32 and u32 that the language does not have.
-IEEE-754 makes sqrt correctly rounded, so libm gives the same bit pattern
-on both targets anyway. The other four are not: IEEE-754 requires
-nothing of sinf, cosf, atan2f or
-powf, 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 -lm.
+The maths in the file is not Flan, and it is libm's:
+(declare sqrt-f32 [x f32] f32 "sqrtf") 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 f32 and u32 that the language does not have.
+A declare 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.
+
+One split is worth knowing before calling any of them. IEEE-754
+specifies sqrt, fabs, floor, ceil,
+round and fmod as exact or correctly rounded, so those give the
+same bit pattern under glibc, musl and wasi-libc. It requires nothing of the
+rest — sin, cos, tan, the inverses, the
+logarithms, exp, pow, hypot, cbrt —
+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
+-lm.
+
+The clock is two clocks and they are not interchangeable.
+monotonic-ns 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. unix-ns 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 — Tick against Time in
+core/time — and the nanosecond integer is the primitive on both sides, with
+the -seconds faces derived from it. The f64 of seconds is the
+shape raylib's get-time 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.
+
+The file surface is split by whether a handler could do anything.
+file-exists? and file-size answer a bool and an
+(Option i64): absence is the reply, not a fault, and a condition would make
+the ordinary case pay for a handler search. slurp, barf,
+delete-file, rename-file and make-directory signal
+FileError instead, under the two restarts Common Lisp establishes for a
+file error — retry, because the handler may have just made the directory,
+and use-value 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.
The primitives underneath are few — a primitive is the only thing implemented
twice per backend: argv,
@@ -1386,14 +1418,19 @@ structural rule could tell them apart.
declare 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.
-(declare cos-f64 [x f64] f64 "cos")
+(declare cosh-f64 [x f64] f64 "cosh")
(defn main [] ()
- (print (cos-f64 0.0)) (println ""))
+ (print (cosh-f64 0.0)) (println ""))
1
-That is 1.0, printed by the same rule as before.
+That is 1.0, printed by the same rule as before. It is cosh and not
+cos because the prelude already declares
+cos-f64, 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 declare is how you reach one it does not
+name.
declare-c 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