std/time
Wall clock, elapsed time, Duration arithmetic, sleep.
Timers, tickers and timeouts live in std/timer.
from "std/time" import { Instant, Duration, now, elapsed, since, sleepMs, sleepFor, epochMillis }Types
Instant
struct Instant {
sec: i64,
usec: i64,
}A point in time from the system clock.
Duration
struct Duration // i64 nanosecondsA signed span of time, stored as i64 nanoseconds: ±292.47 years, 1 ns resolution. Nanoseconds are what benchmarks and profilers need, and nothing measures a span longer than a lifetime — anything that does is a date problem, so use epoch seconds and std/datetime.
Construction and arithmetic past the range trap like any other i64 overflow; Milo is checked by default and a duration that wrapped to negative is exactly the silent nonsense that causes. Duration.parse is the one exception — it takes untrusted text, so overflow is None rather than an abort.
now() reads the wall clock, so a span measured across an NTP step inherits that step. There is no monotonic clock in std yet.
Duration
Constructors
Duration.zero()
Duration.nanos(n: i64)
Duration.micros(n: i64)
Duration.millis(n: i64)
Duration.secs(n: i64)
Duration.mins(n: i64)
Duration.hours(n: i64)
Duration.days(n: i64)
Duration.parse(text: &string): Option<Duration>parse accepts a Go-style duration: an optional sign, then one or more <number><unit> components — "300ms", "1h30m", "-1.5h", "2h45m10.5s", "7d", "0". Units are ns, us (or µs), ms, s, m, h, d.
It returns Option, not Result: every failure — a stray character, a missing unit, a value past ±292 years — leaves the caller with the same move (reject the input and echo it back), and a duration string is short enough that a byte offset tells a user nothing their own eyes don't.
Accessors
d.toNanos(): i64 d.toSecs(): i64 d.toSecsF64(): f64
d.toMicros(): i64 d.toMins(): i64 d.toMillisF64(): f64
d.toMillis(): i64 d.toHours(): i64Integer accessors truncate toward zero.
Arithmetic and comparison
a + b // Add
a - b // Sub
d.times(k: i64): Duration // scale
d.dividedBy(k: i64): Duration // truncating; k == 0 traps like any division by zero
d.ratio(other: &Duration): f64 // "how many times does other fit in d"
d.negated(): Duration
d.abs(): Duration
a == b, a != b // @derive(Eq)
a.compare(b): i64 // -1, 0, 1
a.isLess(b): bool
a.isGreater(b): bool
d.isZero(): bool
d.isNegative(): boolScaling is a method, not *: Milo's operator overloading is homogeneous (Mul is Self × Self) and a duration times a duration is meaningless.
Text
d.toString(): stringGo-style: "0s", "1.5ms", "2m3.5s", "1h30m0s". Round-trips through Duration.parse. Microseconds print as "us", not "µs", so the output is ASCII everywhere it lands; parse accepts both.
Functions
now
fn now(): InstantThe current time.
epochMillis / epochSecs
fn epochMillis(): i64
fn epochSecs(): i64Milliseconds / seconds since the Unix epoch.
elapsed
fn elapsed(start: Instant, end: Instant): DurationThe span between two instants.
since
fn since(start: Instant): DurationThe span from start until now.
sleepMs / sleepSecs / sleepFor
fn sleepMs(ms: i64): void
fn sleepSecs(secs: i64): void
fn sleepFor(d: &Duration): voidWith a scheduler running, a sleep parks on a select timer arm: the caller is off the run queue for the whole interval and every other green task keeps running. Without one it is a plain usleep. sleepFor rounds a non-zero sub-millisecond span up to 1 ms once a scheduler exists — the event loop's deadlines are milliseconds, and rounding down would turn a 100 µs sleep into a busy spin.
ensureTimersLive
fn ensureTimersLive(): voidMake the green scheduler exist so timer and fd arms are live on a program that never spawned a task. std/timer calls this for you; it is exported for callers that arm a Select on the main context themselves.
Example
let start = now()
// ... do work ...
let d = since(start)
print("took ", d.toString())
let timeout = Duration.parse(flagValue) ?? Duration.secs(30)
if d.isGreater(timeout) {
print("over budget by ", (d - timeout).toString())
}