Skip to main content

core.time — Durations, instants, timers

Monotonic time (Instant), wall-clock time (SystemTime), durations, and interval streams.

FileWhat's in it
duration.vrDuration — time span
duration_parse.vrHuman-readable duration string parser ("1h30m", "500ms", ISO 8601 "PT…")
instant.vrInstant — monotonic point in time
system_time.vrSystemTime, SystemTimeError
interval.vrInterval, AsyncInterval — tick streams
rfc3339.vrRFC 3339 timestamp parser and printer
cron.vrPOSIX 5-field crontab parser and next-fire scheduler
julian.vrJulian Day ↔ Unix / Gregorian conversions (Richards 1998)
mod.vrTime namespace + re-exports

Module status​

Each core.time.* module carries an explicit conformance status — same contract as core.base and core.collections. The status row is the truth-table over the module's public API exercised by core-tests/time/<module>/ under both Tier 0 (interpreter) and Tier 2 (AOT). Disagreement between tiers is itself a test failure.

StatusMeaning
completeEverything stable requires, plus the coverage bar the conformance inventory sets for its top mark: algebraic laws pinned by property tests, cross-stdlib integration verified, and the module's audit findings landed or routed. A stable module graduates to complete when those land — the two are not synonyms.
stableEvery public method conformance-tested under interp + AOT; algebraic laws pinned.
partialSubset stable; remainder gated by upstream defects, documented per-module.
regression-onlyTests gate on language-level defects (function-id remap on cross-module helper calls, archive-driven monotonic_nanos resolution, …).
undocumentedSnapshot from source; no runtime conformance pin yet.
unverifiedThe conformance suite has not been run against this module, so nothing on its row is a measurement. Distinct from undocumented: the module IS routed into core-tests/, but no result has been recorded since the liveness check that began demanding one.
ModuleStatusConformance suite
duration.vrpartialcore-tests/time/duration — record-based constructors/accessors, signed decomposition, negative Debug output and the Int.MIN × -1 multiplication guard have regression coverage. Sorting records and test-pipeline d / Int remain pinned limitations. Broader native revalidation is pending.
duration_parse.vrstablecore-tests/time/duration_parse — green under the interpreter; AOT passes most of the suite (a small tail remains; the unit sub-suite re-runs to 34/34 — residual failures are the layer-4 compile-crash flake, not value defects; the historical error-path pair is CLOSED via four stacked fixes ending in the text-free plausibility contract).
instant.vrunverifiedcore-tests/time/instant — Instant.now and Time.monotonic use core.intrinsics.runtime.time.monotonic_nanos. Signed arithmetic and record round-trips have focused controls; record sorting remains pinned. The clock is process-relative, so subtraction controls must start away from zero.
system_time.vrstablecore-tests/time/system_time — interp 55/56 (1 red = sort pin). Carry-normalised signed checked_add/checked_sub (the nanos ∈ [0,1e9) invariant held for negative durations pre-fix only by luck); signed-timeline semantics — Maybe.None signals Int64 overflow only, pre-epoch results are valid (consistent with from_timestamp(-100)); from_timestamp_millis floor-normalises negative inputs. Pinned in regression_test.vr §B–§D.
interval.vrstablecore-tests/time/interval — interpreter coverage includes blocking tick() integration and the negative-period clamp. Live async polling requires separate executor coverage.
rfc3339.vrstablecore-tests/time/rfc3339 — green under the interpreter. Pre-1970 formatting fixed (truncating→Euclidean day split; format_utc(-1) rendered "1970-01-01T00:00:01Z" pre-fix) and round-trip pinned (regression_test.vr §E). Offset unit tests were themselves defective (2026 dates with epoch-anchored expectations) — corrected.
cron.vrstablecore-tests/time/cron — the full suite passes under the interpreter, including the floor semantics of decompose and minute alignment for instants before the epoch.
julian.vrpartialcore-tests/time/julian — green under the interpreter, and all but one case under AOT — the best of the family, being pure Int/Float math with no byte-slice walking.
mod.vrunverifiedcore-tests/time/mod — focused interpreter controls cover negative sleep durations, live-clock constructor/accessor round-trips, monotonic/Instant bracketing and Interval.tick() blocking, missed-period and zero-period behaviour.

Interpreter coverage and native coverage are separate. The per-module audits record pending native checks, including byte-slice and method-dispatch paths. A stable interpreter result does not establish AOT parity; promotion to complete requires the coverage defined in the status convention.

