~torresjrjr/hare

f86f82297ea85f91ba16d6d1e33d1a52a0e0f206 — Byron Torres 2 months ago 3dd5748
time::chrono: handle errors, utc leapsecs init

Error types are now consolidated and organised.

[[time::chrono::utc]] aborts when to_tai() & from_tai() are called with
uninitialized UTC/TAI leap second data.

Fixes: https://todo.sr.ht/~sircmpwn/hare/720
References: https://todo.sr.ht/~sircmpwn/hare/642
Signed-off-by: Byron Torres <b@torresjrjr.com>
M scripts/gen-stdlib => scripts/gen-stdlib +4 -2
@@ 1305,21 1305,23 @@ time_chrono() {
	gen_srcs -plinux time::chrono \
		+linux.ha \
		chronology.ha \
		error.ha \
		leapsec.ha \
		timescale.ha \
		timezone.ha \
		tzdb.ha
	gen_ssa -plinux time::chrono \
		bufio bytes encoding::utf8 endian errors fs io os strconv strings time path
		bufio bytes encoding::utf8 endian errors fmt fs io os strconv strings time path
	gen_srcs -pfreebsd time::chrono \
		+freebsd.ha \
		chronology.ha \
		error.ha \
		leapsec.ha \
		timescale.ha \
		timezone.ha \
		tzdb.ha
	gen_ssa -pfreebsd time::chrono \
		bufio bytes encoding::utf8 endian errors fs io os strconv strings time path
		bufio bytes encoding::utf8 endian errors fmt fs io os strconv strings time path
}

