lib/time+test: add types, ops, now

Replaces the lib/time placeholder (a monotonic(*timespec) shim that
predated lib/os's syscall surface). Ships Hare's time module first
cut per ref/hare/time/{duration,instant,arithm,+linux/functions}.ha:

- duration (i64 ns); nanosecond / microsecond / millisecond / second
  constants.
- instant (sec, nsec) — Hare's layout, NOT POSIX's nsec:u32. Matches
  Linux struct timespec on 64-bit, so &instant lands directly in
  clock_gettime.
- clock enum: realtime + monotonic only. The rest of Hare's set
  (process_cpu / thread_cpu / boot / realtime_alarm / boot_alarm / tai)
  graduates when a caller actually needs it (rule 9).
- now(c: clock) instant — aborts on EINVAL/EFAULT (mirrors Hare's
  abort-on-impossible-errno). Deliberately NOT (instant | oserror) to
  sidestep task #9's 1-word-payload tagged-return trap.
- add / diff / compare on instants per ref/hare/time/arithm.ha
  verbatim.

Deferred to follow-up commits when a caller surfaces: sleep, format /
strftime, time.chrono / time.date calendar, timezone, time.error
sum-return shape, time.unix helpers, time.mult, conversion helpers.

Tests at lib/time/timetest.ww (15 rows, table-pattern, semantic per
Class B doctrine). Driver test/wcc/977_time_run.c slotted between
976_stat_run and 978_intdiv_signed in the 9xx _run band.
examples/cmatrix migrates to the new API in the same commit (sole
pre-existing caller — bisect-clean per rule 11).

Class B bug #16 (sign-extend before IDIVQ) was surfaced by the
negative-duration rows during this work; landed 63332fe..4fa4bcf
before this commit. Rows 8-9 (addneg_noborrow / addneg_borrow) are
the load-bearing Class B exercisers; would have stayed silently wrong
pre-#16.

lib/time's own .s output has a cstage/wwstage label-counter skew in
add (cstage emits add_ct_7/_ce_8/_end_6 vs wwstage _6/_7/_5). Filed
as task #15; non-bootstrap-blocking since lib/time is outside the
selfhost toolchain transitive chain.
This commit is contained in:
2026-05-17 02:51:15 +09:00
parent 4fa4bcf34e
commit 7e9bede6c5
5 changed files with 329 additions and 19 deletions

View File

@@ -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 $@ $<

View File

@@ -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

View File

@@ -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;
};

180
lib/time/timetest.ww Normal file
View File

@@ -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;
};

49
test/wcc/977_time_run.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
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;
}