The status table is the runtime truth, not the file's lifecycle annotation: lifecycle: Lifecycle.Theorem("v0.1") is the spec lifecycle (what the contract promises); the table above is the implementation lifecycle (what the runtime currently delivers). When the two diverge, the table is the source of truth for callers.


Duration​

Time span with nanosecond resolution.

Construction​

Duration.new(secs: Int, nanos: Int) -> Duration
Duration.from_secs(secs)
Duration.from_millis(ms)
Duration.from_micros(us)
Duration.from_nanos(ns)
Duration.from_secs_f64(f)
Duration.ZERO
Duration.MAX

Duration is signed, and every constructor preserves the sign: Duration.from_nanos(-1).as_nanos() is -1, not zero. Subtraction keeps a negative result rather than clamping — reach for saturating_sub when you want clamp-to-zero. This is what lets duration_parse.parse(&Text.from("-15m")) mean fifteen minutes backwards, and it matches Go, Java and C++ rather than Rust.

Each constructor has a short alias without the from_ prefix — Duration.nanos(n), .micros(n), .millis(n), .secs(n) — and the two spellings are the same function.

Literal sugar (on any integer)​

5.nanos() 5.micros() 5.millis()
5.secs() 5.mins() 5.hours()
// The family stops at `hours` on purpose: it mirrors `Duration`'s own
// short constructors exactly, with no synonyms. For longer spans name
// the constructor — `Duration.from_days(5)`, `Duration.from_weeks(2)`.

Inspection​

d.as_nanos() -> Int d.as_micros() -> Int
d.as_millis() -> Int d.as_secs() -> Int
d.as_minutes() -> Int d.as_hours() -> Int
d.as_days() -> Int d.as_weeks() -> Int
d.as_secs_f64() -> Float
d.subsec_nanos() -> Int d.subsec_micros() -> Int d.subsec_millis() -> Int
d.is_zero() -> Bool

Arithmetic​

d + d2 d - d2 d * n d / n
d.checked_add(d2) / checked_sub / checked_mul / checked_div -> Maybe<Duration>
d.saturating_add(d2) / saturating_sub / saturating_mul
// Scaling is by `Int`. There is no float-factor multiply and no
// `saturating_div`: division can only fail on a zero divisor, which
// `checked_div` already reports.

Implements Eq, Ord, Clone, Copy, Hash, Debug, Display.


Instant — monotonic time​

Always moves forward. Unaffected by wall-clock adjustments (NTP, DST, manual time changes). Use for measuring elapsed time.

Instant.now() -> Instant

i.elapsed() -> Duration // since this instant
i.duration_since(earlier) -> Maybe<Duration> // None if i < earlier
i.saturating_duration_since(earlier) -> Duration // zero if i < earlier

i.checked_add(duration) -> Maybe<Instant>
i.checked_sub(duration) -> Maybe<Instant>
Instant.from_nanos(nanos) -> Instant i.as_nanos() -> Int
i + duration i - duration
i < other i == other // comparison

Typical measurement​

let start = Instant.now();
do_work();
let elapsed = start.elapsed();
print(f"took {elapsed.as_millis()} ms");

SystemTime — wall-clock time​

Tied to real-world time. Subject to adjustments (NTP, DST, leap seconds).

SystemTime.now() -> SystemTime
SystemTime.UNIX_EPOCH // 1970-01-01T00:00:00Z

t.duration_since(&earlier) -> Result<Duration, SystemTimeError>
t.elapsed() -> Result<Duration, SystemTimeError>
t.checked_add(duration) -> Maybe<SystemTime>
t.checked_sub(duration) -> Maybe<SystemTime>
t + duration t - duration
t < other t == other

type SystemTimeError is
| WentBackwards(Duration) // `earlier` is later than `self`
| Overflow; // the span will not fit in Int64 nanoseconds

err.duration() -> Duration // the backwards span; zero for `Overflow`

Unix epoch helper​

let now = SystemTime.now();
let unix_ms = now.duration_since(&SystemTime.UNIX_EPOCH)
.unwrap_or(Duration.ZERO)
.as_millis();

When to use which​

NeedUse
Measure elapsed timeInstant
Schedule future workInstant.now() + duration
Timestamp for logs, user displaySystemTime
Compare with filesystem mtimeSystemTime
Store as persistent recordSystemTime (convert to UNIX epoch)

Sleep​

Time.sleep(duration) // blocking
Time.sleep_ms(ms) Time.sleep_secs(secs)

