nova-std 0.3.0

The Nova language's standard library sources (internal to the nova toolchain)
Documentation
//! Clocks, durations, sleeping and time limits.
//!
//! `Instant` reads a monotonic clock, for measuring how long something takes,
//! and `SystemTime` reads the wall clock, for timestamps. A `Duration` is a
//! span of time, which `sleep` takes to suspend a task and `timeout` to limit
//! how long a future may take. Every module sees these names without an
//! import, and a module's own definition of one of them shadows the one here.
//!
//! A `Duration` counts whole nanoseconds in an `Int`. `Duration::from_secs`,
//! `from_millis` and `from_micros` build one, and saturate rather than
//! overflow. A duration can be negative, and `sleep` and `timeout` treat a
//! negative one as zero.
//!
//! A `Duration` built and read back, a `sleep` timed with `Instant`, and a
//! wall-clock timestamp:
//!
//! ```
//! let d = Duration::from_millis(1500)
//! assert_eq(d.as_secs(), 1) // rounded toward zero
//! assert_eq(d.as_millis(), 1500)
//!
//! // Instant measures how long something takes.
//! let start = Instant::now()
//! block_on(sleep(Duration::from_millis(10)))
//! assert(start.elapsed().as_millis() >= 10, "sleep waits at least as long as asked")
//!
//! // SystemTime reads the wall clock, for timestamps.
//! let stamp = SystemTime::now().to_iso8601()
//! assert_eq(stamp.len(), 24)
//! assert(stamp.ends_with("Z"), "always in UTC")
//! ```

// A point on this process's monotonic clock.
//
// `nanos` is not privacy-enforced -- no record field in this language is --
// so a program can build `Instant { nanos: -1 }` naming no real clock
// reading. `now()` is the intended and only supported way to obtain one,
// and two are meant to be compared only through `duration_since` or
// `elapsed`. Held as nanoseconds since the runtime's single process epoch,
// so a real reading is never negative and two real `Instant`s are ordered
// by subtraction.
/// A reading of a monotonic clock, from `Instant::now`, for measuring how
/// much time passes.
///
/// The clock never goes backwards, unlike the wall clock `SystemTime` reads,
/// and its readings mean nothing outside the program that took them. Measure
/// between two readings with `duration_since`, or from one reading to now
/// with `elapsed`.
pub record Instant { nanos: Int }

// A span of time, held in nanoseconds because all three constructors below
// are then exactly representable, and because `elapsed` is a measurement
// whose precision a coarser unit would discard.
//
// `nanos` is not privacy-enforced either, so `Duration { nanos: -1 }`
// compiles and walks straight past every saturating constructor below.
// `from_secs`/`from_millis`/`from_micros` are the intended and only
// supported way to build one. That matters concretely: a negative or
// absurd `nanos` reaching `sleep` is treated as an immediate wake -- the
// same silent no-op the saturating constructors exist to prevent.
/// A span of time, counted in whole nanoseconds; it can be negative.
///
/// Build one with `from_secs`, `from_millis` or `from_micros`, and read it
/// back with `as_secs` or `as_millis`. The constructors saturate: a count too
/// large for an `Int` of nanoseconds gives the longest `Duration`,
/// 9,223,372,036,854,775,807 nanoseconds (just over 292 years), and one too
/// far below zero gives the most negative, -9,223,372,036,854,775,808
/// nanoseconds.
pub record Duration { nanos: Int }

const NANOS_PER_SEC: Int = 1_000_000_000

const NANOS_PER_MILLI: Int = 1_000_000

const NANOS_PER_MICRO: Int = 1_000

// The largest argument each constructor can scale without overflowing an
// `Int` of nanoseconds, and the smallest without underflowing one -- the
// clamped result on either side is whichever extreme of `Int` that
// direction saturates to.
//
// These exist because **Nova's `Int` wraps** -- measured:
// `9223372036854775807 + 1` is `-9223372036854775808`. A wrapped duration can
// land on either sign. Too large wraps negative, and `sleep` treats a
// non-positive duration as an immediate wake, so an unclamped `from_secs`
// would turn "sleep for ages" into "do not sleep at all", silently. Too
// negative wraps positive -- measured: `Duration::from_secs(-9_223_372_037)
// .as_secs()` and `Duration::from_secs(9_223_372_037).as_secs()` both return
// `9223372036`, byte-identical -- so an unclamped low end would turn
// "wake immediately" into "sleep for roughly 292 years", the exact inversion
// clamping exists to prevent, just on the other side of zero. Clamping both
// directions yields a saturated answer instead of a wrong one -- the
// no-panic analogue of Rust's `Duration::from_secs`, which panics on the
// same overflow (Rust's version takes an unsigned argument and so has no low
// side to clamp).
const MAX_SECS: Int = 9_223_372_036

