diff --git a/lib/prelude.ml b/lib/prelude.ml index 353e69f..ff16762 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -823,6 +823,111 @@ let source = {flan| (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 diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index 2ca6ff4..33c0424 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -2749,3 +2749,125 @@ 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; +} 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 4457ba1..5054422 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -285,6 +285,15 @@ let () = 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 diff --git a/web/index.html b/web/index.html index 5536c84..59d57cf 100644 --- a/web/index.html +++ b/web/index.html @@ -1064,6 +1064,8 @@ over.

building bytesappend!, append-i64!, append-f64!, concat, join, repeat-bytes, replace-bytes, slices-new, format-f64 UTF-8decode-rune, rune-at, rune-count, rune-size, rune-start?, valid-utf8?, encode-rune! numberssign-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 +timemonotonic-ns, monotonic-seconds, unix-ns, unix-seconds, sleep-ns, sleep-seconds, and ns-per-second and its two smaller siblings +the operating systemgetenv, which answers an (Option [u8]) viewing the process environment randomrand-seed, rand-u32, rand-f32, rand-i32-range, rand-f32-range forms, for macrosform-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 restpause, which signals the Pause condition the break loop stops on, and embed-find @@ -1103,6 +1105,18 @@ and the three libms do differ in the last bit, so the byte-identical-hash proper 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 primitives underneath are few — a primitive is the only thing implemented twice per backend: argv, write-stdout, exit, len, at,