sleep(duration).await // async (from core.async)
sleep_until(instant).await

Interval — repeating timer​

Two types, and picking the wrong one is the usual mistake: Interval blocks the calling thread, AsyncInterval is a Stream.

Interval.new(period: Duration) -> Interval // first tick after one period
Interval.immediate(period: Duration) -> Interval // first tick fires at once

iv.tick() -> Int // BLOCKS until the next tick; see below
iv.period() -> Duration
iv.reset() // next tick one full period from now

tick() returns how many periods elapsed, normally 1. A slow caller that missed ticks gets the count it fell behind by, and the schedule advances past them — so the interval does not accumulate drift, and you decide what a missed tick means:

let mut iv = Interval.new(1.secs());
loop {
let missed = iv.tick() - 1;
if missed > 0 {
print(f"fell behind by {missed} tick(s)");
}
heartbeat();
}

There is no MissedTickBehavior knob: the return value is the report, and catching up is the fixed policy.

AsyncInterval — the stream form​

interval(period: Duration) -> AsyncInterval
interval_ms(ms: Int) -> AsyncInterval
AsyncInterval.new(period) -> AsyncInterval
aiv.reset()

It implements Stream with Item = (), so it composes with the stream combinators rather than being ticked by hand:

interval(Duration.millis(100))
.take(5)
.for_each(|_| { print("tick"); })
.await;

Time namespace​

Convenience static methods. Wall-clock time is deliberately not here: it is a capability, reached through the Clock context, not a static anyone can call. For a monotonic point use Instant.now().

Time.now() -> Duration // monotonic, since epoch
Time.monotonic() -> Int // raw nanoseconds
Time.sleep(duration)
Time.sleep_ms(ms) Time.sleep_secs(secs)

Low-level intrinsics​

monotonic_nanos() -> UInt64 // CLOCK_MONOTONIC / equivalent
realtime_nanos() -> UInt64 // CLOCK_REALTIME / equivalent
realtime_secs() -> Int64
sleep_ms(ms) sleep_ns(ns)

These are @requires_runtime intrinsics backing the higher-level API.


Timestamps for logs​

Common idiom — record absolute time and monotonic elapsed:

type LogLine is {
wall_time: SystemTime,
elapsed_ms: Int,
message: Text,
};

fn now_line(msg: Text, program_start: Instant) -> LogLine {
LogLine {
wall_time: SystemTime.now(),
elapsed_ms: program_start.elapsed().as_millis(),
message: msg,
}
}

rfc3339 — ISO 8601 timestamps​

mount core.time.rfc3339.{Rfc3339Time, parse, format_utc, format_with_offset};

// Parse.
let t = rfc3339.parse(&Text.from("2026-04-22T14:30:00.123Z"))?;
// t.unix_seconds = 1777213800 (UTC)
// t.nanos = 123_000_000
// t.offset_minutes = 0 (Z preserved)

// Format.
let s = rfc3339.format_utc(t.unix_seconds, t.nanos);
let tz = rfc3339.format_with_offset(t.unix_seconds, 0, 180); // +03:00

Full RFC 3339 grammar with Howard Hinnant civil-from-days date arithmetic (no external math-intrinsic dependency). Case- insensitive T / Z separators, space-for-T tolerance, nanosecond-precision fractions (padded/truncated to 9 digits), offset preserved on parse and applied to shift unix_seconds into true UTC. Out-of-range fields typed as Rfc3339Error.OutOfRange. Pre-2012 :60 leap seconds accepted on parse, collapsed to :59 in the unix-seconds output.

cron — crontab expression evaluator​

mount core.time.cron.{CronExpr};

let c = CronExpr.parse(&Text.from("*/5 8-18 * * MON-FRI"))?;
let next_unix = c.next_after_unix(now_unix)?;

Parses the POSIX 5-field crontab:

┌─── minute (0-59)
│ ┌── hour (0-23)
│ │ ┌─ day-of-month (1-31)
│ │ │ ┌ month (1-12; JAN-DEC)
│ │ │ │ ┌ day-of-week (0-6; SUN-SAT)
│ │ │ │ │
* * * * *