const MAX_MILLIS: Int = 9_223_372_036_854

const MAX_MICROS: Int = 9_223_372_036_854_775

const MAX_NANOS: Int = 9_223_372_036_854_775_807

const MIN_SECS: Int = -MAX_SECS

const MIN_MILLIS: Int = -MAX_MILLIS

const MIN_MICROS: Int = -MAX_MICROS

// One further from zero than `-MAX_NANOS`: `Int` is two's-complement, so its
// negative range holds one more value than its positive range, and this is
// that value -- the most-negative `Int` representable, computed rather than
// spelled out because the positive literal one larger than `MAX_NANOS` would
// itself overflow.
const MIN_NANOS: Int = -MAX_NANOS - 1

impl Instant {
    // The current reading of the monotonic clock.
    /// Returns the current reading of the monotonic clock.
    pub fn now() -> Instant { Instant { nanos: time_now_nanos() } }

    // How long since `self`. Zero if `self` is somehow in the future.
    /// Returns the time that has passed since `self` was read.
    ///
    /// It is never negative.
    pub fn elapsed(self) -> Duration { Instant::now().duration_since(self) }

    // The span from `earlier` to `self`, **saturating at zero** so an elapsed
    // time can never come back negative when the arguments are the wrong way
    // round.
    /// Returns the time from `earlier` to `self`.
    ///
    /// Saturates at zero: if `earlier` is in fact the later reading, the
    /// result is a zero duration, never a negative one.
    pub fn duration_since(self, earlier: Instant) -> Duration {
        if self.nanos <= earlier.nanos {
            Duration { nanos: 0 }
        } else {
            Duration { nanos: self.nanos - earlier.nanos }
        }
    }
}

impl Duration {
    // Whole seconds. Clamps above `MAX_SECS` and below `MIN_SECS`; see the
    // constants above.
    /// Returns a duration of `s` seconds.
    ///
    /// Saturates: an `s` above 9,223,372,036 gives the longest `Duration`, and
    /// one below -9,223,372,036 the most negative.
    pub fn from_secs(s: Int) -> Duration {
        if s > MAX_SECS {
            Duration { nanos: MAX_NANOS }
        } else if s < MIN_SECS {
            Duration { nanos: MIN_NANOS }
        } else {
            Duration { nanos: s * NANOS_PER_SEC }
        }
    }

    // Whole milliseconds. Clamps above `MAX_MILLIS` and below `MIN_MILLIS`.
    /// Returns a duration of `ms` milliseconds.
    ///
    /// Saturates: an `ms` above 9,223,372,036,854 gives the longest
    /// `Duration`, and one below -9,223,372,036,854 the most negative.
    pub fn from_millis(ms: Int) -> Duration {
        if ms > MAX_MILLIS {
            Duration { nanos: MAX_NANOS }
        } else if ms < MIN_MILLIS {
            Duration { nanos: MIN_NANOS }
        } else {
            Duration { nanos: ms * NANOS_PER_MILLI }
        }
    }

    // Whole microseconds. Clamps above `MAX_MICROS` and below `MIN_MICROS`.
    /// Returns a duration of `us` microseconds.
    ///
    /// Saturates: a `us` above 9,223,372,036,854,775 gives the longest
    /// `Duration`, and one below -9,223,372,036,854,775 the most negative.
    pub fn from_micros(us: Int) -> Duration {
        if us > MAX_MICROS {
            Duration { nanos: MAX_NANOS }
        } else if us < MIN_MICROS {
            Duration { nanos: MIN_NANOS }
        } else {
            Duration { nanos: us * NANOS_PER_MICRO }
        }
    }

    // Whole seconds, truncating. `from_millis(1500).as_secs()` is `1`.
    /// Returns the duration in whole seconds, rounded toward zero: 1.5 seconds
    /// gives 1, and -1.5 seconds gives -1.
    pub fn as_secs(self) -> Int { self.nanos / NANOS_PER_SEC }

    // Whole milliseconds, truncating. `from_micros(1).as_millis()` is `0`,
    // which `tests/runtime/time_conversions.nova` pins deliberately.
    /// Returns the duration in whole milliseconds, rounded toward zero: 1,999
    /// microseconds give 1, and -1,999 microseconds give -1.
    pub fn as_millis(self) -> Int { self.nanos / NANOS_PER_MILLI }
}

