Time

Loft's standard library has two time functions, and they answer two different questions:

now()     — milliseconds since the Unix epoch (wall-clock time).
ticks()   — microseconds elapsed since program start (monotonic clock).

Both return an integer (loft's 64-bit integer) — which they need, since a millisecond timestamp passed 32 bits in 1970 plus a few weeks.

Use now() when you want to know WHEN something happened: timestamps, log entries, anything you will compare against a calendar. Use ticks() when you want to know HOW LONG something took: benchmarks and frame timing. It is unaffected by system clock changes or NTP adjustments, which is exactly what now() cannot promise.

Calendars are a library

These two are the whole clock surface: there is no year, month or weekday in the standard library, and no formatting. That is deliberate — calendar arithmetic is a large, opinionated subject — and it is a package away:

use time::*;
fn main() {
    ms = now();
    println(format_iso(ms));                    // 2026-09-01T09:03:58Z
    println("{year(ms)} {month_name(ms)} {day(ms)}, a {weekday_name(ms)}");
    println(format_date(add_days(ms, 30)));     // 2026-10-01
}

The time package works on the same millisecond-since-epoch integer now() returns, so nothing has to be converted. Install it with loft install time.

fn main() {

Wall-clock time: now()

now() returns the current time as milliseconds since 1970-01-01T00:00:00 UTC, so it is well past the range a 32-bit number could hold.

  t = now();
  assert(t > 2147483647, "now() is a 64-bit millisecond stamp: {t}");

It reads the system clock, so it usually moves forward — but it is the system's clock, not yours: an NTP correction or a manual change can step it in either direction. Never subtract two now() values to time something. That is what ticks() is for.

  t2 = now();
  assert(t2 > 2147483647, "a second reading is a timestamp too: {t2}");

Elapsed time: ticks()

ticks() measures microseconds since the program started, so it begins near zero and only ever climbs. It uses a monotonic clock, which is the guarantee now() does not have: it never jumps backward, whatever happens to the system clock while your program runs.

  start = ticks();
  assert(start >= 0, "ticks() must be non-negative");
  end = ticks();
  assert(end >= start, "ticks() must be monotonically non-decreasing");

Measuring elapsed time

Subtract two ticks() values to get the duration in microseconds. Divide by 1000 to convert to milliseconds.

elapsed_us  = end_ticks - start_ticks
elapsed_ms  = elapsed_us / 1000

Example: measure how long a loop takes.

  t_before = ticks();
  sum = 0;
  for i in 0..1000 {
    sum += i
  }
  t_after = ticks();
  elapsed = t_after - t_before;
  assert(elapsed >= 0, "elapsed time must be non-negative: {elapsed}");
  assert(sum == 499500, "loop produced wrong sum: {sum}");

Common patterns

Timestamp a log entry (seconds since epoch): seconds = now() / 1000

Seed the random number generator with the current time. The generator is in the random package rather than the standard library, so the import is part of the pattern — without it the name does not resolve: use random::(rand_seed); rand_seed(now())

Simple stopwatch (integer division, so this truncates to whole ms): start = ticks() ... do work ... log_info("Done in {(ticks() - start) / 1000} ms")

}