types() {

M stdlib.mk => stdlib.mk +8 -4
@@ 1973,12 1973,13 @@ $(HARECACHE)/time/time-freebsd.ssa: $(stdlib_time_freebsd_srcs) $(stdlib_rt) $(s
stdlib_time_chrono_linux_srcs = \
	$(STDLIB)/time/chrono/+linux.ha \
	$(STDLIB)/time/chrono/chronology.ha \
	$(STDLIB)/time/chrono/error.ha \
	$(STDLIB)/time/chrono/leapsec.ha \
	$(STDLIB)/time/chrono/timescale.ha \
	$(STDLIB)/time/chrono/timezone.ha \
	$(STDLIB)/time/chrono/tzdb.ha

$(HARECACHE)/time/chrono/time_chrono-linux.ssa: $(stdlib_time_chrono_linux_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_path_$(PLATFORM))
$(HARECACHE)/time/chrono/time_chrono-linux.ssa: $(stdlib_time_chrono_linux_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_path_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/time/chrono
	@HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Ntime::chrono \


@@ 1988,12 1989,13 @@ $(HARECACHE)/time/chrono/time_chrono-linux.ssa: $(stdlib_time_chrono_linux_srcs)
stdlib_time_chrono_freebsd_srcs = \
	$(STDLIB)/time/chrono/+freebsd.ha \
	$(STDLIB)/time/chrono/chronology.ha \
	$(STDLIB)/time/chrono/error.ha \
	$(STDLIB)/time/chrono/leapsec.ha \
	$(STDLIB)/time/chrono/timescale.ha \
	$(STDLIB)/time/chrono/timezone.ha \
	$(STDLIB)/time/chrono/tzdb.ha

$(HARECACHE)/time/chrono/time_chrono-freebsd.ssa: $(stdlib_time_chrono_freebsd_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_path_$(PLATFORM))
$(HARECACHE)/time/chrono/time_chrono-freebsd.ssa: $(stdlib_time_chrono_freebsd_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_path_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/time/chrono
	@HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Ntime::chrono \


@@ 4200,12 4202,13 @@ $(TESTCACHE)/time/time-freebsd.ssa: $(testlib_time_freebsd_srcs) $(testlib_rt) $
testlib_time_chrono_linux_srcs = \
	$(STDLIB)/time/chrono/+linux.ha \
	$(STDLIB)/time/chrono/chronology.ha \
	$(STDLIB)/time/chrono/error.ha \
	$(STDLIB)/time/chrono/leapsec.ha \
	$(STDLIB)/time/chrono/timescale.ha \
	$(STDLIB)/time/chrono/timezone.ha \
	$(STDLIB)/time/chrono/tzdb.ha

$(TESTCACHE)/time/chrono/time_chrono-linux.ssa: $(testlib_time_chrono_linux_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_path_$(PLATFORM))
$(TESTCACHE)/time/chrono/time_chrono-linux.ssa: $(testlib_time_chrono_linux_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_path_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/time/chrono
	@HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ntime::chrono \


@@ 4215,12 4218,13 @@ $(TESTCACHE)/time/chrono/time_chrono-linux.ssa: $(testlib_time_chrono_linux_srcs
testlib_time_chrono_freebsd_srcs = \
	$(STDLIB)/time/chrono/+freebsd.ha \
	$(STDLIB)/time/chrono/chronology.ha \
	$(STDLIB)/time/chrono/error.ha \
	$(STDLIB)/time/chrono/leapsec.ha \
	$(STDLIB)/time/chrono/timescale.ha \
	$(STDLIB)/time/chrono/timezone.ha \
	$(STDLIB)/time/chrono/tzdb.ha

$(TESTCACHE)/time/chrono/time_chrono-freebsd.ssa: $(testlib_time_chrono_freebsd_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_path_$(PLATFORM))
$(TESTCACHE)/time/chrono/time_chrono-freebsd.ssa: $(testlib_time_chrono_freebsd_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_path_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/time/chrono
	@HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ntime::chrono \

A time/chrono/error.ha => time/chrono/error.ha +34 -0
@@ 0,0 1,34 @@
// License: MPL-2.0
// (c) 2022 Byron Torres <b@torresjrjr.com>
use encoding::utf8;
use fmt;
use fs;
use io;

// All possible errors returned from [[time::chrono]].
export type error = !(invalid | tzdberror | invalidtzif);

// Converts an [[error]] into a human-friendly string.
export fn strerror(err: error) const str = {
	match (err) {
	case invalid =>
		return "Invalid moment";
	case invalidtzif =>
		return "Invalid TZif data";
	case let err: tzdberror =>
		match (err) {
		case let err: fs::error =>
			return fmt::asprintf(
				"Timezone database error: {}",
				fs::strerror(err),
			);
		case let err: io::error =>
			return fmt::asprintf(
				"Timezone database error: {}",
				io::strerror(err),
			);
		case invalidtzif =>
			return "Timezone database error: Invalid TZif data";
		};
	};
};

M time/chrono/leapsec.ha => time/chrono/leapsec.ha +24 -11
@@ 27,34 27,47 @@ use strings;
// design also inhibits our ambitions for dealing with multiple, dynamic
// timescales. Therefore, we have decided to take an alternative approach.

// Error initializing the [[utc]] [[timescale]].
type utciniterror = !(fs::error | io::error | encoding::utf8::invalid);

// The number of seconds between the years 1900 and 1970. This number is
// deliberately hypothetical since timekeeping before atomic clocks was not
// accurate enough to account for small changes in time.
export def SECS_1900_1970: i64 = 2208988800;

// The filepath of the system's leap-seconds.list file.
// The filepath of the system's "leap-seconds.list" file, which contains UTC/TAI
// leap second data.
export def UTC_LEAPSECS_FILE: str = "/usr/share/zoneinfo/leap-seconds.list";

// UTC timestamps and their offsets from TAI, sourced from the system's
// leap-seconds.list file.
// UTC/TAI leap second data; UTC timestamps and their offsets from TAI.
// Sourced from [[UTC_LEAPSECS_FILE]].
let utc_leapsecs: [](i64, i64) = [];

@init fn init_utc_leapsecs() void = {
let utc_isinitialized: bool = false;

// Initializes the [[utc]] [[timescale]].
@init fn init_utc() void = {
	os::init_cwd();
	const file = match (os::open(UTC_LEAPSECS_FILE)) {
	case let file: io::file =>
		yield file;
	case fs::error =>
	match (init_utc_leapsecs()) {
	case void =>
		utc_isinitialized = true;
	case =>
		return;
	};
};

fn init_utc_leapsecs() (void | utciniterror) = {
	const file = os::open(UTC_LEAPSECS_FILE)?;
	defer io::close(file)!;
	read_utc_leapsecs_file(file, &utc_leapsecs)!;
	parse_utc_leapsecs(file, &utc_leapsecs)?;
};

fn read_utc_leapsecs_file(
// Parse UTC/TAI leap second data from [[UTC_LEAPSECS_FILE]].
// See file for format details.
fn parse_utc_leapsecs(
	h: io::handle,
	leapsecs: *[](i64, i64),
) (void | io::error | encoding::utf8::invalid) = {
) (void | encoding::utf8::invalid | io::error) = {
	for (true) {
		const line = match (bufio::scanline(h)) {
		case let err: io::error =>

M time/chrono/timescale.ha => time/chrono/timescale.ha +13 -0
@@ 42,6 42,11 @@ fn conv_tai_tai(i: time::instant) (time::instant | time::error) = {
// Used as the basis of civil timekeeping.
// Based on TAI; time-dependent offset.
// Discontinuous (has leap seconds).
//
// During a program's initialization, this timescale initializes by loading its
// UTC/TAI leap second data from [[UTC_LEAPSECS_FILE]]; otherwise, fails
// silently. If failed, any attempt to consult UTC leapsec data (like calling
// utc.to_tai(), utc.from_tai()) causes an abort. This includes [[chrono::in]].
export const utc: timescale = timescale {
	name = "Coordinated Universal Time",
	abbr = "UTC",


@@ 50,6 55,10 @@ export const utc: timescale = timescale {
};

fn conv_tai_utc(a: time::instant) (time::instant | time::error) = {
	if (!utc_isinitialized) {
		abort("utc timescale uninitialized");
	};

	const idx = lookup_leaps(&utc_leapsecs, time::unix(a));
	const ofst = utc_leapsecs[idx].1;



@@ 65,6 74,10 @@ fn conv_tai_utc(a: time::instant) (time::instant | time::error) = {
};

fn conv_utc_tai(a: time::instant) (time::instant | time::error) = {
	if (!utc_isinitialized) {
		abort("utc timescale uninitialized");
	};

	const idx = lookup_leaps(&utc_leapsecs, time::unix(a));
	const ofst = utc_leapsecs[idx].1;


M time/chrono/tzdb.ha => time/chrono/tzdb.ha +4 -18
@@ 12,30 12,16 @@ use path;
use strings;
use time;

// Error concerning the Timezone database.
export type tzdberror = !(invalidtzif | fs::error | io::error);

// Invalid TZif data.
export type invalidtzif = !void;

// Possible errors returned from [[tz]].
export type error = !(fs::error | io::error | invalidtzif);
// TODO: Move to an appropriate file, and add other chrono error types.

// Converts an [[error]] to a human-friendly representation.
export fn strerror(err: error) const str = {
	match (err) {
	case invalidtzif =>
		return "Invalid TZif data";
	case let err: fs::error =>
		return fs::strerror(err);
	case let err: io::error =>
		return io::strerror(err);
	};
};

// Finds and loads a [[timezone]] from the system's Timezone database, normally
// located at /usr/share/zoneinfo. All timezones provided default to the [[utc]]
// [[timescale]] and [[EARTH_DAY]] day-length.
export fn tz(name: str) (timezone | fs::error | io::error | invalidtzif) = {
// TODO: Consolidate errors (use chrono::error?).
export fn tz(name: str) (timezone | tzdberror) = {
	const filepath = path::init();
	path::add(&filepath, ZONEINFO_PREFIX, name)!;
	const fpath = path::string(&filepath);