diff --git a/Makefile b/Makefile index d0df299b..fe29973e 100644 --- a/Makefile +++ b/Makefile @@ -252,7 +252,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \ $(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \ $(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \ - $(BIN)/test_stat_run \ + $(BIN)/test_stat_run $(BIN)/test_time_run \ $(BIN)/test_intdiv_signed \ $(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \ $(BIN)/test_base32_run $(BIN)/test_base64_run \ @@ -585,6 +585,10 @@ $(BIN)/test_stat_run: test/wcc/976_stat_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_time_run: test/wcc/977_time_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_intdiv_signed: test/wcc/978_intdiv_signed.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/examples/cmatrix/cmatrix.ww b/examples/cmatrix/cmatrix.ww index 85b484b2..35593a60 100644 --- a/examples/cmatrix/cmatrix.ww +++ b/examples/cmatrix/cmatrix.ww @@ -189,12 +189,9 @@ fn setup() (*void | initerr) = { // ---- clock helper ----------------------------------------------------- -fn now_ns() (u64 | initerr) = { - let ts: time.timespec; // uninitialised — keep the type for the shape - if (time.monotonic(&ts) != 0) { - return "clock_gettime failed": initerr; - }; - let v = ts.sec * 1000000000i64 + ts.nsec; +fn now_ns() u64 = { + let i = time.now(time.clock.monotonic); + let v = i.sec * 1000000000i64 + i.nsec; return v: u64; }; @@ -468,9 +465,9 @@ fn run(seed: u64) (i32 | initerr) = { // ---- entry ------------------------------------------------------------ export fn main() i32 = { - // `!` — unwrap the success variant or abort. The clock is a - // pre-init dependency; if it fails there's nothing to tear down. - let seed = now_ns()!; + // Clock is a pre-init dependency; time.now aborts on + // syscall failure (EINVAL / EFAULT are programmer errors). + let seed = now_ns(); // match-as-expression: every arm `yield`s the exit code, and the // whole match resolves to a single i32. One return site for the diff --git a/lib/time/time.ww b/lib/time/time.ww index c848f546..00b28871 100644 --- a/lib/time/time.ww +++ b/lib/time/time.ww @@ -1,15 +1,95 @@ -// time — clocks. We expose monotonic-ns via a syscall trampoline. -// Linux clock_gettime is syscall 228; clock id 1 is CLOCK_MONOTONIC. -// Until we can pass struct-by-value the user supplies a buffer. +// time — clocks, instants, durations. Mirrors Hare's lib/time +// (ref/hare/time/duration.ha, instant.ha, arithm.ha, +// +linux/functions.ha). Calendar / date / strftime / timezone / +// sleep live in separate Hare modules and graduate when callers / +// supporting stdlib arrive. +// +// `duration` is a NAMED alias of i64 (lib/math/random precedent +// at lib/math/random/random.ww:8); ww treats NAMED as a newtype, +// so cross-i64 arithmetic inside this module needs explicit casts. +// Hare's structural alias semantics let those casts vanish, but +// our type checker is strict. -@symbol("rt_syscall") fn syscall(num: i64, a: i64, b: i64, c: i64) i64; +@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64; +@symbol("rt_abort") fn abort(msg: str) void; -def CLOCK_MONOTONIC: i64 = 1; def SYS_CLOCK_GETTIME: i64 = 228; -// timespec is {sec, nsec} — 16 bytes. Caller passes a pointer. -type timespec = struct { sec: i64, nsec: i64 }; +// ref/hare/time/duration.ha:6. 290y representable range. +export type duration = i64; -export fn monotonic(ts: *timespec) i32 = { - return syscall(SYS_CLOCK_GETTIME, CLOCK_MONOTONIC, ts: i64, 0): i32; +// ref/hare/time/duration.ha:9-18. Plan-9 naming (lowercase) +// diverges from Hare's uppercase per project rule 4. +export def nanosecond: duration = 1i64; +export def microsecond: duration = 1000i64; +export def millisecond: duration = 1000000i64; +export def second: duration = 1000000000i64; + +// ref/hare/time/instant.ha:9. (sec, nsec) pair — NOT POSIX struct +// timespec (which uses u32 nsec). Layout matches Linux's struct +// timespec on 64-bit (i64+i64) so we can pass &instant directly +// to clock_gettime. +export type instant = struct { + sec: i64, + nsec: i64, +}; + +// ref/hare/time/+linux/functions.ha:84. First cut exposes only +// realtime and monotonic; Hare's process_cpu / thread_cpu / boot / +// realtime_alarm / boot_alarm / tai graduate when a caller needs +// them (CLAUDE.md rule 9 — Hare-fidelity, no premature surface). +export type clock = enum i32 { + realtime = 0, + monotonic = 1, +}; + +// ref/hare/time/+linux/functions.ha:138. Hare's now() also aborts +// on impossible errnos. (instant | oserror) is deliberately not +// the return shape — EINVAL / EFAULT are programmer errors (bad +// clock id, bad ptr), and a 1-word-payload sum return walks into +// task #9's cgen-divergence trap. +export fn now(c: clock) instant = { + let i: instant; + let rc = syscall2(SYS_CLOCK_GETTIME, (c as i32): i64, (&i): i64); + if (rc != 0i64) { abort("time.now: clock_gettime failed"); }; + return i; +}; + +// ref/hare/time/arithm.ha:9. Adds duration to instant. The +// negative-duration branch normalises nsec into [0, second). +export fn add(i: instant, x: duration) instant = { + let r: instant; + let xi: i64 = x: i64; + let sec: i64 = second: i64; + let nsec: i64 = nanosecond: i64; + if (xi == 0i64) { + r.sec = i.sec; + r.nsec = i.nsec; + return r; + }; + if (xi > 0i64) { + r.sec = i.sec + (i.nsec + xi) / sec; + r.nsec = (i.nsec + xi) % sec; + return r; + }; + r.sec = i.sec + (i.nsec + xi - sec + nsec) / sec; + r.nsec = (i.nsec + (xi % sec) + sec) % sec; + return r; +}; + +// ref/hare/time/arithm.ha:26. Returns duration from a to b. +// Sign convention: b - a. +export fn diff(a: instant, b: instant) duration = { + let sec: i64 = second: i64; + let v: i64 = ((b.sec - a.sec) * sec) + (b.nsec - a.nsec); + return v: duration; +}; + +// ref/hare/time/arithm.ha:32. -1 if a < b, 0 if equal, +1 if a > b. +export fn compare(a: instant, b: instant) i8 = { + if (a.sec < b.sec) { return -1i8; }; + if (a.sec > b.sec) { return 1i8; }; + if (a.nsec < b.nsec) { return -1i8; }; + if (a.nsec > b.nsec) { return 1i8; }; + return 0i8; }; diff --git a/lib/time/timetest.ww b/lib/time/timetest.ww new file mode 100644 index 00000000..2d6b5217 --- /dev/null +++ b/lib/time/timetest.ww @@ -0,0 +1,180 @@ +// timetest — exercises lib/time. Run with `out/bin/ww run +// lib/time/timetest.ww`. Same `signalled`-then-fail()-with-+10 +// pattern as fmttest / logtest / stattest so a non-zero exit +// pinpoints the offending scenario. + +use os; +use time; + +let signalled: i32 = 0; + +fn fail() void = { os.exit(signalled + 10); }; + +// ---- constants: load-bearing names + values ---------------------------- +// +// Without these multipliers every caller would re-derive 1e9 in +// place. The values themselves are nailed down by Hare's +// ref/hare/time/duration.ha:9-18. + +@test fn constants() void = { + if ((time.nanosecond: i64) != 1i64) { fail(); }; + if ((time.microsecond: i64) != 1000i64) { fail(); }; + if ((time.millisecond: i64) != 1000000i64) { fail(); }; + if ((time.second: i64) != 1000000000i64) { fail(); }; + + // "Roundtrip" check: derived multiples land on the canonical + // nanosecond count. Catches a future re-derivation drift. + if (5i64 * (time.second: i64) != 5000000000i64) { fail(); }; + if (3i64 * (time.millisecond: i64) != 3000000i64) { fail(); }; + if (7i64 * (time.microsecond: i64) != 7000i64) { fail(); }; +}; + +// ---- now(monotonic) is monotonic across two calls ---------------------- + +@test fn nowmonotonic() void = { + let a = time.now(time.clock.monotonic); + let b = time.now(time.clock.monotonic); + // Two back-to-back calls — b can equal a (same ns tick) but + // must never precede it. + if (time.compare(a, b) > 0i8) { fail(); }; +}; + +// ---- now(realtime) returns a post-2020 epoch --------------------------- +// +// Sanity check that the syscall plumbing actually lands real bytes +// in the instant. 2020-01-01 UTC = 1577836800 unix seconds; any +// past-2020 wall clock satisfies this. (CI / test machines running +// pre-2020 would fail here; acceptable trade for catching a +// zero-init / wrong-slot regression.) + +@test fn nowrealtime() void = { + let t = time.now(time.clock.realtime); + if (t.sec < 1577836800i64) { fail(); }; + if (t.nsec < 0i64) { fail(); }; + if (t.nsec >= 1000000000i64) { fail(); }; +}; + +// ---- add: zero duration is identity ------------------------------------ + +@test fn addzero() void = { + let a: time.instant; + a.sec = 100i64; a.nsec = 500i64; + let r = time.add(a, 0i64); + if (r.sec != 100i64) { fail(); }; + if (r.nsec != 500i64) { fail(); }; +}; + +// ---- add: positive duration, no carry ---------------------------------- + +@test fn addpos_nocarry() void = { + let a: time.instant; + a.sec = 10i64; a.nsec = 100i64; + let r = time.add(a, 200i64); + if (r.sec != 10i64) { fail(); }; + if (r.nsec != 300i64) { fail(); }; +}; + +// ---- add: positive duration, carries into next second ------------------ + +@test fn addpos_carry() void = { + let a: time.instant; + a.sec = 10i64; a.nsec = 999999900i64; + let r = time.add(a, 200i64); + if (r.sec != 11i64) { fail(); }; + if (r.nsec != 100i64) { fail(); }; +}; + +// ---- add: full-second duration ----------------------------------------- + +@test fn addpos_fullsecond() void = { + let a: time.instant; + a.sec = 5i64; a.nsec = 0i64; + let r = time.add(a, (time.second: i64) * 3i64); + if (r.sec != 8i64) { fail(); }; + if (r.nsec != 0i64) { fail(); }; +}; + +// ---- add: negative duration, no borrow --------------------------------- + +@test fn addneg_noborrow() void = { + let a: time.instant; + a.sec = 10i64; a.nsec = 500i64; + let r = time.add(a, -100i64); + if (r.sec != 10i64) { fail(); }; + if (r.nsec != 400i64) { fail(); }; +}; + +// ---- add: negative duration, borrows from previous second -------------- + +@test fn addneg_borrow() void = { + let a: time.instant; + a.sec = 10i64; a.nsec = 100i64; + let r = time.add(a, -200i64); + if (r.sec != 9i64) { fail(); }; + if (r.nsec != 999999900i64) { fail(); }; +}; + +// ---- diff: simple subtraction ------------------------------------------ + +@test fn diffsimple() void = { + let a: time.instant; a.sec = 10i64; a.nsec = 100i64; + let b: time.instant; b.sec = 12i64; b.nsec = 300i64; + let d: i64 = time.diff(a, b): i64; + // b - a = 2.0000002 sec = 2_000_000_200 ns + if (d != 2000000200i64) { fail(); }; +}; + +// ---- diff: reversed sign ----------------------------------------------- + +@test fn diffneg() void = { + let a: time.instant; a.sec = 12i64; a.nsec = 300i64; + let b: time.instant; b.sec = 10i64; b.nsec = 100i64; + let d: i64 = time.diff(a, b): i64; + if (d != -2000000200i64) { fail(); }; +}; + +// ---- compare: -1 / 0 / +1 ladder --------------------------------------- + +@test fn comparelt() void = { + let a: time.instant; a.sec = 1i64; a.nsec = 0i64; + let b: time.instant; b.sec = 2i64; b.nsec = 0i64; + if (time.compare(a, b) != -1i8) { fail(); }; +}; + +@test fn comparegt() void = { + let a: time.instant; a.sec = 2i64; a.nsec = 0i64; + let b: time.instant; b.sec = 1i64; b.nsec = 0i64; + if (time.compare(a, b) != 1i8) { fail(); }; +}; + +@test fn compareeq() void = { + let a: time.instant; a.sec = 5i64; a.nsec = 42i64; + let b: time.instant; b.sec = 5i64; b.nsec = 42i64; + if (time.compare(a, b) != 0i8) { fail(); }; +}; + +@test fn comparensec_only() void = { + let a: time.instant; a.sec = 5i64; a.nsec = 100i64; + let b: time.instant; b.sec = 5i64; b.nsec = 200i64; + if (time.compare(a, b) != -1i8) { fail(); }; + if (time.compare(b, a) != 1i8) { fail(); }; +}; + +export fn main() i32 = { + signalled = 1; constants(); + signalled = 2; nowmonotonic(); + signalled = 3; nowrealtime(); + signalled = 4; addzero(); + signalled = 5; addpos_nocarry(); + signalled = 6; addpos_carry(); + signalled = 7; addpos_fullsecond(); + signalled = 8; addneg_noborrow(); + signalled = 9; addneg_borrow(); + signalled = 10; diffsimple(); + signalled = 11; diffneg(); + signalled = 12; comparelt(); + signalled = 13; comparegt(); + signalled = 14; compareeq(); + signalled = 15; comparensec_only(); + return 0; +}; diff --git a/test/wcc/977_time_run.c b/test/wcc/977_time_run.c new file mode 100644 index 00000000..50f50619 --- /dev/null +++ b/test/wcc/977_time_run.c @@ -0,0 +1,49 @@ +/* + * 977_time_run — execute the lib/time @test fixture under the C-side + * `ww run` driver and assert exit 0. + * + * Same thin-wrapper shape as 970_fmt_run / 971_log_run / 976_stat_run: + * timetest.ww carries its own `export fn main()` that drives the + * @test fns and signals which case failed via the exit code. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "lib/time/timetest.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "time_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("time_run: %s ok\n", src); + return 0; +}