// Suspend the current task for at least `d`.
//
// Parks rather than spins: this registers a deadline with the executor and is
// not polled again until that deadline passes, so a sleeping task costs no
// polls at all. Contrast `std/task`'s `yield_now`, which is re-queued
// immediately and deliberately.
//
// "At least" is exact, and platform-dependent in its granularity only. The
// executor waits with microsecond granularity on Unix and whole milliseconds
// on Windows, truncating a remaining duration the ordinary way and lifting
// only a result that truncated all the way to zero back up to one unit --
// never down to zero, not always up. So a sub-millisecond sleep on Windows
// takes about a millisecond, but a 1.5ms remainder there becomes 1ms, not
// 2ms. What makes "at least" hold regardless is the executor's own wake
// check: a task parked on a deadline is only woken once that deadline is
// `<=` the clock reading it is checked against, so a platform wait that
// returns early just re-parks on the same deadline instead of waking the
// sleeper ahead of schedule. A non-positive `d` wakes immediately rather
// than being rejected.
/// Suspends the calling task for at least `d`.
///
/// An `async fn`: other tasks run meanwhile, and a sleeping task uses no
/// processor time. It can wake a little after `d` has passed, never before. A
/// zero or negative `d` returns at once without suspending, so no other task
/// gets a turn; `yield_now` gives them one.
pub async fn sleep(d: Duration) { task_sleep_future_nanos(d.nanos).await }

// What `task_timeout_future` reports when the inner future finished first.
const TIMEOUT_COMPLETED: Int = 0

// The error `timeout` reports when `d` elapsed before the inner future
// completed.
//
// Carries nothing, and needs to: the caller passed `d` in, so it already has
// the only fact there is. It is the one record in this library with no field
// to disclose as un-enforced, because it has no field.
/// The error `timeout` returns when its time runs out before the future
/// finishes. It holds nothing.
pub record TimeoutError {}

// Run `fut`, giving up after `d`.
//
// Returns `Ok` with the future's value if it completed within `d`, and
// `Err(TimeoutError {})` if `d` elapsed first. A non-positive `d` still polls
// `fut` once, so an already-complete future succeeds rather than reporting a
// timeout it never had a chance to beat.
//
// **On timeout the inner future is ABANDONED, not cancelled.** Nothing tells
// it that it was dropped. That is free for a `sleep` (its state is collected),
// for a `join` (the joined task runs on independently), and for a `read` or
// `write` (the caller still holds the `TcpStream` and can close it). It is
// **not** free for `std/net`'s `connect`: a timed-out connect leaves a socket
// registered that nothing can reach or close, and it leaks until the process
// exits -- which is what `docs/adr/0012-file-descriptor-lifecycle.md` already
// says of any descriptor nobody closes.
/// Runs `fut` in the calling task, giving up once `d` has passed.
///
/// An `async fn`: it suspends the calling task while `fut` waits, and other
/// tasks run meanwhile. Returns `Ok` with `fut`'s value if it finishes in
/// time, or `Err(TimeoutError {})` if `d` passes first. `fut` runs before the
/// time is checked, so even with a zero or negative `d`, a future that
/// finishes without waiting gives `Ok`. The time is checked only when `fut`
/// waits: a future that computes past `d` without awaiting anything still
/// gives `Ok`.
///
/// On a timeout, `fut` is abandoned, not cancelled: it is never resumed, and
/// nothing tells it so. A task it was joining runs on, and a stream it was
/// reading or writing stays open, but a `connect` it was making leaves its
/// socket open until the program exits. Today, a future that waits by
/// yielding and trying again, as `std/sync`'s `lock`, `send` and `recv` and
/// `yield_now` do, is tried again only once `d` has passed, so under
/// `timeout` it takes all of `d` even when it could have finished sooner.
///
/// One future in time, one too slow:
///
/// ```
/// async fn answer_after(ms: Int) -> Int {
///     sleep(Duration::from_millis(ms)).await
///     42
/// }
///
/// async fn main() {
///     // In time: `Ok` with the future's value.
///     let fast = timeout(Duration::from_secs(5), answer_after(1)).await
///     assert_eq(fast.unwrap_or(0), 42)
///
///     // Too slow: `Err(TimeoutError {})`. The future is abandoned, not
///     // cancelled, and nothing waits for it to finish.
///     let late = timeout(Duration::from_millis(10), answer_after(5000)).await
///     assert(late.is_err(), "10 ms passed before the answer came")
/// }
/// ```
pub async fn timeout<T>(d: Duration, fut: Future<T>) -> Result<T, TimeoutError> {
    if task_timeout_future(d.nanos, fut).await == TIMEOUT_COMPLETED {
        Ok(task_output(fut))
    } else {
        Err(TimeoutError {})
    }
}