Every syntactic form (*, literal, a-b, a-b/s, */s, a,b,c), case-insensitive JAN..DEC / SUN..SAT aliases, and vixie-cron OR-semantics when both DOM and DOW are explicitly constrained (the default cron behaviour since Paul Vixie's 1987 rewrite).

next_after_unix uses coarsest-field skip scheduling to reduce the worst-case scan from minute-by-minute to month-by-month when far from a match. 8-year search ceiling guards against pathological specs that admit no firing.


duration_parse — human-readable duration strings​

mount core.time.duration_parse;

let d: Duration = duration_parse.parse(&Text.from("1h30m"))?; // 1 h + 30 min
let t: Duration = duration_parse.parse(&Text.from("500ms"))?; // 500 ms
let f: Duration = duration_parse.parse(&Text.from("1.5s"))?; // 1.5 s
let n: Duration = duration_parse.parse(&Text.from("-15m"))?; // negative span
let i: Duration = duration_parse.parse(&Text.from("PT1H30M"))?; // ISO 8601

Two grammars recognised:

FormExampleNotes
Compact Go-style1h30m, 500ms, 2h 30mwhitespace tolerated; fractional OK
ISO 8601 durationPT1H30M, P1D, PT0.5Scross-language config files

Supported units​

UnitSuffixExample
nanosecondsns100ns
microsecondsus / µs250us
millisecondsms500ms
secondss30s
minutesm5m
hoursh2h
daysd7d
weeksw1w

Used in config files (timeout = "30s"), CLI flags (--interval 5m), and scheduler APIs. Typos surface as DurationParseError.UnknownUnit rather than silently defaulting.

julian — Julian Day ↔ Unix / Gregorian​

mount core.time.julian;

Julian Day (JD) is the continuous count of days since noon UTC on 4713-01-01 BC (proleptic Julian calendar). SQLite's julianday(...) / strftime('%J', ...) store timestamps in this form; astronomy and ephemeris computations use it too.

Epoch constants​

ConstantValueReference
JD_UNIX_EPOCH2440587.51970-01-01 00:00 UTC
JD_J20002451545.02000-01-01 12:00 UTC
JD_MJD_EPOCH2400000.5Modified Julian Day base (1858-11-17)

Conversion surface​

public fn julian_from_unix_ms(ms: Int64) -> Float64;
public fn unix_ms_from_julian(jd: Float64) -> Int64;
public fn julian_from_unix_secs(s: Int64) -> Float64;
public fn unix_secs_from_julian(jd: Float64) -> Int64;

public fn julian_from_ymd(year: Int, month: Int, day: Int) -> Float64;
public fn ymd_from_julian(jd: Float64) -> (Int, Int, Int);

public fn time_fraction_from_hms(hour: Int, min: Int, sec: Int, ms: Int) -> Float64;
public fn hms_from_julian(jd: Float64) -> (Int, Int, Int, Int); // (h, m, s, ms)

public fn julian_from_gregorian(y: Int, mo: Int, d: Int,
h: Int, mi: Int, s: Int, ms: Int) -> Float64;
public fn gregorian_from_julian(jd: Float64)
-> (Int, Int, Int, Int, Int, Int, Int); // (y, mo, d, h, mi, s, ms)

public fn mjd_from_julian(jd: Float64) -> Float64;
public fn julian_from_mjd(mjd: Float64) -> Float64;

Algorithms are Richards (Mapping Time, 1998). The day-number path is integer arithmetic; only the fractional time-of-day uses Float64 — Float64's 53-bit mantissa carries millisecond resolution losslessly for ±80 million years around 1970.


Open defects​

Duration is signed: Duration.nanos(-1).as_nanos() and Duration.from_nanos(-1).as_nanos() both return -1, and duration_parse.parse(&Text.from("-15m")) accepts a negative span. duration_since reports Overflow beyond the Int64 nanosecond range rather than wrapping the result. Remaining limitations are listed below.

IDModuleSurfaceResolution path
cron §Acron.vrNo support for vixie-cron extensions (@hourly/W/L/#n).A documented feature gap; the shape it would take is an extensions: bool constructor flag.
rfc3339 §A/§B/§Crfc3339.vrEmpty fraction / 10+ digit truncation / out-of-range offset pins missing.Pins for the three cases plus a boundary guard.
interval §A/§Binterval.vrBlocking Interval.tick() and AsyncInterval.poll_next live-poll tests gated on @slow marker + executor harness.Pin in vcs/specs/L2-standard/async/ once executor harness lands.

See also​

  • async → timers — sleep, timeout, Interval.
  • intrinsics — monotonic_nanos, rdtsc, rdtscp.
  • sys — platform clock_gettime / libSystem equivalents.
  • stdlib/database — native SQLite consumes julian for julianday(...) / date(...) timestamps.