// Days since 1970-01-01 to [year, month, day], by Howard Hinnant's
// civil_from_days. Shifting the era to start in March makes leap-day
// handling fall out of integer arithmetic with no conditionals and no
// table: February becomes the last month of the previous shifted year, so
// its variable length never lands mid-sequence.
fn civil_from_days(days: Int) -> [Int] {
    let z = days + 719_468
    let era = z / 146_097
    let doe = z - era * 146_097
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
    let y = yoe + era * 400
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
    let mp = (5 * doy + 2) / 153
    let d = doy - (153 * mp + 2) / 5 + 1
    let m = if mp < 10 { mp + 3 } else { mp - 9 }
    let year = if m <= 2 { y + 1 } else { y }
    // Bound to `ymd` and returned on its own line, not written as a bare
    // `[year, m, d]` right after the `let year = ...` above: Nova has no
    // statement separator at all, so a postfix operator continues onto
    // whatever precedes it regardless of that statement's terminal token --
    // a bare integer literal, a bare identifier and a call ending in `)` all
    // fail the same way a `}`-closed expression does. Only a `let`/`return`
    // prefix breaks the continuation. Measured here as
    // `error[P0001]: expected ']' (in index expression), found ','`.
    let ymd = [year, m, d]
    ymd
}

// A wall-clock reading: nanoseconds since the Unix epoch.
//
// Deliberately a separate type from `Instant`, not a method on it.
// `Instant`'s whole contract is that it is monotonic and comparable by
// subtraction within one process; a wall clock is neither, and can jump
// backwards when NTP corrects it. Two types that cannot be mistaken for
// one another is the point.
//
// `nanos` is not privacy-enforced either, so `SystemTime { nanos: -1 }`
// compiles and walks straight past the clamp at zero that `now()`'s
// intrinsic applies (`crates/nova-runtime/src/time.rs`). `now()` is the
// intended and only supported way to obtain one. A negative `nanos` does
// not mean the wrong instant so much as a nonsensical one: Nova's `/`
// truncates toward zero and `%` follows the dividend's sign (measured:
// `-7 / 2 = -3`, `-7 % 2 = -1`), so a negative `nanos` leaves `rem` in
// `to_iso8601` negative and every field derived from it with it. A small
// negative `nanos` (e.g. `-1`) still renders a well-formed 24-character
// string, `1970-01-01T00:00:00.000Z` -- byte-identical to `nanos: 0`, the
// sign silently lost. Only once a field's magnitude is large enough that
// `Int::pad`'s sign digit eats the padding budget does the width itself
// break: `nanos: -43_200_000_000_000` (a half day) renders
// `1970-01-01T-12:00:00.000Z`, 25 characters, not 24.
/// A reading of the wall clock, from `SystemTime::now`: the time since
/// 1970-01-01 00:00:00 UTC, in nanoseconds.
///
/// The wall clock can jump forwards or backwards when the system's time is
/// set, so measure how much time passes with `Instant` instead. `to_iso8601`
/// renders a reading as text.
pub record SystemTime { nanos: Int }

impl SystemTime {
    // ISO-8601 UTC to milliseconds: `2026-08-19T02:40:13.123Z`. Fixed
    // width, so log lines align with no padding logic in the caller.
    //
    // UTC only, permanently: `00-MASTER-SPEC.md` ยง6's crate list is FINAL
    // and has no date/time crate, so there is no timezone database to
    // consult and a local-time offset would be a guess that is wrong twice
    // a year in every DST zone.
    /// Returns the time as ISO 8601 text in UTC, to the millisecond, such as
    /// `2026-08-19T02:40:13.123Z`.
    ///
    /// The text is 24 characters long for any reading from `now`. Fractions
    /// of a millisecond are dropped, not rounded, and there is no local time
    /// zone.
    pub fn to_iso8601(self) -> String {
        let nanos_per_day = 86_400_000_000_000
        let days = self.nanos / nanos_per_day
        let rem = self.nanos % nanos_per_day
        let ymd = civil_from_days(days)
        let hour = rem / 3_600_000_000_000
        let minute = rem / 60_000_000_000 % 60
        let second = rem / 1_000_000_000 % 60
        let milli = rem / 1_000_000 % 1_000
        "${ymd[0]}-${ymd[1].pad(2)}-${ymd[2].pad(2)}T${hour.pad(2)}:${minute.pad(2)}:${second.pad(2)}.${milli.pad(3)}Z"
    }

    // The live wall clock: nanoseconds since the Unix epoch, read from the
    // OS at call time.
    /// Returns the current wall-clock time.
    ///
    /// A system clock set before 1970 reads as 1970-01-01T00:00:00.000Z, and
    /// one set past April 2262, beyond what an `Int` of nanoseconds holds,
    /// reads as that limit.
    pub fn now() -> SystemTime { SystemTime { nanos: time_now_epoch_nanos() } }
}